Files
gm_ctflow/README.md

142 lines
4.4 KiB
Markdown

gm_ctflow
=====
A helper application for cross-testcase test flows in Common Test
Description
----
When testing complex systems it often feels convenient to organize test
sequences as a sequential group where a series of test cases perform a
flow of operations. A challenge may arise when the results from one
test case need to be reused by later cases - Common Test does cater for
this through the `save_config` return.
However, occasionally, one also needs to have bespoke servers running
for the duration of such a sequence, and this tends to lead to hacking
a plain process with a custom protocol.
This application attempts to make that a bit more structured.
In its initial version, there are a few different APIs:
* `gm_ctflow_fun`, where one can instantiate a stateful handler fun,
which can be called by test cases as needed.
* `gm_ctflow_worker`, a gen_server where the server logic is given
by a user-provided fun. The server is spawned under a
`simple_one_for_one` supervisor.
* `gm_ctflow_reg`, used to register and find `gm_ctflow_worker` processes.
Complies with the `via` addressing scheme for OTP behaviors.
* `gm_ctflow`, providing a shared dictionary, logging and status support.
It also exports `spawn/1` and `spawn_opt/2`, which work as expected, but
also register with Common Test to enable use of `ct:log/2`.
The application `gm_ctflow` is intended to be started in
`init_per_suite/1` and stopped in `end_per_suite/1`, or in
`init_per_group/2` / `end_per_group/2`.
Some examples from `test/gm_ctflow_SUITE.erl`:
```erlang
groups() ->
[ {statefuns, [sequence], [ init_counter
, incr
, check1 %% state = 1
, incr
, check2 %% state = 2
, incr
, check3 %% state = 3
]}
, {workers, [sequence], [ init_counter
, init_worker
, append
, checkl1 %% [A0]
, append
, checkl2 %% [A0, A1]
, append
, checkl3 %% [A0, A1, A2]
]}
].
```
Here, we interleave test cases with checks of the flow fun states.
This test suite does nothing but exercize the flow funs, but normally,
these funs would be used to e.g. spawn an endpoint needed for the tests,
or thread some state through a test sequence.
The `gm_ctflow` application is started and stopped per-group here:
```erlang
init_per_group(_Grp, Config) ->
ok = application:start(gm_ctflow, permanent),
Config.
end_per_group(_Grp, _Config) ->
ok = application:stop(gm_ctflow),
ok.
```
This resets the state and removes all helper processes for each group.
A simple way to keep track of the flow state:
```erlang
init_per_testcase(_Case, Config) ->
gm_ctflow:status(),
Config.
end_per_testcase(_Case, _Config) ->
gm_ctflow:status(),
ok.
```
This gives output like this, from the `gm_ctflow_SUITE:incr/1` testcase:
```
*** User 2026-05-30 13:06:02.679 ***🔗
== Summary for flow my_counter
= Worker State: - none -
= Fun State: 0
= State: - none -
= Log History:
2026-05-30 13:06:02.654:
New flow: my_counter
*** User 2026-05-30 13:06:02.679 ***🔗
my_counter[<0.332.0>]: F(incr, 0) -> {ok,1,1}
*** User 2026-05-30 13:06:02.679 ***🔗
== Summary for flow my_counter
= Worker State: - none -
= Fun State: 1
= State: - none -
= Log History:
2026-05-30 13:06:02.654:
New flow: my_counter
2026-05-30 13:06:02.679:
F(incr, 0) -> {ok,1,1}
```
Normally, of course, there would be more going on in the test, making the
bookends a bit less dominant, but here we see the initial state of the flow
`my_counter`: The "Fun state" is `0`, while there's no worker, and no shared state.
The log history shows one previous message, from instantiating the fun.
The next log output shows the effect of calling the fun. Log messages produced
with `gm_ctflow:ct_log/[2,3]` go both to `ct:log/2` and the log history. The
flow is derived automatically, if possible, if not provided.
In the ending `status` output, we see the accumulated history, with timestamps
to make it easier to find the context in CT or SUT logs.
Build
-----
$ rebar3 compile
Test
-----
$ rebar ct