ff46bbbcac52fe1d9837f598654985dec9154900
[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([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 %% }}}
159 %%----------------------------------------------------------
160 %% printing messages {{{
161
162 %% @doc Print a regular message to test output.
163
164 -spec info(test_run_id(), iolist()) ->
165   ok.
166
167 info(TestRunId, Message) ->
168   gen_server:call(TestRunId, {info, Message}).
169
170 %% @doc Print a diagnostic message (warning) to output, typically
171 %%   <i>STDERR</i>.
172
173 -spec warning(test_run_id(), iolist()) ->
174   ok.
175
176 warning(TestRunId, Message) ->
177   gen_server:call(TestRunId, {warning, Message}).
178
179 %% }}}
180 %%----------------------------------------------------------
181
182 %%%---------------------------------------------------------------------------
183 %%% supervision tree API
184 %%%---------------------------------------------------------------------------
185
186 %% @private
187 %% @doc Start the process.
188
189 -spec start(plan(), non_neg_integer()) ->
190   {ok, pid()} | {error, term()}.
191
192 start(Plan, Level) ->
193   gen_server:start(?MODULE, [Plan, Level], []).
194
195 %% @private
196 %% @doc Start the process.
197
198 -spec start_link(plan(), non_neg_integer()) ->
199   {ok, pid()} | {error, term()}.
200
201 start_link(Plan, Level) ->
202   gen_server:start_link(?MODULE, [Plan, Level], []).
203
204 %%%---------------------------------------------------------------------------
205 %%% gen_server callbacks
206 %%%---------------------------------------------------------------------------
207
208 %%----------------------------------------------------------
209 %% initialization/termination {{{
210
211 %% @private
212 %% @doc Initialize event handler.
213
214 init([Plan, Level] = _Args) ->
215   State = #state{plan = Plan, level = Level},
216   case Plan of
217     no_plan -> skip;
218     C when is_integer(C) -> print("1..~B", [C], State)
219   end,
220   {ok, State}.
221
222 %% @private
223 %% @doc Clean up after event handler.
224
225 terminate(_Arg, _State) ->
226   ok.
227
228 %% }}}
229 %%----------------------------------------------------------
230 %% communication {{{
231
232 %% @private
233 %% @doc Handle {@link gen_server:call/2}.
234
235 handle_call(done = _Request, _From,
236             State = #state{plan = Plan, test = {LastTestNo, _}}) ->
237   case Plan of
238     no_plan ->
239       print("1..~B", [LastTestNo], State);
240     C when is_integer(C) ->
241       print("# ran ~B of ~B tests", [LastTestNo, C], State)
242   end,
243   {stop, normal, ok, State};
244
245 handle_call({next, Desc} = _Request, _From,
246             State = #state{test = Test}) ->
247   NextTestNo = case Test of
248     undefined -> 1;
249     {TestNo, _} -> TestNo + 1
250   end,
251   NewState = State#state{test = {NextTestNo, Desc}},
252   {reply, ok, NewState};
253
254 handle_call({result, TestResult} = _Request, _From,
255             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
256   case TestResult of
257     success ->
258       print("ok ~B - ~s", [TestNo, TestDesc], State);
259     {failure, Reason} ->
260       print("not ok ~B - ~s # result: ~s",
261             [TestNo, TestDesc, format(Reason)], State);
262     {dubious, Value} ->
263       print("not ok ~B - ~s # dubious result: ~s",
264             [TestNo, TestDesc, format(Value)], State);
265     {died, Reason} ->
266       print("not ok ~B - ~s # died: ~s",
267             [TestNo, TestDesc, format(Reason)], State)
268   end,
269   NewState = State#state{counters = add_result(TestResult, Counters)},
270   {reply, ok, NewState};
271
272 handle_call({todo, TestResult, Why} = _Request, _From,
273             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
274   case TestResult of
275     success ->
276       print("ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
277     {failure, _Reason} ->
278       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
279     {dubious, _Value} ->
280       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
281     {died, _Reason} ->
282       print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State)
283   end,
284   NewState = State#state{counters = add_todo(TestResult, Counters)},
285   {reply, ok, NewState};
286
287 handle_call({skipped, Reason} = _Request, _From,
288             State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
289   print("ok ~B - ~s # SKIP ~s", [TestNo, TestDesc, Reason], State),
290   NewState = State#state{counters = add_skipped(Counters)},
291   {reply, ok, NewState};
292
293 handle_call(status = _Request, _From, State = #state{counters = Counters}) ->
294   Planned = case State of
295     #state{plan = no_plan} -> undefined;
296     #state{plan = C} -> C
297   end,
298   Total = Counters#counters.tests,
299   Failed = Counters#counters.failures,
300   TODO = Counters#counters.todo_failures,
301   {reply, {Planned, Total, Failed, TODO}, State};
302
303 handle_call({info, Message} = _Request, _From, State) ->
304   print_info("~s", [Message], State),
305   {reply, ok, State};
306
307 handle_call({warning, Message} = _Request, _From, State) ->
308   print_warning("~s", [Message], State),
309   {reply, ok, State};
310
311 %% unknown calls
312 handle_call(_Request, _From, State) ->
313   {reply, {error, unknown_call}, State}.
314
315 %% @private
316 %% @doc Handle {@link gen_server:cast/2}.
317
318 %% unknown casts
319 handle_cast(_Request, State) ->
320   {noreply, State}.
321
322 %% @private
323 %% @doc Handle incoming messages.
324
325 %% unknown messages
326 handle_info(_Message, State) ->
327   {noreply, State}.
328
329 %% }}}
330 %%----------------------------------------------------------
331 %% code change {{{
332
333 %% @private
334 %% @doc Handle code change.
335
336 code_change(_OldVsn, State, _Extra) ->
337   {ok, State}.
338
339 %% }}}
340 %%----------------------------------------------------------
341
342 %%----------------------------------------------------------
343 %% helper functions
344 %%----------------------------------------------------------
345 %% printing messages {{{
346
347 %% @doc Format term for printing to screen.
348
349 -spec format(term()) ->
350   iolist().
351
352 format(Term) ->
353   % no term should weigh 1MB
354   io_lib:print(Term, 1, 1024 * 1024, -1).
355
356 %% @doc Print text to screen.
357 %%   The message doesn't need to end with NL character.
358
359 -spec print(string(), [term()], #state{}) ->
360   ok.
361
362 print(Format, Args, _State = #state{level = Level}) ->
363   Indent = ["    " || _ <- lists:seq(1, Level)],
364   print(standard_io, Indent, Format, Args).
365
366 %% @doc Print informational message to screen.
367
368 -spec print_info(string(), [term()], #state{}) ->
369   ok.
370
371 print_info(Format, Args, _State = #state{level = Level}) ->
372   Indent = case Level > 0 of
373     true -> ["    # " || _ <- lists:seq(1, Level)];
374     false -> "# "
375   end,
376   print(standard_io, Indent, Format, Args).
377
378 %% @doc Print diagnostic (warning) message to screen.
379
380 -spec print_warning(string(), [term()], #state{}) ->
381   ok.
382
383 print_warning(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_error, Indent, Format, Args).
389
390 %% @doc Print message to specified IO device, each line indented.
391
392 -spec print(io:device(), iolist(), string(), [term()]) ->
393   ok.
394
395 print(Output, Indent, Format, Args) ->
396   Text = iolist_to_binary(io_lib:format(Format, Args)),
397   Lines = binary:split(Text, <<"\n">>, [global, trim]),
398   [io:put_chars(Output, [Indent, L, "\n"]) || L <- Lines],
399   ok.
400
401 %% }}}
402 %%----------------------------------------------------------
403 %% `#counter{}' handling {{{
404
405 %% @doc Add 1 to skipped tests counter.
406
407 add_skipped(Counters = #counters{tests = T, skipped = N}) ->
408   _NewCounters = Counters#counters{tests = T + 1, skipped = N + 1}.
409
410 %% @doc Add 1 to successes or failures counter, depending on the `Result'.
411
412 add_result(success = _Result,
413            Counters = #counters{tests = T, successes = N}) ->
414   _NewCounters = Counters#counters{tests = T + 1, successes = N + 1};
415
416 add_result({failure, _} = _Result,
417            Counters = #counters{tests = T, failures = N}) ->
418   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
419
420 add_result({dubious, _} = _Result,
421            Counters = #counters{tests = T, failures = N}) ->
422   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
423
424 add_result({died, _} = _Result,
425            Counters = #counters{tests = T, failures = N}) ->
426   _NewCounters = Counters#counters{tests = T + 1, failures = N + 1}.
427
428 %% @doc Add 1 to TODO successes or failures counter, depending on the
429 %%   `Result'.
430
431 add_todo(success = _Result,
432          Counters = #counters{tests = T, todo_successes = N}) ->
433   _NewCounters = Counters#counters{tests = T + 1, todo_successes = N + 1};
434
435 add_todo({failure, _} = _Result,
436          Counters = #counters{tests = T, todo_failures = N}) ->
437   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
438
439 add_todo({dubious, _} = _Result,
440          Counters = #counters{tests = T, todo_failures = N}) ->
441   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
442
443 add_todo({died, _} = _Result,
444          Counters = #counters{tests = T, todo_failures = N}) ->
445   _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1}.
446
447 %% }}}
448 %%----------------------------------------------------------
449
450 %%%---------------------------------------------------------------------------
451 %%% vim:ft=erlang:foldmethod=marker