summaryrefslogtreecommitdiff
path: root/src/etap/etap.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/etap/etap.erl')
-rw-r--r--src/etap/etap.erl330
1 files changed, 263 insertions, 67 deletions
diff --git a/src/etap/etap.erl b/src/etap/etap.erl
index c76b980b9..7380013f0 100644
--- a/src/etap/etap.erl
+++ b/src/etap/etap.erl
@@ -44,22 +44,66 @@
%% 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,
- diag/1, diag/2, plan/1, end_tests/0, not_ok/2, ok/2, is/3, isnt/3,
- any/3, none/3, fun_is/3, is_greater/3, skip/1, skip/2,
- ensure_coverage_starts/0, ensure_coverage_ends/0, coverage_report/0,
- datetime/1, skip/3, bail/0, bail/1
+ 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 = ""}).
--vsn("0.3.4").
+
+
+-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_coverage_starts(),
ensure_test_server(),
etap_server ! {self(), plan, unknown},
ok;
@@ -68,7 +112,6 @@ plan(skip) ->
plan({skip, Reason}) ->
io:format("1..0 # skip ~s~n", [Reason]);
plan(N) when is_integer(N), N > 0 ->
- ensure_coverage_starts(),
ensure_test_server(),
etap_server ! {self(), plan, N},
ok.
@@ -78,8 +121,10 @@ plan(N) when is_integer(N), N > 0 ->
%% @todo This should probably be done in the test_server process.
end_tests() ->
timer:sleep(100),
- ensure_coverage_ends(),
- etap_server ! {self(), state},
+ case whereis(etap_server) of
+ undefined -> self() ! true;
+ _ -> etap_server ! {self(), state}
+ end,
State = receive X -> X end,
if
State#test_state.planned == -1 ->
@@ -92,58 +137,52 @@ end_tests() ->
_ -> etap_server ! done, ok
end.
-%% @private
-ensure_coverage_starts() ->
- case os:getenv("COVER") of
- false -> ok;
- _ ->
- BeamDir = case os:getenv("COVER_BIN") of false -> "ebin"; X -> X end,
- cover:compile_beam_directory(BeamDir)
- end.
-
-%% @private
-%% @doc Attempts to write out any collected coverage data to the cover/
-%% directory. This function should not be called externally, but it could be.
-ensure_coverage_ends() ->
- case os:getenv("COVER") of
- false -> ok;
- _ ->
- filelib:ensure_dir("cover/"),
- Name = lists:flatten([
- io_lib:format("~.16b", [X]) || X <- binary_to_list(erlang:md5(
- term_to_binary({make_ref(), now()})
- ))
- ]),
- cover:export("cover/" ++ Name ++ ".coverdata")
- end.
-
-%% @spec coverage_report() -> ok
-%% @doc Use the cover module's covreage report builder to create code coverage
-%% reports from recently created coverdata files.
-coverage_report() ->
- [cover:import(File) || File <- filelib:wildcard("cover/*.coverdata")],
- lists:foreach(
- fun(Mod) ->
- cover:analyse_to_file(Mod, atom_to_list(Mod) ++ "_coverage.txt", [])
- end,
- cover:imported_modules()
- ),
- ok.
-
bail() ->
bail("").
bail(Reason) ->
etap_server ! {self(), diag, "Bail out! " ++ Reason},
- ensure_coverage_ends(),
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) -> etap_server ! {self(), diag, "# " ++ S}, ok.
+diag(S) -> msg("# " ++ S).
%% @spec diag(Format, Data) -> ok
%% Format = atom() | string() | binary()
@@ -154,19 +193,56 @@ diag(S) -> etap_server ! {self(), diag, "# " ++ S}, ok.
%% 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) -> mk_tap(Expr == true, Desc).
+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) -> mk_tap(Expr == false, Desc).
+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()
@@ -174,17 +250,7 @@ not_ok(Expr, Desc) -> mk_tap(Expr == false, Desc).
%% Desc = string()
%% Result = true | false
%% @doc Assert that two values are the same.
-is(Got, Expected, Desc) ->
- case mk_tap(Got == Expected, Desc) of
- false ->
- etap_server ! {self(), diag, " ---"},
- etap_server ! {self(), diag, io_lib:format(" description: ~p", [Desc])},
- etap_server ! {self(), diag, io_lib:format(" found: ~p", [Got])},
- etap_server ! {self(), diag, io_lib:format(" wanted: ~p", [Expected])},
- etap_server ! {self(), diag, " ..."},
- false;
- true -> true
- end.
+is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc).
%% @spec isnt(Got, Expected, Desc) -> Result
%% Got = any()
@@ -192,7 +258,7 @@ is(Got, Expected, Desc) ->
%% Desc = string()
%% Result = true | false
%% @doc Assert that two values are not the same.
-isnt(Got, Expected, Desc) -> mk_tap(Got /= Expected, Desc).
+isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc).
%% @spec is_greater(ValueA, ValueB, Desc) -> Result
%% ValueA = number()
@@ -209,6 +275,8 @@ is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) ->
%% 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).
@@ -218,6 +286,8 @@ any(Got, Items, Desc) ->
%% 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).
@@ -230,6 +300,27 @@ none(Got, Items, Desc) ->
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, "").
@@ -276,8 +367,113 @@ begin_skip(Reason) ->
end_skip() ->
etap_server ! {self(), end_skip}.
-% ---
-% Internal / Private functions
+%% @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.