|
|
- %% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net>
- %%
- %% 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 <nick@gerakines.net> [http://socklabs.com/]
- %% @author Jeremy Wall <jeremy@marzhillstudios.com>
- %% @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.
- %%
- %% <blockquote cite="http://en.wikipedia.org/wiki/Test_Anything_Protocol"><p>
- %% 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.
- %% </p></blockquote>
- %%
- %% 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.
|