aboutsummaryrefslogtreecommitdiffstats
path: root/lib/eunit/src/eunit_serial.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/eunit/src/eunit_serial.erl')
-rw-r--r--lib/eunit/src/eunit_serial.erl186
1 files changed, 186 insertions, 0 deletions
diff --git a/lib/eunit/src/eunit_serial.erl b/lib/eunit/src/eunit_serial.erl
new file mode 100644
index 0000000000..d9ccae86f9
--- /dev/null
+++ b/lib/eunit/src/eunit_serial.erl
@@ -0,0 +1,186 @@
+%% This library is free software; you can redistribute it and/or modify
+%% it under the terms of the GNU Lesser General Public License as
+%% published by the Free Software Foundation; either version 2 of the
+%% License, or (at your option) any later version.
+%%
+%% This library is distributed in the hope that it will be useful, but
+%% WITHOUT ANY WARRANTY; without even the implied warranty of
+%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%% Lesser General Public License for more details.
+%%
+%% You should have received a copy of the GNU Lesser General Public
+%% License along with this library; if not, write to the Free Software
+%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+%% USA
+%%
+%% $Id$
+%%
+%% @author Richard Carlsson <[email protected]>
+%% @copyright 2006 Richard Carlsson
+%% @private
+%% @see eunit
+%% @doc Event serializing and multiplexing process, to be used as the
+%% main "supervisor" process for en EUnit test runner. See eunit_proc
+%% for details about the events that will be sent to the listeners
+%% (provided to this process at startup). This process guarantees that
+%% listeners will receive events in order, even if tests execute in
+%% parallel. For every received 'begin' event, there will be exactly one
+%% 'end' or 'cancel' event. For a cancelling event with identifier Id,
+%% no further events will arrive whose identifiers have Id as prefix.
+
+-module(eunit_serial).
+
+-include("eunit.hrl").
+-include("eunit_internal.hrl").
+
+-export([start/1]).
+
+%% Notes:
+%% * Due to concurrency, there are no guarantees that we will receive
+%% all status messages for the items within a group before we receive
+%% the 'end' message of the group itself.
+%%
+%% * A cancelling event may arrive at any time, and may concern items we
+%% are not yet expecting (if tests are executed in parallel), or may
+%% concern not only the current item but possibly a group ancestor of
+%% the current item (as in the case of a group timeout).
+%%
+%% * It is not possible to use selective receive to extract only those
+%% cancelling messages that affect the current item and its parents;
+%% basically, because we cannot have a dynamically computed prefix as a
+%% pattern in a receive. Hence, we must extract each cancelling event as
+%% it arrives and keep track of them separately.
+%%
+%% * Before we wait for a new item, we must check whether it (and thus
+%% also all its subitems, if any) is already cancelled.
+%%
+%% * When a new cancelling event arrives, we must either store it for
+%% future use, and/or cancel the current item and possibly one or more
+%% of its parent groups.
+
+-record(state, {listeners :: set(),
+ cancelled = eunit_lib:trie_new() :: gb_tree(),
+ messages = dict:new() :: dict()}).
+
+start(Pids) ->
+ spawn(fun () -> serializer(Pids) end).
+
+serializer(Pids) ->
+ St = #state{listeners = sets:from_list(Pids),
+ cancelled = eunit_lib:trie_new(),
+ messages = dict:new()},
+ expect([], undefined, 0, St),
+ exit(normal).
+
+%% collect beginning and end of an expected item; return {Done, NewSt}
+%% where Done is true if there are no more items of this group
+expect(Id, ParentId, GroupMinSize, St0) ->
+ case wait(Id, 'begin', ParentId, GroupMinSize, St0) of
+ {done, St1} ->
+ {true, St1};
+ {cancel, prefix, _Msg, St1} ->
+ %% if a parent caused the cancel, signal done with group and
+ %% cast no cancel event (since the item might not exist)
+ {true, St1};
+ {cancel, exact, Msg, St1} ->
+ cast_cancel(Id, Msg, St1),
+ {false, St1};
+ {ok, Msg, St1} ->
+ %%?debugVal({got_begin, Id, Msg}),
+ cast(Msg, St1),
+ St2 = case Msg of
+ {status, _, {progress, 'begin', {group, _Info}}} ->
+ group(Id, 0, St1);
+ _ ->
+ St1
+ end,
+ case wait(Id, 'end', ParentId, GroupMinSize, St2) of
+ {cancel, Why, Msg1, St3} ->
+ %% we know the item exists, so always cast a cancel
+ %% event, and signal done with the group if a parent
+ %% caused the cancel
+ cast_cancel(Id, Msg1, St3),
+ {(Why =:= prefix), St3};
+ {ok, Msg1, St3} ->
+ %%?debugVal({got_end, Id, Msg1}),
+ cast(Msg1, St3),
+ {false, St3}
+ end
+ end.
+
+%% collect group items in order until group is done
+group(ParentId, GroupMinSize, St) ->
+ N = GroupMinSize + 1,
+ case expect(ParentId ++ [N], ParentId, GroupMinSize, St) of
+ {false, St1} ->
+ group(ParentId, N, St1);
+ {true, St1} ->
+ St1
+ end.
+
+cast_cancel(Id, undefined, St) ->
+ %% reasonable message for implicitly cancelled events
+ cast({status, Id, {cancel, undefined}}, St);
+cast_cancel(_Id, Msg, St) ->
+ cast(Msg, St).
+
+cast(Msg, St) ->
+ sets:fold(fun (L, M) -> L ! M end, Msg, St#state.listeners),
+ ok.
+
+%% wait for a particular begin or end event, that might have arrived or
+%% been cancelled already, or might become cancelled later, or might not
+%% even exist (for the last+1 element of a group)
+wait(Id, Type, ParentId, GroupMinSize, St) ->
+ %%?debugVal({wait, Id, Type}),
+ case check_cancelled(Id, St) of
+ no ->
+ case recall(Id, St) of
+ undefined ->
+ wait_1(Id, Type, ParentId, GroupMinSize, St);
+ Msg ->
+ {ok, Msg, forget(Id, St)}
+ end;
+ Why ->
+ %%?debugVal({cancelled, Why, Id, ParentId}),
+ {cancel, Why, recall(Id, St), forget(Id, St)}
+ end.
+
+%% the event has not yet arrived or been cancelled - wait for more info
+wait_1(Id, Type, ParentId, GroupMinSize, St) ->
+ receive
+ {status, Id, {progress, Type, _}}=Msg ->
+ %%?debugVal({Type, ParentId, Id}),
+ {ok, Msg, St};
+ {status, ParentId, {progress, 'end', {GroupMinSize, _}}}=Msg ->
+ %% the parent group ended (the final status of a group is
+ %% the count of its subitems), and we have seen all of its
+ %% subtests, so the currently expected event does not exist
+ %%?debugVal({end_group, ParentId, Id, GroupMinSize}),
+ {done, remember(ParentId, Msg, St)};
+ {status, SomeId, {cancel, _Cause}}=Msg ->
+ %%?debugVal({got_cancel, SomeId, _Cause}),
+ St1 = set_cancelled(SomeId, Msg, St),
+ wait(Id, Type, ParentId, GroupMinSize, St1)
+ end.
+
+set_cancelled(Id, Msg, St0) ->
+ St = remember(Id, Msg, St0),
+ St#state{cancelled = eunit_lib:trie_store(Id, St0#state.cancelled)}.
+
+check_cancelled(Id, St) ->
+ %% returns 'no', 'exact', or 'prefix'
+ eunit_lib:trie_match(Id, St#state.cancelled).
+
+remember(Id, Msg, St) ->
+ St#state{messages = dict:store(Id, Msg, St#state.messages)}.
+
+forget(Id, St) ->
+ %% this is just to enable garbage collection of old messages
+ St#state{messages = dict:store(Id, undefined, St#state.messages)}.
+
+recall(Id, St) ->
+ case dict:find(Id, St#state.messages) of
+ {ok, Msg} -> Msg;
+ error -> undefined
+ end.