{-# 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