{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RecordWildCards #-} module Main where import Data.Char import Data.List --import Data.Either import Data.Time.Clock import Data.Algorithm.Patience import Control.Applicative import Control.Arrow import Control.Concurrent import Control.Concurrent.Async import Control.Monad import Control.Monad.Reader import Control.Exception hiding (catch) import Control.Monad.Trans.Control import Control.DeepSeq import System.Exit import System.Directory import System.FilePath import System.IO import Options.Applicative import qualified Data.Text as T import qualified Data.Text.IO as TIO import Text.Printf import LambdaCube.Compiler import LambdaCube.Compiler.Pretty hiding (()) ------------------------------------------ utils readFileStrict :: FilePath -> IO String readFileStrict = fmap T.unpack . TIO.readFile getDirectoryContentsRecursive path = do l <- map (path ) . filter (`notElem` [".",".."]) <$> getDirectoryContents path (++) <$> filterM doesFileExist l <*> (fmap mconcat . traverse getDirectoryContentsRecursive =<< filterM doesDirectoryExist l) takeExtensions' :: FilePath -> [String] takeExtensions' = snd . splitExtensions' splitExtensions' fn = case splitExtension fn of (a, "") -> (a, []) (fn', ext) -> second (ext:) $ splitExtensions' fn' getYNChar = do c <- getChar case c of _ | c `elem` ("yY" :: String) -> putChar '\n' >> return True | c `elem` ("nN" :: String) -> putChar '\n' >> return False | otherwise -> getYNChar showTime delta | t > 1e-1 = printf "%.3fs" t | t > 1e-3 = printf "%.1fms" (t/1e-3) | otherwise = printf "%.0fus" (t/1e-6) where t = realToFrac delta :: Double timeOut :: MonadBaseControl IO m => NominalDiffTime -> a -> m a -> m (NominalDiffTime, a) timeOut dt d m = control $ \runInIO -> race' (runInIO $ timeDiff m) (runInIO $ timeDiff $ liftIO (threadDelay $ round $ dt * 1000000) >> return d) where liftIO = liftBaseWith . const race' a b = either id id <$> race a b timeDiff m = (\s x e -> (diffUTCTime e s, x)) <$> liftIO getCurrentTime <*> m <*> liftIO getCurrentTime ------------------------------------------ testDataPath = "./testdata" data Config = Config { cfgVerbose :: Bool , cfgReject :: Bool , cfgTimeout :: NominalDiffTime , cfgIgnore :: [String] , cfgOverallTime :: Bool } deriving Show arguments :: Parser (Config, [String]) arguments = (,) <$> (Config <$> switch (short 'v' <> long "verbose" <> help "Verbose output during test runs") <*> switch (short 'r' <> long "reject" <> help "Reject test cases with missing, new or different .out files") <*> option (realToFrac <$> (auto :: ReadM Double)) (value 60 <> short 't' <> long "timeout" <> help "Timeout for tests in seconds") <*> many (option (eitherReader Right) (short 'i' <> long "ignore" <> help "Ignore test")) <*> switch (long "overall-time" <> help "Writes overall time to overall-time.txt") ) <*> many (strArgument idm) data Res = Passed | Accepted | NewRes | TimedOut | Rejected | Failed | ErrorCatched deriving (Eq, Ord, Show) showRes = \case ErrorCatched -> "crashed test" Failed -> "failed test" Rejected -> "rejected result" TimedOut -> "timed out test" NewRes -> "new result" Accepted -> "accepted result" Passed -> "passed test" instance NFData Res where rnf a = a `seq` () erroneous = (>= TimedOut) isWip = (".wip" `elem`) . takeExtensions' isReject = (".reject" `elem`) . takeExtensions' main :: IO () main = do hSetBuffering stdout NoBuffering hSetBuffering stdin NoBuffering (cfg@Config{..}, samplesToTest) <- execParser $ info (helper <*> arguments) (fullDesc <> header "LambdaCube 3D compiler test suite") testData <- filter ((".lc" ==) . takeExtension) <$> getDirectoryContentsRecursive testDataPath -- select test set: all test or user selected let (ignoredTests, testSet) = partition (\d -> any (`isInfixOf` d) cfgIgnore) . map head . group . sort $ [d | d <- testData, s <- if null samplesToTest then [""] else samplesToTest, s `isInfixOf` d] unless (null ignoredTests) $ do putStrLn $ "------------------------------------ Ignoring " ++ show (length ignoredTests) ++ " tests" forM_ ignoredTests putStrLn when (null testSet) $ do putStrLn $ "test files not found: " ++ show samplesToTest exitFailure putStrLn $ "------------------------------------ Running " ++ show (length testSet) ++ " tests" resultDiffs <- runMM (ioFetch [".", testDataPath]) $ forM (zip [1..] testSet) $ doTest cfg let sh :: (FilePath -> Res -> Bool) -> String -> [String] sh p b = [ (if any (\(ty, s) -> erroneous ty && not (isWip s)) ss then "!" else "") ++ show noOfResult ++ " " ++ pad 10 (b ++ plural ++ ": ") ++ "\n" ++ unlines (map snd ss) | not $ null ss ] where ss = [(ty, s) | ((_, ty), s) <- zip resultDiffs testSet, p s ty] noOfResult = length ss plural = ['s' | noOfResult > 1] putStrLn "------------------------------------ Summary" putStrLn $ unlines $ reverse $ concat [ sh (\s ty -> ty == x && p s) (w ++ showRes x) | (w, p) <- [("", not . isWip), ("wip ", isWip)] , x <- [ErrorCatched, Failed, Rejected, TimedOut, NewRes, Accepted] ] ++ sh (\s ty -> ty == Passed && isWip s) "wip passed test" let overallTime = sum $ map fst resultDiffs putStrLn $ "Overall time: " ++ showTime overallTime when cfgOverallTime $ writeFile "overall-time.txt" $ show (realToFrac overallTime :: Double) when (or [erroneous r | ((_, r), f) <- zip resultDiffs testSet, not $ isWip f]) exitFailure putStrLn "All OK" when (or [erroneous r | ((_, r), f) <- zip resultDiffs testSet, isWip f]) $ putStrLn "Only work in progress test cases are failing." splitMPath fn = (joinPath $ reverse as, foldr1 () $ reverse bs ++ [y], intercalate "." $ reverse bs ++ [y]) where (bs, as) = span (\x -> not (null x) && isUpper (head x)) $ reverse xs (xs, y) = map takeDirectory . splitPath *** id $ splitFileName $ dropExtension fn doTest Config{..} (i, fn) = do liftIO $ putStr $ pa ++ " " ++ mn ++ " " ++ concat exts ++ " " (runtime, res) <- mapMMT (timeOut cfgTimeout $ Left ("!Timed Out", TimedOut)) $ catchErr (\e -> return $ Left (tab "!Crashed" e, ErrorCatched)) $ liftIO . evaluate =<< (force . f <$> getMain) liftIO $ putStr $ "(" ++ showTime runtime ++ ")" ++ " " (msg, result) <- case res of Left x -> return x Right (op, x) -> liftIO $ compareResult (pad 15 op) (dropExtension fn ++ ".out") x liftIO $ putStrLn msg return (runtime, result) where (splitMPath -> (pa, mn', mn), reverse -> exts) = splitExtensions' $ dropExtension fn getMain = do (is, res) <- local (const $ ioFetch [pa]) $ getDef (mn' ++ concat exts ++ ".lc") "main" Nothing (,) is <$> case res of Left err -> return $ Left err Right (fname, x@Left{}) -> return $ Right (fname, x) Right (fname, x@Right{}) -> Right (fname, x) <$ removeFromCache fname f (i, e) | not $ isReject fn = case e of Left e -> Left (unlines $ tab "!Failed" e: listTraceInfos i, Failed) Right (fname, Left e) -> Right ("typechecked module" , unlines $ e: listAllInfos i) Right (fname, Right (e, te)) | te == outputType -> Right ("compiled pipeline", prettyShowUnlines $ compilePipeline OpenGL33 (e, te)) | e == trueExp -> Right ("reducted main", ppShow $ unfixlabel e) | te == boolType -> Left (tab "!Failed" $ "main should be True but it is \n" ++ ppShow e, Failed) | otherwise -> Right ("reduced main " ++ ppShow te, ppShow e) | otherwise = case e of Left e -> Right ("error message", unlines $ e: listAllInfos i) Right _ -> Left (tab "!Failed" "failed to catch error", Failed) tab msg | isWip fn && cfgReject = const msg | otherwise = ((msg ++ "\n") ++) . unlines . map (" " ++) . lines compareResult msg ef e = doesFileExist ef >>= \b -> case b of False | cfgReject -> return ("!Missing .out file", Rejected) | otherwise -> writeFile ef e >> return ("New .out file", NewRes) True -> do e' <- lines <$> readFileStrict ef let d = diff e' $ lines e case d of _ | all (\case Both{} -> True; _ -> False) d -> return ("OK", Passed) rs | cfgReject-> return ("!Different .out file", Rejected) | otherwise -> do mapM_ putStrLn $ printOldNew msg d putStrLn $ ef ++ " has changed." putStr $ "Accept new " ++ msg ++ " (y/n)? " c <- getYNChar if c then writeFile ef e >> return ("Accepted .out file", Accepted) else return ("!Rejected .out file", Rejected) printOldNew :: String -> [Item String] -> [String] printOldNew msg d = (msg ++ " has changed.") : ff [] 0 d where ff acc n (x@(Both a b): ds) = [a' | n < 5] ++ ff (a':acc) (n+1) ds where a' = " " ++ a ff acc n (Old a: ds) = g acc n ++ (ESC "42" ("< " ++ ESC "49" a)): ff [] 0 ds ff acc n (New b: ds) = g acc n ++ (ESC "41" ("> " ++ ESC "49" b)): ff [] 0 ds ff _ _ [] = [] g acc n | n < 5 = [] g acc n | n > 10 = "___________": reverse (take 5 acc) g acc n = reverse (take (n-5) acc) pad n s = s ++ replicate (n - length s) ' ' limit :: String -> Int -> String -> String limit msg n s = take n s ++ if null (drop n s) then "" else msg