fancier tests

This commit is contained in:
rhiannon morris 2022-05-24 12:09:17 +02:00
parent 25f9df34ab
commit b0692c15b9
4 changed files with 200 additions and 89 deletions

View file

@ -18,7 +18,7 @@ lib:
.PHONY: test .PHONY: test
test: test:
nix run -- '.#quox-tests' -V 14 nix run -- '.#quox-tests' -V 14 -c
.PHONY: prove .PHONY: prove
prove: prove:

View file

@ -4,41 +4,110 @@ import Data.String
import System import System
import System.Console.GetOpt import System.Console.GetOpt
%default total
public export
data TAPVersion = V13 | V14
export
readVersion : String -> Maybe TAPVersion
readVersion "13" = Just V13
readVersion "14" = Just V14
readVersion _ = Nothing
export Show TAPVersion where show V13 = "13"; show V14 = "14"
public export public export
record Options where record Options where
constructor Opts constructor Opts
tapVersion : String version : TAPVersion
pattern : Maybe String
color : Bool
defaultOpts = Opts {tapVersion = "13"} export
defaultOpts : Options
defaultOpts = Opts {
version = V13,
pattern = Nothing,
color = False
}
OptMod = Options -> Options public export
Mod : Type
opts : List (OptDescr OptMod) Mod = Options -> IO Options
opts = [
MkOpt ['V'] ["version"]
(ReqArg (\v => the OptMod {tapVersion := v}) "VERSION")
"TAP version to output (13 or 14, default 13)"
]
-- [todo] get rid of "the OptMod" when type inference is better, maybe
makeOpts : List OptMod -> Options
makeOpts = foldl (flip ($)) defaultOpts
export
failureWith : List String -> IO a
failureWith msgs = do
traverse_ (\s => putStrLn "# \{s}") msgs
putStrLn "\nBail out!"
exitFailure
private
setTapVer : String -> Mod
setTapVer ver opts =
case readVersion ver of
Just v => pure $ {version := v} opts
Nothing => failureWith ["unrecognised TAP version '\{ver}'"]
private
setPat : String -> Mod
setPat str opts = pure $ {pattern := Just str} opts
mutual
export
opts : List (OptDescr Mod)
opts =
[ MkOpt {
description = "show this help",
shortNames = ['h', '?'], longNames = ["help"],
argDescr = NoArg $ const $ failureWith usage
},
MkOpt {
description = "TAP version to output (13 or 14, default 13)",
shortNames = ['V'], longNames = ["version"],
argDescr = ReqArg setTapVer "VERSION"
},
MkOpt {
description = "only run tests containing STR in their group or label",
shortNames = ['F'], longNames = ["filter"],
argDescr = ReqArg setPat "STR"
},
MkOpt {
description = "don't colour-code results (default)",
shortNames = ['C'], longNames = ["no-color", "no-colour"],
argDescr = NoArg $ pure . {color := False}
},
MkOpt {
description = "colour-code results (not TAP compliant)",
shortNames = ['c'], longNames = ["color", "colour"],
argDescr = NoArg $ pure . {color := True}
}
]
export
usage : List String
usage = assert_total $ "quox test suite" :: lines (usageInfo "" opts)
export
makeOpts : List Mod -> IO Options
makeOpts = foldlM (\x, f => f x) defaultOpts
export
getArgs1 : IO (List String) getArgs1 : IO (List String)
getArgs1 = getArgs1 =
case !getArgs of case !getArgs of
_ :: args => pure args _ :: args => pure args
[] => do [] => failureWith ["expected getArgs to start with exe name"]
putStrLn "expecting getArgs to start with exe name"
exitFailure
export export
getTestOpts : IO Options getTestOpts : IO Options
getTestOpts = getTestOpts =
case getOpt Permute opts !getArgs1 of case getOpt Permute opts !getArgs1 of
MkResult opts [] [] [] => pure $ makeOpts opts MkResult opts [] [] [] => makeOpts opts
res => do res => failureWith $ res.errors ++ usage
traverse_ putStrLn $ res.errors ++ [usageInfo "quox test suite" opts]
exitFailure

