Files

4.4 KiB

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:

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:

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:

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