2020-10-19 11:26:23 +03:00
|
|
|
||| Core features required to perform Golden file testing.
|
|
|
|
|||
|
|
|
|
||| We provide the core functionality to run a *single* golden file test, or
|
|
|
|
||| a whole test tree.
|
|
|
|
||| This allows the developer freedom to use as is or design the rest of the
|
|
|
|
||| test harness to their liking.
|
|
|
|
|||
|
|
|
|
||| This was originally used as part of Idris2's own test suite and
|
|
|
|
||| the core functionality is useful for the many and not the few.
|
|
|
|
||| Please see Idris2 test harness for example usage.
|
|
|
|
|||
|
|
|
|
||| # Test Structure
|
|
|
|
|||
|
|
|
|
||| This harness works from the assumption that each individual golden test
|
|
|
|
||| comprises of a directory with the following structure:
|
|
|
|
|||
|
|
|
|
||| + `run` a *shell* script that runs the test. We expect it to:
|
|
|
|
||| * Use `$1` as the variable standing for the idris executable to be tested
|
|
|
|
||| * May use `${IDRIS2_TESTS_CG}` to pick a codegen that ought to work
|
|
|
|
||| * Clean up after itself (e.g. by running `rm -rf build/`)
|
|
|
|
|||
|
|
|
|
||| + `expected` a file containting the expected output of `run`
|
|
|
|
|||
|
2021-03-15 20:46:41 +03:00
|
|
|
||| During testing, the test harness will generate an artefact named `output`
|
|
|
|
||| and display both outputs if there is a failure.
|
|
|
|
||| During an interactive session the following command is used to compare them
|
|
|
|
||| as they are:
|
2020-10-19 11:26:23 +03:00
|
|
|
|||
|
|
|
|
||| ```sh
|
|
|
|
||| git diff --no-index --exit-code --word-diff=color expected output
|
|
|
|
||| ```
|
|
|
|
|||
|
|
|
|
||| If `git` fails then the runner will simply present the expected and 'given'
|
|
|
|
||| files side-by-side.
|
|
|
|
|||
|
2021-03-15 20:46:41 +03:00
|
|
|
||| Of note, it is helpful to add `output` to a local `.gitignore` instance
|
2020-10-19 11:26:23 +03:00
|
|
|
||| to ensure that it is not mistakenly versioned.
|
|
|
|
|||
|
|
|
|
||| # Options
|
|
|
|
|||
|
|
|
|
||| The test harness has several options that may be set:
|
|
|
|
|||
|
|
|
|
||| + `idris2` The path of the executable we are testing.
|
2021-03-15 20:46:41 +03:00
|
|
|
||| + `codegen` The backend to use for code generation.
|
|
|
|
||| + `onlyNames` The tests to run relative to the generated executable.
|
2020-10-19 11:26:23 +03:00
|
|
|
||| + `interactive` Whether to offer to update the expected file or not.
|
|
|
|
||| + `timing` Whether to display time taken for each test.
|
2021-03-15 20:46:41 +03:00
|
|
|
||| + `threads` The maximum numbers to use (default: number of cores).
|
2020-10-19 11:26:23 +03:00
|
|
|
|||
|
2021-03-15 20:46:41 +03:00
|
|
|
||| We provide an options parser (`options`) that takes the list of command line
|
|
|
|
||| arguments and constructs this for you.
|
2020-10-19 11:26:23 +03:00
|
|
|
|||
|
|
|
|
||| # Usage
|
|
|
|
|||
|
|
|
|
||| When compiled to an executable the expected usage is:
|
|
|
|
|||
|
|
|
|
|||```sh
|
2021-03-15 20:46:41 +03:00
|
|
|
|||runtests <path to executable under test> [--timing] [--interactive] [--cg CODEGEN] [--threads N] [--only [NAMES]]
|
2020-10-19 11:26:23 +03:00
|
|
|
|||```
|
|
|
|
|||
|
|
|
|
||| assuming that the test runner is compiled to an executable named `runtests`.
|
|
|
|
|
|
|
|
module Test.Golden
|
|
|
|
|
|
|
|
import Data.Maybe
|
|
|
|
import Data.List
|
|
|
|
import Data.List1
|
|
|
|
import Data.Strings
|
|
|
|
|
|
|
|
import System
|
|
|
|
import System.Clock
|
|
|
|
import System.Directory
|
|
|
|
import System.File
|
2020-12-07 23:06:19 +03:00
|
|
|
import System.Future
|
2020-10-19 11:26:23 +03:00
|
|
|
import System.Info
|
|
|
|
import System.Path
|
|
|
|
|
|
|
|
-- [ Options ]
|
|
|
|
|
|
|
|
||| Options for the test driver.
|
|
|
|
public export
|
|
|
|
record Options where
|
|
|
|
constructor MkOptions
|
|
|
|
||| Name of the idris2 executable
|
|
|
|
exeUnderTest : String
|
|
|
|
||| Which codegen should we use?
|
|
|
|
codegen : Maybe String
|
|
|
|
||| Should we only run some specific cases?
|
|
|
|
onlyNames : List String
|
|
|
|
||| Should we run the test suite interactively?
|
|
|
|
interactive : Bool
|
|
|
|
||| Should we time and display the tests
|
|
|
|
timing : Bool
|
2021-03-15 20:46:41 +03:00
|
|
|
||| How many threads should we use?
|
|
|
|
threads : Nat
|
|
|
|
|
|
|
|
export
|
|
|
|
initOptions : String -> Options
|
|
|
|
initOptions exe
|
|
|
|
= MkOptions exe
|
|
|
|
Nothing
|
|
|
|
[]
|
|
|
|
False
|
|
|
|
False
|
|
|
|
1
|
2020-10-19 11:26:23 +03:00
|
|
|
|
|
|
|
export
|
|
|
|
usage : String -> String
|
2021-03-15 20:46:41 +03:00
|
|
|
usage exe = unwords
|
|
|
|
["Usage:", exe
|
|
|
|
, "runtests <path>"
|
|
|
|
, "[--timing]"
|
|
|
|
, "[--interactive]"
|
|
|
|
, "[--cg CODEGEN]"
|
|
|
|
, "[--threads N]"
|
|
|
|
, "[--only [NAMES]]"
|
|
|
|
]
|
2020-10-19 11:26:23 +03:00
|
|
|
|
|
|
|
||| Process the command line options.
|
|
|
|
export
|
|
|
|
options : List String -> Maybe Options
|
|
|
|
options args = case args of
|
2021-03-15 20:46:41 +03:00
|
|
|
(_ :: exe :: rest) => go rest (initOptions exe)
|
2020-10-19 11:26:23 +03:00
|
|
|
_ => Nothing
|
|
|
|
|
|
|
|
where
|
|
|
|
|
|
|
|
go : List String -> Options -> Maybe Options
|
|
|
|
go rest opts = case rest of
|
2021-03-15 20:46:41 +03:00
|
|
|
[] => pure opts
|
|
|
|
("--timing" :: xs) => go xs (record { timing = True} opts)
|
|
|
|
("--interactive" :: xs) => go xs (record { interactive = True } opts)
|
|
|
|
("--cg" :: cg :: xs) => go xs (record { codegen = Just cg } opts)
|
|
|
|
("--threads" :: n :: xs) => do let pos : Nat = !(parsePositive n)
|
|
|
|
go xs (record { threads = pos } opts)
|
|
|
|
("--only" :: xs) => pure $ record { onlyNames = xs } opts
|
2020-10-19 11:26:23 +03:00
|
|
|
_ => Nothing
|
|
|
|
|
|
|
|
-- [ Core ]
|
|
|
|
|
|
|
|
export
|
|
|
|
fail : String -> IO ()
|
|
|
|
fail err
|
|
|
|
= do putStrLn err
|
|
|
|
exitWith (ExitFailure 1)
|
|
|
|
|
|
|
|
|
|
|
|
||| Normalise strings between different OS.
|
|
|
|
|||
|
|
|
|
||| on Windows, we just ignore backslashes and slashes when comparing,
|
|
|
|
||| similarity up to that is good enough. Leave errors that depend
|
|
|
|
||| on the confusion of slashes and backslashes to unix machines.
|
|
|
|
normalize : String -> String
|
|
|
|
normalize str =
|
|
|
|
if isWindows
|
|
|
|
then pack $ filter (\ch => ch /= '/' && ch /= '\\') (unpack str)
|
|
|
|
else str
|
|
|
|
|
|
|
|
||| Run the specified Golden test with the supplied options.
|
|
|
|
|||
|
|
|
|
||| See the module documentation for more information.
|
|
|
|
|||
|
2021-03-09 21:27:05 +03:00
|
|
|
||| @testPath the directory that contains the test.
|
2020-10-19 11:26:23 +03:00
|
|
|
export
|
2020-12-07 23:06:19 +03:00
|
|
|
runTest : Options -> String -> IO (Future Bool)
|
|
|
|
runTest opts testPath = forkIO $ do
|
|
|
|
start <- clockTime Thread
|
|
|
|
let cg = case codegen opts of
|
|
|
|
Nothing => ""
|
|
|
|
Just cg => "env IDRIS2_TESTS_CG=" ++ cg ++ " "
|
2021-02-24 14:07:16 +03:00
|
|
|
ignore $ system $ "cd " ++ testPath ++ " && " ++
|
2020-12-07 23:06:19 +03:00
|
|
|
cg ++ "sh ./run " ++ exeUnderTest opts ++ " | tr -d '\\r' > output"
|
|
|
|
end <- clockTime Thread
|
|
|
|
|
|
|
|
Right out <- readFile $ testPath ++ "/output"
|
|
|
|
| Left err => do print err
|
|
|
|
pure False
|
|
|
|
|
|
|
|
Right exp <- readFile $ testPath ++ "/expected"
|
|
|
|
| Left FileNotFound => do
|
|
|
|
if interactive opts
|
|
|
|
then mayOverwrite Nothing out
|
|
|
|
else print FileNotFound
|
|
|
|
pure False
|
|
|
|
| Left err => do print err
|
|
|
|
pure False
|
|
|
|
|
|
|
|
let result = normalize out == normalize exp
|
|
|
|
let time = timeDifference end start
|
|
|
|
|
|
|
|
if result
|
|
|
|
then printTiming (timing opts) time $ testPath ++ ": success"
|
|
|
|
else do
|
|
|
|
printTiming (timing opts) time $ testPath ++ ": FAILURE"
|
|
|
|
if interactive opts
|
|
|
|
then mayOverwrite (Just exp) out
|
|
|
|
else putStrLn . unlines $ expVsOut exp out
|
|
|
|
|
|
|
|
pure result
|
|
|
|
|
|
|
|
where
|
|
|
|
getAnswer : IO Bool
|
|
|
|
getAnswer = do
|
|
|
|
str <- getLine
|
|
|
|
case str of
|
|
|
|
"y" => pure True
|
|
|
|
"n" => pure False
|
|
|
|
_ => do putStrLn "Invalid Answer."
|
|
|
|
getAnswer
|
|
|
|
|
|
|
|
expVsOut : String -> String -> List String
|
|
|
|
expVsOut exp out = ["Expected:", exp, "Given:", out]
|
|
|
|
|
|
|
|
mayOverwrite : Maybe String -> String -> IO ()
|
|
|
|
mayOverwrite mexp out = do
|
|
|
|
case mexp of
|
|
|
|
Nothing => putStr $ unlines
|
|
|
|
[ "Golden value missing. I computed the following result:"
|
|
|
|
, out
|
|
|
|
, "Accept new golden value? [yn]"
|
|
|
|
]
|
|
|
|
Just exp => do
|
|
|
|
code <- system $ "git diff --no-index --exit-code --word-diff=color " ++
|
|
|
|
testPath ++ "/expected " ++ testPath ++ "/output"
|
|
|
|
putStrLn . unlines $
|
|
|
|
["Golden value differs from actual value."] ++
|
|
|
|
(if (code < 0) then expVsOut exp out else []) ++
|
|
|
|
["Accept actual value as new golden value? [yn]"]
|
2021-02-22 06:14:09 +03:00
|
|
|
b <- getAnswer
|
|
|
|
when b $ do Right _ <- writeFile (testPath ++ "/expected") out
|
|
|
|
| Left err => print err
|
|
|
|
pure ()
|
2020-10-19 11:26:23 +03:00
|
|
|
|
2020-12-07 23:06:19 +03:00
|
|
|
printTiming : Bool -> Clock type -> String -> IO ()
|
|
|
|
printTiming True clock msg = putStrLn (unwords [msg, show clock])
|
|
|
|
printTiming False _ msg = putStrLn msg
|
2020-10-19 11:26:23 +03:00
|
|
|
|
|
|
|
||| Find the first occurrence of an executable on `PATH`.
|
|
|
|
export
|
|
|
|
pathLookup : List String -> IO (Maybe String)
|
|
|
|
pathLookup names = do
|
|
|
|
path <- getEnv "PATH"
|
2021-04-08 23:11:34 +03:00
|
|
|
let extensions = if isWindows then [".exe", ".cmd", ".bat", ""] else [""]
|
2020-10-19 11:26:23 +03:00
|
|
|
let pathList = forget $ split (== pathSeparator) $ fromMaybe "/usr/bin:/usr/local/bin" path
|
2021-04-08 23:11:34 +03:00
|
|
|
let candidates = [p ++ "/" ++ x ++ y | p <- pathList,
|
|
|
|
x <- names,
|
|
|
|
y <- extensions]
|
2020-10-19 11:26:23 +03:00
|
|
|
firstExists candidates
|
|
|
|
|
|
|
|
|
|
|
|
||| Some test may involve Idris' backends and have requirements.
|
|
|
|
||| We define here the ones supported by Idris
|
|
|
|
public export
|
2021-03-09 21:27:05 +03:00
|
|
|
data Requirement = C | Chez | Node | Racket | Gambit
|
2020-10-19 11:26:23 +03:00
|
|
|
|
|
|
|
export
|
|
|
|
Show Requirement where
|
2021-03-09 21:27:05 +03:00
|
|
|
show C = "C"
|
2020-10-19 11:26:23 +03:00
|
|
|
show Chez = "Chez"
|
|
|
|
show Node = "node"
|
|
|
|
show Racket = "racket"
|
2021-03-09 21:27:05 +03:00
|
|
|
show Gambit = "gambit"
|
2020-10-19 11:26:23 +03:00
|
|
|
|
|
|
|
export
|
|
|
|
checkRequirement : Requirement -> IO (Maybe String)
|
|
|
|
checkRequirement req
|
2021-04-12 15:52:04 +03:00
|
|
|
= if platformSupport req
|
|
|
|
then do let (envvar, paths) = requirement req
|
|
|
|
Just exec <- getEnv envvar | Nothing => pathLookup paths
|
|
|
|
pure (Just exec)
|
|
|
|
else pure Nothing
|
2020-10-19 11:26:23 +03:00
|
|
|
where
|
|
|
|
requirement : Requirement -> (String, List String)
|
2021-03-09 21:27:05 +03:00
|
|
|
requirement C = ("CC", ["cc"])
|
2021-04-08 23:11:34 +03:00
|
|
|
requirement Chez = ("CHEZ", ["chez", "chezscheme9.5", "scheme"])
|
2020-10-19 11:26:23 +03:00
|
|
|
requirement Node = ("NODE", ["node"])
|
|
|
|
requirement Racket = ("RACKET", ["racket"])
|
2021-03-09 21:27:05 +03:00
|
|
|
requirement Gambit = ("GAMBIT", ["gsc"])
|
2021-04-12 15:52:04 +03:00
|
|
|
platformSupport : Requirement -> Bool
|
|
|
|
platformSupport C = not isWindows
|
|
|
|
platformSupport Racket = not isWindows
|
|
|
|
platformSupport _ = True
|
2020-10-19 11:26:23 +03:00
|
|
|
|
|
|
|
export
|
|
|
|
findCG : IO (Maybe String)
|
|
|
|
findCG
|
|
|
|
= do Nothing <- getEnv "IDRIS2_TESTS_CG" | p => pure p
|
|
|
|
Nothing <- checkRequirement Chez | p => pure (Just "chez")
|
|
|
|
Nothing <- checkRequirement Node | p => pure (Just "node")
|
|
|
|
Nothing <- checkRequirement Racket | p => pure (Just "racket")
|
2021-03-09 21:27:05 +03:00
|
|
|
Nothing <- checkRequirement Gambit | p => pure (Just "gsc")
|
2021-02-02 18:31:30 +03:00
|
|
|
Nothing <- checkRequirement C | p => pure (Just "refc")
|
2020-10-19 11:26:23 +03:00
|
|
|
pure Nothing
|
|
|
|
|
|
|
|
||| A test pool is characterised by
|
|
|
|
||| + a list of requirement
|
|
|
|
||| + and a list of directory paths
|
|
|
|
public export
|
|
|
|
record TestPool where
|
|
|
|
constructor MkTestPool
|
|
|
|
constraints : List Requirement
|
|
|
|
testCases : List String
|
|
|
|
|
|
|
|
||| Only keep the tests that have been asked for
|
|
|
|
export
|
|
|
|
filterTests : Options -> List String -> List String
|
|
|
|
filterTests opts = case onlyNames opts of
|
|
|
|
[] => id
|
|
|
|
xs => filter (\ name => any (`isInfixOf` name) xs)
|
|
|
|
|
|
|
|
||| A runner for a test pool
|
|
|
|
export
|
2020-12-07 23:06:19 +03:00
|
|
|
poolRunner : Options -> TestPool -> IO (List Bool)
|
|
|
|
poolRunner opts pool
|
2020-10-19 11:26:23 +03:00
|
|
|
= do -- check that we indeed want to run some of these tests
|
|
|
|
let tests = filterTests opts (testCases pool)
|
|
|
|
let (_ :: _) = tests
|
|
|
|
| [] => pure []
|
|
|
|
-- if so make sure the constraints are satisfied
|
|
|
|
cs <- for (constraints pool) $ \ req => do
|
|
|
|
mfp <- checkRequirement req
|
|
|
|
putStrLn $ case mfp of
|
|
|
|
Nothing => show req ++ " not found"
|
|
|
|
Just fp => "Found " ++ show req ++ " at " ++ fp
|
|
|
|
pure mfp
|
|
|
|
let Just _ = the (Maybe (List String)) (sequence cs)
|
|
|
|
| Nothing => pure []
|
|
|
|
-- if so run them all!
|
2021-03-15 20:46:41 +03:00
|
|
|
loop [] tests
|
|
|
|
|
|
|
|
where
|
|
|
|
loop : List (List Bool) -> List String -> IO (List Bool)
|
|
|
|
loop acc [] = pure (concat $ reverse acc)
|
|
|
|
loop acc tests
|
|
|
|
= do let (now, later) = splitAt opts.threads tests
|
|
|
|
bs <- map await <$> traverse (runTest opts) now
|
|
|
|
loop (bs :: acc) later
|
2020-10-19 11:26:23 +03:00
|
|
|
|
2021-03-09 21:27:05 +03:00
|
|
|
|
2020-10-19 11:26:23 +03:00
|
|
|
||| A runner for a whole test suite
|
|
|
|
export
|
|
|
|
runner : List TestPool -> IO ()
|
|
|
|
runner tests
|
|
|
|
= do args <- getArgs
|
|
|
|
let (Just opts) = options args
|
|
|
|
| _ => do print args
|
|
|
|
putStrLn (usage "runtests")
|
|
|
|
-- if no CG has been set, find a sensible default based on what is available
|
|
|
|
opts <- case codegen opts of
|
|
|
|
Nothing => pure $ record { codegen = !findCG } opts
|
|
|
|
Just _ => pure opts
|
|
|
|
-- run the tests
|
2020-12-07 23:06:19 +03:00
|
|
|
res <- concat <$> traverse (poolRunner opts) tests
|
2020-10-19 11:26:23 +03:00
|
|
|
putStrLn (show (length (filter id res)) ++ "/" ++ show (length res)
|
|
|
|
++ " tests successful")
|
|
|
|
if (any not res)
|
|
|
|
then exitWith (ExitFailure 1)
|
|
|
|
else exitWith ExitSuccess
|
|
|
|
|
|
|
|
-- [ EOF ]
|