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