Every now and then, we run into situations where we need to test something that’s best broken up into a group of separate test cases, where we maintain state across cases, and some helper processes assist e.g. with reactivity in the background.
There are many issues that arise when trying to do this, so I decided to write a helper application for it. gm_ctflow allows you to use a global state dictionary, instantiate helper funs, and fire up gen_server processes which you control from the test suite, and from which you can do ct:log/2 logging. This detail alone is non-trivial, as it relies on undocumented APIs in Common Test. Better to keep such tricks in one place, I think.
As such ct:log/2 output, when coming from background processes gets scattered across test case logs, and sometimes can fall into the cracks between logs, gm_ctflow keeps a log history for each “flow”, and calling a status() function at the beginning and end of a testcase, helps illustrate what happened before, and during each given step.
Some very trivial examples of how it works can be found in the app’s own test suite. It shows how you can start the gm_ctflow app in init_per_group/2, stop it in end_per_group/2, and have a fresh environment for each test group.
Now, common_test does have some support for threading state through a sequence of test cases. The main overlap is where gm_ctflow offers gm_ctflow:put(Key, Value), gm_ctflow:get(Key), and gm_ctflow:erase(Key), but for those who prefer the {save_config, Cfg} returns, there is no conflict. The key is to evolve techniques for making the test flows understandable and debuggable. I personally think gm_ctflow helps with this.
I can give an example of what I’m working on.
In the gajumaru blockchain, we have a feature called witness-based finality, where transactions mature to finality within 2-4 minutes in a way that allows users to observe the path to persistence: basically, within seconds, you should see the transaction on-chain, and be able to see that witnessing is on-line, the network is healthy and the next keyblock will finalize the transaction’s parent keyblock. I’m adding HTTP 2 event stream support in an endpoint that waits for finality for a given transaction, and the simplest way to test it seemed to be to use it in the witnessing test suite. This particular test suite is designed as groups of test case sequences where each testcase builds on the previous. So the streaming test would need to run parallel with the existing tests.
start_request_worker(_Config) ->
Node = aecore_suite_utils:node_name(dev1),
Cfg = http_config(Node),
gm_ctflow_worker:start(http_client,
fun() ->
{ok, fun http_client_f/3, #{cfg => Cfg}}
end),
ok.
http_client_f(cast, {await_finality, TxHash}, #{cfg := Cfg} = St) ->
Opts = [ {stream, {self, once}}, {sync, false}, {stream_collector, {forward, self()}} ],
Addr = proplists:get_value(ext_http, Cfg),
TxHashEnc = gmser_api_encoder:encode(tx_hash, TxHash),
Path = unicode:characters_to_list(["transactions/", TxHashEnc, "/finality"]),
gm_ctflow:ct_log("Addr = ~p, Path = ~p, Opts = ~p", [Addr, Path, Opts]),
Me = self(),
{ReqPid, _} =
gm_ctflow:spawn_opt(
fun() ->
Res = aecore_suite_utils:http_request(
Addr, get,
#{path => Path,
options => Opts},
#{wait => true,
stream => true}),
Me ! {self(), http_response, Res}
end, [monitor]),
gm_ctflow:ct_log("ReqPid = ~p", [ReqPid]),
{noreply, St#{request_pid => ReqPid}};
http_client_f(info, Msg, St) ->
Msgs = maps:get(msgs, St, []),
{noreply, St#{msgs => Msgs ++ [Msg]}};
http_client_f({call, _From}, get_result, #{result := Result} = St) ->
{reply, Result, St}.
Of course, it gets complex in a hurry. I want a gen_server ( gm_ctflow_worker), which spawns a helper process that calls httpc_client then forwards the events to the gen_server, which collects them for later inspection.
That test case isn’t finished yet, as you can see, but I feel that the gm_ctflow app provides the support I need to get the final details in place.