Implemented bailing out from test script.
[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, bail_out/2]).
13 -export([get_status/1]).
14 -export([info/2, warning/2]).
15 -export([running/2, report_result/2, report_result_todo/3, report_skipped/2]).
16
17 %% supervision tree API
18 -export([start/2, start_link/2]).
19
20 %% gen_server callbacks
21 -export([init/1, terminate/2]).
22 -export([handle_call/3, handle_cast/2, handle_info/2]).
23 -export([code_change/3]).
24
25 -export_type([test_run_id/0]).
26
27 %%%---------------------------------------------------------------------------
28
29 -type test_run_id() :: pid().
30
31 -type plan() :: no_plan | pos_integer().
32
33 -record(counters, {
34   tests     = 0,
35   successes = 0,
36   failures  = 0,
37   todo_successes = 0,
38   todo_failures  = 0,
39   skipped   = 0
40 }).
41
42 -record(state, {
43   plan :: plan(),
44   level = 0 :: non_neg_integer(),
45   test :: {TestNo :: pos_integer(), Description :: string()},
46   counters = #counters{}
47 }).
48
49 %%%---------------------------------------------------------------------------
50 %%% public interface
51 %%%---------------------------------------------------------------------------
52
53 %%----------------------------------------------------------
54 %% test plan setup {{{
55
56 %% @doc "No plan" plan.
57
58 -spec no_plan() ->
59   test_run_id().
60
61 no_plan() ->
62   {ok, Pid} = start_link(no_plan, 0),
63   Pid.
64
65 %% @doc Test plan.
66
67 -spec plan(pos_integer()) ->
68   test_run_id().
69
70 plan(TestCount) when is_integer(TestCount) ->
71   {ok, Pid} = start_link(TestCount, 0),
72   Pid.
73
74 %% @doc Plan for subtests.
75
76 -spec subplan(plan(), pos_integer()) ->
77   test_run_id().
78
79 subplan(Plan, Level)
80 when Plan == no_plan orelse is_integer(Plan), is_integer(Level) ->
81   {ok, Pid} = start_link(Plan, Level),
82   Pid.
83
84 %% @doc Mark the end of tests in this run.
85
86 -spec done(test_run_id()) ->
87   ok.
88
89 done(TestRunId) ->
90   gen_server:call(TestRunId, done).
91
92 %% @doc Return summary in form of numbers of tests.
93 %%   `TODO' tests are the ones marked as TODO that failed.
94
95 -spec get_status(test_run_id()) ->
96   {Planned :: integer() | undefined,
97     Total :: integer(), Failed :: integer(), TODO :: integer()}.
98
99 get_status(TestRunId) ->
100   gen_server:call(TestRunId, status).
101
102 %% }}}
103 %%----------------------------------------------------------
104 %% test plan execution {{{
105
106 %% @doc Mark the beginning of new test.
107
108 -spec running(test_run_id(), string()) ->
109   ok.
110
111 running(TestRunId, Description) ->
112   gen_server:call(TestRunId, {next, Description}).
113
114 %% @doc Report result of running a test started at {@link running/2}.
115
116 -spec report_result(test_run_id(),
117                     {success | failure | dubious | died, term()}) ->
118   ok.
119
120 report_result(TestRunId, {success, _Value} = _TestResult) ->
121   gen_server:call(TestRunId, {result, success});
122
123 report_result(TestRunId, {failure, ReturnValue} = _TestResult) ->
124   gen_server:call(TestRunId, {result, {failure, ReturnValue}});
125
126 report_result(TestRunId, {dubious, ReturnValue} = _TestResult) ->
127   gen_server:call(TestRunId, {result, {dubious, ReturnValue}});
128
129 report_result(TestRunId, {died, Reason} = _TestResult) ->
130   gen_server:call(TestRunId, {result, {died, Reason}}).
131
132 %% @doc Report result of running a TODO test started at {@link running/2}.
133
134 -spec report_result_todo(test_run_id(), string(),
135                          {success | failure | dubious | died, term()}) ->
136   ok.
137
138 report_result_todo(TestRunId, Why, {success, _Value} = _TestResult) ->
139   gen_server:call(TestRunId, {todo, success, Why});
140
141 report_result_todo(TestRunId, Why, {failure, ReturnValue} = _TestResult) ->
142   gen_server:call(TestRunId, {todo, {failure, ReturnValue}, Why});
143
144 report_result_todo(TestRunId, Why, {dubious, ReturnValue} = _TestResult) ->
145   gen_server:call(TestRunId, {todo, {dubious, ReturnValue}, Why});
146
147 report_result_todo(TestRunId, Why, {died, Reason} = _TestResult) ->
148   gen_server:call(TestRunId, {todo, {died, Reason}, Why}).
149
150 %% @doc Report that a test started at {@link running/2} was skipped.
151
152 -spec report_skipped(test_run_id(), string()) ->
153   ok.
154
155 report_skipped(TestRunId, Why) ->
156   gen_server:call(TestRunId, {skipped, Why}).
157
158 %% @doc Abort the test script because of fatal error.
159
160 -spec bail_out(test_run_id(), string()) ->
161   ok.
162
163 bail_out(TestRunId, Reason) ->
164   gen_server:call(TestRunId, {bail_out, Reason}).
165
166 %% }}}
167 %%----------------------------------------------------------
168 %% printing messages {{{
169
170 %% @doc Print a regular message to test output.
171
172 -spec info(test_run_id(), iolist()) ->
173   ok.
174
175 info(TestRunId, Message) ->
176   gen_server:call(TestRunId, {info, Message}).
177
178 %% @doc Print a diagnostic message (warning) to output, typically
179 %%   <i>STDERR</i>.
180
181 -spec warning(test_run_id(), iolist()) ->
182   ok.
183
184 warning(TestRunId, Message) ->
185   gen_server:call(TestRunId, {warning, Message}).
186
187 %% }}}
188 %%----------------------------------------------------------
189
190 %%%---------------------------------------------------------------------------
191 %%% supervision tree API
192 %%%---------------------------------------------------------------------------
193
194 %% @private
195 %% @doc Start the process.
196
197 -spec start(plan(), non_neg_integer()) ->
198   {ok, pid()} | {error, term()}.
199
200 start(Plan, Level) ->
201   gen_server:start(?MODULE, [Plan, Level], []).
202
203 %% @private
204 %% @doc Start the process.
205
206 -spec start_link(plan(), non_neg_integer()) ->
207   {ok, pid()} | {error, term()}.
208
209 start_link(Plan, Level) ->
210   gen_server:start_link(?MODULE, [Plan, Level], []).
211
212 %%%---------------------------------------------------------------------------
213 %%% gen_server callbacks
214 %%%---------------------------------------------------------------------------
215
216 %%----------------------------------------------------------
217 %% initialization/termination {{{
218
219 %% @private
220 %% @doc Initialize event handler.
221
222 init([Plan, Level] = _Args) ->
223   State = #state{plan = Plan, level = Level},
224   case Plan of
225     no_plan -> skip;
226     C when is_integer(C) -> print("1..~B", [C], State)
227   end,
228   {ok, State}.
229
230 %% @private
231 %% @doc Clean up after event handler.
232
233 terminate(_Arg, _State) ->
234   ok.
235
236 %% }}}
237 %%----------------------------------------------------------
238 %% communication {{{
239
240 %% @private
241 %% @doc Handle {@link gen_server:call/2}.
242
243 handle_call(done = _Request, _From,
244             State = #state{plan = Plan, test = {LastTestNo, _}}) ->
245   case Plan of
246     no_plan ->
247       print("1..~B", [LastTestNo], State);
248     C when is_integer(C) ->
249       print("# ran ~B of ~B tests", [LastTestNo, C], State)
250   end,
251   {stop, normal, ok, State};
252
253 handle_call({bail_out, Reason} = _Request, _From, State) ->
254   print("Bail out! ~s", [Reason], State),
255   {stop, normal, ok, State};
256
257 handle_call({next, Desc} = _Request, _From,
258             State = #state{test = Test}) ->
259   NextTestNo = case Test of
260     undefined -> 1;
261     {TestNo, _} -> TestNo + 1
262   end,
263   NewState = State#state{test = {NextTestNo, Desc}},
264   {reply, ok, NewState};
265
266 handle_call({result, TestResult} = _Request, _From,
267             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
268   case TestResult of
269     success ->
270       print("ok ~B - ~s", [TestNo, TestDesc], State);
271     {failure, Reason} ->
272       print("not ok ~B - ~s # result: ~s",
273             [TestNo, TestDesc, format(Reason)], State);
274     {dubious, Value} ->
275       print("not ok ~B - ~s # dubious result: ~s",
276             [TestNo, TestDesc, format(Value)], State);
277     {died, Reason} ->
278       print("not ok ~B - ~s # died: ~s",
279             [TestNo, TestDesc, format(Reason)], State)
280   end,
281   NewState = State#state{counters = add_result(TestResult, Counters)},
282   {reply, ok, NewState};
283
284 handle_call({todo, TestResult, Why} = _Request, _From,
285             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
286   case TestResult of
287     success ->
288       print("ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
289     {failure, _Reason} ->
290       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
291     {dubious, _Value} ->
292       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
293     {died, _Reason} ->
294       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State)
295   end,
296   NewState = State#state{counters = add_todo(TestResult, Counters)},
297   {reply, ok, NewState};
298
299 handle_call({skipped, Reason} = _Request, _From,
300             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
301   print("ok ~B - ~s # SKIP ~s", [TestNo, TestDesc, Reason], State),
302   NewState = State#state{counters = add_skipped(Counters)},
303   {reply, ok, NewState};
304
305 handle_call(status = _Request, _From, State = #state{counters = Counters}) ->
306   Planned = case State of
307     #state{plan = no_plan} -> undefined;
308     #state{plan = C} -> C
309   end,
310   Total = Counters#counters.tests,
311   Failed = Counters#counters.failures,
312   TODO = Counters#counters.todo_failures,
313   {reply, {Planned, Total, Failed, TODO}, State};
314
315 handle_call({info, Message} = _Request, _From, State) ->
316   print_info("~s", [Message], State),
317   {reply, ok, State};
318
319 handle_call({warning, Message} = _Request, _From, State) ->
320   print_warning("~s", [Message], State),
321   {reply, ok, State};
322
323 %% unknown calls
324 handle_call(_Request, _From, State) ->
325   {reply, {error, unknown_call}, State}.
326
327 %% @private
328 %% @doc Handle {@link gen_server:cast/2}.
329
330 %% unknown casts
331 handle_cast(_Request, State) ->
332   {noreply, State}.
333
334 %% @private
335 %% @doc Handle incoming messages.
336
337 %% unknown messages
338 handle_info(_Message, State) ->
339   {noreply, State}.
340
341 %% }}}
342 %%----------------------------------------------------------
343 %% code change {{{
344
345 %% @private
346 %% @doc Handle code change.
347
348 code_change(_OldVsn, State, _Extra) ->
349   {ok, State}.
350
351 %% }}}
352 %%----------------------------------------------------------
353
354 %%----------------------------------------------------------
355 %% helper functions
356 %%----------------------------------------------------------
357 %% printing messages {{{
358
359 %% @doc Format term for printing to screen.
360
361 -spec format(term()) ->
362   iolist().
363
364 format(Term) ->
365   % no term should weigh 1MB
366   io_lib:print(Term, 1, 1024 * 1024, -1).
367
368 %% @doc Print text to screen.
369 %%   The message doesn't need to end with NL character.
370
371 -spec print(string(), [term()], #state{}) ->
372   ok.
373
374 print(Format, Args, _State = #state{level = Level}) ->
375   Indent = ["    " || _ <- lists:seq(1, Level)],
376   print(standard_io, Indent, Format, Args).
377
378 %% @doc Print informational message to screen.
379
380 -spec print_info(string(), [term()], #state{}) ->
381   ok.
382
383 print_info(Format, Args, _State = #state{level = Level}) ->
384   Indent = case Level > 0 of
385     true -> ["    # " || _ <- lists:seq(1, Level)];
386     false -> "# "
387   end,
388   print(standard_io, Indent, Format, Args).
389
390 %% @doc Print diagnostic (warning) message to screen.
391
392 -spec print_warning(string(), [term()], #state{}) ->
393   ok.
394
395 print_warning(Format, Args, _State = #state{level = Level}) ->
396   Indent = case Level > 0 of
397     true -> ["    # " || _ <- lists:seq(1, Level)];
398     false -> "# "
399   end,
400   print(standard_error, Indent, Format, Args).
401
402 %% @doc Print message to specified IO device, each line indented.
403
404 -spec print(io:device(), iolist(), string(), [term()]) ->
405   ok.
406
407 print(Output, Indent, Format, Args) ->
408   Text = iolist_to_binary(io_lib:format(Format, Args)),
409   Lines = binary:split(Text, <<"\n">>, [global, trim]),
410   [io:put_chars(Output, [Indent, L, "\n"]) || L <- Lines],
411   ok.
412
413 %% }}}
414 %%----------------------------------------------------------
415 %% `#counter{}' handling {{{
416
417 %% @doc Add 1 to skipped tests counter.
418
419 add_skipped(Counters = #counters{tests = T, skipped = N}) ->
420   _NewCounters = Counters#counters{tests = T + 1, skipped = N + 1}.
421
422 %% @doc Add 1 to successes or failures counter, depending on the `Result'.
423
424 add_result(success = _Result,
425            Counters = #counters{tests = T, successes = N}) ->
426   _NewCounters = Counters#counters{tests = T + 1, successes = N + 1};
427
428 add_result({failure, _} = _Result,
429            Counters = #counters{tests = T, failures = N}) ->
430   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
431
432 add_result({dubious, _} = _Result,
433            Counters = #counters{tests = T, failures = N}) ->
434   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
435
436 add_result({died, _} = _Result,
437            Counters = #counters{tests = T, failures = N}) ->
438   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1}.
439
440 %% @doc Add 1 to TODO successes or failures counter, depending on the
441 %%   `Result'.
442
443 add_todo(success = _Result,
444          Counters = #counters{tests = T, todo_successes = N}) ->
445   _NewCounters = Counters#counters{tests = T + 1, todo_successes = N + 1};
446
447 add_todo({failure, _} = _Result,
448          Counters = #counters{tests = T, todo_failures = N}) ->
449   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
450
451 add_todo({dubious, _} = _Result,
452          Counters = #counters{tests = T, todo_failures = N}) ->
453   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
454
455 add_todo({died, _} = _Result,
456          Counters = #counters{tests = T, todo_failures = N}) ->
457   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1}.
458
459 %% }}}
460 %%----------------------------------------------------------
461
462 %%%---------------------------------------------------------------------------
463 %%% vim:ft=erlang:foldmethod=marker