%% Copyright (c) 2008-2009 Nick Gerakines %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. %% %% @author Nick Gerakines [http://socklabs.com/] %% @author Jeremy Wall %% @version 0.3.4 %% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines %% @reference http://testanything.org/wiki/index.php/Main_Page %% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol %% @todo Finish implementing the skip directive. %% @todo Document the messages handled by this receive loop. %% @todo Explain in documentation why we use a process to handle test input. %% @doc etap is a TAP testing module for Erlang components and applications. %% This module allows developers to test their software using the TAP method. %% %%

%% TAP, the Test Anything Protocol, is a simple text-based interface between %% testing modules in a test harness. TAP started life as part of the test %% harness for Perl but now has implementations in C/C++, Python, PHP, Perl %% and probably others by the time you read this. %%

%% %% The testing process begins by defining a plan using etap:plan/1, running %% a number of etap tests and then calling eta:end_tests/0. Please refer to %% the Erlang modules in the t directory of this project for example tests. -module(etap). -vsn("0.3.4"). -export([ ensure_test_server/0, start_etap_server/0, test_server/1, msg/1, msg/2, diag/1, diag/2, expectation_mismatch_message/3, plan/1, end_tests/0, not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3, fun_is/3, expect_fun/3, expect_fun/4, is_greater/3, skip/1, skip/2, datetime/1, skip/3, bail/0, bail/1, test_state/0, failure_count/0 ]). -export([ contains_ok/3, is_before/4 ]). -export([ is_pid/2, is_alive/2, is_mfa/3 ]). -export([ loaded_ok/2, can_ok/2, can_ok/3, has_attrib/2, is_attrib/3, is_behaviour/2 ]). -export([ dies_ok/2, lives_ok/2, throws_ok/3 ]). -record(test_state, { planned = 0, count = 0, pass = 0, fail = 0, skip = 0, skip_reason = "" }). %% @spec plan(N) -> Result %% N = unknown | skip | {skip, string()} | integer() %% Result = ok %% @doc Create a test plan and boot strap the test server. plan(unknown) -> ensure_test_server(), etap_server ! {self(), plan, unknown}, ok; plan(skip) -> io:format("1..0 # skip~n"); plan({skip, Reason}) -> io:format("1..0 # skip ~s~n", [Reason]); plan(N) when is_integer(N), N > 0 -> ensure_test_server(), etap_server ! {self(), plan, N}, ok. %% @spec end_tests() -> ok %% @doc End the current test plan and output test results. %% @todo This should probably be done in the test_server process. end_tests() -> case whereis(etap_server) of undefined -> self() ! true; _ -> etap_server ! {self(), state} end, State = receive X -> X end, if State#test_state.planned == -1 -> io:format("1..~p~n", [State#test_state.count]); true -> ok end, case whereis(etap_server) of undefined -> ok; _ -> etap_server ! done, ok end. bail() -> bail(""). bail(Reason) -> etap_server ! {self(), diag, "Bail out! " ++ Reason}, etap_server ! done, ok, ok. %% @spec test_state() -> Return %% Return = test_state_record() | {error, string()} %% @doc Return the current test state test_state() -> etap_server ! {self(), state}, receive X when is_record(X, test_state) -> X after 1000 -> {error, "Timed out waiting for etap server reply.~n"} end. %% @spec failure_count() -> Return %% Return = integer() | {error, string()} %% @doc Return the current failure count failure_count() -> case test_state() of #test_state{fail=FailureCount} -> FailureCount; X -> X end. %% @spec msg(S) -> ok %% S = string() %% @doc Print a message in the test output. msg(S) -> etap_server ! {self(), diag, S}, ok. %% @spec msg(Format, Data) -> ok %% Format = atom() | string() | binary() %% Data = [term()] %% UnicodeList = [Unicode] %% Unicode = int() %% @doc Print a message in the test output. %% Function arguments are passed through io_lib:format/2. msg(Format, Data) -> msg(io_lib:format(Format, Data)). %% @spec diag(S) -> ok %% S = string() %% @doc Print a debug/status message related to the test suite. diag(S) -> msg("# " ++ S). %% @spec diag(Format, Data) -> ok %% Format = atom() | string() | binary() %% Data = [term()] %% UnicodeList = [Unicode] %% Unicode = int() %% @doc Print a debug/status message related to the test suite. %% Function arguments are passed through io_lib:format/2. diag(Format, Data) -> diag(io_lib:format(Format, Data)). %% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok %% Got = any() %% Expected = any() %% Desc = string() %% @doc Print an expectation mismatch message in the test output. expectation_mismatch_message(Got, Expected, Desc) -> msg(" ---"), msg(" description: ~p", [Desc]), msg(" found: ~p", [Got]), msg(" wanted: ~p", [Expected]), msg(" ..."), ok. % @spec evaluate(Pass, Got, Expected, Desc) -> Result %% Pass = true | false %% Got = any() %% Expected = any() %% Desc = string() %% Result = true | false %% @doc Evaluate a test statement, printing an expectation mismatch message %% if the test failed. evaluate(Pass, Got, Expected, Desc) -> case mk_tap(Pass, Desc) of false -> expectation_mismatch_message(Got, Expected, Desc), false; true -> true end. %% @spec ok(Expr, Desc) -> Result %% Expr = true | false %% Desc = string() %% Result = true | false %% @doc Assert that a statement is true. ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc). %% @spec not_ok(Expr, Desc) -> Result %% Expr = true | false %% Desc = string() %% Result = true | false %% @doc Assert that a statement is false. not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc). %% @spec is_ok(Expr, Desc) -> Result %% Expr = any() %% Desc = string() %% Result = true | false %% @doc Assert that two values are the same. is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc). %% @spec is(Got, Expected, Desc) -> Result %% Got = any() %% Expected = any() %% Desc = string() %% Result = true | false %% @doc Assert that two values are the same. is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc). %% @spec isnt(Got, Expected, Desc) -> Result %% Got = any() %% Expected = any() %% Desc = string() %% Result = true | false %% @doc Assert that two values are not the same. isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc). %% @spec is_greater(ValueA, ValueB, Desc) -> Result %% ValueA = number() %% ValueB = number() %% Desc = string() %% Result = true | false %% @doc Assert that an integer is greater than another. is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> mk_tap(ValueA > ValueB, Desc). %% @spec any(Got, Items, Desc) -> Result %% Got = any() %% Items = [any()] %% Desc = string() %% Result = true | false %% @doc Assert that an item is in a list. any(Got, Items, Desc) when is_function(Got) -> is(lists:any(Got, Items), true, Desc); any(Got, Items, Desc) -> is(lists:member(Got, Items), true, Desc). %% @spec none(Got, Items, Desc) -> Result %% Got = any() %% Items = [any()] %% Desc = string() %% Result = true | false %% @doc Assert that an item is not in a list. none(Got, Items, Desc) when is_function(Got) -> is(lists:any(Got, Items), false, Desc); none(Got, Items, Desc) -> is(lists:member(Got, Items), false, Desc). %% @spec fun_is(Fun, Expected, Desc) -> Result %% Fun = function() %% Expected = any() %% Desc = string() %% Result = true | false %% @doc Use an anonymous function to assert a pattern match. fun_is(Fun, Expected, Desc) when is_function(Fun) -> is(Fun(Expected), true, Desc). %% @spec expect_fun(ExpectFun, Got, Desc) -> Result %% ExpectFun = function() %% Got = any() %% Desc = string() %% Result = true | false %% @doc Use an anonymous function to assert a pattern match, using actual %% value as the argument to the function. expect_fun(ExpectFun, Got, Desc) -> evaluate(ExpectFun(Got), Got, ExpectFun, Desc). %% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result %% ExpectFun = function() %% Got = any() %% Desc = string() %% ExpectStr = string() %% Result = true | false %% @doc Use an anonymous function to assert a pattern match, using actual %% value as the argument to the function. expect_fun(ExpectFun, Got, Desc, ExpectStr) -> evaluate(ExpectFun(Got), Got, ExpectStr, Desc). %% @equiv skip(TestFun, "") skip(TestFun) when is_function(TestFun) -> skip(TestFun, ""). %% @spec skip(TestFun, Reason) -> ok %% TestFun = function() %% Reason = string() %% @doc Skip a test. skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) -> begin_skip(Reason), catch TestFun(), end_skip(), ok. %% @spec skip(Q, TestFun, Reason) -> ok %% Q = true | false | function() %% TestFun = function() %% Reason = string() %% @doc Skips a test conditionally. The first argument to this function can %% either be the 'true' or 'false' atoms or a function that returns 'true' or %% 'false'. skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) -> case QFun() of true -> begin_skip(Reason), TestFun(), end_skip(); _ -> TestFun() end, ok; skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true -> begin_skip(Reason), TestFun(), end_skip(), ok; skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) -> TestFun(), ok. %% @private begin_skip(Reason) -> etap_server ! {self(), begin_skip, Reason}. %% @private end_skip() -> etap_server ! {self(), end_skip}. %% @spec contains_ok(string(), string(), string()) -> true | false %% @doc Assert that a string is contained in another string. contains_ok(Source, String, Desc) -> etap:isnt( string:str(Source, String), 0, Desc ). %% @spec is_before(string(), string(), string(), string()) -> true | false %% @doc Assert that a string comes before another string within a larger body. is_before(Source, StringA, StringB, Desc) -> etap:is_greater( string:str(Source, StringB), string:str(Source, StringA), Desc ). %% @doc Assert that a given variable is a pid. is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); is_pid(_, Desc) -> etap:ok(false, Desc). %% @doc Assert that a given process/pid is alive. is_alive(Pid, Desc) -> etap:ok(erlang:is_process_alive(Pid), Desc). %% @doc Assert that the current function of a pid is a given {M, F, A} tuple. is_mfa(Pid, MFA, Desc) -> etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). %% @spec loaded_ok(atom(), string()) -> true | false %% @doc Assert that a module has been loaded successfully. loaded_ok(M, Desc) when is_atom(M) -> etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). %% @spec can_ok(atom(), atom()) -> true | false %% @doc Assert that a module exports a given function. can_ok(M, F) when is_atom(M), is_atom(F) -> Matches = [X || {X, _} <- M:module_info(exports), X == F], etap:ok(Matches > 0, lists:concat([M, " can ", F])). %% @spec can_ok(atom(), atom(), integer()) -> true | false %% @doc Assert that a module exports a given function with a given arity. can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> Matches = [X || X <- M:module_info(exports), X == {F, A}], etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). %% @spec has_attrib(M, A) -> true | false %% M = atom() %% A = atom() %% @doc Asserts that a module has a given attribute. has_attrib(M, A) when is_atom(M), is_atom(A) -> etap:isnt( proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), 'asdlkjasdlkads', lists:concat([M, " has attribute ", A]) ). %% @spec has_attrib(M, A. V) -> true | false %% M = atom() %% A = atom() %% V = any() %% @doc Asserts that a module has a given attribute with a given value. is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> etap:is( proplists:get_value(A, M:module_info(attributes)), [V], lists:concat([M, "'s ", A, " is ", V]) ). %% @spec is_behavior(M, B) -> true | false %% M = atom() %% B = atom() %% @doc Asserts that a given module has a specific behavior. is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> is_attrib(M, behaviour, B). %% @doc Assert that an exception is raised when running a given function. dies_ok(F, Desc) -> case (catch F()) of {'EXIT', _} -> etap:ok(true, Desc); _ -> etap:ok(false, Desc) end. %% @doc Assert that an exception is not raised when running a given function. lives_ok(F, Desc) -> etap:is(try_this(F), success, Desc). %% @doc Assert that the exception thrown by a function matches the given exception. throws_ok(F, Exception, Desc) -> try F() of _ -> etap:ok(nok, Desc) catch _:E -> etap:is(E, Exception, Desc) end. %% @private %% @doc Run a function and catch any exceptions. try_this(F) when is_function(F, 0) -> try F() of _ -> success catch throw:E -> {throw, E}; error:E -> {error, E}; exit:E -> {exit, E} end. %% @private %% @doc Start the etap_server process if it is not running already. ensure_test_server() -> case whereis(etap_server) of undefined -> proc_lib:start(?MODULE, start_etap_server,[]); _ -> diag("The test server is already running.") end. %% @private %% @doc Start the etap_server loop and register itself as the etap_server %% process. start_etap_server() -> catch register(etap_server, self()), proc_lib:init_ack(ok), etap:test_server(#test_state{ planned = 0, count = 0, pass = 0, fail = 0, skip = 0, skip_reason = "" }). %% @private %% @doc The main etap_server receive/run loop. The etap_server receive loop %% responds to seven messages apperatining to failure or passing of tests. %% It is also used to initiate the testing process with the {_, plan, _} %% message that clears the current test state. test_server(State) -> NewState = receive {_From, plan, unknown} -> io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), State#test_state{ planned = -1, count = 0, pass = 0, fail = 0, skip = 0, skip_reason = "" }; {_From, plan, N} -> io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), io:format("1..~p~n", [N]), State#test_state{ planned = N, count = 0, pass = 0, fail = 0, skip = 0, skip_reason = "" }; {_From, begin_skip, Reason} -> State#test_state{ skip = 1, skip_reason = Reason }; {_From, end_skip} -> State#test_state{ skip = 0, skip_reason = "" }; {_From, pass, Desc} -> FullMessage = skip_diag( " - " ++ Desc, State#test_state.skip, State#test_state.skip_reason ), io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), State#test_state{ count = State#test_state.count + 1, pass = State#test_state.pass + 1 }; {_From, fail, Desc} -> FullMessage = skip_diag( " - " ++ Desc, State#test_state.skip, State#test_state.skip_reason ), io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), State#test_state{ count = State#test_state.count + 1, fail = State#test_state.fail + 1 }; {From, state} -> From ! State, State; {_From, diag, Message} -> io:format("~s~n", [Message]), State; {From, count} -> From ! State#test_state.count, State; {From, is_skip} -> From ! State#test_state.skip, State; done -> exit(normal) end, test_server(NewState). %% @private %% @doc Process the result of a test and send it to the etap_server process. mk_tap(Result, Desc) -> IsSkip = lib:sendw(etap_server, is_skip), case [IsSkip, Result] of [_, true] -> etap_server ! {self(), pass, Desc}, true; [1, _] -> etap_server ! {self(), pass, Desc}, true; _ -> etap_server ! {self(), fail, Desc}, false end. %% @private %% @doc Format a date/time string. datetime(DateTime) -> {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime, io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B", [Year, Month, Day, Hour, Min, Sec]). %% @private %% @doc Craft an output message taking skip/todo into consideration. skip_diag(Message, 0, _) -> Message; skip_diag(_Message, 1, "") -> " # SKIP"; skip_diag(_Message, 1, Reason) -> " # SKIP : " ++ Reason.