6727ba79c74a554f822c0f84d2aeefb6c23b481f
[erlang-estap.git] / src / estap.erl
1 %%%---------------------------------------------------------------------------
2 %%% @doc
3 %%%   Functions to use in test cases.
4 %%%
5 %%%   Test passes if it returns or throws (`throw()'): `ok', `{ok, Value}', or
6 %%%   `true'.
7 %%%
8 %%%   Test fails if it returns or throws `error', `{error, Reason}', `false',
9 %%%   or calls `exit(...)' or `erlang:error(...)' (or simply dies).
10 %%%
11 %%%   Any other returned value is also considered a failure, but a dubious
12 %%%   one. Stick to saying explicitly that the test failed.
13 %%% @end
14 %%%---------------------------------------------------------------------------
15
16 -module(estap).
17
18 %% public interface
19 -export([ok/2, is/3, isnt/3, eq/3, ne/3, cmp/4, like/3, unlike/3, matches/3]).
20 -export([bail_out/1, no_plan/0, plan/1, all_ok/0]).
21 -export([diag/1, diag/2, info/1, info/2, explain/1]).
22 -export([test_dir/0, test_dir/1]).
23
24 -export_type([value/0, cmp/0, regexp/0, match_fun/0]).
25
26 %%%---------------------------------------------------------------------------
27 %%% types {{{
28
29 -type value() :: term().
30
31 -type cmp() :: '<' | '>' | '=<' | '>=' | '/=' | '=/=' | '==' | '=:='.
32
33 -type regexp() :: iolist().
34
35 -type match_fun() :: fun((value()) -> any()).
36
37 -type message() :: iolist().
38
39 -type description() :: iolist().
40
41 -type info() :: atom() | iolist() |
42                 {FieldName :: atom() | iolist(), Value :: atom() | iolist()}.
43
44 %%% }}}
45 %%%---------------------------------------------------------------------------
46 %%% public interface
47 %%%---------------------------------------------------------------------------
48
49 %% @doc Check if `Value' is any of the recognized truth values.
50
51 -spec ok(value(), description()) ->
52   ok.
53
54 ok(Value, Description) ->
55   TestRun = get_test_run(),
56   estap_server:running(TestRun, Description),
57   estap_server:report_result(TestRun, estap_test:success_or_failure(Value)).
58
59 %% @doc Check if `Value' is the same as `Expected'.
60
61 -spec is(value(), value(), description()) ->
62   ok.
63
64 is(Value, Expected, Description) ->
65   TestRun = get_test_run(),
66   estap_server:running(TestRun, Description),
67   case Value of
68     Expected ->
69       estap_server:report_result(TestRun, {success, true});
70     _ ->
71       estap_server:report_result(TestRun, {failure, false})
72   end.
73
74 %% @doc Check if `Value' is different than `Expected'.
75
76 -spec isnt(value(), value(), description()) ->
77   ok.
78
79 isnt(Value, Expected, Description) ->
80   TestRun = get_test_run(),
81   estap_server:running(TestRun, Description),
82   case Value of
83     Expected ->
84       estap_server:report_result(TestRun, {failure, false});
85     _ ->
86       estap_server:report_result(TestRun, {success, true})
87   end.
88
89 %% @doc Check if `Value' is equal (`==') to `Expected'.
90
91 -spec eq(value(), value(), description()) ->
92   ok.
93
94 eq(Value, Expected, Description) ->
95   % XXX: no `get_test_run()' call
96   cmp(Value, '==', Expected, Description).
97
98 %% @doc Check if `Value' is not equal (`/=') to `Expected'.
99
100 -spec ne(value(), value(), description()) ->
101   ok.
102
103 ne(Value, Expected, Description) ->
104   % XXX: no `get_test_run()' call
105   cmp(Value, '/=', Expected, Description).
106
107 %% @doc Compare `Value' and `Expected' using comparison operator.
108
109 -spec cmp(value(), cmp(), value(), description()) ->
110   ok.
111
112 cmp(Value, Cmp, Expected, Description) ->
113   TestRun = get_test_run(),
114   estap_server:running(TestRun, Description),
115   CmpResult = case Cmp of
116     '<'   -> Value <   Expected;
117     '>'   -> Value >   Expected;
118     '=<'  -> Value =<  Expected;
119     '>='  -> Value >=  Expected;
120     '/='  -> Value /=  Expected;
121     '=/=' -> Value =/= Expected;
122     '=='  -> Value ==  Expected;
123     '=:=' -> Value =:= Expected
124   end,
125   case CmpResult of
126     true  -> estap_server:report_result(TestRun, {success, true});
127     false -> estap_server:report_result(TestRun, {failure, false})
128   end.
129
130 %% @doc Check if `Value' matches a regexp.
131
132 -spec like(value(), regexp(), description()) ->
133   ok.
134
135 like(Value, Expected, Description) ->
136   TestRun = get_test_run(),
137   estap_server:running(TestRun, Description),
138   case re:run(Value, Expected) of
139     {match, _Capture} -> estap_server:report_result(TestRun, {success, true});
140     nomatch           -> estap_server:report_result(TestRun, {failure, false})
141   end.
142
143 %% @doc Check if `Value' not matches a regexp.
144
145 -spec unlike(value(), regexp(), description()) ->
146   ok.
147
148 unlike(Value, Expected, Description) ->
149   TestRun = get_test_run(),
150   estap_server:running(TestRun, Description),
151   case re:run(Value, Expected) of
152     {match, _Capture} -> estap_server:report_result(TestRun, {failure, false});
153     nomatch           -> estap_server:report_result(TestRun, {success, true})
154   end.
155
156 %% @doc Check if `Value' pattern-matches.
157 %%   Pattern is specified as a fun that has clauses defined only for what
158 %%   should match, i.e., calling the fun should fail with `function_clause'
159 %%   error. Return value of the fun is ignored.
160
161 -spec matches(value(), match_fun(), description()) ->
162   ok.
163
164 matches(Value, MatchSpec, Description) ->
165   TestRun = get_test_run(),
166   estap_server:running(TestRun, Description),
167   try
168     MatchSpec(Value),
169     estap_server:report_result(TestRun, {success, true})
170   catch
171     error:function_clause ->
172       estap_server:report_result(TestRun, {failure, false})
173   end.
174
175 %%%---------------------------------------------------------------------------
176
177 %% @doc Stop testing current suite because something terrible happened.
178 %%
179 %% @TODO Implement this function.
180
181 -spec bail_out(message()) ->
182   no_return().
183
184 bail_out(_Message) ->
185   'TODO'.
186
187 %% @doc Set the "no plan" plan for sub-tests.
188 %%   Calling this function may be safely skipped.
189 %%
190 %% @see all_ok/0
191
192 -spec no_plan() ->
193   ok.
194
195 no_plan() ->
196   _TestRun = get_test_run(),
197   ok.
198
199 %% @doc Set expected number of sub-tests.
200
201 -spec plan(pos_integer()) ->
202   ok.
203
204 plan(TestCount) when is_integer(TestCount) ->
205   TestRun = estap_server:subplan(TestCount, 1),
206   set_test_run(TestRun),
207   ok.
208
209 %% @doc Check if all the current sub-tests were OK.
210 %%   Function intended to be called at the end of a sequence of sub-tests, to
211 %%   indicate that the test sequence passed or failed.
212
213 -spec all_ok() ->
214   true | false.
215
216 all_ok() ->
217   TestRun = get_test_run(),
218   {Planned, Total, Failed, _TODO} = estap_server:get_status(TestRun),
219   estap_server:done(TestRun), % this ends estap_server, so it goes last
220   (Failed == 0) and ((Planned == undefined) or (Planned == Total)).
221
222 %%%---------------------------------------------------------------------------
223
224 %% @doc Get a directory containing this test script.
225 %%
226 %%   <b>NOTE</b>: This function doesn't work in processes spawned from test
227 %%   function. You need to get the directory in parent process and pass it as
228 %%   an argument.
229
230 test_dir() ->
231   case get(test_dir) of
232     undefined -> erlang:error({undefined, test_dir});
233     Directory -> Directory
234   end.
235
236 %% @doc Get a subdirectory of the directory containing this test script.
237 %%
238 %%   <b>NOTE</b>: This function doesn't work in processes spawned from test
239 %%   function. You need to get the directory in parent process and pass it as
240 %%   an argument.
241
242 test_dir(Subdir) ->
243   filename:join(test_dir(), Subdir).
244
245 %%%---------------------------------------------------------------------------
246
247 %% @doc Print a diagnostic message.
248 %%   Typically, diagnostic message is a warning, but may be notice important
249 %%   enough to print it along with test progress by TAP consumer.
250 %%
251 %%   Before first call to {@link plan/1}, {@link no_plan/0} or test functions
252 %%   ({@link ok/2}, {@link is/3} etc.) message is printed at the level of
253 %%   parent test. After any of those, it's printed at sub-test level.
254 %%
255 %%   Normally diagnostic output goes to <i>STDERR</i>, but under TODO tests it
256 %%   goes to <i>STDOUT</i>.
257 %%
258 %% @TODO Make the diagnostic output go to <i>STDOUT</i> under TODO
259
260 -spec diag(message()) ->
261   ok.
262
263 diag(Message) ->
264   TestRun = get_test_run_or_parent(),
265   estap_server:warning(TestRun, Message).
266
267 %% @doc Print a warning with some context.
268 %%   Typically, diagnostic message is a warning, but may be notice important
269 %%   enough to print it along with test progress by TAP consumer.
270 %%
271 %%   Before first call to {@link plan/1}, {@link no_plan/0} or test functions
272 %%   ({@link ok/2}, {@link is/3} etc.) message is printed at the level of
273 %%   parent test. After any of those, it's printed at sub-test level.
274 %%
275 %%   Normally diagnostic output goes to <i>STDERR</i>, but under TODO tests it
276 %%   goes to <i>STDOUT</i>.
277 %%
278 %% @TODO Make the diagnostic output go to <i>STDOUT</i> under TODO
279
280 -spec diag(message(), [info()]) ->
281   ok.
282
283 diag(Message, Info) ->
284   TestRun = get_test_run_or_parent(),
285   InfoLines = [["  ", format_info(I), "\n"] || I <- Info],
286   estap_server:warning(TestRun, [Message, "\n", InfoLines]).
287
288 %% @doc Print a message.
289 %%
290 %%   Before first call to {@link plan/1}, {@link no_plan/0} or test functions
291 %%   ({@link ok/2}, {@link is/3} etc.) message is printed at the level of
292 %%   parent test. After any of those, it's printed at sub-test level.
293
294 -spec info(message()) ->
295   ok.
296
297 info(Message) ->
298   TestRun = get_test_run_or_parent(),
299   estap_server:info(TestRun, Message).
300
301 %% @doc Print a message with some context.
302 %%
303 %%   Before first call to {@link plan/1}, {@link no_plan/0} or test functions
304 %%   ({@link ok/2}, {@link is/3} etc.) message is printed at the level of
305 %%   parent test. After any of those, it's printed at sub-test level.
306
307 -spec info(message(), [info()]) ->
308   ok.
309
310 info(Message, Info) ->
311   TestRun = get_test_run_or_parent(),
312   InfoLines = [["  ", format_info(I), "\n"] || I <- Info],
313   estap_server:info(TestRun, [Message, "\n", InfoLines]).
314
315 %% @doc Format a single info entry for printing it on screen.
316
317 -spec format_info(info()) ->
318   iolist().
319
320 format_info(Info) when is_list(Info); is_binary(Info) ->
321   iolist_to_binary(Info);
322 format_info(Info) when is_atom(Info) ->
323   atom_to_binary(Info, unicode);
324 format_info({K, V} = _Info) ->
325   [format_info(K), ": ", format_info(V)].
326
327 %% @doc Format term so it can be printed to screen.
328 %%   Convenience wrapper for {@link io_lib:format/2}.
329 %%
330 %% @see info/2
331 %% @see diag/2
332
333 -spec explain(term()) ->
334   iolist().
335
336 explain(Term) ->
337   % no term should weigh 1MB
338   io_lib:print(Term, 1, 1024 * 1024, -1).
339
340 %%%---------------------------------------------------------------------------
341
342 %% @doc Set previously started {@link estap_server}.
343
344 set_test_run(TestRun) ->
345   put(estap_server, TestRun).
346
347 %% @doc Get associated {@link estap_server}, starting it if necessary.
348
349 get_test_run() ->
350   case get(estap_server) of
351     undefined ->
352       TestRun = estap_server:subplan(no_plan, 1),
353       put(estap_server, TestRun),
354       TestRun;
355     TestRun when is_pid(TestRun) ->
356       TestRun
357   end.
358
359 %% @doc Get associated {@link estap_server} or parent one if none is started
360 %%   yet. Necessary for top-level {@link info/1} and {@link diag/1} to work.
361
362 get_test_run_or_parent() ->
363   case get(estap_server) of
364     undefined ->
365       % XXX: this must be set in `estap_test:run()'
366       get(estap_server_parent);
367     TestRun when is_pid(TestRun) ->
368       TestRun
369   end.
370
371 %%%---------------------------------------------------------------------------
372 %%% vim:ft=erlang:foldmethod=marker