6

I wrote some eunit test on my gen_server:

-module(st_db_tests).
-include_lib("eunit/include/eunit.hrl").

main_test_() ->
    {foreach,
     fun setup/0,
     fun cleanup/1,
     [
      fun db_server_up/1
     ]}.

setup() -> 
    {ok,Pid} = st_db:start_link(), Pid.
cleanup(Pid) -> 
    gen_server:call(Pid, stop).

db_server_up(Pid) ->    
    ?_assertMatch({[{<<"couchdb">>,<<"Welcome">>},{<<"version">>, _}]},
                  gen_server:call(Pid, test)).

When I make the test I have this:

./rebar eunit suite=st_db_tests skip_deps=true
==> site_stater (eunit)
Compiled test/st_db_tests.erl

... loading stuff ...

=PROGRESS REPORT==== 27-Jun-2011::12:33:21 ===
          supervisor: {local,kernel_safe_sup}
             started: [{pid,<0.127.0>},
                       {name,inet_gethost_native_sup},
                       {mfargs,{inet_gethost_native,start_link,[]}},
                       {restart_type,temporary},
                       {shutdown,1000},
                       {child_type,worker}]
module 'st_db_tests'
*** context cleanup failed ***
::exit:{normal,{gen_server,call,[<0.99.0>,stop]}}
  in function gen_server:call/2


=======================================================
  Failed: 0.  Skipped: 0.  Passed: 1.

Seems like the test has passed, but there is en error in context cleanup, which is not right, right?)

How can I fix this?

PS: my gen_server

-module(st_db).

-behaviour(gen_server).
%% --------------------------------------------------------------------
%% Include files
%% --------------------------------------------------------------------

%% --------------------------------------------------------------------
%% External exports
-export([start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-record(state, {db_pid, couch_server_pid}).

%% ====================================================================
%% External functions
%% ====================================================================

%%--------------------------------------------------------------------
%% @doc Starts the server.
%%
%% @spec start_link() -> {ok, Pid}
%% where
%%  Pid = pid()
%% @end
%%--------------------------------------------------------------------
start_link() ->
    gen_server:start_link({global, st_db}, ?MODULE, ["localhost", 5984, "site_stater"], []).

%% ====================================================================
%% Server internal functions
%% ====================================================================

%% --------------------------------------------------------------------
%% Function: init/1
%% Description: Initiates the server
%% Returns: {ok, State}          |
%%          {ok, State, Timeout} |
%%          ignore               |
%%          {stop, Reason}
%% --------------------------------------------------------------------
init([Server, Port, DB]) ->
    couchbeam:start(),
    CouchServer = couchbeam:server_connection(Server, Port, "", []),
    {ok, CouchDB} = couchbeam:open_or_create_db(CouchServer, DB, []),
    {ok, #state{db_pid=CouchDB, couch_server_pid=CouchServer}}.


%% ====================================================================
%% DB manipulation functions
%% ====================================================================


%% --------------------------------------------------------------------
%% Function: handle_call/3
%% Description: Handling call messages
%% Returns: {reply, Reply, State}          |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
%% --------------------------------------------------------------------
handle_call (test, _From, #state{couch_server_pid = Couch_server_pid} = State) ->
    {ok, Version} = couchbeam:server_info(Couch_server_pid),
    {reply, Version, State};


handle_call(stop, _From, State) -> 
    {stop, normal, State}.

%% --------------------------------------------------------------------
%% Function: handle_cast/2
%% Description: Handling cast messages
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%% --------------------------------------------------------------------

handle_cast(_Msg, State) ->
    {noreply, State}.

%% --------------------------------------------------------------------
%% Function: handle_info/2
%% Description: Handling all non call/cast messages
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%% --------------------------------------------------------------------
handle_info(_Info, State) ->
    {noreply, State}.

%% --------------------------------------------------------------------
%% Function: terminate/2
%% Description: Shutdown the server
%% Returns: any (ignored by gen_server)
%% --------------------------------------------------------------------
terminate(_Reason, _State) ->
    ok.

%% --------------------------------------------------------------------
%% Func: code_change/3
%% Purpose: Convert process state when code is changed
%% Returns: {ok, NewState}
%% --------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.
Roberto Aloi
  • 30,570
  • 21
  • 75
  • 112
Dmitrii Dushkin
  • 3,070
  • 4
  • 26
  • 37

2 Answers2

8

In the handle_call/2 function, you're returning:

{stop, normal, State}

rather than something like:

{stop, normal, ok, State}

In other words, you're not replying to the call. The client then see the server terminating (with normal reason) and it cries.

Didn't try it, but this would be my first guess.

Roberto Aloi
  • 30,570
  • 21
  • 75
  • 112
  • whoa, that helped. But I didn't get it. Manual says "the gen_server calls handle_call(Request, From, State) which is expected to return a tuple {reply, Reply, State1}." There are 3 atoms, not 4, in feedback – Dmitrii Dushkin Jun 27 '11 at 12:38
  • 2
    But here you are using the `stop` tuple, which can have either 3 or 4 elements. – Adam Lindberg Jun 27 '11 at 12:42
5

It could be that the test finishes before the gen_server process has time to shut down. The gen_server is linked to the test process (because it is started with gen_server:start_link/4) and when the test finishes, the process is still running and is killed by EUnit.

Even if you return ok using {stop, normal, ok, State} as suggested by Roberto, the gen_server might be slower executing terminate/2 than the test takes clean up.

I usually use such a function in my tests or teardowns to wait for processes:

wait_for_exit(Pid) ->
    MRef = erlang:monitor(process, Pid),
    receive {'DOWN', MRef, _, _, _} -> ok end.

EUnit has a default timeout of 5000 ms that will be triggered if this function blocks for too long time.

Adam Lindberg
  • 16,447
  • 6
  • 65
  • 85
  • Uhm, I would imagine that the terminate is function is called "before" returning the reply to the client. EUnit shouldn't kill a process which is still executing (Dimitry is issuing a stop request using a synchronous call). Also, in this case looks like the terminate function isn't that complicated, so I doubt it could take that long. In any case, I'm curious now :) – Roberto Aloi Jun 27 '11 at 10:22
  • 1
    From the documentation: `If the function returns {stop,Reason,Reply,NewState}, Reply will be given back to From. [...] The gen_server will then call Module:terminate(Reason,NewState) and terminate.` Also, it doesn't matter if the terminate is simple, there can always be a race condition. EUnit will terminate the test process when the test code has finished executing. It doesn't care if there happens to be a linked process still alive somewhere. – Adam Lindberg Jun 27 '11 at 11:23
  • From the gen_server code (if I'm looking at the right spot, I should have more time for this): [...] {stop, Reason, Reply, NState} -> {'EXIT', R} = (catch terminate(Reason, Name, Msg, Mod, NState, [])), reply(From, Reply), exit(R) [...] – Roberto Aloi Jun 27 '11 at 12:01
  • 1
    @roberto: You are correct, it does indeed reply after `terminate/2` is called. However, during replying (and some internal `gen_server` debug code that's run afterwards), the test process might still exit. So the possibility for a race condition persists. – Adam Lindberg Jun 27 '11 at 12:08
  • 1
    Furthermore, I would argue that it is always good practice to monitor processes that are supposed to die during testing. It's the best type of assertion you can have for such a test case. – Adam Lindberg Jun 27 '11 at 12:09