1e5813d55SPaul Joseph Davis%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net>
2bc6c5ea6SVolker Mische%%
3e5813d55SPaul Joseph Davis%% Permission is hereby granted, free of charge, to any person
4e5813d55SPaul Joseph Davis%% obtaining a copy of this software and associated documentation
5e5813d55SPaul Joseph Davis%% files (the "Software"), to deal in the Software without
6e5813d55SPaul Joseph Davis%% restriction, including without limitation the rights to use,
7e5813d55SPaul Joseph Davis%% copy, modify, merge, publish, distribute, sublicense, and/or sell
8e5813d55SPaul Joseph Davis%% copies of the Software, and to permit persons to whom the
9e5813d55SPaul Joseph Davis%% Software is furnished to do so, subject to the following
10e5813d55SPaul Joseph Davis%% conditions:
11bc6c5ea6SVolker Mische%%
12e5813d55SPaul Joseph Davis%% The above copyright notice and this permission notice shall be
13e5813d55SPaul Joseph Davis%% included in all copies or substantial portions of the Software.
14bc6c5ea6SVolker Mische%%
22e5813d55SPaul Joseph Davis%% OTHER DEALINGS IN THE SOFTWARE.
23bc6c5ea6SVolker Mische%%
24e5813d55SPaul Joseph Davis%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/]
25e5813d55SPaul Joseph Davis%% @author Jeremy Wall <jeremy@marzhillstudios.com>
26e5813d55SPaul Joseph Davis%% @version 0.3.4
27e5813d55SPaul Joseph Davis%% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines
28e5813d55SPaul Joseph Davis%% @reference http://testanything.org/wiki/index.php/Main_Page
29e5813d55SPaul Joseph Davis%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol
30e5813d55SPaul Joseph Davis%% @todo Finish implementing the skip directive.
31e5813d55SPaul Joseph Davis%% @todo Document the messages handled by this receive loop.
32e5813d55SPaul Joseph Davis%% @todo Explain in documentation why we use a process to handle test input.
33e5813d55SPaul Joseph Davis%% @doc etap is a TAP testing module for Erlang components and applications.
34e5813d55SPaul Joseph Davis%% This module allows developers to test their software using the TAP method.
35bc6c5ea6SVolker Mische%%
36e5813d55SPaul Joseph Davis%% <blockquote cite="http://en.wikipedia.org/wiki/Test_Anything_Protocol"><p>
37e5813d55SPaul Joseph Davis%% TAP, the Test Anything Protocol, is a simple text-based interface between
38e5813d55SPaul Joseph Davis%% testing modules in a test harness. TAP started life as part of the test
39e5813d55SPaul Joseph Davis%% harness for Perl but now has implementations in C/C++, Python, PHP, Perl
40e5813d55SPaul Joseph Davis%% and probably others by the time you read this.
41e5813d55SPaul Joseph Davis%% </p></blockquote>
42bc6c5ea6SVolker Mische%%
43e5813d55SPaul Joseph Davis%% The testing process begins by defining a plan using etap:plan/1, running
44e5813d55SPaul Joseph Davis%% a number of etap tests and then calling eta:end_tests/0. Please refer to
45e5813d55SPaul Joseph Davis%% the Erlang modules in the t directory of this project for example tests.
46e5813d55SPaul Joseph Davis-module(etap).
47bc6c5ea6SVolker Mische-vsn("0.3.4").
48bc6c5ea6SVolker Mische
49e5813d55SPaul Joseph Davis-export([
50bc6c5ea6SVolker Mische    ensure_test_server/0,
51bc6c5ea6SVolker Mische    start_etap_server/0,
52bc6c5ea6SVolker Mische    test_server/1,
53bc6c5ea6SVolker Mische    msg/1, msg/2,
54bc6c5ea6SVolker Mische    diag/1, diag/2,
55bc6c5ea6SVolker Mische    expectation_mismatch_message/3,
56bc6c5ea6SVolker Mische    plan/1,
57bc6c5ea6SVolker Mische    end_tests/0,
58bc6c5ea6SVolker Mische    not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3,
59bc6c5ea6SVolker Mische    fun_is/3, expect_fun/3, expect_fun/4,
60bc6c5ea6SVolker Mische    is_greater/3,
61bc6c5ea6SVolker Mische    skip/1, skip/2,
62bc6c5ea6SVolker Mische    datetime/1,
63bc6c5ea6SVolker Mische    skip/3,
64bc6c5ea6SVolker Mische    bail/0, bail/1,
65bc6c5ea6SVolker Mische    test_state/0, failure_count/0
66bc6c5ea6SVolker Mische]).
67bc6c5ea6SVolker Mische
68bc6c5ea6SVolker Mische-export([
69bc6c5ea6SVolker Mische    contains_ok/3,
70bc6c5ea6SVolker Mische    is_before/4
71bc6c5ea6SVolker Mische]).
72bc6c5ea6SVolker Mische
73bc6c5ea6SVolker Mische-export([
74bc6c5ea6SVolker Mische    is_pid/2,
75bc6c5ea6SVolker Mische    is_alive/2,
76bc6c5ea6SVolker Mische    is_mfa/3
77bc6c5ea6SVolker Mische]).
78bc6c5ea6SVolker Mische
79bc6c5ea6SVolker Mische-export([
80bc6c5ea6SVolker Mische    loaded_ok/2,
81bc6c5ea6SVolker Mische    can_ok/2, can_ok/3,
82bc6c5ea6SVolker Mische    has_attrib/2, is_attrib/3,
83bc6c5ea6SVolker Mische    is_behaviour/2
84bc6c5ea6SVolker Mische]).
85bc6c5ea6SVolker Mische
86bc6c5ea6SVolker Mische-export([
87bc6c5ea6SVolker Mische    dies_ok/2,
88bc6c5ea6SVolker Mische    lives_ok/2,
89bc6c5ea6SVolker Mische    throws_ok/3
90e5813d55SPaul Joseph Davis]).
91bc6c5ea6SVolker Mische
92bc6c5ea6SVolker Mische
93bc6c5ea6SVolker Mische-record(test_state, {
94bc6c5ea6SVolker Mische    planned = 0,
95bc6c5ea6SVolker Mische    count = 0,
96bc6c5ea6SVolker Mische    pass = 0,
97bc6c5ea6SVolker Mische    fail = 0,
98bc6c5ea6SVolker Mische    skip = 0,
99bc6c5ea6SVolker Mische    skip_reason = ""
100bc6c5ea6SVolker Mische}).
101e5813d55SPaul Joseph Davis
102e5813d55SPaul Joseph Davis%% @spec plan(N) -> Result
103e5813d55SPaul Joseph Davis%%       N = unknown | skip | {skip, string()} | integer()
104e5813d55SPaul Joseph Davis%%       Result = ok
105e5813d55SPaul Joseph Davis%% @doc Create a test plan and boot strap the test server.
106e5813d55SPaul Joseph Davisplan(unknown) ->
107e5813d55SPaul Joseph Davis    ensure_test_server(),
108e5813d55SPaul Joseph Davis    etap_server ! {self(), plan, unknown},
109e5813d55SPaul Joseph Davis    ok;
110e5813d55SPaul Joseph Davisplan(skip) ->
111e5813d55SPaul Joseph Davis    io:format("1..0 # skip~n");
112e5813d55SPaul Joseph Davisplan({skip, Reason}) ->
113e5813d55SPaul Joseph Davis    io:format("1..0 # skip ~s~n", [Reason]);
114e5813d55SPaul Joseph Davisplan(N) when is_integer(N), N > 0 ->
115e5813d55SPaul Joseph Davis    ensure_test_server(),
116e5813d55SPaul Joseph Davis    etap_server ! {self(), plan, N},
117e5813d55SPaul Joseph Davis    ok.
118e5813d55SPaul Joseph Davis
119e5813d55SPaul Joseph Davis%% @spec end_tests() -> ok
120e5813d55SPaul Joseph Davis%% @doc End the current test plan and output test results.
121e5813d55SPaul Joseph Davis%% @todo This should probably be done in the test_server process.
122e5813d55SPaul Joseph Davisend_tests() ->
123bc6c5ea6SVolker Mische    Ref = make_ref(),
124bc6c5ea6SVolker Mische    case whereis(etap_server) of
125bc6c5ea6SVolker Mische        undefined -> self() ! {Ref, true};
126bc6c5ea6SVolker Mische        _ -> etap_server ! {self(), state, Ref}
127bc6c5ea6SVolker Mische    end,
128610eba80SDamien Katz    State = receive {Ref, X} -> X end,
129e5813d55SPaul Joseph Davis    if
130bc6c5ea6SVolker Mische        is_record(State, test_state) andalso State#test_state.planned == -1 ->
131e5813d55SPaul Joseph Davis            io:format("1..~p~n", [State#test_state.count]);
132e5813d55SPaul Joseph Davis        true ->
133e5813d55SPaul Joseph Davis            ok
134e5813d55SPaul Joseph Davis    end,
13504bc29f3SArtem Stemkovski    kill().
136e5813d55SPaul Joseph Davis
137e5813d55SPaul Joseph Davisbail() ->
138e5813d55SPaul Joseph Davis    bail("").
139e5813d55SPaul Joseph Davis
140e5813d55SPaul Joseph Davisbail(Reason) ->
141e5813d55SPaul Joseph Davis    etap_server ! {self(), diag, "Bail out! " ++ Reason},
14204bc29f3SArtem Stemkovski    kill().
14304bc29f3SArtem Stemkovski
14404bc29f3SArtem Stemkovskikill() ->
14504bc29f3SArtem Stemkovski    case whereis(etap_server) of
14604bc29f3SArtem Stemkovski        undefined ->
14704bc29f3SArtem Stemkovski            ok;
14804bc29f3SArtem Stemkovski        Pid ->
14904bc29f3SArtem Stemkovski            MRef = erlang:monitor(process, Pid),
15004bc29f3SArtem Stemkovski            Pid ! done,
15104bc29f3SArtem Stemkovski            receive
15204bc29f3SArtem Stemkovski                {'DOWN', MRef, process, _, _} ->
15304bc29f3SArtem Stemkovski                    ok
15404bc29f3SArtem Stemkovski            end
15504bc29f3SArtem Stemkovski    end.
156e5813d55SPaul Joseph Davis
157bc6c5ea6SVolker Mische%% @spec test_state() -> Return
158bc6c5ea6SVolker Mische%%       Return = test_state_record() | {error, string()}
159bc6c5ea6SVolker Mische%% @doc Return the current test state
160bc6c5ea6SVolker Mischetest_state() ->
161bc6c5ea6SVolker Mische    etap_server ! {self(), state},
162bc6c5ea6SVolker Mische    receive
163bc6c5ea6SVolker Mische	X when is_record(X, test_state) -> X
164bc6c5ea6SVolker Mische    after
165bc6c5ea6SVolker Mische	1000 -> {error, "Timed out waiting for etap server reply.~n"}
166bc6c5ea6SVolker Mische    end.
167bc6c5ea6SVolker Mische
168bc6c5ea6SVolker Mische%% @spec failure_count() -> Return
169bc6c5ea6SVolker Mische%%       Return = integer() | {error, string()}
170bc6c5ea6SVolker Mische%% @doc Return the current failure count
171bc6c5ea6SVolker Mischefailure_count() ->
172bc6c5ea6SVolker Mische    case test_state() of
173bc6c5ea6SVolker Mische        #test_state{fail=FailureCount} -> FailureCount;
174bc6c5ea6SVolker Mische        X -> X
175bc6c5ea6SVolker Mische    end.
176bc6c5ea6SVolker Mische
177bc6c5ea6SVolker Mische%% @spec msg(S) -> ok
178bc6c5ea6SVolker Mische%%       S = string()
179bc6c5ea6SVolker Mische%% @doc Print a message in the test output.
180bc6c5ea6SVolker Mischemsg(S) -> etap_server ! {self(), diag, S}, ok.
181bc6c5ea6SVolker Mische
182bc6c5ea6SVolker Mische%% @spec msg(Format, Data) -> ok
183bc6c5ea6SVolker Mische%%      Format = atom() | string() | binary()
184bc6c5ea6SVolker Mische%%      Data = [term()]
185bc6c5ea6SVolker Mische%%      UnicodeList = [Unicode]
186bc6c5ea6SVolker Mische%%      Unicode = int()
187bc6c5ea6SVolker Mische%% @doc Print a message in the test output.
188bc6c5ea6SVolker Mische%% Function arguments are passed through io_lib:format/2.
189bc6c5ea6SVolker Mischemsg(Format, Data) -> msg(io_lib:format(Format, Data)).
190e5813d55SPaul Joseph Davis
191e5813d55SPaul Joseph Davis%% @spec diag(S) -> ok
192e5813d55SPaul Joseph Davis%%       S = string()
193e5813d55SPaul Joseph Davis%% @doc Print a debug/status message related to the test suite.
194bc6c5ea6SVolker Mischediag(S) -> msg("# " ++ S).
195e5813d55SPaul Joseph Davis
196e5813d55SPaul Joseph Davis%% @spec diag(Format, Data) -> ok
197e5813d55SPaul Joseph Davis%%      Format = atom() | string() | binary()
198e5813d55SPaul Joseph Davis%%      Data = [term()]
199e5813d55SPaul Joseph Davis%%      UnicodeList = [Unicode]
200e5813d55SPaul Joseph Davis%%      Unicode = int()
201e5813d55SPaul Joseph Davis%% @doc Print a debug/status message related to the test suite.
202e5813d55SPaul Joseph Davis%% Function arguments are passed through io_lib:format/2.
203e5813d55SPaul Joseph Davisdiag(Format, Data) -> diag(io_lib:format(Format, Data)).
204e5813d55SPaul Joseph Davis
205bc6c5ea6SVolker Mische%% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok
206bc6c5ea6SVolker Mische%%       Got = any()
207bc6c5ea6SVolker Mische%%       Expected = any()
208bc6c5ea6SVolker Mische%%       Desc = string()
209bc6c5ea6SVolker Mische%% @doc Print an expectation mismatch message in the test output.
210bc6c5ea6SVolker Mischeexpectation_mismatch_message(Got, Expected, Desc) ->
211bc6c5ea6SVolker Mische    msg("    ---"),
212bc6c5ea6SVolker Mische    msg("    description: ~p", [Desc]),
213bc6c5ea6SVolker Mische    msg("    found:       ~p", [Got]),
214bc6c5ea6SVolker Mische    msg("    wanted:      ~p", [Expected]),
215bc6c5ea6SVolker Mische    msg("    ..."),
216bc6c5ea6SVolker Mische    ok.
217bc6c5ea6SVolker Mische
218bc6c5ea6SVolker Mische% @spec evaluate(Pass, Got, Expected, Desc) -> Result
219bc6c5ea6SVolker Mische%%       Pass = true | false
220bc6c5ea6SVolker Mische%%       Got = any()
221bc6c5ea6SVolker Mische%%       Expected = any()
222bc6c5ea6SVolker Mische%%       Desc = string()
223bc6c5ea6SVolker Mische%%       Result = true | false
224bc6c5ea6SVolker Mische%% @doc Evaluate a test statement, printing an expectation mismatch message
225bc6c5ea6SVolker Mische%%       if the test failed.
226bc6c5ea6SVolker Mischeevaluate(Pass, Got, Expected, Desc) ->
227bc6c5ea6SVolker Mische    case mk_tap(Pass, Desc) of
228bc6c5ea6SVolker Mische        false ->
229bc6c5ea6SVolker Mische            expectation_mismatch_message(Got, Expected, Desc),
230bc6c5ea6SVolker Mische            false;
231bc6c5ea6SVolker Mische        true ->
232bc6c5ea6SVolker Mische            true
233bc6c5ea6SVolker Mische    end.
234bc6c5ea6SVolker Mische
235e5813d55SPaul Joseph Davis%% @spec ok(Expr, Desc) -> Result
236e5813d55SPaul Joseph Davis%%       Expr = true | false
237e5813d55SPaul Joseph Davis%%       Desc = string()
238e5813d55SPaul Joseph Davis%%       Result = true | false
239e5813d55SPaul Joseph Davis%% @doc Assert that a statement is true.
240bc6c5ea6SVolker Mischeok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc).
241e5813d55SPaul Joseph Davis
242e5813d55SPaul Joseph Davis%% @spec not_ok(Expr, Desc) -> Result
243e5813d55SPaul Joseph Davis%%       Expr = true | false
244e5813d55SPaul Joseph Davis%%       Desc = string()
245e5813d55SPaul Joseph Davis%%       Result = true | false
246e5813d55SPaul Joseph Davis%% @doc Assert that a statement is false.
247bc6c5ea6SVolker Mischenot_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc).
248bc6c5ea6SVolker Mische
249bc6c5ea6SVolker Mische%% @spec is_ok(Expr, Desc) -> Result
250bc6c5ea6SVolker Mische%%       Expr = any()
251bc6c5ea6SVolker Mische%%       Desc = string()
252bc6c5ea6SVolker Mische%%       Result = true | false
253bc6c5ea6SVolker Mische%% @doc Assert that two values are the same.
254bc6c5ea6SVolker Mischeis_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc).
255e5813d55SPaul Joseph Davis
256e5813d55SPaul Joseph Davis%% @spec is(Got, Expected, Desc) -> Result
257e5813d55SPaul Joseph Davis%%       Got = any()
258e5813d55SPaul Joseph Davis%%       Expected = any()
259e5813d55SPaul Joseph Davis%%       Desc = string()
260e5813d55SPaul Joseph Davis%%       Result = true | false
261e5813d55SPaul Joseph Davis%% @doc Assert that two values are the same.
262bc6c5ea6SVolker Mischeis(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc).
263e5813d55SPaul Joseph Davis
264e5813d55SPaul Joseph Davis%% @spec isnt(Got, Expected, Desc) -> Result
265e5813d55SPaul Joseph Davis%%       Got = any()
266e5813d55SPaul Joseph Davis%%       Expected = any()
267e5813d55SPaul Joseph Davis%%       Desc = string()
268e5813d55SPaul Joseph Davis%%       Result = true | false
269e5813d55SPaul Joseph Davis%% @doc Assert that two values are not the same.
270bc6c5ea6SVolker Mischeisnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc).
271e5813d55SPaul Joseph Davis
272e5813d55SPaul Joseph Davis%% @spec is_greater(ValueA, ValueB, Desc) -> Result
273e5813d55SPaul Joseph Davis%%       ValueA = number()
274e5813d55SPaul Joseph Davis%%       ValueB = number()
275e5813d55SPaul Joseph Davis%%       Desc = string()
276e5813d55SPaul Joseph Davis%%       Result = true | false
277e5813d55SPaul Joseph Davis%% @doc Assert that an integer is greater than another.
278e5813d55SPaul Joseph Davisis_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) ->
279e5813d55SPaul Joseph Davis    mk_tap(ValueA > ValueB, Desc).
280e5813d55SPaul Joseph Davis
281e5813d55SPaul Joseph Davis%% @spec any(Got, Items, Desc) -> Result
282e5813d55SPaul Joseph Davis%%       Got = any()
283e5813d55SPaul Joseph Davis%%       Items = [any()]
284e5813d55SPaul Joseph Davis%%       Desc = string()
285e5813d55SPaul Joseph Davis%%       Result = true | false
286e5813d55SPaul Joseph Davis%% @doc Assert that an item is in a list.
287bc6c5ea6SVolker Mischeany(Got, Items, Desc) when is_function(Got) ->
288bc6c5ea6SVolker Mische    is(lists:any(Got, Items), true, Desc);
289e5813d55SPaul Joseph Davisany(Got, Items, Desc) ->
290e5813d55SPaul Joseph Davis    is(lists:member(Got, Items), true, Desc).
291e5813d55SPaul Joseph Davis
292e5813d55SPaul Joseph Davis%% @spec none(Got, Items, Desc) -> Result
293e5813d55SPaul Joseph Davis%%       Got = any()
294e5813d55SPaul Joseph Davis%%       Items = [any()]
295e5813d55SPaul Joseph Davis%%       Desc = string()
296e5813d55SPaul Joseph Davis%%       Result = true | false
297e5813d55SPaul Joseph Davis%% @doc Assert that an item is not in a list.
298bc6c5ea6SVolker Mischenone(Got, Items, Desc) when is_function(Got) ->
299bc6c5ea6SVolker Mische    is(lists:any(Got, Items), false, Desc);
300e5813d55SPaul Joseph Davisnone(Got, Items, Desc) ->
301e5813d55SPaul Joseph Davis    is(lists:member(Got, Items), false, Desc).
302e5813d55SPaul Joseph Davis
303e5813d55SPaul Joseph Davis%% @spec fun_is(Fun, Expected, Desc) -> Result
304e5813d55SPaul Joseph Davis%%       Fun = function()
305e5813d55SPaul Joseph Davis%%       Expected = any()
306e5813d55SPaul Joseph Davis%%       Desc = string()
307e5813d55SPaul Joseph Davis%%       Result = true | false
308e5813d55SPaul Joseph Davis%% @doc Use an anonymous function to assert a pattern match.
309e5813d55SPaul Joseph Davisfun_is(Fun, Expected, Desc) when is_function(Fun) ->
310e5813d55SPaul Joseph Davis    is(Fun(Expected), true, Desc).
311e5813d55SPaul Joseph Davis
312bc6c5ea6SVolker Mische%% @spec expect_fun(ExpectFun, Got, Desc) -> Result
313bc6c5ea6SVolker Mische%%       ExpectFun = function()
314bc6c5ea6SVolker Mische%%       Got = any()
315bc6c5ea6SVolker Mische%%       Desc = string()
316bc6c5ea6SVolker Mische%%       Result = true | false
317bc6c5ea6SVolker Mische%% @doc Use an anonymous function to assert a pattern match, using actual
318bc6c5ea6SVolker Mische%%       value as the argument to the function.
319bc6c5ea6SVolker Mischeexpect_fun(ExpectFun, Got, Desc) ->
320bc6c5ea6SVolker Mische    evaluate(ExpectFun(Got), Got, ExpectFun, Desc).
321bc6c5ea6SVolker Mische
322bc6c5ea6SVolker Mische%% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result
323bc6c5ea6SVolker Mische%%       ExpectFun = function()
324bc6c5ea6SVolker Mische%%       Got = any()
325bc6c5ea6SVolker Mische%%       Desc = string()
326bc6c5ea6SVolker Mische%%       ExpectStr = string()
327bc6c5ea6SVolker Mische%%       Result = true | false
328bc6c5ea6SVolker Mische%% @doc Use an anonymous function to assert a pattern match, using actual
329bc6c5ea6SVolker Mische%%       value as the argument to the function.
330bc6c5ea6SVolker Mischeexpect_fun(ExpectFun, Got, Desc, ExpectStr) ->
331bc6c5ea6SVolker Mische    evaluate(ExpectFun(Got), Got, ExpectStr, Desc).
332bc6c5ea6SVolker Mische
333e5813d55SPaul Joseph Davis%% @equiv skip(TestFun, "")
334e5813d55SPaul Joseph Davisskip(TestFun) when is_function(TestFun) ->
335e5813d55SPaul Joseph Davis    skip(TestFun, "").
336e5813d55SPaul Joseph Davis
337e5813d55SPaul Joseph Davis%% @spec skip(TestFun, Reason) -> ok
338e5813d55SPaul Joseph Davis%%       TestFun = function()
339e5813d55SPaul Joseph Davis%%       Reason = string()
340e5813d55SPaul Joseph Davis%% @doc Skip a test.
341e5813d55SPaul Joseph Davisskip(TestFun, Reason) when is_function(TestFun), is_list(Reason) ->
342e5813d55SPaul Joseph Davis    begin_skip(Reason),
343e5813d55SPaul Joseph Davis    catch TestFun(),
344e5813d55SPaul Joseph Davis    end_skip(),
345e5813d55SPaul Joseph Davis    ok.
346e5813d55SPaul Joseph Davis
347e5813d55SPaul Joseph Davis%% @spec skip(Q, TestFun, Reason) -> ok
348bc6c5ea6SVolker Mische%%       Q = true | false | function()
349e5813d55SPaul Joseph Davis%%       TestFun = function()
350e5813d55SPaul Joseph Davis%%       Reason = string()
351e5813d55SPaul Joseph Davis%% @doc Skips a test conditionally. The first argument to this function can
352e5813d55SPaul Joseph Davis%% either be the 'true' or 'false' atoms or a function that returns 'true' or
353e5813d55SPaul Joseph Davis%% 'false'.
354e5813d55SPaul Joseph Davisskip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) ->
355e5813d55SPaul Joseph Davis    case QFun() of
356e5813d55SPaul Joseph Davis        true -> begin_skip(Reason), TestFun(), end_skip();
357e5813d55SPaul Joseph Davis        _ -> TestFun()
358e5813d55SPaul Joseph Davis    end,
359e5813d55SPaul Joseph Davis    ok;
360e5813d55SPaul Joseph Davis
361e5813d55SPaul Joseph Davisskip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true ->
362e5813d55SPaul Joseph Davis    begin_skip(Reason),
363e5813d55SPaul Joseph Davis    TestFun(),
364e5813d55SPaul Joseph Davis    end_skip(),
365e5813d55SPaul Joseph Davis    ok;
366e5813d55SPaul Joseph Davis
367e5813d55SPaul Joseph Davisskip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) ->
368e5813d55SPaul Joseph Davis    TestFun(),
369e5813d55SPaul Joseph Davis    ok.
370e5813d55SPaul Joseph Davis
371e5813d55SPaul Joseph Davis%% @private
372e5813d55SPaul Joseph Davisbegin_skip(Reason) ->
373e5813d55SPaul Joseph Davis    etap_server ! {self(), begin_skip, Reason}.
374e5813d55SPaul Joseph Davis
375e5813d55SPaul Joseph Davis%% @private
376e5813d55SPaul Joseph Davisend_skip() ->
377e5813d55SPaul Joseph Davis    etap_server ! {self(), end_skip}.
378e5813d55SPaul Joseph Davis
379bc6c5ea6SVolker Mische%% @spec contains_ok(string(), string(), string()) -> true | false
380bc6c5ea6SVolker Mische%% @doc Assert that a string is contained in another string.
381bc6c5ea6SVolker Mischecontains_ok(Source, String, Desc) ->
382bc6c5ea6SVolker Mische    etap:isnt(
383bc6c5ea6SVolker Mische        string:str(Source, String),
384bc6c5ea6SVolker Mische        0,
385bc6c5ea6SVolker Mische        Desc
386bc6c5ea6SVolker Mische    ).
387bc6c5ea6SVolker Mische
388bc6c5ea6SVolker Mische%% @spec is_before(string(), string(), string(), string()) -> true | false
389bc6c5ea6SVolker Mische%% @doc Assert that a string comes before another string within a larger body.
390bc6c5ea6SVolker Mischeis_before(Source, StringA, StringB, Desc) ->
391bc6c5ea6SVolker Mische    etap:is_greater(
392bc6c5ea6SVolker Mische        string:str(Source, StringB),
393bc6c5ea6SVolker Mische        string:str(Source, StringA),
394bc6c5ea6SVolker Mische        Desc
395bc6c5ea6SVolker Mische    ).
396bc6c5ea6SVolker Mische
397bc6c5ea6SVolker Mische%% @doc Assert that a given variable is a pid.
398bc6c5ea6SVolker Mischeis_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc);
399bc6c5ea6SVolker Mischeis_pid(_, Desc) -> etap:ok(false, Desc).
400bc6c5ea6SVolker Mische
401bc6c5ea6SVolker Mische%% @doc Assert that a given process/pid is alive.
402bc6c5ea6SVolker Mischeis_alive(Pid, Desc) ->
403bc6c5ea6SVolker Mische    etap:ok(erlang:is_process_alive(Pid), Desc).
404bc6c5ea6SVolker Mische
405bc6c5ea6SVolker Mische%% @doc Assert that the current function of a pid is a given {M, F, A} tuple.
406bc6c5ea6SVolker Mischeis_mfa(Pid, MFA, Desc) ->
407bc6c5ea6SVolker Mische    etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc).
408bc6c5ea6SVolker Mische
409bc6c5ea6SVolker Mische%% @spec loaded_ok(atom(), string()) -> true | false
410bc6c5ea6SVolker Mische%% @doc Assert that a module has been loaded successfully.
411bc6c5ea6SVolker Mischeloaded_ok(M, Desc) when is_atom(M) ->
412bc6c5ea6SVolker Mische    etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc).
413bc6c5ea6SVolker Mische
414bc6c5ea6SVolker Mische%% @spec can_ok(atom(), atom()) -> true | false
415bc6c5ea6SVolker Mische%% @doc Assert that a module exports a given function.
416bc6c5ea6SVolker Mischecan_ok(M, F) when is_atom(M), is_atom(F) ->
417bc6c5ea6SVolker Mische    Matches = [X || {X, _} <- M:module_info(exports), X == F],
418bc6c5ea6SVolker Mische    etap:ok(Matches > 0, lists:concat([M, " can ", F])).
419bc6c5ea6SVolker Mische
420bc6c5ea6SVolker Mische%% @spec can_ok(atom(), atom(), integer()) -> true | false
421bc6c5ea6SVolker Mische%% @doc Assert that a module exports a given function with a given arity.
422bc6c5ea6SVolker Mischecan_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) ->
423bc6c5ea6SVolker Mische    Matches = [X || X <- M:module_info(exports), X == {F, A}],
424bc6c5ea6SVolker Mische    etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])).
425bc6c5ea6SVolker Mische
426bc6c5ea6SVolker Mische%% @spec has_attrib(M, A) -> true | false
427bc6c5ea6SVolker Mische%%       M = atom()
428bc6c5ea6SVolker Mische%%       A = atom()
429bc6c5ea6SVolker Mische%% @doc Asserts that a module has a given attribute.
430bc6c5ea6SVolker Mischehas_attrib(M, A) when is_atom(M), is_atom(A) ->
431bc6c5ea6SVolker Mische    etap:isnt(
432bc6c5ea6SVolker Mische        proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'),
433bc6c5ea6SVolker Mische        'asdlkjasdlkads',
434bc6c5ea6SVolker Mische        lists:concat([M, " has attribute ", A])
435bc6c5ea6SVolker Mische    ).
436bc6c5ea6SVolker Mische
437bc6c5ea6SVolker Mische%% @spec has_attrib(M, A. V) -> true | false
438bc6c5ea6SVolker Mische%%       M = atom()
439bc6c5ea6SVolker Mische%%       A = atom()
440bc6c5ea6SVolker Mische%%       V = any()
441bc6c5ea6SVolker Mische%% @doc Asserts that a module has a given attribute with a given value.
442bc6c5ea6SVolker Mischeis_attrib(M, A, V) when is_atom(M) andalso is_atom(A) ->
443bc6c5ea6SVolker Mische    etap:is(
444bc6c5ea6SVolker Mische        proplists:get_value(A, M:module_info(attributes)),
445bc6c5ea6SVolker Mische        [V],
446bc6c5ea6SVolker Mische        lists:concat([M, "'s ", A, " is ", V])
447bc6c5ea6SVolker Mische    ).
448bc6c5ea6SVolker Mische
449bc6c5ea6SVolker Mische%% @spec is_behavior(M, B) -> true | false
450bc6c5ea6SVolker Mische%%       M = atom()
451bc6c5ea6SVolker Mische%%       B = atom()
452bc6c5ea6SVolker Mische%% @doc Asserts that a given module has a specific behavior.
453bc6c5ea6SVolker Mischeis_behaviour(M, B) when is_atom(M) andalso is_atom(B) ->
454bc6c5ea6SVolker Mische    is_attrib(M, behaviour, B).
455bc6c5ea6SVolker Mische
456bc6c5ea6SVolker Mische%% @doc Assert that an exception is raised when running a given function.
457bc6c5ea6SVolker Mischedies_ok(F, Desc) ->
458bc6c5ea6SVolker Mische    case (catch F()) of
459bc6c5ea6SVolker Mische        {'EXIT', _} -> etap:ok(true, Desc);
460bc6c5ea6SVolker Mische        _ -> etap:ok(false, Desc)
461bc6c5ea6SVolker Mische    end.
462bc6c5ea6SVolker Mische
463bc6c5ea6SVolker Mische%% @doc Assert that an exception is not raised when running a given function.
464bc6c5ea6SVolker Mischelives_ok(F, Desc) ->
465bc6c5ea6SVolker Mische    etap:is(try_this(F), success, Desc).
466bc6c5ea6SVolker Mische
467bc6c5ea6SVolker Mische%% @doc Assert that the exception thrown by a function matches the given exception.
468bc6c5ea6SVolker Mischethrows_ok(F, Exception, Desc) ->
469bc6c5ea6SVolker Mische    try F() of
470bc6c5ea6SVolker Mische        _ -> etap:ok(nok, Desc)
471bc6c5ea6SVolker Mische    catch
472bc6c5ea6SVolker Mische        _:E ->
473bc6c5ea6SVolker Mische            etap:is(E, Exception, Desc)
474bc6c5ea6SVolker Mische    end.
475bc6c5ea6SVolker Mische
476bc6c5ea6SVolker Mische%% @private
477bc6c5ea6SVolker Mische%% @doc Run a function and catch any exceptions.
478bc6c5ea6SVolker Mischetry_this(F) when is_function(F, 0) ->
479bc6c5ea6SVolker Mische    try F() of
480bc6c5ea6SVolker Mische        _ -> success
481bc6c5ea6SVolker Mische    catch
482bc6c5ea6SVolker Mische        throw:E -> {throw, E};
483bc6c5ea6SVolker Mische        error:E -> {error, E};
484bc6c5ea6SVolker Mische        exit:E -> {exit, E}
485bc6c5ea6SVolker Mische    end.
486e5813d55SPaul Joseph Davis
487e5813d55SPaul Joseph Davis%% @private
488e5813d55SPaul Joseph Davis%% @doc Start the etap_server process if it is not running already.
489e5813d55SPaul Joseph Davisensure_test_server() ->
490e5813d55SPaul Joseph Davis    case whereis(etap_server) of
491e5813d55SPaul Joseph Davis        undefined ->
492e5813d55SPaul Joseph Davis            proc_lib:start(?MODULE, start_etap_server,[]);
493e5813d55SPaul Joseph Davis        _ ->
494e5813d55SPaul Joseph Davis            diag("The test server is already running.")
495e5813d55SPaul Joseph Davis    end.
496e5813d55SPaul Joseph Davis
497e5813d55SPaul Joseph Davis%% @private
498e5813d55SPaul Joseph Davis%% @doc Start the etap_server loop and register itself as the etap_server
499e5813d55SPaul Joseph Davis%% process.
500e5813d55SPaul Joseph Davisstart_etap_server() ->
501e5813d55SPaul Joseph Davis    catch register(etap_server, self()),
502e5813d55SPaul Joseph Davis    proc_lib:init_ack(ok),
503e5813d55SPaul Joseph Davis    etap:test_server(#test_state{
504e5813d55SPaul Joseph Davis        planned = 0,
505e5813d55SPaul Joseph Davis        count = 0,
506e5813d55SPaul Joseph Davis        pass = 0,