80 | import Data.String.Extra
84 | import System.Directory
88 | import System.Concurrency
94 | record Options where
95 | constructor MkOptions
97 | exeUnderTest : String
99 | codegen : Maybe String
101 | onlyNames : Maybe (String -> Bool)
111 | failureFile : Maybe String
114 | initOptions : String -> Bool -> Options
115 | initOptions exe color
129 | , "runtests <path>"
131 | , "[--interactive]"
132 | , "[--[no-]color, --[no-]colour]"
135 | , "[--failure-file PATH]"
136 | , "[--only-file PATH]"
137 | , "[[--only|--except] [NAMES]]"
140 | optionsTestsFilter : List String -> List String -> Maybe (String -> Bool)
141 | optionsTestsFilter [] [] = Nothing
142 | optionsTestsFilter only except = Just $
\ name =>
143 | let onlyCheck = null only || any (`isInfixOf` name) only in
144 | let exceptCheck = all (not . (`isInfixOf` name)) except in
145 | onlyCheck && exceptCheck
149 | onlyFile : Maybe String
150 | onlyNames : List String
151 | exceptNames : List String
153 | initAcc = MkAcc Nothing [] []
157 | options : List String -> IO (Maybe Options)
158 | options args = case args of
159 | (_ :: exe :: rest) => mkOptions exe rest
164 | isFlag : String -> Bool
165 | isFlag str = "--" `isPrefixOf` str
167 | go : List String -> Acc -> Options -> Maybe (Acc, Options)
168 | go rest acc opts = case rest of
169 | [] => pure (acc, opts)
170 | ("--timing" :: xs) => go xs acc ({ timing := True} opts)
171 | ("--interactive" :: xs) => go xs acc ({ interactive := True } opts)
172 | ("--color" :: xs) => go xs acc ({ color := True } opts)
173 | ("--colour" :: xs) => go xs acc ({ color := True } opts)
174 | ("--no-color" :: xs) => go xs acc ({ color := False } opts)
175 | ("--no-colour" :: xs) => go xs acc ({ color := False } opts)
176 | ("--cg" :: cg :: xs) => go xs acc ({ codegen := Just cg } opts)
177 | ("--threads" :: n :: xs) => do let pos : Nat = !(parsePositive n)
178 | go xs acc ({ threads := pos } opts)
179 | ("--failure-file" :: p :: xs) => go xs acc ({ failureFile := Just p } opts)
180 | ("--only" :: p :: xs) =>
181 | ifThenElse (isFlag p)
182 | (go (p :: xs) acc opts)
183 | (go xs ({ onlyNames $= (words p ++) } acc) opts)
184 | ("--except" :: p :: xs) =>
185 | ifThenElse (isFlag p)
186 | (go (p :: xs) acc opts)
187 | (go xs ({ exceptNames $= (words p ++) } acc) opts)
188 | ("--only-file" :: p :: xs) => go xs ({ onlyFile := Just p } acc) opts
189 | [p] => do guard (p `elem` ["--only", "--except"])
195 | mkOptions : String -> List String -> IO (Maybe Options)
197 | = do color <- [ (isNothing noc) && tty | noc <- getEnv "NO_COLOR", tty <- isTTY stdout ]
198 | let Just (acc, opts) = go rest initAcc (initOptions exe color)
199 | | Nothing => pure Nothing
200 | extraOnlyNames <- the (IO (List String)) $
case acc.onlyFile of
202 | Just fp => do Right only <- readFile fp
203 | | Left err => die (show err)
205 | pure $
Just $
{ onlyNames := optionsTestsFilter (extraOnlyNames ++ acc.onlyNames) acc.exceptNames } opts
212 | normalize : String -> String
215 | then pack $
filter (\ch => ch /= '/' && ch /= '\\') (unpack str)
221 | Result = Either String String
227 | runTest : Options -> (testPath : String) -> IO Result
228 | runTest opts testPath = do
229 | start <- clockTime UTC
230 | let cg = maybe "" (" --cg " ++) (codegen opts)
231 | let exe = "\"" ++ exeUnderTest opts ++ cg ++ "\""
232 | ignore $
system $
"cd " ++ escapeArg testPath ++ " && " ++
233 | "sh ./run " ++ exe ++ " | tr -d '\\r' > output"
234 | end <- clockTime UTC
236 | Right out <- readFile $
testPath ++ "/output"
237 | | Left err => do putStrLn $
(testPath ++ "/output") ++ ": " ++ show err
238 | pure (Left testPath)
240 | Right exp <- readFile $
testPath ++ "/expected"
241 | | Left FileNotFound => do
242 | if interactive opts
243 | then mayOverwrite Nothing out
244 | else putStrLn $
(testPath ++ "/expected") ++ ": " ++ show FileNotFound
245 | pure (Left testPath)
246 | | Left err => do putStrLn $
(testPath ++ "/expected") ++ ": " ++ show err
247 | pure (Left testPath)
249 | let result = normalize out == normalize exp
250 | let time = timeDifference end start
253 | then printTiming opts.timing time testPath $
maybeColored BrightGreen "success"
255 | printTiming opts.timing time testPath $
maybeColored BrightRed "FAILURE"
256 | if interactive opts
257 | then mayOverwrite (Just exp) out
258 | else putStr . unlines $
expVsOut exp out
260 | pure $
if result then Right testPath else Left testPath
263 | getAnswer : IO Bool
271 | _ => do putStrLn "Invalid answer."
274 | maybeColored : Color -> String -> String
275 | maybeColored c = if opts.color then show . colored c else id
277 | expVsOut : String -> String -> List String
278 | expVsOut exp out = ["Expected:", maybeColored Green exp, "Given:", maybeColored Red out]
280 | badSystemExitCode : Int -> Bool
281 | badSystemExitCode code = code < 0 || code == 127
283 | mayOverwrite : Maybe String -> String -> IO ()
284 | mayOverwrite mexp out = do
286 | Nothing => putStr $
unlines
287 | [ "Golden value missing. I computed the following result:"
288 | , maybeColored BrightBlue out
289 | , "Accept new golden value? [y/N]"
292 | code <- system $
"git diff --no-index --exit-code --word-diff-regex=. " ++
293 | (if opts.color then "--color " else "") ++
294 | escapeArg testPath ++ "/expected " ++ escapeArg testPath ++ "/output"
296 | ["Golden value differs from actual value."] ++
297 | (if badSystemExitCode code then expVsOut exp out else []) ++
298 | ["Accept actual value as new golden value? [y/N]"]
300 | when b $
do Right _ <- writeFile (testPath ++ "/expected") out
301 | | Left err => putStrLn $
(testPath ++ "/expected") ++ ": " ++ show err
304 | printTiming : Bool -> Clock type -> String -> String -> IO ()
305 | printTiming False _ path msg = putStrLn $
concat [path, ": ", msg]
306 | printTiming True clock path msg =
307 | let time = showTime 2 3 clock
313 | path = leftEllipsis (width `minus` (1 + msgl + length time)) "(...)" path
314 | spent = length time + length path + msgl
315 | pad = pack $
replicate (width `minus` spent) ' '
316 | in putStrLn $
concat [path, ": ", msg, pad, time]
320 | pathLookup : List String -> IO (Maybe String)
321 | pathLookup names = do
322 | path <- getEnv "PATH"
323 | let extensions = if isWindows then [".exe", ".cmd", ".bat", ""] else [""]
324 | let pathList = forget $
split (== pathSeparator) $
fromMaybe "/usr/bin:/usr/local/bin" path
325 | let candidates = [p ++ "/" ++ x ++ y | p <- pathList,
328 | firstExists candidates
334 | record Requirement where
337 | satisfy : IO (Maybe String)
340 | Show Requirement where
344 | Eq Requirement where
345 | (==) = (==) `on` name
350 | data BackendRequirement = ReqC | ReqChez | ReqNodeJS | ReqRacket | ReqGambit
353 | checkRequirement : BackendRequirement -> IO (Maybe String)
354 | checkRequirement req
355 | = if platformSupport req
356 | then do let (envvar, paths) = requirement req
357 | Just exec <- getEnv envvar | Nothing => pathLookup paths
361 | requirement : BackendRequirement -> (String, List String)
362 | requirement ReqC = ("CC", ["cc"])
363 | requirement ReqChez = ("CHEZ", ["chez", "chezscheme9.5", "chezscheme", "chez-scheme", "scheme"])
364 | requirement ReqNodeJS = ("NODE", ["node"])
365 | requirement ReqRacket = ("RACKET", ["racket"])
366 | requirement ReqGambit = ("GAMBIT", ["gsc"])
368 | platformSupport : BackendRequirement -> Bool
369 | platformSupport ReqC = not isWindows
370 | platformSupport ReqRacket = not isWindows
371 | platformSupport _ = True
375 | C = MkReq "refc" (checkRequirement ReqC)
379 | Chez = MkReq "chez" (checkRequirement ReqChez)
383 | Node = MkReq "node" (checkRequirement ReqNodeJS)
386 | Racket : Requirement
387 | Racket = MkReq "racket" (checkRequirement ReqRacket)
390 | Gambit : Requirement
391 | Gambit = MkReq "gambit" (checkRequirement ReqGambit)
394 | findCG : IO (Maybe String)
396 | = do Nothing <- getEnv "IDRIS2_TESTS_CG" | p => pure p
397 | Nothing <- checkRequirement ReqChez | p => pure (Just "chez")
398 | Nothing <- checkRequirement ReqNodeJS | p => pure (Just "node")
399 | Nothing <- checkRequirement ReqRacket | p => pure (Just "racket")
400 | Nothing <- checkRequirement ReqGambit | p => pure (Just "gsc")
401 | Nothing <- checkRequirement ReqC | p => pure (Just "refc")
416 | codegenRequirement : Codegen -> List Requirement
417 | codegenRequirement (Just r) = [r]
418 | codegenRequirement _ = []
426 | record TestPool where
427 | constructor MkTestPool
429 | constraints : List Requirement
431 | testCases : List String
433 | findTests : (String -> Bool) -> Codegen -> String -> String -> IO (List String)
434 | findTests pred codegen poolName dirName = do
435 | Right names <- listDir dirName
436 | | Left e => die $
"failed to list " ++ dirName ++ ": " ++ show e
437 | let names = [n | n <- names, pred n]
438 | let testNames = [dirName ++ "/" ++ n | n <- names]
442 | isTest : (path : String) -> IO Bool
443 | isTest path = exists (path ++ "/run")
445 | filter : (testPaths : List String) -> IO (List String)
446 | filter [] = pure []
448 | do rem <- filter ps
449 | case !(isTest p) of
450 | True => pure $
p :: rem
457 | (dirName : String) ->
458 | {default (const True) pred : String -> Bool} ->
459 | (poolName : String) ->
460 | {default [] requirements : List Requirement} ->
461 | {default Nothing codegen : Codegen} ->
463 | testsInDir dirName poolName = do
464 | testNames <- findTests pred codegen poolName dirName
465 | when (length testNames == 0) $
die $
"no tests found in " ++ dirName
466 | pure $
MkTestPool poolName requirements codegen testNames
470 | filterTests : Options -> List String -> List String
471 | filterTests opts = case onlyNames opts of
477 | record Summary where
478 | constructor MkSummary
479 | success : List String
480 | failure : List String
484 | initSummary : Summary
485 | initSummary = MkSummary [] []
489 | updateSummary : (newRes : Result) -> Summary -> Summary
490 | updateSummary newRes =
492 | Left l => { failure $= (l ::) }
493 | Right w => { success $= (w ::) }
497 | bulkUpdateSummary : (newRess : List Result) -> Summary -> Summary
498 | bulkUpdateSummary newRess =
499 | let (ls, ws) = partitionEithers newRess in
500 | { success $= (ws ++)
501 | , failure $= (ls ++)
505 | Semigroup Summary where
506 | MkSummary ws1 ls1 <+> MkSummary ws2 ls2
507 | = MkSummary (ws1 ++ ws2) (ls1 ++ ls2)
510 | Monoid Summary where
511 | neutral = initSummary
515 | data ThreadInstruction : Type where
517 | Run : (test : String) -> ThreadInstruction
519 | Stop : ThreadInstruction
528 | testSender : (testChan : Channel ThreadInstruction) -> (nThreads : Nat)
529 | -> (tests : List String) -> IO ()
530 | testSender testChan 0 [] = pure ()
531 | testSender testChan (S k) [] =
533 | do channelPut testChan Stop
534 | testSender testChan k []
535 | testSender testChan nThreads (test :: tests) =
536 | do channelPut testChan (Run test)
537 | testSender testChan nThreads tests
541 | data ThreadResult : Type where
543 | Res : (res : Result) -> ThreadResult
545 | Done : ThreadResult
558 | testReceiver : (resChan : Channel ThreadResult) -> (acc : Summary)
559 | -> (accChan : Channel Summary) -> (nThreads : Nat) -> IO ()
560 | testReceiver resChan acc accChan 0 = channelPut accChan acc
561 | testReceiver resChan acc accChan nThreads@(S k) =
562 | do (Res res) <- channelGet resChan
563 | | Done => testReceiver resChan acc accChan k
564 | testReceiver resChan (updateSummary res acc) accChan nThreads
571 | testThread : (opts : Options) -> (testChan : Channel ThreadInstruction)
572 | -> (resChan : Channel ThreadResult) -> IO ()
573 | testThread opts testChan resChan =
574 | do (Run test) <- channelGet testChan
575 | | Stop => channelPut resChan Done
576 | res <- runTest opts test
577 | channelPut resChan (Res res)
578 | testThread opts testChan resChan
587 | poolRunner : Options -> TestPool -> IO Summary
588 | poolRunner opts pool
590 | let tests = filterTests opts (testCases pool)
591 | let (_ :: _) = tests
592 | | [] => pure initSummary
594 | cs <- for (codegenRequirement pool.codegen ++ pool.constraints) $
\ req => do
596 | let msg = case mfp of
597 | Nothing => "✗ " ++ req.name ++ " not found"
598 | Just fp => "✓ Found " ++ req.name ++ " at " ++ fp
600 | let (cs, msgs) = unzip cs
602 | putStrLn (banner msgs)
604 | let Just _ = the (Maybe (List String)) (sequence cs)
605 | | Nothing => pure initSummary
608 | let opts = case pool.codegen of
609 | Nothing => { codegen := Nothing } opts
610 | Just cg => { codegen := Just cg.name } opts
614 | accChan <- makeChannel
615 | resChan <- makeChannel
616 | testChan <- makeChannel
620 | for_ (replicate opts.threads 0) $
\_ =>
621 | fork (testThread opts testChan resChan)
623 | senderTID <- fork $
testSender testChan opts.threads tests
625 | receiverTID <- fork $
testReceiver resChan initSummary accChan opts.threads
627 | acc <- channelGet accChan
632 | checkReq : {0 req : Type} -> Requirement -> IO (Maybe String, String)
635 | let msg = case mfp of
636 | Nothing => "✗ \{name req} not found"
637 | Just fp => "✓ Found \{name req} at \{fp}"
641 | separator = fastPack $
replicate 72 '-'
643 | banner : List String -> String
644 | banner msgs = fastUnlines
645 | $
[ "", separator, pool.poolName ]
650 | runnerWith : Options -> List TestPool -> IO ()
651 | runnerWith opts tests = do
654 | opts <- case codegen opts of
655 | Nothing => pure $
{ codegen := !findCG } opts
656 | Just _ => pure opts
659 | res <- concat <$> traverse (poolRunner opts) tests
662 | let nsucc = length res.success
663 | let nfail = length res.failure
664 | let ntotal = nsucc + nfail
665 | putStrLn (show nsucc ++ "/" ++ show ntotal ++ " tests successful")
668 | let list = fastUnlines res.failure
670 | do putStrLn "Failing tests:"
673 | whenJust opts.failureFile $
\ path =>
674 | do Right _ <- writeFile path list
675 | | Left err => die (show err)
685 | runner : List TestPool -> IO ()
687 | = do args <- getArgs
688 | Just opts <- options args
689 | | _ => do printLn args
691 | runnerWith opts tests