doctest-0.24.2: src/Runner.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
module Runner (
runModules
, FastMode(..)
, PreserveIt(..)
, FailFast(..)
, Verbose(..)
, Summary(..)
, isSuccess
, formatSummary
#ifdef TEST
, Report
, ReportState(..)
, runReport
, Interactive(..)
, report
, reportTransient
#endif
) where
import Prelude ()
import Imports hiding (putStr, putStrLn, error)
import Text.Printf (printf)
import System.IO hiding (putStr, putStrLn)
import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.State (StateT, evalStateT)
import qualified Control.Monad.Trans.State as State
import Control.Monad.IO.Class
import Data.IORef
import Interpreter (Interpreter, PreserveIt(..), safeEvalWith)
import qualified Interpreter
import Parse
import Location
import Property
import Runner.Example
-- | Summary of a test run.
data Summary = Summary {
sExamples :: !Int
, sTried :: !Int
, sErrors :: !Int
, sFailures :: !Int
} deriving Eq
instance Show Summary where
show = formatSummary
isSuccess :: Summary -> Bool
isSuccess s = sErrors s == 0 && sFailures s == 0
formatSummary :: Summary -> String
formatSummary (Summary examples tried errors failures) =
printf "Examples: %d Tried: %d Errors: %d Failures: %d" examples tried errors failures
-- | Sum up summaries.
instance Monoid Summary where
mempty = Summary 0 0 0 0
instance Semigroup Summary where
Summary x1 x2 x3 x4 <> Summary y1 y2 y3 y4 = Summary (x1 + y1) (x2 + y2) (x3 + y3) (x4 + y4)
withLineBuffering :: Handle -> IO c -> IO c
withLineBuffering h action = bracket (hGetBuffering h) (hSetBuffering h) $ \ _ -> do
hSetBuffering h LineBuffering
action
-- | Run all examples from a list of modules.
runModules :: FastMode -> PreserveIt -> FailFast -> Verbose -> Interpreter -> [Module [Located DocTest]] -> IO Summary
runModules fastMode preserveIt failFast verbose repl modules = withLineBuffering stderr $ do
interactive <- hIsTerminalDevice stderr <&> \ case
False -> NonInteractive
True -> Interactive
summary <- newIORef mempty {sExamples = n}
let
reportFinalResult :: IO ()
reportFinalResult = do
final <- readIORef summary
hPutStrLn stderr (formatSummary final)
run :: IO ()
run = runReport (ReportState interactive failFast verbose summary) $ do
reportProgress
forM_ modules $ runModule fastMode preserveIt repl
verboseReport "# Final summary:"
run `finally` reportFinalResult
readIORef summary
where
n :: Int
n = sum (map countExpressions modules)
countExpressions :: Module [Located DocTest] -> Int
countExpressions (Module _ setup tests) = sum (map length tests) + maybe 0 length setup
type Report = MaybeT (StateT ReportState IO)
data Interactive = NonInteractive | Interactive
data FastMode = NoFastMode | FastMode
data FailFast = NoFailFast | FailFast
data Verbose = NonVerbose | Verbose
data ReportState = ReportState {
reportStateInteractive :: Interactive
, reportStateFailFast :: FailFast
, reportStateVerbose :: Verbose
, reportStateSummary :: IORef Summary
}
runReport :: ReportState -> Report () -> IO ()
runReport st = void . flip evalStateT st . runMaybeT
getSummary :: Report Summary
getSummary = gets reportStateSummary >>= liftIO . readIORef
gets :: (ReportState -> a) -> Report a
gets = lift . State.gets
-- | Add output to the report.
report :: String -> Report ()
report = liftIO . hPutStrLn stderr
-- | Add intermediate output to the report.
--
-- This will be overwritten by subsequent calls to `report`/`report_`.
-- Intermediate out may not contain any newlines.
reportTransient :: String -> Report ()
reportTransient msg = gets reportStateInteractive >>= \ case
NonInteractive -> pass
Interactive -> liftIO $ do
hPutStr stderr msg
hFlush stderr
hPutStr stderr $ '\r' : (replicate (length msg) ' ') ++ "\r"
-- | Run all examples from given module.
runModule :: FastMode -> PreserveIt -> Interpreter -> Module [Located DocTest] -> Report ()
runModule fastMode preserveIt repl (Module module_ setup examples) = do
Summary _ _ e0 f0 <- getSummary
forM_ setup $
runTestGroup preserveIt repl reload
Summary _ _ e1 f1 <- getSummary
-- only run tests, if setup does not produce any errors/failures
when (e0 == e1 && f0 == f1) $
forM_ examples $ runTestGroup preserveIt repl setup_
where
reload :: IO ()
reload = do
case fastMode of
NoFastMode -> void $ Interpreter.safeEval repl ":reload"
FastMode -> pass
void $ Interpreter.safeEval repl $ ":m *" ++ module_
case preserveIt of
NoPreserveIt -> pass
PreserveIt -> do
-- Evaluate a dumb expression to populate the 'it' variable.
--
-- NOTE: This is one reason why we cannot just always use PreserveIt:
-- 'it' isn't set in a fresh GHCi session.
void $ Interpreter.safeEval repl $ "()"
setup_ :: IO ()
setup_ = do
reload
forM_ setup $ \l -> forM_ l $ \(Located _ x) -> case x of
Property _ -> return ()
Example e _ -> void $ safeEvalWith preserveIt repl e
reportStart :: Location -> Expression -> String -> Report ()
reportStart loc expression testType = do
verboseReport (printf "### Started execution at %s.\n### %s:\n%s" (show loc) testType expression)
reportFailure :: Location -> Expression -> [String] -> Report ()
reportFailure loc expression err = do
report (printf "%s: failure in expression `%s'" (show loc) expression)
mapM_ report err
report ""
updateSummary (Summary 0 1 0 1)
reportError :: Location -> Expression -> String -> Report ()
reportError loc expression err = do
report (printf "%s: error in expression `%s'" (show loc) expression)
report err
report ""
updateSummary (Summary 0 1 1 0)
reportSuccess :: Report ()
reportSuccess = do
verboseReport "### Successful!\n"
updateSummary (Summary 0 1 0 0)
verboseReport :: String -> Report ()
verboseReport msg = gets reportStateVerbose >>= \ case
NonVerbose -> pass
Verbose -> report msg
updateSummary :: Summary -> Report ()
updateSummary summary = do
ref <- gets reportStateSummary
liftIO $ modifyIORef' ref $ mappend summary
reportProgress
gets reportStateFailFast >>= \ case
NoFailFast -> pass
FailFast -> unless (isSuccess summary) abort
abort :: Report ()
abort = MaybeT $ return Nothing
reportProgress :: Report ()
reportProgress = gets reportStateVerbose >>= \ case
NonVerbose -> do
summary <- getSummary
reportTransient (formatSummary summary)
Verbose -> pass
-- | Run given test group.
--
-- The interpreter state is zeroed with @:reload@ first. This means that you
-- can reuse the same 'Interpreter' for several test groups.
runTestGroup :: PreserveIt -> Interpreter -> IO () -> [Located DocTest] -> Report ()
runTestGroup preserveIt repl setup tests = do
liftIO setup
runExampleGroup preserveIt repl examples
forM_ properties $ \(loc, expression) -> do
r <- do
liftIO setup
reportStart loc expression "property"
liftIO $ runProperty repl expression
case r of
Success ->
reportSuccess
Error err -> do
reportError loc expression err
Failure msg -> do
reportFailure loc expression [msg]
where
properties = [(loc, p) | Located loc (Property p) <- tests]
examples :: [Located Interaction]
examples = [Located loc (e, r) | Located loc (Example e r) <- tests]
type Interaction = (Expression, ExpectedResult)
-- |
-- Execute all expressions from given example in given 'Interpreter' and verify
-- the output.
runExampleGroup :: PreserveIt -> Interpreter -> [Located Interaction] -> Report ()
runExampleGroup preserveIt repl = go
where
go ((Located loc (expression, expected)) : xs) = do
reportStart loc expression "example"
r <- fmap lines <$> liftIO (safeEvalWith preserveIt repl expression)
case r of
Left err -> do
reportError loc expression err
Right actual -> case mkResult expected actual of
NotEqual err -> do
reportFailure loc expression err
Equal -> do
reportSuccess
go xs
go [] = return ()