mirror of
https://github.com/barrucadu/dejafu.git
synced 2024-11-26 20:30:30 +03:00
251 lines
8.7 KiB
ReStructuredText
251 lines
8.7 KiB
ReStructuredText
Unit Testing
|
|
============
|
|
|
|
Writing tests with Déjà Fu is a little different to traditional unit
|
|
testing, as your test case may have multiple results. A "test" is a
|
|
combination of your code, and a predicate which says something about
|
|
the set of allowed results.
|
|
|
|
Most tests will look something like this:
|
|
|
|
.. code-block:: haskell
|
|
|
|
dejafu "Assert the thing holds" myPredicate myAction
|
|
|
|
The ``dejafu`` function comes from ``Test.DejaFu``. Another useful
|
|
function is ``dejafuWithSettings``; see :ref:`settings`.
|
|
|
|
|
|
Actions
|
|
-------
|
|
|
|
An action is just something with the type ``MonadConc m => m a``, or
|
|
``(MonadConc m, MonadIO m) => m a`` for some ``a`` that your chosen
|
|
predicate can deal with.
|
|
|
|
For example, some users on Reddit found a couple of apparent bugs in
|
|
the :hackage:`auto-update` package a while ago (`thread here`__). As
|
|
the package is simple and self-contained, I translated it to the
|
|
``MonadConc`` abstraction and wrote a couple of tests to replicate the
|
|
bugs. Here they are:
|
|
|
|
.. code-block:: haskell
|
|
|
|
deadlocks :: MonadConc m => m ()
|
|
deadlocks = do
|
|
auto <- mkAutoUpdate defaultUpdateSettings
|
|
auto
|
|
|
|
nondeterministic :: forall m. MonadConc m => m Int
|
|
nondeterministic = do
|
|
var <- newIORef 0
|
|
let settings = (defaultUpdateSettings :: UpdateSettings m ())
|
|
{ updateAction = atomicModifyIORef var (\x -> (x+1, x)) }
|
|
auto <- mkAutoUpdate settings
|
|
auto
|
|
auto
|
|
|
|
.. __: https://www.reddit.com/r/haskell/comments/2i5d7m/updating_autoupdate/
|
|
|
|
These actions action could be tested with ``autocheck``, and the
|
|
issues would be revealed. The use of ``ScopedTypeVariables`` in the
|
|
second is an unfortunate example of what can happen when everything
|
|
becomes more polymorphic. But other than that, note how there is no
|
|
special mention of Déjà Fu in the actions: it's just normal concurrent
|
|
Haskell, simply written against a different interface.
|
|
|
|
The modified package is included :github:`in the test suite
|
|
<blob/2a15549d97c2fa12f5e8b92ab918fdb34da78281/dejafu-tests/Examples/AutoUpdate.hs>`,
|
|
if you want to see the full code. [#]_
|
|
|
|
.. [#] The predicates in dejafu-tests are a little confusing, as
|
|
they're the opposite of what you would normally write! These
|
|
predicates are checking that the bug is found, not that the
|
|
code is correct.
|
|
|
|
If the RTS supports bound threads (the ``-threaded`` flag was passed
|
|
to GHC when linking), then the main thread of an action given to Déjà
|
|
Fu will be bound, and further bound threads can be forked with the
|
|
``forkOS`` functions. If not, then attempting to fork a bound thread
|
|
will raise an error.
|
|
|
|
|
|
Conditions
|
|
----------
|
|
|
|
When a concurrent program of type ``MonadConc m => m a`` is executed,
|
|
it may produce a value of type ``a``, or it may experience a
|
|
**condition** such as deadlock.
|
|
|
|
A condition does not necessarily cause your test to fail. It's
|
|
important to be aware of what exactly your test is testing, to avoid
|
|
drawing the wrong conclusions from a passing (or failing) test.
|
|
|
|
|
|
Setup and Teardown
|
|
------------------
|
|
|
|
Because dejafu drives the execution of the program under test, there
|
|
are some tricks available to you which are not possible using normal
|
|
concurrent Haskell.
|
|
|
|
If your test does some set-up work which is required for your test to
|
|
work, but which is not the actual thing you are testing, you can
|
|
define that as a **setup action**:
|
|
|
|
.. code-block:: haskell
|
|
|
|
withSetup
|
|
:: Program Basic n x
|
|
-- ^ Setup action
|
|
-> (x -> Program Basic n a)
|
|
-- ^ Main program
|
|
-> Program (WithSetup x) n a
|
|
|
|
dejafu will save the state at the end of the setup action, and
|
|
efficiently restore that state in subsequent runs of the same test
|
|
with a different schedule. This can be much more efficient than
|
|
dejafu running the setup action normally every single time.
|
|
|
|
If you want to examine some state you created in your setup action
|
|
even if your actual test case deadlocks or something, you can define a
|
|
**teardown action**:
|
|
|
|
.. code-block:: haskell
|
|
|
|
withSetupAndTeardown
|
|
:: Program Basic n x
|
|
-- ^ Setup action
|
|
-> (x -> Either Condition y -> Program Basic n a)
|
|
-- ^ Teardown action
|
|
-> (x -> Program Basic n y)
|
|
-- ^ Main program
|
|
-> Program (WithSetupAndTeardown x y) n a
|
|
|
|
The teardown action is always executed.
|
|
|
|
Finally, if you want to ensure that some invariant holds over some
|
|
shared state, you can define invariants in the setup action, which are
|
|
checked atomically during the main action:
|
|
|
|
.. code-block:: haskell
|
|
|
|
-- slightly contrived example
|
|
let setup = do
|
|
var <- newEmptyMVar
|
|
registerInvariant $ do
|
|
value <- inspectMVar var
|
|
when (x == Just 1) (throwM Overflow)
|
|
pure var
|
|
in withSetup setup $ \var -> do
|
|
fork $ putMVar var 0
|
|
fork $ putMVar var 1
|
|
tryReadMVar var
|
|
|
|
If the main action violates the invariant, it is terminated with an
|
|
``InvariantFailure`` condition, and any teardown action is run.
|
|
|
|
|
|
Predicates
|
|
----------
|
|
|
|
There are a few predicates built in, and some helpers to define your
|
|
own.
|
|
|
|
.. csv-table::
|
|
:widths: 25, 75
|
|
|
|
``abortsNever``,"checks that the computation never aborts"
|
|
``abortsAlways``,"checks that the computation always aborts"
|
|
``abortsSometimes``,"checks that the computation aborts at least once"
|
|
|
|
An **abort** is where the scheduler chooses to terminate execution
|
|
early. If you see it, it probably means that a test didn't terminate
|
|
before it hit the execution length limit. Aborts are hidden unless
|
|
you use explicitly enable them, see :ref:`settings`.
|
|
|
|
.. csv-table::
|
|
:widths: 25, 75
|
|
|
|
``deadlocksNever``,"checks that the computation never deadlocks"
|
|
``deadlocksAlways``,"checks that the computation always deadlocks"
|
|
``deadlocksSometimes``,"checks that the computation deadlocks at least once"
|
|
|
|
**Deadlocking** is where every thread becomes blocked. This can be,
|
|
for example, if every thread is trying to read from an ``MVar`` that
|
|
has been emptied.
|
|
|
|
.. csv-table::
|
|
:widths: 25, 75
|
|
|
|
``exceptionsNever``,"checks that the main thread is never killed by an exception"
|
|
``exceptionsAlways``,"checks that the main thread is always killed by an exception"
|
|
``exceptionsSometimes``,"checks that the main thread is killed by an exception at least once"
|
|
|
|
An uncaught **exception** in the main thread kills the process. These
|
|
can be synchronous (thrown in the main thread) or asynchronous (thrown
|
|
to it from a different thread).
|
|
|
|
.. csv-table::
|
|
:widths: 25, 75
|
|
|
|
``alwaysSame``,"checks that the computation is deterministic and always produces a value"
|
|
``alwaysSameOn f``,"is like ``alwaysSame``, but transforms the results with ``f`` first"
|
|
``alwaysSameBy f``,"is like ``alwaysSame``, but uses ``f`` instead of ``(==)`` to compare"
|
|
``notAlwaysSame``,"checks that the computation is nondeterministic"
|
|
``notAlwaysSameOn f``,"is like ``notAlwaysSame``, but transforms the results with ``f`` first"
|
|
``notAlwaysSameBy f``,"is like ``notAlwaysSame``, but uses ``f`` instead of ``(==)`` to compare"
|
|
|
|
Checking for **determinism** will also find nondeterministic failures:
|
|
deadlocking (for instance) is still a result of a test!
|
|
|
|
.. csv-table::
|
|
:widths: 25, 75
|
|
|
|
``alwaysTrue p``,"checks that ``p`` is true for every result"
|
|
``somewhereTrue p``,"checks that ``p`` is true for at least one result"
|
|
|
|
These can be used to check custom predicates. For example, you might
|
|
want all your results to be less than five.
|
|
|
|
.. csv-table::
|
|
:widths: 25, 75
|
|
|
|
``gives xs``,"checks that the set of results is exactly ``xs`` (which may include conditions)"
|
|
``gives' xs``,"checks that the set of results is exactly ``xs`` (which may not include conditions)"
|
|
|
|
These let you say exactly what you want the results to be. Your test
|
|
will fail if it has any extra results, or misses a result.
|
|
|
|
You can check multiple predicates against the same collection of
|
|
results using the ``dejafus`` and ``dejafusWithSettings`` functions.
|
|
These avoid recomputing the results, and so may be faster than
|
|
multiple ``dejafu`` / ``dejafuWithSettings`` calls; see
|
|
:ref:`performance`.
|
|
|
|
|
|
Using HUnit and Tasty
|
|
---------------------
|
|
|
|
By itself, Déjà Fu has no framework in place for named test groups and
|
|
parallel execution or anything like that. It does one thing and does
|
|
it well, which is running test cases for concurrent programs.
|
|
:hackage:`HUnit` and :hackage:`tasty` integration is provided to get
|
|
more of the features you'd expect from a testing framework.
|
|
|
|
The integration is provided by the :hackage:`hunit-dejafu` and
|
|
:hackage:`tasty-dejafu` packages.
|
|
|
|
There's a simple naming convention used: the ``Test.DejaFu`` function
|
|
``dejafuFoo`` is wrapped in the appropriate way and exposed as
|
|
``testDejafuFoo`` from ``Test.HUnit.DejaFu`` and
|
|
``Test.Tasty.DejaFu``.
|
|
|
|
Our example from the start becomes:
|
|
|
|
.. code-block:: haskell
|
|
|
|
testDejafu "Assert the thing holds" myPredicate myAction
|
|
|
|
The ``autocheck`` function is exposed as ``testAuto``.
|