From cac3ab3a6bddd8b91bf17cd9e1b45950ab1a6c71 Mon Sep 17 00:00:00 2001 From: Paul Chiusano Date: Tue, 3 Jan 2017 13:30:53 -0500 Subject: [PATCH] some easy test polish, added docs --- .travis.yml | 4 +- development.markdown | 6 +- node/tests/Suite.hs | 4 +- node/unison-node.cabal | 3 +- shared/unison-shared.cabal | 3 +- yaks/easytest/README.markdown | 243 +++++++++++++++++++++++++++++++--- yaks/easytest/easytest.cabal | 11 ++ yaks/easytest/src/EasyTest.hs | 72 +++++++--- yaks/easytest/tests/Suite.hs | 7 + 9 files changed, 308 insertions(+), 45 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5eee2400e..60c5a8879 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,5 +14,7 @@ before_install: - stack setup script: - - stack --no-terminal --skip-ghc-check test unison-shared + - stack --no-terminal --skip-ghc-check build unison-shared + - stack --no-terminal --skip-ghc-check exec shared-tests - stack --no-terminal --skip-ghc-check test unison-node + - stack --no-terminal --skip-ghc-check exec node-tests diff --git a/development.markdown b/development.markdown index 6152956d5..f451d3226 100644 --- a/development.markdown +++ b/development.markdown @@ -2,10 +2,10 @@ These are commands that will likely be useful during development. For doing compilation you can do: - stack repl --test unison-shared - stack repl --test unison-node + stack repl unison-shared + stack repl unison-node -to launch a REPL with access to the tests in either the `shared` or `node` project. From here, do `Main.main` to run the tests (or `import Unison.Test.Interpreter as I` and then `I.main` to run a specific test), and `:r` for rapid recompile. +and select the `node-tests` or `shared-tests` executable to launch a REPL with access to the tests in either the `shared` or `node` project. From here, do `Main.main` to run the tests and `:r` for rapid recompile. To build/run the node container: diff --git a/node/tests/Suite.hs b/node/tests/Suite.hs index 251324a91..7634e99d8 100644 --- a/node/tests/Suite.hs +++ b/node/tests/Suite.hs @@ -1,9 +1,8 @@ {-# LANGUAGE CPP #-} module Main where -import System.Random import EasyTest -import Unison.Test.Util +-- import Unison.Test.Util import qualified Unison.Test.BlockStore.FileBlockStore as FBS #ifdef leveldb import qualified Unison.Test.BlockStore.LevelDbStore as LBS @@ -27,6 +26,7 @@ test = scope "unison-node" . tests $ , Html.test , Http.test , FBS.test + , ResourcePool.test #ifdef leveldb , LBS.test #endif diff --git a/node/unison-node.cabal b/node/unison-node.cabal index 6d004be6e..9749e46f1 100644 --- a/node/unison-node.cabal +++ b/node/unison-node.cabal @@ -323,8 +323,7 @@ executable codebase build-depends: exceptions, leveldb-haskell cpp-options: -Dleveldb -test-suite tests - type: exitcode-stdio-1.0 +executable node-tests main-is: Suite.hs ghc-options: -w -threaded -rtsopts -with-rtsopts=-N -v0 hs-source-dirs: tests diff --git a/shared/unison-shared.cabal b/shared/unison-shared.cabal index 180cfacca..db62b72ea 100644 --- a/shared/unison-shared.cabal +++ b/shared/unison-shared.cabal @@ -108,8 +108,7 @@ library if flag(quiet) ghc-options: -v0 -test-suite tests - type: exitcode-stdio-1.0 +executable shared-tests main-is: Suite.hs ghc-options: -w -threaded -rtsopts -with-rtsopts=-N -v0 hs-source-dirs: tests diff --git a/yaks/easytest/README.markdown b/yaks/easytest/README.markdown index df015ed4c..561cb8016 100644 --- a/yaks/easytest/README.markdown +++ b/yaks/easytest/README.markdown @@ -1,31 +1,53 @@ -EasyTest is a simple testing toolkit, meant to replace QuickCheck, SmallCheck, HUnit, Tasty, etc. Here's an example usage: +EasyTest is a simple testing toolkit, meant to replace most uses of QuickCheck, SmallCheck, HUnit, and frameworks like Tasty, etc. Here's an example usage: ```Haskell -import EasyTest -import Control.Monad -import Control.Applicative +module Main where -main = runOnly "addition" $ tests - [ expect (1 + 1 == 2) - , fork $ do +import EasyTest +import Control.Applicative +import Control.Monad + +suite :: Test () +suite = tests + [ scope "addition.ex1" $ expect (1 + 1 == 2) + , scope "addition.ex2" $ expect (2 + 3 == 5) + , scope "list.reversal" . fork $ do -- generate lists from size 0 to 10, of Ints in (0,43) -- shorthand: listsOf [0..10] (int' 0 43) ns <- [0..10] `forM` \n -> replicateM n (int' 0 43) ns `forM_` \ns -> expect (reverse (reverse ns) == ns) - , scope "addition" $ expect (3 + 3 == 6) + -- equivalent to `scope "addition.ex3"` + , scope "addition" . scope "ex3" $ expect (3 + 3 == 6) , scope "always passes" $ do note "I'm running this test, even though it always passes!" ok -- like `pure ()`, but records a success result , scope "failing test" $ crash "oh noes!!" ] + +-- NB: `run suite` would run all tests, but we only run +-- tests whose scopes are prefixed by "addition" +main = runOnly "addition" suite +``` + +This generates the output: + +``` +Randomness seed for this run is 5104092164859451056 +Raw test output to follow ... +------------------------------------------------------------ +OK addition.ex1 +OK addition.ex2 +OK addition.ex3 +------------------------------------------------------------ +✅ 3 tests passed, no failures! 👍 🎉 ``` The idea here is to write tests with ordinary Haskell code, with control flow explicit and under programmer control. Tests are values of type `Test a`, and `Test` forms a monad with access to: * repeatable randomness (the `random` and `random'` functions for random and bounded random values, or handy specialized `int`, `int'`, `double`, `double'`, etc) -* I/O (via `liftIO`) -* failure (via `crash`, which yields a stack trace!) +* I/O (via `liftIO` or `EasyTest.io`, which is an alias for `liftIO`) +* failure (via `crash`, which yields a stack trace, or `fail`, which does not) * logging (via `note`, `noteScoped`, or `note'`) -* hierarchically-named subcomputations which can be switched on and off (in the above code, `"always passes" and "failing test"`-scoped tests would not be run, and we could do `run` instead of `runOnly` if we wanted to run the whole suite) +* hierarchically-named subcomputations which can be switched on and off (in the above code, notice that only the tests scoped under `"addition"` are run, and we could do `run` instead of `runOnly` if we wanted to run the whole suite) * parallelism (note the `fork` which runs that subtree of the test suite in a parallel thread). * conjunction of tests via `MonadPlus` (the `<|>` operation runs both tests, even if the first test fails, and the `tests` function used above is just `msum`). @@ -44,14 +66,199 @@ ex = do ns `forM_` \ns -> expect (reverse (reverse ns) == ns) ``` -This library is opinionated and might not be for everyone. But here's some of my thinking in writing it: +This library is opinionated and might not be for everyone. If you're curious about any of the design decisions made, see [my rationale](#rationale) for writing it. -* Testing should uncomplicated, minimal friction, and ideally: FUN. -* A lot of testing frameworks are weirdly optimized for adding lots of diagnostic information up front, as if whatever diagnostic information you happen to think to capture will be exactly what is needed to fix whatever bugs your tests reveal. EasyTest takes the opposite approach: be lazy about adding diagnostics and labeling subexpressions, but make it trivial to reproduce failing tests without running your entire suite. If a test fails, you can easily rerun just that test, with the same random seed, and add whatever diagnostics or print statements you need to track down what's wrong. +### User guide + +The simplest tests are `ok`, `crash`, and `expect`: + +```Haskell +-- Record a success +ok :: Test () + +-- Record a failure +crash :: String -> Test a + +-- Record a success if `True`, otherwise record a failure +expect :: Bool -> Test () +``` + +NB: `fail` is equivalent to `crash`, but doesn't provide a stack trace on failure. + +We can lift I/O into `Test` using `io` (or `liftIO`, but I always forget where to import that from): + +``` +io :: IO a -> Test a +``` + +`Test` is also a `Monad`. Note that `return` and `pure` do not record a result. Use `ok`, `expect`, or `crash` for that purpose. + +We often want to label tests so we can see when they succeed or fail. For that we use `scope`: + +``` +-- | Label a test. Can be nested. A `'.'` is placed between nested +-- scopes, so `scope "foo" . scope "bar"` is equivalent to `scope "foo.bar"` +scope :: String -> Test a -> Test a +``` + +Here's an example usage, putting all these primitives together: + +```Haskell +module Main where + +import EasyTest (ok, scope, crash, expect, run) + +suite :: Test () +suite = do + ok + scope "test-crash" $ crash "oh noes!" + expect (1 + 1 == 2) + +main = run suite +``` + +This example is _sequencing_ the `ok`, `crash`, and `expect` monadically, so the test halts at the first failure. The output is: + +``` +Randomness seed for this run is 1830293182471192517 +Raw test output to follow ... +------------------------------------------------------------ +test-crash FAILURE oh noes! CallStack (from HasCallStack): + crash, called at /Users/pchiusano/code/easytest/tests/Suite.hs:10:24 in main:Main +OK +FAILED test-crash +------------------------------------------------------------ + + + 1 passed + 1 FAILED (failed scopes below) + "test-crash" + + To rerun with same random seed: + + EasyTest.rerun 1830293182471192517 + EasyTest.rerunOnly 1830293182471192517 "test-crash" + + +------------------------------------------------------------ +❌ +``` + +In the output (which is streamed to the console), we get a stack trace pointing to the line where `crash` was called (`..tests/Suite.hs:10:24`), information about failing tests, and instructions for rerunning the tests with an identical random seed (in this case, there's no randomness, so `rerun` would work fine, but if our test generated random data, we might want to rerun with the exact same random numbers). + +The last line of the output always indicates success or failure of the overall suite... and information about any failing tests is _immediately_ above that. You should NEVER have to scroll through a bunch of test output just to find out which tests actually failed! Also, the streaming output always has `OK` or `FAILED` as the _leftmost_ text for ease of scanning. + +If you try running a test suite that has no results recorded (like if you have a typo in a call to `runOnly`, or you forget to use `ok` or `expect` to record a test result), you'll see a warning like this: + +``` +😶 hmm ... no test results recorded +Tip: use `ok`, `expect`, or `crash` to record results +Tip: if running via `runOnly` or `rerunOnly`, check for typos +``` + +The various `run` functions (`run`, `runOnly`, `rerun`, and `rerunOnly`) all exit the process with a nonzero status in the event of a failure, so they can be used for continuous integration or test running tools that key off the process exit code to determine whether the suite succeeded or failed. For instance, here's the relevant portion of a typical cabal file: + +``` +-- Preferred way to run EasyTest-based test suite +executable runtests + main-is: NameOfYourTestSuite.hs + ghc-options: -w -threaded -rtsopts -with-rtsopts=-N -v0 + hs-source-dirs: tests + other-modules: + build-depends: + base, + easytest + +-- I really have no idea why you'd ever use this, unless you +-- really feel the need to run your tests via cabal's "test runner" +-- which "conveniently" hides all output unless you pass it some +-- random flag I never remember +test-suite tests + type: exitcode-stdio-1.0 + main-is: NameOfYourTestSuite.hs + ghc-options: -w -threaded -rtsopts -with-rtsopts=-N -v0 + hs-source-dirs: tests + other-modules: + build-depends: + base, + easytest +``` + +For tests that are logically separate, we usually combine them into a suite using `tests` (which is just `msum`), as in: + +```Haskell +suite = tests + [ scope "ex1" $ expect (1 + 1 == 2) + , scope "ex2" $ expect (2 + 2 == 4) ] + +-- equivalently +suite = + (scope "ex1" $ expect (1 + 1 == 2)) <|> + (scope "ex2" $ expect (2 + 2 == 4)) +``` + +Importantly, each branch of a `<|>` or `tests` gets its own copy of the randomness source, so even when branches of the test suite are switched on or off, the randomness received by a branch is the same. This is important for being able to quickly iterate on a test failure! + +Sometimes, tests take a while to run and we want to make use of parallelism. For that, use `EasyTest.fork` or `fork'`: + +```Haskell +-- | Run a test in a separate thread, not blocking for its result. +fork :: Test a -> Test () + +-- | Run a test in a separate thread, not blocking for its result, but +-- return a future which can be used to block on the result. +fork' :: Test a -> Test (Test a) +``` + +Note: There's no "framework global" parallelism configuration setting. + +We often want to generate random data for testing purposes: + +```Haskell +reverseTest :: Test () +reverseTest = scope "list reversal" $ do + nums <- listsOf [0..100] (int' 0 99) + nums `forM_` \nums -> expect (reverse (reverse nums) == nums) +``` + +Tip: generate your test cases in order of increasing size. If you get a failure, your test case is closer to "minimal". + +The above code generates lists of sizes `0` through `100`, consisting of `Int` values in the range `0` through `99`. `int' :: Int -> Int -> Test Int`, and there are analogous functions for `Double`, `Word`, etc. The most general functions are: + +```Haskell +random :: Random a => Test a +random' :: Random a => a -> a -> Test a +``` + +The functions `int`, `char`, `bool`, `double`, etc are just specialized aliases for `random`, and `int'`, `char'`, etc are just aliases for `random'`. The aliases are sometimes useful in situations where use of the generic `random` or `random'` would require type annotations. + +If our list reversal test failed, we might use `runOnly "list reversal"` or `rerunOnly "list reversal"` to rerun just that subtree of the test suite, and we might add some additional diagnostics to see what was going on: + +```Haskell +reverseTest :: Test () +reverseTest = scope "list reversal" $ do + nums <- listsOf [0..100] (int' 0 99) + nums `forM_` \nums -> do + note $ "nums: " ++ show nums + let r = reverse (reverse nums) + note $ "reverse (reverse nums): " ++ show r + expect (r == nums) +``` + +The idea is that these sorts of detailed diagnostics are added lazily (and temporarily) to find and fix failing tests. You can also add diagnostics via `io (putStrLn "blah")`, but if you have tests running in parallel this can sometimes get confusing. + +That's it! Just use ordinary monadic code to generate any testing data and to run your tests. + +### Why? + +Here's some of my thinking in the design of this library: + +* Testing should uncomplicated, minimal friction, and ideally: FUN. If I have to think too much or remember arbitrary framework magic, I get irritated. +* A lot of testing frameworks are weirdly optimized for adding lots of diagnostic information up front, as if whatever diagnostic information you happen to think to capture will be exactly what is needed to fix whatever bugs your tests reveal. In my experience this is almost never the case, so EasyTest takes the opposite approach: be EXTREMELY LAZY about adding diagnostics and labeling subexpressions, but make it trivial to reproduce failing tests without running your entire suite. If a test fails, you can easily rerun just that test, with the exact same random seed, and add whatever diagnostics or print statements you need to track down what's wrong. And EasyTest helpfully tells you how to do this rerunning whenever your tests fail, because otherwise I'd never remember. (Again: keep the friction LOW!) * Another reason not to add diagnostics up front: you avoid needing to remember two different versions of every function or operator (the one you use in your regular code, and the one you use with your testing "framework" to supply diagnostics). HUnit has operators named `(@=?)`, `(~?=)`, and a bunch of others for asserting equality with diagnostics on failure. QuickCheck has `(.&&.)` and `(.||.)`. Just... no. -* HUnit, QuickCheck, SmallCheck, Tasty, and whatever else are frameworks that hide control flow from the programmer and make some forms of control flow difficult or impossible to specify (for instance, you can't do I/O in your QuickCheck tests!). In contrast, EasyTest is just a single data type with a monadic API and a few helper functions. You assemble your tests using ordinary monadic code, and there is never any magic. Want to abstract over something? _Write a regular function._ Need to generate some testing data? Write regular functions. -* "How do I modify the number of generated test cases for QuickCheck for just one of my properties?" Or control the maximum size for these `Gen` and `Arbitrary` types? Some arbitrary "configuration setting" that you have to look up every time. No thanks. -* Global configuration settings are evil. I want fine-grained control over the amount of parallelism, test case sizes, and so on. +* HUnit, QuickCheck, SmallCheck, Tasty, and whatever else are frameworks that hide control flow from the programmer and make some forms of control flow difficult or impossible to specify (for instance, you can't do I/O in your regular QuickCheck tests... unless you use `Test.QuickCheck.Monadic`, which has yet another API you have to learn!). In contrast, EasyTest is just a single data type with a monadic API and a few helper functions. You assemble your tests using ordinary monadic code, and there is never any magic. Want to abstract over something? _Write a regular function._ Need to generate some testing data? Write regular functions. +* "How do I modify the number of generated test cases for QuickCheck for just one of my properties?" Or control the maximum size for these `Gen` and `Arbitrary` types? Some arbitrary "configuration setting" that you have to look up every time. No thanks! +* Seriously, global configuration settings are evil! I want fine-grained control over the amount of parallelism, test case sizes, and so on. And if I find I'm repeating myself a lot... I'll _introduce a regular Haskell variable or function!_. DOWN WITH FRAMEWORKS AND THEIR DAMN CONFIGURATION SETTINGS!! * Most of the functionality of QuickCheck is overkill anyway! There's no need for `Arbitrary` instances (explicit generation is totally fine, and even preferred in most cases), `Coarbitrary` (cute, but not useful when the HOF you are testing is parametric), or shrinking (just generate your test cases in increasing sizes, and your first failure will be the smallest!). -I hope that you enjoy the library and that it proves useful. +I hope that you enjoy writing your tests with this library! diff --git a/yaks/easytest/easytest.cabal b/yaks/easytest/easytest.cabal index d152691d9..154afc4dd 100644 --- a/yaks/easytest/easytest.cabal +++ b/yaks/easytest/easytest.cabal @@ -52,6 +52,17 @@ library if flag(quiet) ghc-options: -v0 +-- Preferred way to run EasyTest-based test suite +executable runtests + main-is: Suite.hs + ghc-options: -w -threaded -rtsopts -with-rtsopts=-N -v0 + hs-source-dirs: tests + other-modules: + build-depends: + base, + easytest + +-- I really have no idea why you'd ever use this, just use an executable as above test-suite tests type: exitcode-stdio-1.0 main-is: Suite.hs diff --git a/yaks/easytest/src/EasyTest.hs b/yaks/easytest/src/EasyTest.hs index ba4e70ee4..7a0e2518c 100644 --- a/yaks/easytest/src/EasyTest.hs +++ b/yaks/easytest/src/EasyTest.hs @@ -65,12 +65,14 @@ expectRight (Right a) = ok >> pure a tests :: [Test ()] -> Test () tests = msum +-- | Run all tests whose scope starts with the given prefix runOnly :: String -> Test a -> IO () runOnly prefix t = do logger <- atomicLogger seed <- abs <$> Random.randomIO :: IO Int run' seed logger prefix t +-- | Run all tests with the given seed and whose scope starts with the given prefix rerunOnly :: Int -> String -> Test a -> IO () rerunOnly seed prefix t = do logger <- atomicLogger @@ -87,7 +89,7 @@ run' seed note allow (Test t) = do let !rng = Random.mkStdGen seed resultsQ <- atomically (newTBQueue 50) rngVar <- newTVarIO rng - note $ "Random number generation (RNG) state for this run is " ++ show seed ++ "" + note $ "Randomness seed for this run is " ++ show seed ++ "" results <- atomically $ newTVar Map.empty rs <- A.async . forever $ do -- note, totally fine if this bombs once queue is empty @@ -97,7 +99,7 @@ run' seed note allow (Test t) = do resultsMap <- readTVarIO results case Map.findWithDefault Skipped msgs resultsMap of Skipped -> pure () - Passed n -> note $ "OK " ++ (if n == 0 then msgs else "(" ++ show n ++ ") " ++ msgs) + Passed n -> note $ "OK " ++ (if n <= 1 then msgs else "(" ++ show n ++ ") " ++ msgs) Failed -> note $ "FAILED " ++ msgs let line = "------------------------------------------------------------" note "Raw test output to follow ... " @@ -141,6 +143,8 @@ run' seed note allow (Test t) = do note "❌" exitWith (ExitFailure 1) +-- | Label a test. Can be nested. A `'.'` is placed between nested +-- scopes, so `scope "foo" . scope "bar"` is equivalent to `scope "foo.bar"` scope :: String -> Test a -> Test a scope msg (Test t) = Test $ do env <- ask @@ -149,15 +153,18 @@ scope msg (Test t) = Test $ do False -> putResult Skipped >> pure Nothing True -> liftIO $ runReaderT t (env { messages = messages', allow = drop (length msg + 1) (allow env) }) +-- | Log a message note :: String -> Test () note msg = do note_ <- asks note_ liftIO $ note_ msg pure () +-- | Log a showable value note' :: Show s => s -> Test () note' = note . show +-- | Generate a random value random :: Random a => Test a random = do rng <- asks rng @@ -167,6 +174,7 @@ random = do writeTVar rng rng1 pure a +-- | Generate a bounded random value. Inclusive on both sides. random' :: Random a => a -> a -> Test a random' lower upper = do rng <- asks rng @@ -176,39 +184,53 @@ random' lower upper = do writeTVar rng rng1 pure a -int :: Test Int -int = random - -char :: Test Char -char = random - -double :: Test Double -double = random - -word :: Test Word -word = random +bool :: Test Bool +bool = random word8 :: Test Word8 word8 = random +-- | Generate a random `Char` +char :: Test Char +char = random + +-- | Generate a random `Int` +int :: Test Int +int = random + +-- | Generate a random `Double` +double :: Test Double +double = random + +-- | Generate a random `Word` +word :: Test Word +word = random + +-- | Generate a random `Int` in the given range +-- Note: `int' 0 5` includes both `0` and `5` int' :: Int -> Int -> Test Int int' = random' +-- | Generate a random `Char` in the given range +-- Note: `char' 'a' 'z'` includes both `'a'` and `'z'`. char' :: Char -> Char -> Test Char char' = random' +-- | Generate a random `Double` in the given range +-- Note: `double' 0 1` includes both `0` and `1`. double' :: Double -> Double -> Test Double double' = random' +-- | Generate a random `Double` in the given range +-- Note: `word' 0 10` includes both `0` and `10`. word' :: Word -> Word -> Test Word word' = random' +-- | Generate a random `Double` in the given range +-- Note: `word8' 0 10` includes both `0` and `10`. word8' :: Word8 -> Word8 -> Test Word8 word8' = random' -bool :: Test Bool -bool = random - -- | Sample uniformly from the given list of possibilities pick :: [a] -> Test a pick as = let n = length as; ind = picker n as in do @@ -227,21 +249,28 @@ picker size as = go where rpicker = picker rsize r go i = if i < lsize then lpicker i else rpicker (i - lsize) +-- | Alias for `replicateM` listOf :: Int -> Test a -> Test [a] listOf = replicateM +-- | Generate a list of lists of the given sizes, +-- an alias for `sizes `forM` \n -> listOf n gen` listsOf :: [Int] -> Test a -> Test [[a]] listsOf sizes gen = sizes `forM` \n -> listOf n gen +-- | Alias for `liftA2 (,)`. pair :: Test a -> Test b -> Test (a,b) pair = liftA2 (,) +-- | Generate a `Data.Map k v` of the given size. mapOf :: Ord k => Int -> Test k -> Test v -> Test (Map k v) mapOf n k v = Map.fromList <$> listOf n (pair k v) +-- | Generate a `[Data.Map k v]` of the given sizes. mapsOf :: Ord k => [Int] -> Test k -> Test v -> Test [Map k v] mapsOf sizes k v = sizes `forM` \n -> mapOf n k v +-- | Catch all exceptions that could occur in the given `Test` wrap :: Test a -> Test a wrap (Test t) = Test $ do env <- ask @@ -256,6 +285,7 @@ runWrap env t = do pure Nothing Right a -> pure a +-- | A test with a setup and teardown using :: IO r -> (r -> IO ()) -> (r -> Test a) -> Test a using r cleanup use = Test $ do r <- liftIO r @@ -265,20 +295,25 @@ using r cleanup use = Test $ do liftIO (cleanup r) pure a +-- | The current scope currentScope :: Test String currentScope = asks messages +-- | Prepend the current scope to a logging message noteScoped :: String -> Test () noteScoped msg = do s <- currentScope - note (s ++ ": " ++ msg) + note (s ++ (if null s then "" else " ") ++ msg) +-- | Record a successful test at the current scope ok :: Test () ok = Test (Just <$> putResult (Passed 1)) +-- | Explicitly skip this test skip :: Test () skip = Test (Nothing <$ putResult Skipped) +-- | Record a failure at the current scope crash :: HasCallStack => String -> Test a crash msg = do let trace = callStack @@ -345,9 +380,12 @@ instance MonadPlus Test where mzero = empty mplus = (<|>) +-- | Run a test in a separate thread, not blocking for its result. fork :: Test a -> Test () fork t = void (fork' t) +-- | Run a test in a separate thread, return a future which can be used +-- to block on its result. fork' :: Test a -> Test (Test a) fork' (Test t) = do env <- ask diff --git a/yaks/easytest/tests/Suite.hs b/yaks/easytest/tests/Suite.hs index bbd80aacf..9e4bd3f4d 100644 --- a/yaks/easytest/tests/Suite.hs +++ b/yaks/easytest/tests/Suite.hs @@ -2,6 +2,7 @@ module Main where import EasyTest import Control.Applicative +import Control.Monad suite1 :: Test () suite1 = tests @@ -11,6 +12,11 @@ suite1 = tests , scope "b" . scope "c" . scope "d" $ ok , scope "c" ok ] +reverseTest :: Test () +reverseTest = scope "list reversal" $ do + nums <- listsOf [0..100] (int' 0 99) + nums `forM_` \nums -> expect (reverse (reverse nums) == nums) + main :: IO () main = do run suite1 @@ -18,3 +24,4 @@ main = do runOnly "b" suite1 runOnly "b" $ tests [suite1, scope "xyz" (crash "never run")] runOnly "b.c" $ tests [suite1, scope "b" (crash "never run")] + run reverseTest