summaryrefslogtreecommitdiff
path: root/test-framework/Test/Framework.hs
blob: 962268625edb30014932b55b6254a96a47fb1dd7 (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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImplicitParams #-}
module Test.Framework (
  TestTree,
  testGroup,
  testGroupCollapse,
  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.Maybe (isJust)
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 Bool String [TestTree]
  | forall a. Resource (IO a) (a -> IO ()) (a -> TestTree)
  | HP String H.Property

type TestName = String

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

testGroupCollapse :: String -> [TestTree] -> TestTree
testGroupCollapse = Group True

-- | 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 True name trees) = maximum (2*indent + length name : map (go (indent+1)) trees)
    go indent (Group False _ 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
  (success, stats) <- runStateT m initStats
  endtm <- getCurrentTime
  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)
      starttm <- liftIO getCurrentTime
      mlns <- fmap (fmap sum . sequence) . forM trees $ go (indent + 1)
      endtm <- liftIO getCurrentTime
      case mlns of
        Just lns | collapse -> 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
      value <- liftIO make
      success <- go indent (fun value)
      liftIO $ cleanup value
      return success
    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)

      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
  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 ()
printStats stats timeTaken
  | statsOK stats == statsTotal stats = do
      putStrLn $ "\x1B[32mAll " ++ show (statsTotal stats) ++
                 " tests passed." ++ prettyDuration True (realToFrac timeTaken) ++ "\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"

prettyDuration :: Bool -> Double -> String
prettyDuration False x | x < 0.5 = ""
prettyDuration _ 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])