907c1cef6225966d815bf9c01bf802cb57d6b275
[erlang-estap.git] / src / estap_server.erl
1 %%%---------------------------------------------------------------------------
2 %%% @doc
3 %%%   Input/output and test plan tracking process.
4 %%% @end
5 %%%---------------------------------------------------------------------------
6
7 -module(estap_server).
8
9 -behaviour(gen_server).
10
11 %% public interface
12 -export([no_plan/0, plan/1, subplan/2, done/1]).
13 -export([get_status/1]).
14 -export([running/2, report_result/2, report_result_todo/3, report_skipped/2]).
15
16 %% supervision tree API
17 -export([start/2, start_link/2]).
18
19 %% gen_server callbacks
20 -export([init/1, terminate/2]).
21 -export([handle_call/3, handle_cast/2, handle_info/2]).
22 -export([code_change/3]).
23
24 -export_type([test_run_id/0]).
25
26 %%%---------------------------------------------------------------------------
27
28 -type test_run_id() :: pid().
29
30 -type plan() :: no_plan | pos_integer().
31
32 -record(counters, {
33   tests     = 0,
34   successes = 0,
35   failures  = 0,
36   todo_successes = 0,
37   todo_failures  = 0,
38   skipped   = 0
39 }).
40
41 -record(state, {
42   plan :: plan(),
43   level = 0 :: non_neg_integer(),
44   test :: {TestNo :: pos_integer(), Description :: string()},
45   counters = #counters{}
46 }).
47
48 %%%---------------------------------------------------------------------------
49 %%% public interface
50 %%%---------------------------------------------------------------------------
51
52 %% @doc "No plan" plan.
53
54 -spec no_plan() ->
55   test_run_id().
56
57 no_plan() ->
58   {ok, Pid} = start_link(no_plan, 0),
59   Pid.
60
61 %% @doc Test plan.
62
63 -spec plan(pos_integer()) ->
64   test_run_id().
65
66 plan(TestCount) when is_integer(TestCount) ->
67   {ok, Pid} = start_link(TestCount, 0),
68   Pid.
69
70 %% @doc Plan for subtests.
71
72 -spec subplan(plan(), pos_integer()) ->
73   test_run_id().
74
75 subplan(Plan, Level)
76 when Plan == no_plan orelse is_integer(Plan), is_integer(Level) ->
77   {ok, Pid} = start_link(Plan, Level),
78   Pid.
79
80 %% @doc Mark the end of tests in this run.
81
82 -spec done(test_run_id()) ->
83   ok.
84
85 done(TestRunId) ->
86   gen_server:call(TestRunId, done).
87
88 %% @doc Return summary in form of numbers of tests.
89 %%   `TODO' tests are the ones marked as TODO that failed.
90
91 -spec get_status(test_run_id()) ->
92   {Planned :: integer() | undefined,
93     Total :: integer(), Failed :: integer(), TODO :: integer()}.
94
95 get_status(TestRunId) ->
96   gen_server:call(TestRunId, status).
97
98 %% @doc Mark the beginning of new test.
99
100 -spec running(test_run_id(), string()) ->
101   ok.
102
103 running(TestRunId, Description) ->
104   gen_server:call(TestRunId, {next, Description}).
105
106 %% @doc Report result of running a test started at {@link running/2}.
107
108 -spec report_result(test_run_id(),
109                     {success | failure | dubious | died, term()}) ->
110   ok.
111
112 report_result(TestRunId, {success, _Value} = _TestResult) ->
113   gen_server:call(TestRunId, {result, success});
114
115 report_result(TestRunId, {failure, ReturnValue} = _TestResult) ->
116   gen_server:call(TestRunId, {result, {failure, ReturnValue}});
117
118 report_result(TestRunId, {dubious, ReturnValue} = _TestResult) ->
119   gen_server:call(TestRunId, {result, {dubious, ReturnValue}});
120
121 report_result(TestRunId, {died, Reason} = _TestResult) ->
122   gen_server:call(TestRunId, {result, {died, Reason}}).
123
124 %% @doc Report result of running a TODO test started at {@link running/2}.
125
126 -spec report_result_todo(test_run_id(), string(),
127                          {success | failure | dubious | died, term()}) ->
128   ok.
129
130 report_result_todo(TestRunId, Why, {success, _Value} = _TestResult) ->
131   gen_server:call(TestRunId, {todo, success, Why});
132
133 report_result_todo(TestRunId, Why, {failure, ReturnValue} = _TestResult) ->
134   gen_server:call(TestRunId, {todo, {failure, ReturnValue}, Why});
135
136 report_result_todo(TestRunId, Why, {dubious, ReturnValue} = _TestResult) ->
137   gen_server:call(TestRunId, {todo, {dubious, ReturnValue}, Why});
138
139 report_result_todo(TestRunId, Why, {died, Reason} = _TestResult) ->
140   gen_server:call(TestRunId, {todo, {died, Reason}, Why}).
141
142 %% @doc Report that a test started at {@link running/2} was skipped.
143
144 -spec report_skipped(test_run_id(), string()) ->
145   ok.
146
147 report_skipped(TestRunId, Why) ->
148   gen_server:call(TestRunId, {skipped, Why}).
149
150 %%%---------------------------------------------------------------------------
151 %%% supervision tree API
152 %%%---------------------------------------------------------------------------
153
154 %% @private
155 %% @doc Start the process.
156
157 -spec start(plan(), non_neg_integer()) ->
158   {ok, pid()} | {error, term()}.
159
160 start(Plan, Level) ->
161   gen_server:start(?MODULE, [Plan, Level], []).
162
163 %% @private
164 %% @doc Start the process.
165
166 -spec start_link(plan(), non_neg_integer()) ->
167   {ok, pid()} | {error, term()}.
168
169 start_link(Plan, Level) ->
170   gen_server:start_link(?MODULE, [Plan, Level], []).
171
172 %%%---------------------------------------------------------------------------
173 %%% gen_server callbacks
174 %%%---------------------------------------------------------------------------
175
176 %%----------------------------------------------------------
177 %% initialization/termination {{{
178
179 %% @private
180 %% @doc Initialize event handler.
181
182 init([Plan, Level] = _Args) ->
183   State = #state{plan = Plan, level = Level},
184   case Plan of
185     no_plan -> skip;
186     C when is_integer(C) -> print("1..~B", [C], State)
187   end,
188   {ok, State}.
189
190 %% @private
191 %% @doc Clean up after event handler.
192
193 terminate(_Arg, _State) ->
194   ok.
195
196 %% }}}
197 %%----------------------------------------------------------
198 %% communication {{{
199
200 %% @private
201 %% @doc Handle {@link gen_server:call/2}.
202
203 handle_call(done = _Request, _From,
204             State = #state{plan = Plan, test = {LastTestNo, _}}) ->
205   case Plan of
206     no_plan ->
207       print("1..~B", [LastTestNo], State);
208     C when is_integer(C) ->
209       print("# ran ~B of ~B tests", [LastTestNo, C], State)
210   end,
211   {stop, normal, ok, State};
212
213 handle_call({next, Desc} = _Request, _From,
214             State = #state{test = Test}) ->
215   NextTestNo = case Test of
216     undefined -> 1;
217     {TestNo, _} -> TestNo + 1
218   end,
219   NewState = State#state{test = {NextTestNo, Desc}},
220   {reply, ok, NewState};
221
222 handle_call({result, TestResult} = _Request, _From,
223             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
224   case TestResult of
225     success ->
226       print("ok ~B - ~s", [TestNo, TestDesc], State);
227     {failure, Reason} ->
228       print("not ok ~B - ~s # result: ~s",
229             [TestNo, TestDesc, format(Reason)], State);
230     {dubious, Value} ->
231       print("not ok ~B - ~s # dubious result: ~s",
232             [TestNo, TestDesc, format(Value)], State);
233     {died, Reason} ->
234       print("not ok ~B - ~s # died: ~s",
235             [TestNo, TestDesc, format(Reason)], State)
236   end,
237   NewState = State#state{counters = add_result(TestResult, Counters)},
238   {reply, ok, NewState};
239
240 handle_call({todo, TestResult, Why} = _Request, _From,
241             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
242   case TestResult of
243     success ->
244       print("ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
245     {failure, _Reason} ->
246       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
247     {dubious, _Value} ->
248       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
249     {died, _Reason} ->
250       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State)
251   end,
252   NewState = State#state{counters = add_todo(TestResult, Counters)},
253   {reply, ok, NewState};
254
255 handle_call({skipped, Reason} = _Request, _From,
256             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
257   print("ok ~B - ~s # SKIP ~s", [TestNo, TestDesc, Reason], State),
258   NewState = State#state{counters = add_skipped(Counters)},
259   {reply, ok, NewState};
260
261 handle_call(status = _Request, _From, State = #state{counters = Counters}) ->
262   Planned = case State of
263     #state{plan = no_plan} -> undefined;
264     #state{plan = C} -> C
265   end,
266   Total = Counters#counters.tests,
267   Failed = Counters#counters.failures,
268   TODO = Counters#counters.todo_failures,
269   {reply, {Planned, Total, Failed, TODO}, State};
270
271 %% unknown calls
272 handle_call(_Request, _From, State) ->
273   {reply, {error, unknown_call}, State}.
274
275 %% @private
276 %% @doc Handle {@link gen_server:cast/2}.
277
278 %% unknown casts
279 handle_cast(_Request, State) ->
280   {noreply, State}.
281
282 %% @private
283 %% @doc Handle incoming messages.
284
285 %% unknown messages
286 handle_info(_Message, State) ->
287   {noreply, State}.
288
289 %% }}}
290 %%----------------------------------------------------------
291 %% code change {{{
292
293 %% @private
294 %% @doc Handle code change.
295
296 code_change(_OldVsn, State, _Extra) ->
297   {ok, State}.
298
299 %% }}}
300 %%----------------------------------------------------------
301
302 %% @doc Format term for printing to screen.
303
304 -spec format(term()) ->
305   iolist().
306
307 format(Term) ->
308   % no term should weigh 1MB
309   io_lib:print(Term, 1, 1024 * 1024, -1).
310
311 %% @doc Print message to screen.
312 %%   The message doesn't need to end with NL character.
313
314 -spec print(string(), [term()], #state{}) ->
315   ok.
316
317 print(Format, Args, _State = #state{level = Level}) ->
318   Indent = ["    " || _ <- lists:seq(1, Level)],
319   Text = iolist_to_binary(io_lib:format(Format, Args)),
320   Lines = binary:split(Text, <<"\n">>, [global, trim]),
321   [io:put_chars([Indent, L, "\n"]) || L <- Lines],
322   ok.
323
324 %%----------------------------------------------------------
325
326 %% @doc Add 1 to skipped tests counter.
327
328 add_skipped(Counters = #counters{tests = T, skipped = N}) ->
329   _NewCounters = Counters#counters{tests = T + 1, skipped = N + 1}.
330
331 %% @doc Add 1 to successes or failures counter, depending on the `Result'.
332
333 add_result(success = _Result,
334            Counters = #counters{tests = T, successes = N}) ->
335   _NewCounters = Counters#counters{tests = T + 1, successes = N + 1};
336
337 add_result({failure, _} = _Result,
338            Counters = #counters{tests = T, failures = N}) ->
339   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
340
341 add_result({dubious, _} = _Result,
342            Counters = #counters{tests = T, failures = N}) ->
343   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
344
345 add_result({died, _} = _Result,
346            Counters = #counters{tests = T, failures = N}) ->
347   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1}.
348
349 %% @doc Add 1 to TODO successes or failures counter, depending on the
350 %%   `Result'.
351
352 add_todo(success = _Result,
353          Counters = #counters{tests = T, todo_successes = N}) ->
354   _NewCounters = Counters#counters{tests = T + 1, todo_successes = N + 1};
355
356 add_todo({failure, _} = _Result,
357          Counters = #counters{tests = T, todo_failures = N}) ->
358   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
359
360 add_todo({dubious, _} = _Result,
361          Counters = #counters{tests = T, todo_failures = N}) ->
362   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
363
364 add_todo({died, _} = _Result,
365          Counters = #counters{tests = T, todo_failures = N}) ->
366   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1}.
367
368 %%----------------------------------------------------------
369
370 %%%---------------------------------------------------------------------------
371 %%% vim:ft=erlang:foldmethod=marker