(this message has also been sent to /r/haskell and haskell-cafe)

I’m pleased to announce a new super-major release of dejafu, a library for testing concurrent Haskell programs.

While there are breaking changes, common use-cases shouldn’t be affected too significantly (or not at all). There is a brief guide to the changes, and how to migrate if necessary, on the website.

What’s dejafu?

dejafu is a unit-testing library for concurrent Haskell programs. Tests are deterministic, and work by systematically exploring the possible schedules of your concurrency-using test case, allowing you to confidently check your threaded code.

HUnit and Tasty bindings are available.

dejafu requires your test case to be written against the MonadConc typeclass from the concurrency package. This is a necessity, dejafu cannot peek inside your IO or STM actions, so it needs to be able to plug in an alternative implementation of the concurrency primitives for testing. There is some guidance for how to switch from IO code to MonadConc code on the website.

If you really need IO, you can use MonadIO - but make sure it’s deterministic enough to not invalidate your tests!

Here’s a small example reproducing a deadlock found in an earlier version of the auto-update library:

> :{
autocheck $ do
  auto <- mkAutoUpdate defaultUpdateSettings
[fail] Successful
    [deadlock] S0--------S1-----------S0-
[fail] Deterministic
    [deadlock] S0--------S1-----------S0-

    () S0--------S1--------p0--

dejafu finds the deadlock, and gives a simplified execution trace for each distinct result. More in-depth traces showing exactly what each thread did are also available. This is using a version of auto-update modified to use the MonadConc typeclass. The source is in the dejafu testsuite.

What’s new?

The highlights for this release are setup actions, teardown actions, and invariants:

  • Setup actions are for things which are not really a part of your test case, but which are needed for it (for example, setting up a test distributed system). As dejafu can run a single test case many times, repeating this work can be a significant overhead. By defining this as a setup action, dejafu can “snapshot” the state at the end of the action, and efficiently reload it in subsequent executions of the same test.

  • Teardown actions are for things you want to run after your test case completes, in all cases, even if the test deadlocks (for example). As dejafu controls the concurrent execution of the test case, inspecting shared state is possible even if the test case fails to complete.

  • Invariants are effect-free atomically-checked conditions over shared state which must always hold. If an invariant throws an exception, the test case is aborted, and any teardown action run.

Here is an example of a setup action with an invariant:

> :{
autocheck $
  let setup = do
        var <- newEmptyMVar
        registerInvariant $ do
          value <- inspectMVar var
          when (value == Just 1) $
            throwM Overflow
        pure var
  in withSetup setup $ \var -> do
       fork $ putMVar var 0
       fork $ putMVar var 1
       tryReadMVar var
[fail] Successful
    [invariant failure] S0--P2-
[fail] Deterministic
    [invariant failure] S0--P2-

    Nothing S0----

    Just 0 S0--P1--S0--

In the [invariant failure] case, thread 2 is scheduled, writing the forbidden value “1” to the MVar, which terminates the test.

Here is an example of a setup action with a teardown action:

> :{
autocheck $
  let setup = newMVar ()
      teardown var (Right _) = show <$> tryReadMVar var
      teardown _   (Left  e) = pure (show e)
  in withSetupAndTeardown setup teardown $ \var -> do
       fork $ takeMVar var
       takeMVar var
[pass] Successful
[fail] Deterministic
    "Nothing" S0---

    "Deadlock" S0-P1--S0-

The teardown action can perform arbitrary concurrency effects, including inspecting any mutable state returned by the setup action.

Setup and teardown actions were previously available in a slightly different form as the dontCheck and subconcurrency functions, which have been removed (see the migration guide if you used these).