View file

@ -1,14 +1,20 @@
module TAP module TAP
-- [todo] extract this and Quox.Error to their own packages -- [todo] extract this to its own package?
import Options
import public Control.Monad.Either import public Control.Monad.Either
import Data.String import Data.String
import Data.List
import Data.List.Elem import Data.List.Elem
import Data.SnocList import Data.SnocList
import Control.Monad.Reader import Control.Monad.Reader
import Control.Monad.State import Control.Monad.State
import Control.ANSI
import System import System
%default total
public export public export
Info : Type Info : Type
Info = List (String, String) Info = List (String, String)
@ -39,24 +45,20 @@ toLines xs = "---" :: concatMap toLines1 xs <+> ["..."]
public export interface ToInfo e where toInfo : e -> Info public export interface ToInfo e where toInfo : e -> Info
export %inline ToInfo () where toInfo () = [] export ToInfo () where toInfo () = []
export %inline Show a => ToInfo (List (String, a)) where toInfo = map (map show) export Show a => ToInfo (List (String, a)) where toInfo = map (map show)
export export
data Test = One TestBase | Group String (List Test) data Test = One TestBase | Group String (List Test)
private %inline private
success : ToInfo a => a -> IO Result result : ToInfo a => Bool -> a -> IO Result
success = pure . Tried True . toInfo result ok = pure . Tried ok . toInfo
private %inline private
failure : ToInfo e => e -> IO Result
failure = pure . Tried False . toInfo
private %inline
lazyToIO : Lazy a -> IO a lazyToIO : Lazy a -> IO a
lazyToIO val = primIO $ \w => MkIORes (force val) w lazyToIO val = primIO $ \w => MkIORes (force val) w
@ -64,32 +66,31 @@ export
testIO : (ToInfo e, ToInfo a) => String -> EitherT e IO a -> Test testIO : (ToInfo e, ToInfo a) => String -> EitherT e IO a -> Test
testIO label act = One $ MakeTest label $ do testIO label act = One $ MakeTest label $ do
case !(runEitherT act) of case !(runEitherT act) of
Right val => success val Right val => result True val
Left err => failure err Left err => result False err
export %inline export
test : (ToInfo e, ToInfo a) => String -> Lazy (Either e a) -> Test test : (ToInfo e, ToInfo a) => String -> Lazy (Either e a) -> Test
test label val = test label val = testIO label $ MkEitherT $ lazyToIO val
testIO label $ MkEitherT $ lazyToIO val
export %inline export
todoWith : String -> String -> Test todoWith : String -> String -> Test
todoWith label reason = One $ MakeTest label $ pure $ Todo reason todoWith label reason = One $ MakeTest label $ pure $ Todo reason
export %inline export
todo : String -> Test todo : String -> Test
todo label = todoWith label "" todo label = todoWith label ""
private %inline private
makeSkip : String -> String -> Test makeSkip : String -> String -> Test
makeSkip label reason = One $ MakeTest label $ pure $ Skip reason makeSkip label reason = One $ MakeTest label $ pure $ Skip reason
export %inline export
skipWith : Test -> String -> Test skipWith : Test -> String -> Test
skipWith (One t) reason = makeSkip t.label reason skipWith (One t) reason = makeSkip t.label reason
skipWith (Group l _) reason = makeSkip l reason skipWith (Group l _) reason = makeSkip l reason
export %inline export
skip : Test -> Test skip : Test -> Test
skip test = skipWith test "" skip test = skipWith test ""
@ -98,15 +99,15 @@ testThrows : (ToInfo e, Show a) =>
String -> (e -> Bool) -> Lazy (Either e a) -> Test String -> (e -> Bool) -> Lazy (Either e a) -> Test
testThrows label p act = One $ MakeTest label $ do testThrows label p act = One $ MakeTest label $ do
case !(lazyToIO act) of case !(lazyToIO act) of
Left err => if p err then success () else failure err Left err => if p err then result True () else result False err
Right val => failure [("success", val)] Right val => result False [("success", val)]
infix 1 :- infix 1 :-
export %inline export
(:-) : String -> List Test -> Test (:-) : String -> List Test -> Test
(:-) = Group (:-) = Group
export %inline export
bailOut : Test bailOut : Test
bailOut = One $ MakeTest "bail out" $ do bailOut = One $ MakeTest "bail out" $ do
putStrLn "Bail out!" putStrLn "Bail out!"
@ -114,7 +115,7 @@ bailOut = One $ MakeTest "bail out" $ do
export %inline export
header : List a -> String header : List a -> String
header tests = "1..\{show $ length tests}" header tests = "1..\{show $ length tests}"
@ -123,45 +124,53 @@ makePrefix : SnocList String -> String
makePrefix [<] = "" makePrefix [<] = ""
makePrefix (xs :< x) = foldr (\a, b => "\{a}/\{b}") x xs makePrefix (xs :< x) = foldr (\a, b => "\{a}/\{b}") x xs
private %inline private
withPrefix : SnocList String -> TestBase -> Test withPrefix : SnocList String -> TestBase -> Test
withPrefix pfx b = One $ {label := "[\{makePrefix pfx}] \{b.label}"} b withPrefix pfx b = One $ {label := "[\{makePrefix pfx}] \{b.label}"} b
mutual mutual
export %inline export
flattenWith : SnocList String -> List Test -> List Test flattenWith : SnocList String -> List Test -> List Test
flattenWith pfx = concatMap (flatten1With pfx) flattenWith pfx tests =
concatMap (\t => flatten1With pfx (assert_smaller tests t)) tests
export export
flatten1With : SnocList String -> Test -> List Test flatten1With : SnocList String -> Test -> List Test
flatten1With pfx (One t) = [withPrefix pfx t] flatten1With pfx (One t) = [withPrefix pfx t]
flatten1With pfx (Group x ts) = flattenWith (pfx :< x) ts flatten1With pfx (Group x ts) = flattenWith (pfx :< x) ts
export %inline export
flatten : List Test -> List Test flatten : List Test -> List Test
flatten = flattenWith [<] flatten = flattenWith [<]
export %inline export
flatten1 : Test -> List Test flatten1 : Test -> List Test
flatten1 = flatten1With [<] flatten1 = flatten1With [<]
private
record RunnerEnv where
constructor RE
indent : Nat
color : Bool
private private
Runner : Type -> Type Runner : Type -> Type
Runner = ReaderT Nat IO Runner = ReaderT RunnerEnv IO
private %inline private
putIndentLines : List String -> Runner () putIndentLines : List String -> Runner ()
putIndentLines xs = traverse_ (putStrLn . indent !ask) xs putIndentLines xs = traverse_ (putStrLn . indent (!ask).indent) xs
private %inline private
isOk : Bool -> String isOk : Bool -> String
isOk b = if b then "ok" else "not ok" isOk b = if b then "ok" else "not ok"
private %inline private
toBool : Result -> Bool toBool : Result -> Bool
toBool (Tried ok _) = ok toBool (Tried ok _) = ok
toBool _ = True toBool _ = True
private private
@ -171,18 +180,45 @@ numbered = go 1 where
go _ [] = [] go _ [] = []
go i (x :: xs) = (i, x) :: go (S i) xs go i (x :: xs) = (i, x) :: go (S i) xs
private
col : Color -> String -> Runner String
col c str = pure $ if (!ask).color then show $ colored c str else str
private
putColor : Color -> String -> Runner ()
putColor c str = putIndentLines [!(col c str)]
private
okCol : Bool -> Color
okCol True = Green
okCol False = Red
private
putOk' : Color -> Bool -> Nat -> String -> Runner ()
putOk' c ok index label =
putIndentLines [!(col c "\{isOk ok} \{show index}") ++ " - \{label}"]
private
putOk : Bool -> Nat -> String -> Runner ()
putOk ok = putOk' (okCol ok) ok
private
putVersion : TAPVersion -> Runner ()
putVersion ver = putColor Cyan "TAP version \{show ver}"
private private
run1' : (Nat, TestBase) -> Runner Bool run1' : (Nat, TestBase) -> Runner Bool
run1' (index, test) = do run1' (index, test) = do
res <- liftIO test.run res <- liftIO test.run
case res of case res of
Tried ok info => do Tried ok info => do
putIndentLines ["\{isOk ok} \{show index} - \{test.label}"] putOk ok index test.label
local (plus 2) $ putIndentLines $ toLines info local {indent $= plus 2} $ putIndentLines $ toLines info
Skip reason => putIndentLines Skip reason =>
["ok \{show index} - \{test.label} # skip \{reason}"] putOk' Yellow True index "test.label # skip \{reason}"
Todo reason => putIndentLines Todo reason =>
["ok \{show index} - \{test.label} # todo \{reason}"] putOk' Yellow True index "test.label # todo \{reason}"
pure $ toBool res pure $ toBool res
mutual mutual
@ -190,30 +226,42 @@ mutual
run' : (Nat, Test) -> Runner Bool run' : (Nat, Test) -> Runner Bool
run' (index, One test) = run1' (index, test) run' (index, One test) = run1' (index, test)
run' (index, Group label tests) = do run' (index, Group label tests) = do
putIndentLines ["# Subtest: \{label}"] putColor Magenta "# Subtest: \{label}"
res <- local (plus 4) $ runList tests res <- local {indent $= plus 4} $ runList tests
putIndentLines ["\{isOk res} \{show index} - \{label}"] putOk res index label
pure res pure res
private private
runList : List Test -> Runner Bool runList : List Test -> Runner Bool
runList tests = do runList tests = do
putIndentLines [header tests] putColor Cyan $ header tests
all id <$> traverse run' (numbered tests) all id <$> traverse (\t => run' (assert_smaller tests t)) (numbered tests)
mutual
export
filterMatch : Maybe String -> List Test -> List Test
filterMatch Nothing tests = tests
filterMatch (Just pat) tests =
mapMaybe (\t => filterMatch1 pat (assert_smaller tests t)) tests
export
filterMatch1 : String -> Test -> Maybe Test
filterMatch1 pat test@(One base) =
guard (pat `isInfixOf` base.label) $> test
filterMatch1 pat all@(Group label tests) =
if pat `isInfixOf` label then Just all else
case filterMatch (Just pat) tests of
[] => Nothing
res => Just $ Group label res
export export
run : (ver : Nat) -> List Test -> IO ExitCode main : Options -> List Test -> IO ExitCode
run ver tests = do main opts tests = do
putStrLn "TAP version \{show ver}" let tests = filterMatch opts.pattern $
pure $ if !(runReaderT 0 $ runList tests) case opts.version of V13 => flatten tests; V14 => tests
let act = do putVersion opts.version; runList tests
pure $ if !(runReaderT (RE 0 opts.color) act)
then ExitSuccess then ExitSuccess
else ExitFailure 70 else ExitFailure 70
export
main : List Test -> IO ()
main tests = exitWith !(run 14 tests)
export
mainFlat : List Test -> IO ()
mainFlat tests = exitWith !(run 13 $ flatten tests)

View file

@ -16,10 +16,4 @@ allTests = [
Equal.tests Equal.tests
] ]
main = do main = TAP.main !getTestOpts allTests
opts <- getTestOpts
go <- case opts.tapVersion of
"13" => pure TAP.mainFlat
"14" => pure TAP.main
_ => do putStrLn "unrecognised TAP version; use 13 or 14"; exitFailure
go allTests