summaryrefslogtreecommitdiff
path: root/test-framework/Test/Framework.hs
blob: 0eee830c7e8b7f508f33ce7e345fbbbaa5cbbd60 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImplicitParams #-}
module Test.Framework (
  TestTree,
  testGroup,
  testProperty,
  withResource,
  withResource',
  runTests,
  defaultMain,

  -- * Compatibility
  TestName,
) where

import Control.Monad (forM_)
import Control.Monad.Trans.State.Strict
import Control.Monad.IO.Class
import Data.String (fromString)
import Data.Time.Clock
import System.Exit
import System.IO (hFlush, stdout)

import qualified Hedgehog as H
import qualified Hedgehog.Internal.Config as H
import qualified Hedgehog.Internal.Property as H
import qualified Hedgehog.Internal.Report as H
import qualified Hedgehog.Internal.Runner as H
import qualified Hedgehog.Internal.Seed as H.Seed


data TestTree
  = Group String [TestTree]
  | forall a. Resource (IO a) (a -> IO ()) (a -> TestTree)
  | HP String H.Property

type TestName = String

testGroup :: String -> [TestTree] -> TestTree
testGroup = Group

-- | The @a -> TestTree@ function must use the @a@ only inside properties: when
-- not actually running properties, it will be passed 'undefined'.
withResource :: IO a -> (a -> IO ()) -> (a -> TestTree) -> TestTree
withResource = Resource

-- | Same caveats as 'withResource'.
withResource' :: IO a -> (a -> TestTree) -> TestTree
withResource' make fun = withResource make (\_ -> return ()) fun

testProperty :: String -> H.Property -> TestTree
testProperty = HP

computeMaxLen :: TestTree -> Int
computeMaxLen = go 0
  where
    go :: Int -> TestTree -> Int
    -- go indent (Group name trees) = maximum (2*indent + length name : map (go (indent+1)) trees)
    go indent (Group _ trees) = maximum (0 : map (go (indent+1)) trees)
    go indent (Resource _ _ fun) = go indent (fun undefined)
    go indent (HP name _) = 2 * indent + length name

data Stats = Stats
  { statsOK :: Int
  , statsTotal :: Int }
  deriving (Show)

initStats :: Stats
initStats = Stats 0 0

newtype M a = M (StateT Stats IO a)
  deriving newtype (Functor, Applicative, Monad, MonadIO)

modifyStats :: (Stats -> Stats) -> M ()
modifyStats f = M (modify f)

defaultMain :: TestTree -> IO ()
defaultMain tree = runTests tree >>= exitWith

runTests :: TestTree -> IO ExitCode
runTests = \tree -> do
  let M m = let ?maxlen = computeMaxLen tree in go 0 tree
  starttm <- getCurrentTime
  stats <- execStateT m initStats
  endtm <- getCurrentTime
  printStats stats (diffUTCTime endtm starttm)
  return (if statsOK stats == statsTotal stats
            then ExitSuccess else ExitFailure 1)
  where
    go :: (?maxlen :: Int) => Int -> TestTree -> M ()
    go indent (Group name trees) = do
      liftIO $ putStrLn (replicate (2 * indent) ' ' ++ name)
      forM_ trees $ go (indent + 1)
    go indent (Resource make cleanup fun) = do
      value <- liftIO make
      go indent (fun value)
      liftIO $ cleanup value
      return ()
    go indent (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

      seed <- H.Seed.random

      starttm <- liftIO getCurrentTime
      report <- liftIO $ H.checkReport config 0 seed test (outputProgress (?maxlen + 2))
      endtm <- liftIO getCurrentTime

      liftIO $ printResult report (diffUTCTime endtm starttm)

      modifyStats $ \stats -> stats { statsOK = fromEnum (H.reportStatus report == H.OK) + statsOK stats
                                    , statsTotal = 1 + statsTotal stats }

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
  str <- H.renderResult H.EnableColor (Just (fromString "")) report
  if timeTaken >= 0.5 && H.reportStatus report == H.OK
    then putStrLn ("\x1B[K" ++ str ++ " (" ++ prettySeconds (realToFrac timeTaken) ++ ")")
    else putStrLn ("\x1B[K" ++ str)

printStats :: Stats -> NominalDiffTime -> IO ()
printStats stats timeTaken
  | statsOK stats == statsTotal stats = do
      putStrLn $ "\x1B[32mAll " ++ show (statsTotal stats) ++
                 " tests passed. (" ++ prettySeconds (realToFrac timeTaken) ++ ")\x1B[0m"
  | otherwise =
      let nfailed = statsTotal stats - statsOK stats
      in putStrLn $ "\x1B[31mFailed " ++ show nfailed ++ " out of " ++ show (statsTotal stats) ++
                    " tests. (" ++ prettySeconds (realToFrac timeTaken) ++ ")\x1B[0m"

prettySeconds :: Double -> String
prettySeconds x =
  let str = show (round (x * 100) :: Int)
      str' = replicate (3 - length str) '0' ++ str
      (pre, post) = splitAt (length str' - 2) str'
  in pre ++ "." ++ post ++ "s"

replace :: Eq a => a -> [a] -> [a] -> [a]
replace x ys = concatMap (\y -> if y == x then ys else [y])