EUnit examples: Cleaning up server processes

3 Apr 2023 16:33 erlang eunit

In several of the previous posts, for example Using ‘setup’, I’ve started a server in suite_setup/0 and needed to kill it in suite_cleanup/1. I showed a simple way to do that, but it’s not the best. Here’s a better way to do it.

To recap, I showed this:

suite_setup() ->
    {ok, Pid} = some_server:start_link(),
    Pid.

suite_cleanup(Pid) ->
    unlink(Pid),    % ...so we don't also get killed
    exit(Pid, kill),
    ok.

Because of the call to some_server:start_link/0, the server process is linked to the test process. Specifically: it’s linked to the test setup (and cleanup) process: the tests run in a separate process.

When cleaning up, we want to kill the server process, so we want to do something like this:

suite_cleanup(Pid) ->
    exit(Pid, kill),
    ok.

But killing the server process also kills us (because we’re linked to it).

There are a number of ways to deal with this. I showed the simplest: simply unlink from the server process first using unlink(Pid). Another way is to trap exits, as follows:

suite_cleanup(Pid) ->
    process_flag(trap_exit, true),
    exit(Pid, kill),
    ok.

If you do that, then – instead of killing the test process – the exit signal is converted into a message which can be ignored (and we do).

It’s still not perfect, however. Consider the case where our server has a name. For example:

-module(some_server).
-export([start_link/0]).
-export([init/1, handle_call/3, handle_info/2, handle_cast/2, terminate/2, code_change/3]).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

You’ll soon discover that you’ve got a race condition: the server isn’t stopped immediately, and you’ll get {error,{already_started,Pid}} errors in the next fixture setup.

To solve this, you’ll need to wait until the server has actually stopped. The following will help:

exit_and_wait(Pid, Reason) ->
    MRef = monitor(process, Pid),
    exit(Pid, Reason),
    receive
        {'DOWN', MRef, process, Pid, _Reason} ->
            ok
    end.

Use it as follows:

suite_cleanup(Pid) ->
    process_flag(trap_exit, true),
    exit_and_wait(Pid, kill),
    ok.