diff options
Diffstat (limited to 'test-framework/Test')
-rw-r--r-- | test-framework/Test/Framework.hs | 137 |
1 files changed, 95 insertions, 42 deletions
diff --git a/test-framework/Test/Framework.hs b/test-framework/Test/Framework.hs index 4c7799b..1b2b7d7 100644 --- a/test-framework/Test/Framework.hs +++ b/test-framework/Test/Framework.hs @@ -2,6 +2,7 @@ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ImplicitParams #-} +{-# LANGUAGE TupleSections #-} module Test.Framework ( TestTree, testGroup, @@ -17,16 +18,17 @@ module Test.Framework ( TestName, ) where -import Control.Monad (forM) +import Control.Monad (forM, when) import Control.Monad.Trans.State.Strict import Control.Monad.IO.Class -import Data.List (isInfixOf) +import Data.List (isInfixOf, intercalate) import Data.Maybe (isJust, mapMaybe, fromJust) import Data.String (fromString) import Data.Time.Clock import System.Environment import System.Exit -import System.IO (hFlush, hPutStrLn, stdout, stderr) +import System.IO (hFlush, hPutStrLn, stdout, stderr, hIsTerminalDevice) +import Text.Read (readMaybe) import qualified Hedgehog as H import qualified Hedgehog.Internal.Config as H @@ -62,10 +64,10 @@ testProperty :: String -> H.Property -> TestTree testProperty = HP filterTree :: Options -> TestTree -> Maybe TestTree -filterTree (Options { optsPattern = pat }) = go "" +filterTree (Options { optsPattern = pat }) = go [] where go path (Group collapse name trees) = - case mapMaybe (go (path++"/"++name)) trees of + case mapMaybe (go (name:path)) trees of [] -> Nothing trees' -> Just (Group collapse name trees') go path (Resource make free fun) = @@ -73,9 +75,11 @@ filterTree (Options { optsPattern = pat }) = go "" Nothing -> Nothing Just _ -> Just $ Resource make free (fromJust . go path . fun) go path hp@(HP name _) - | pat `isInfixOf` (path ++ "/" ++ name) = Just hp + | pat `isInfixOf` renderPath (name:path) = Just hp | otherwise = Nothing + renderPath comps = "^" ++ intercalate "/" (reverse comps) ++ "$" + computeMaxLen :: TestTree -> Int computeMaxLen = go 0 where @@ -101,11 +105,14 @@ modifyStats f = M (modify f) data Options = Options { optsPattern :: String - , optsHelp :: Bool } + , optsHelp :: Bool + , optsHedgehogReplay :: Maybe (H.Skip, H.Seed) + , optsHedgehogShrinks :: Maybe Int + } deriving (Show) defaultOptions :: Options -defaultOptions = Options "" False +defaultOptions = Options "" False Nothing Nothing parseOptions :: [String] -> Options -> Either String Options parseOptions [] opts = pure opts @@ -114,6 +121,19 @@ parseOptions ("--help":args) opts = parseOptions args opts { optsHelp = True } parseOptions ("-p":arg:args) opts | optsPattern opts == "" = parseOptions args opts { optsPattern = arg } | otherwise = Left "Multiple '-p' arguments given" +parseOptions ("--hedgehog-replay":arg:args) opts + | Nothing <- optsHedgehogReplay opts = + let parsed = do + (skipstr, ' ' : seedstr) <- return $ span (/= ' ') arg + (,) <$> H.skipDecompress skipstr <*> readMaybe seedstr + in case parsed of + Just res -> parseOptions args opts { optsHedgehogReplay = Just res } + Nothing -> Left "Invalid argument to '--hedgehog-replay'" + | otherwise = Left "Multiple '--hedgehog-replay' arguments given" +parseOptions ("--hedgehog-shrinks":arg:args) opts = + case readMaybe arg of + Just n -> parseOptions args opts { optsHedgehogShrinks = Just n } + Nothing -> Left "Invalid argument to '--hedgehog-shrinks'" parseOptions (arg:_) _ = Left $ "Unrecognised argument: '" ++ arg ++ "'" printUsage :: IO () @@ -124,7 +144,10 @@ printUsage = do ,"Options:" ," -h / --help Show this help" ," -p PATTERN Only tests whose path contains PATTERN are run. The path of a" - ," test looks like: '/group1/group2/testname'."] + ," test looks like: '^group1/group2/testname$'." + ," --hedgehog-replay '{skip} {seed}'" + ," Skip to a particular generated Hedgehog test. Should be used" + ," with -p. Overrides 'propertySkip' in 'PropertyConfig' if set."] defaultMain :: TestTree -> IO () defaultMain tree = do @@ -141,74 +164,100 @@ runTests options = \tree' -> Nothing -> do hPutStrLn stderr "No tests matched the given pattern." return (ExitFailure 1) Just tree -> do - let M m = let ?maxlen = computeMaxLen tree in go 0 tree + isterm <- hIsTerminalDevice stdout + let M m = let ?maxlen = computeMaxLen tree + ?istty = isterm + in go 0 id tree starttm <- getCurrentTime (success, stats) <- runStateT m initStats endtm <- getCurrentTime - printStats stats (diffUTCTime endtm starttm) + let ?istty = isterm in printStats stats (diffUTCTime endtm starttm) return (if isJust success then ExitSuccess else ExitFailure 1) where -- If all tests are successful, returns the number of output lines produced - go :: (?maxlen :: Int) => Int -> TestTree -> M (Maybe Int) - go indent (Group collapse name trees) = do - liftIO $ putStrLn (replicate (2 * indent) ' ' ++ name) + go :: (?maxlen :: Int, ?istty :: Bool) => Int -> (String -> String) -> TestTree -> M (Maybe Int) + go indent path (Group collapse name trees) = do + liftIO $ putStrLn (replicate (2 * indent) ' ' ++ name) >> hFlush stdout starttm <- liftIO getCurrentTime - mlns <- fmap (fmap sum . sequence) . forM trees $ go (indent + 1) + mlns <- fmap (fmap sum . sequence) . forM trees $ + go (indent + 1) (path . (name++) . ('/':)) endtm <- liftIO getCurrentTime case mlns of - Just lns | collapse -> do + Just lns | collapse, ?istty -> do let thislen = 2*indent + length name liftIO $ putStrLn $ concat (replicate (lns+1) "\x1B[A\x1B[2K") ++ "\x1B[G" ++ replicate (2 * indent) ' ' ++ name ++ ": " ++ replicate (?maxlen - thislen) ' ' ++ "\x1B[32mOK\x1B[0m" ++ prettyDuration False (realToFrac (diffUTCTime endtm starttm)) return (Just 1) - _ -> return mlns - go indent (Resource make cleanup fun) = do + _ -> return ((+1) <$> mlns) + go indent path (Resource make cleanup fun) = do value <- liftIO make - success <- go indent (fun value) + success <- go indent path (fun value) liftIO $ cleanup value return success - go indent (HP name (H.Property config test)) = do + go indent path (HP name (H.Property config test)) = do let thislen = 2*indent + length name - liftIO $ putStr (replicate (2*indent) ' ' ++ name ++ ": " ++ replicate (?maxlen - thislen) ' ') - liftIO $ hFlush stdout + let outputPrefix = replicate (2*indent) ' ' ++ name ++ ": " ++ replicate (?maxlen - thislen) ' ' + when ?istty $ liftIO $ putStr outputPrefix >> hFlush stdout - seed <- H.Seed.random + let (config', seedfun) = applyHedgehogOptions options config + seed <- seedfun starttm <- liftIO getCurrentTime - report <- liftIO $ H.checkReport config 0 seed test (outputProgress (?maxlen + 2)) + report <- liftIO $ H.checkReport config' 0 seed test (outputProgress (?maxlen + 2)) endtm <- liftIO getCurrentTime - liftIO $ printResult report (diffUTCTime endtm starttm) + liftIO $ do + when (not ?istty) $ putStr outputPrefix + printResult report (path name) (diffUTCTime endtm starttm) + hFlush stdout let ok = H.reportStatus report == H.OK modifyStats $ \stats -> stats { statsOK = fromEnum ok + statsOK stats , statsTotal = 1 + statsTotal stats } return (if ok then Just 1 else Nothing) -outputProgress :: Int -> H.Report H.Progress -> IO () -outputProgress indent report = do - str <- H.renderProgress H.EnableColor (Just (fromString "")) report - putStr (replace '\n' " " str ++ "\x1B[" ++ show (indent+1) ++ "G") - hFlush stdout - -printResult :: H.Report H.Result -> NominalDiffTime -> IO () -printResult report timeTaken = do +applyHedgehogOptions :: MonadIO m => Options -> H.PropertyConfig -> (H.PropertyConfig, m H.Seed) +applyHedgehogOptions opts config0 = + let (f1, seedfun) = case optsHedgehogReplay opts of + Just (skip, seed) -> (\c -> c { H.propertySkip = Just skip }, return seed) + Nothing -> (id, H.Seed.random) + f2 = case optsHedgehogShrinks opts of + Just n -> \c -> c { H.propertyShrinkLimit = H.ShrinkLimit n } + Nothing -> id + in (f2 . f1 $ config0, seedfun) + +outputProgress :: (?istty :: Bool) => Int -> H.Report H.Progress -> IO () +outputProgress indent report + | ?istty = do + str <- H.renderProgress H.EnableColor (Just (fromString "")) report + putStr (replace '\n' " " str ++ "\x1B[" ++ show (indent+1) ++ "G") + hFlush stdout + | otherwise = return () + +printResult :: (?istty :: Bool) => H.Report H.Result -> String -> NominalDiffTime -> IO () +printResult report path timeTaken = do str <- H.renderResult H.EnableColor (Just (fromString "")) report - if H.reportStatus report == H.OK - then putStrLn ("\x1B[K" ++ str ++ prettyDuration False (realToFrac timeTaken)) - else putStrLn ("\x1B[K" ++ str) - -printStats :: Stats -> NominalDiffTime -> IO () + case H.reportStatus report of + H.OK -> putStrLn (ansi "\x1B[K" ++ str ++ prettyDuration False (realToFrac timeTaken)) + H.Failed failure -> do + let H.Report { H.reportTests = count, H.reportDiscards = discards } = report + replayInfo = H.skipCompress (H.SkipToShrink count discards (H.failureShrinkPath failure)) ++ + " " ++ show (H.reportSeed report) + suffix = "\n Flags to reproduce: `-p '" ++ path ++ "' --hedgehog-replay '" ++ replayInfo ++ "'`" + putStrLn (ansi "\x1B[K" ++ str ++ suffix) + _ -> putStrLn (ansi "\x1B[K" ++ str) + +printStats :: (?istty :: Bool) => Stats -> NominalDiffTime -> IO () printStats stats timeTaken | statsOK stats == statsTotal stats = do - putStrLn $ "\x1B[32mAll " ++ show (statsTotal stats) ++ - " tests passed." ++ prettyDuration True (realToFrac timeTaken) ++ "\x1B[0m" + putStrLn $ ansi "\x1B[32m" ++ "All " ++ show (statsTotal stats) ++ + " tests passed." ++ prettyDuration True (realToFrac timeTaken) ++ ansi "\x1B[0m" | otherwise = let nfailed = statsTotal stats - statsOK stats - in putStrLn $ "\x1B[31mFailed " ++ show nfailed ++ " out of " ++ show (statsTotal stats) ++ - " tests." ++ prettyDuration True (realToFrac timeTaken) ++ "\x1B[0m" + in putStrLn $ ansi "\x1B[31m" ++ "Failed " ++ show nfailed ++ " out of " ++ show (statsTotal stats) ++ + " tests." ++ prettyDuration True (realToFrac timeTaken) ++ ansi "\x1B[0m" prettyDuration :: Bool -> Double -> String prettyDuration False x | x < 0.5 = "" @@ -220,3 +269,7 @@ prettyDuration _ x = replace :: Eq a => a -> [a] -> [a] -> [a] replace x ys = concatMap (\y -> if y == x then ys else [y]) + +ansi :: (?istty :: Bool) => String -> String +ansi | ?istty = id + | otherwise = const "" |