#+TITLE: Snabbkaffe
[[https://travis-ci.org/k32/snabbkaffe.svg?branch=master]]
* Introduction
This library provides functions for simple trace-based testing.
It works like this:
1) Programmer manually instruments the code with trace points
2) Testcases are split in two parts:
- *Run stage* where the program runs and emits event trace
- *Check stage* where trace is collected and validated against the
spec(s)
3) Trace points become ordinary log messages in the release build
This approach can be used in a component test involving an ensemble of
interacting processes. It has a few nice properties:
+ Checks can be separated from the program execution
+ Checks are independent from each other and fully composable
+ Trace contains complete history of the process execution, thus
making certain types of concurrency bugs, like livelocks, easier to
detect
* Usage
** Instrumenting the code
Code instrumentation is done manually by inserting =tp= macros at the
points of interest:
#+BEGIN_SRC erlang
?tp(my_server_state_change, #{old_state => foo, next_state => bar})
#+END_SRC
The above line of code, when compiled in test mode, emits an event of
/kind/ =my_server_state_change=, with some additional data specified
in the second argument. Any event has a /kind/, which is an atom
identifying type of the event. The second argument must be a map, and
it can contain any data.
In the release build this macro will become a [[https://github.com/tolbrino/hut/][hut]] log message with
=debug= level. One can as well tune trace point log level:
#+BEGIN_SRC erlang
?tp(notice, my_server_state_change, #{old_state => foo, next_state => bar})
#+END_SRC
** Collecting trace
Trace collector process must be started before running the test. Full
workflow looks like this:
#+BEGIN_SRC erlang
ok = snabbkaffe:start_trace(),
Return = RunYourCode(),
Trace = snabbkaffe:collect_trace(Timeout),
snabbkaffe_collector:stop(),
RunCheck1(Return, Trace),
RunCheck2(Return, Trace),
...
RunCheckN(Return, Trace).
#+END_SRC
Note that =collect_trace= function is destructive: it cleans event
buffer of the collector process. Its argument =Timeout= specifies how
long the call will wait after the last event is received. Setting this
parameter to a non-zero value is useful when /run stage/ starts some
asynchronous tasks. By default =Timeout= is 0.
** Checking traces
Trace is just a list of maps. Any standard function can work with
it. Nonetheless, /Snabbkaffe/ comes with a few useful macros for trace
analysis.
Select events of certain kind:
#+BEGIN_SRC erlang
?of_kind(foo, Trace)
#+END_SRC
Extract values of a certain event field(s) to a list:
#+BEGIN_SRC erlang
[1, 2] = ?projection(foo, [#{foo => 1, quux => 1}, #{foo => 2}]),
[{1, 1}, {2, 2}] = ?projection([foo, bar], [#{foo => 1, bar => 1}, #{foo => 2, bar => 2}])
#+END_SRC
Check that events occur in certain order, and throw an exception
otherwise:
#+BEGIN_SRC erlang
?causality( #{kind := foo, foo := _A}
, #{kind := bar, foo := _A}
, Trace
)
#+END_SRC
First argument of =?causality= macro matches an event that is called
the /cause/, and the second one matches so called /effect/. The above
example checks events of kind =bar= occur only after corresponding
events of kind =foo=.
This macro can be extended with a guard as well. Here guard checks
that two events actually make up a pair:
#+BEGIN_SRC erlang
?causality( #{kind := foo, foo := _A}
, #{kind := bar, foo := _B}
, _A + 1 =:= _B
, Trace
)
#+END_SRC
There is a version of the above macro that checks that all cause have
an effect:
#+BEGIN_SRC erlang
?strict_causality( #{kind := foo, foo := _A}
, #{kind := bar, foo := _A}
, Trace
)
#+END_SRC
Otherwise it works just like =?causality=.
Both =?causality= and =?strict_causality= are actually based on a more
powerful =?find_pairs= macro that is invoked like this:
#+BEGIN_SRC erlang
?find_pairs( Strict
, MatchCause
, MatchEffect
[, Guard]
, Trace
)
#+END_SRC
where =Strict= is a boolean that determines whever events that matched
as =Effect= may preceed their cause.
It returns a list of tuples of type =snabbkaffe:maybe_pair/0= that is
defined like that:
#+BEGIN_SRC erlang
-type maybe_pair() :: {pair, event(), event()}
| {singleton, event()}.
#+END_SRC
** Gathering it all together
=?check_trace= is a convenience wrapper that starts the trace
collector process, executes /run stage/, collects traces and then
executes /check stage/:
#+BEGIN_SRC erlang
?check_trace(begin
RunStage
end,
fun(ReturnValue, Trace) ->
CheckStage
end).
#+END_SRC
There is an extended version of this macro that takes additional
configuration as the first argument:
#+BEGIN_SRC erlang
?check_trace(#{timeout => Timeout, bucket => Bucket},
begin
RunStage
end,
fun(ReturnValue, Trace) ->
CheckStage
end).
#+END_SRC
or:
#+BEGIN_SRC erlang
?check_trace(Bucket,
begin
RunStage
end,
fun(ReturnValue, Trace) ->
CheckStage
end).
#+END_SRC
=Bucket= is a parameter used for benchmarking, more on that later.
** PropER integration
There are two useful macros for running /snabbkaffe/ together with [[https://proper-testing.github.io/][propER]]:
#+BEGIN_SRC erlang
?run_prop(Config, PROP)
#+END_SRC
where =PROP= is a proper spec that looks something like this:
#+BEGIN_SRC erlang
?FORALL({Ret, L}, {term(), list()},
?check_trace(
begin
RunStage
end,
fun(Return, Trace) ->
CheckStage
end))
#+END_SRC
There is another macro for the most common type of proper checks where
property is a simple =?FORALL= clause (like in the above example).
#+BEGIN_SRC erlang
?forall_trace({Ret, L}, {term(), list()},
begin
RunStage
end,
fun(Return, Trace) ->
CheckStage
end)
#+END_SRC
It combines =?FORALL= and =?run_prop=.
* Benchmarking
/Snabbkaffe/ automatically adds timestamps to the events, which makes
it a very unscientific benchmarking library.
There is a family of functions for reporting metric data.
Report a scalar metric called =my_metric1=:
#+BEGIN_SRC erlang
snabbkaffe:push_stat(my_metric1, 42),
snabbkaffe:push_stats(my_metric1, [42, 43, 42]),
%% Or even:
snabbkaffe:push_stats(my_metric1, [{pair, Event1, Event2}, {pair, Event3, Event4}, ...]),
#+END_SRC
Sometimes it is entertaining to see how metric value depends on the
size of the input data:
#+BEGIN_SRC erlang
snabbkaffe:push_stat(my_metric1, SizeOfData, 42),
snabbkaffe:push_stats(my_metric1, SizeOfData, [42, 43, 42])
#+END_SRC
Metrics can be reported by calling =snabbkaffe:analyze_statistics/0=
function that prints statistics for each reported metric, like in the
above example:
#+BEGIN_EXAMPLE
-------------------------------
foo_bar statistics:
[{min,9.999999999999999e-6},
{max,9.999999999999999e-6},
{arithmetic_mean,1.000000000000002e-5},
{geometric_mean,1.0000000000000123e-5},
{harmonic_mean,9.999999999999997e-6},
{median,9.999999999999999e-6},
{variance,4.174340734454146e-40},
{standard_deviation,2.0431203426264804e-20},
{skewness,-0.9850375627355535},
{kurtosis,-2.0199000000000003},
{percentile,[{50,9.999999999999999e-6},
{75,9.999999999999999e-6},
{90,9.999999999999999e-6},
{95,9.999999999999999e-6},
{99,9.999999999999999e-6},
{999,9.999999999999999e-6}]},
{histogram,[{9.999999999999999e-6,100}]},
{n,100}]
Statisitics of test
100.479087 ^ *
| *
| *
| *
|
| *
| *
|
| *
| *
| *
|
| *
| *
0 +--------------------------------------------------------------------->
0 1100
N min max avg
110 1.23984e+0 1.09774e+1 5.97581e+0
209 1.10121e+1 2.08884e+1 1.60011e+1
308 2.13004e+1 3.09071e+1 2.60224e+1
407 3.10212e+1 4.09074e+1 3.59904e+1
506 4.10095e+1 5.09904e+1 4.60456e+1
605 5.11370e+1 6.08557e+1 5.60354e+1
704 6.10493e+1 7.09071e+1 6.59642e+1
803 7.11237e+1 8.07733e+1 7.59588e+1
902 8.10944e+1 9.09766e+1 8.60179e+1
1001 9.10459e+1 9.99404e+1 9.54548e+1
1100 1.00004e+2 1.00939e+2 1.00479e+2
#+END_EXAMPLE
Note: =?run_prop= does this automatically.