Added support for subtests.
authorStanislaw Klekot <dozzie@jarowit.net>
Mon, 15 Jun 2015 23:16:31 +0000 (01:16 +0200)
committerStanislaw Klekot <dozzie@jarowit.net>
Mon, 15 Jun 2015 23:16:31 +0000 (01:16 +0200)
src/estap.erl
src/estap_server.erl

index 51969d1..5d59449 100644 (file)
@@ -17,7 +17,7 @@
 
 %% public interface
 -export([ok/2, is/3, isnt/3, eq/3, ne/3, cmp/4, like/3, unlike/3, matches/3]).
--export([bail_out/1, plan/0, plan/1, all_ok/0]).
+-export([bail_out/1, no_plan/0, plan/1, all_ok/0]).
 -export([diag/1, diag/2, note/1, note/2, explain/1]).
 
 -export_type([value/0, cmp/0, regexp/0, match_fun/0]).
 %% @doc Check if `Value' is any of the recognized truth values.
 
 -spec ok(value(), description()) ->
-  'TODO'.
+  ok.
 
-ok(_Value, _Description) ->
-  'TODO'.
+ok(Value, Description) ->
+  TestRun = get_test_run(),
+  estap_server:running(TestRun, Description),
+  estap_server:report_result(TestRun, estap_test:success_or_failure(Value)).
 
 %% @doc Check if `Value' is the same as `Expected'.
 
 -spec is(value(), value(), description()) ->
-  'TODO'.
-
-is(_Value, _Expected, _Description) ->
-  'TODO'.
+  ok.
+
+is(Value, Expected, Description) ->
+  TestRun = get_test_run(),
+  estap_server:running(TestRun, Description),
+  case Value of
+    Expected ->
+      estap_server:report_result(TestRun, {success, true});
+    _ ->
+      estap_server:report_result(TestRun, {failure, false})
+  end.
 
 %% @doc Check if `Value' is different than `Expected'.
 
 -spec isnt(value(), value(), description()) ->
-  'TODO'.
-
-isnt(_Value, _Expected, _Description) ->
-  'TODO'.
+  ok.
+
+isnt(Value, Expected, Description) ->
+  TestRun = get_test_run(),
+  estap_server:running(TestRun, Description),
+  case Value of
+    Expected ->
+      estap_server:report_result(TestRun, {failure, false});
+    _ ->
+      estap_server:report_result(TestRun, {success, true})
+  end.
 
 %% @doc Check if `Value' is equal (`==') to `Expected'.
 
 -spec eq(value(), value(), description()) ->
-  'TODO'.
+  ok.
 
 eq(Value, Expected, Description) ->
+  % XXX: no `get_test_run()' call
   cmp(Value, '==', Expected, Description).
 
 %% @doc Check if `Value' is not equal (`/=') to `Expected'.
 
 -spec ne(value(), value(), description()) ->
-  'TODO'.
+  ok.
 
 ne(Value, Expected, Description) ->
+  % XXX: no `get_test_run()' call
   cmp(Value, '/=', Expected, Description).
 
 %% @doc Compare `Value' and `Expected' using comparison operator.
 
 -spec cmp(value(), cmp(), value(), description()) ->
-  'TODO'.
-
-cmp(_Value, _Cmp, _Expected, _Description) ->
-  'TODO'.
+  ok.
+
+cmp(Value, Cmp, Expected, Description) ->
+  TestRun = get_test_run(),
+  estap_server:running(TestRun, Description),
+  CmpResult = case Cmp of
+    '<'   -> Value <   Expected;
+    '>'   -> Value >   Expected;
+    '=<'  -> Value =<  Expected;
+    '>='  -> Value >=  Expected;
+    '/='  -> Value /=  Expected;
+    '=/=' -> Value =/= Expected;
+    '=='  -> Value ==  Expected;
+    '=:=' -> Value =:= Expected
+  end,
+  case CmpResult of
+    true  -> estap_server:report_result(TestRun, {success, true});
+    false -> estap_server:report_result(TestRun, {failure, false})
+  end.
 
 %% @doc Check if `Value' matches a regexp.
 
 -spec like(value(), regexp(), description()) ->
-  'TODO'.
+  ok.
 
-like(_Value, _Expected, _Description) ->
-  'TODO'.
+like(Value, Expected, Description) ->
+  TestRun = get_test_run(),
+  estap_server:running(TestRun, Description),
+  case re:run(Value, Expected) of
+    {match, _Capture} -> estap_server:report_result(TestRun, {success, true});
+    nomatch           -> estap_server:report_result(TestRun, {failure, false})
+  end.
 
 %% @doc Check if `Value' not matches a regexp.
 
 -spec unlike(value(), regexp(), description()) ->
-  'TODO'.
+  ok.
 
-unlike(_Value, _Expected, _Description) ->
-  'TODO'.
+unlike(Value, Expected, Description) ->
+  TestRun = get_test_run(),
+  estap_server:running(TestRun, Description),
+  case re:run(Value, Expected) of
+    {match, _Capture} -> estap_server:report_result(TestRun, {failure, false});
+    nomatch           -> estap_server:report_result(TestRun, {success, true})
+  end.
 
 %% @doc Check if `Value' pattern-matches.
 %%   Pattern is specified as a fun that has clauses defined only for what
@@ -114,14 +157,24 @@ unlike(_Value, _Expected, _Description) ->
 %%   error. Return value of the fun is ignored.
 
 -spec matches(value(), match_fun(), description()) ->
-  'TODO'.
-
-matches(_Value, _MatchSpec, _Description) ->
-  'TODO'.
+  ok.
+
+matches(Value, MatchSpec, Description) ->
+  TestRun = get_test_run(),
+  estap_server:running(TestRun, Description),
+  try
+    MatchSpec(Value),
+    estap_server:report_result(TestRun, {success, true})
+  catch
+    error:function_clause ->
+      estap_server:report_result(TestRun, {failure, false})
+  end.
 
 %%%---------------------------------------------------------------------------
 
 %% @doc Stop testing current suite because something terrible happened.
+%%
+%% @TODO Implement this function.
 
 -spec bail_out(message()) ->
   no_return().
@@ -134,33 +187,41 @@ bail_out(_Message) ->
 %%
 %% @see all_ok/0
 
--spec plan() ->
-  'TODO'.
+-spec no_plan() ->
+  ok.
 
-plan() ->
-  'TODO'.
+no_plan() ->
+  _TestRun = get_test_run(),
+  ok.
 
 %% @doc Set expected number of sub-tests.
 
 -spec plan(pos_integer()) ->
-  'TODO'.
+  ok.
 
-plan(_TestCount) ->
-  'TODO'.
+plan(TestCount) when is_integer(TestCount) ->
+  TestRun = estap_server:subplan(TestCount, 1),
+  set_test_run(TestRun),
+  ok.
 
 %% @doc Check if all the current sub-tests were OK.
 %%   Function intended to be called at the end of a sequence of sub-tests, to
 %%   indicate that the test sequence passed or failed.
 
 -spec all_ok() ->
-  'TODO'.
+  true | false.
 
 all_ok() ->
-  'TODO'.
+  TestRun = get_test_run(),
+  {Planned, Total, Failed, _TODO} = estap_server:get_status(TestRun),
+  estap_server:done(TestRun), % this ends estap_server, so it goes last
+  (Failed == 0) and ((Planned == undefined) or (Planned == Total)).
 
 %%%---------------------------------------------------------------------------
 
 %% @doc Print a warning.
+%%
+%% @TODO Implement this function.
 
 -spec diag(message()) ->
   'TODO'.
@@ -169,6 +230,8 @@ diag(_Message) ->
   'TODO'.
 
 %% @doc Print a warning with some context.
+%%
+%% @TODO Implement this function.
 
 -spec diag(message(), [info()]) ->
   'TODO'.
@@ -177,6 +240,8 @@ diag(_Message, _Info) ->
   'TODO'.
 
 %% @doc Print a message.
+%%
+%% @TODO Implement this function.
 
 -spec note(message()) ->
   'TODO'.
@@ -185,6 +250,8 @@ note(_Message) ->
   'TODO'.
 
 %% @doc Print a message with some context.
+%%
+%% @TODO Implement this function.
 
 -spec note(message(), [info()]) ->
   'TODO'.
@@ -194,12 +261,35 @@ note(_Message, _Info) ->
 
 %% @doc Format term so it can be printed to screen.
 %%   Convenience wrapper for {@link io_lib:format/2}.
+%% @spec explain(term()) ->
+%%   iolist()
+%%
+%% @TODO Implement this function.
 
 -spec explain(term()) ->
-  iolist().
+  'TODO'.
 
 explain(_Term) ->
   'TODO'.
 
 %%%---------------------------------------------------------------------------
+
+%% @doc Set previously started {@link estap_server}.
+
+set_test_run(TestRun) ->
+  put(estap_server, TestRun).
+
+%% @doc Get associated {@link estap_server}, starting it if necessary.
+
+get_test_run() ->
+  case get(estap_server) of
+    undefined ->
+      TestRun = estap_server:subplan(no_plan, 1),
+      put(estap_server, TestRun),
+      TestRun;
+    TestRun when is_pid(TestRun) ->
+      TestRun
+  end.
+
+%%%---------------------------------------------------------------------------
 %%% vim:ft=erlang:foldmethod=marker
index 37b443e..907c1ce 100644 (file)
@@ -10,6 +10,7 @@
 
 %% public interface
 -export([no_plan/0, plan/1, subplan/2, done/1]).
+-export([get_status/1]).
 -export([running/2, report_result/2, report_result_todo/3, report_skipped/2]).
 
 %% supervision tree API
 
 -type plan() :: no_plan | pos_integer().
 
+-record(counters, {
+  tests     = 0,
+  successes = 0,
+  failures  = 0,
+  todo_successes = 0,
+  todo_failures  = 0,
+  skipped   = 0
+}).
+
 -record(state, {
   plan :: plan(),
   level = 0 :: non_neg_integer(),
-  test :: {TestNo :: pos_integer(), Description :: string()}
+  test :: {TestNo :: pos_integer(), Description :: string()},
+  counters = #counters{}
 }).
 
 %%%---------------------------------------------------------------------------
@@ -74,6 +85,16 @@ when Plan == no_plan orelse is_integer(Plan), is_integer(Level) ->
 done(TestRunId) ->
   gen_server:call(TestRunId, done).
 
+%% @doc Return summary in form of numbers of tests.
+%%   `TODO' tests are the ones marked as TODO that failed.
+
+-spec get_status(test_run_id()) ->
+  {Planned :: integer() | undefined,
+    Total :: integer(), Failed :: integer(), TODO :: integer()}.
+
+get_status(TestRunId) ->
+  gen_server:call(TestRunId, status).
+
 %% @doc Mark the beginning of new test.
 
 -spec running(test_run_id(), string()) ->
@@ -136,8 +157,8 @@ report_skipped(TestRunId, Why) ->
 -spec start(plan(), non_neg_integer()) ->
   {ok, pid()} | {error, term()}.
 
-start(Plan, _Level) ->
-  gen_server:start(?MODULE, [Plan], []).
+start(Plan, Level) ->
+  gen_server:start(?MODULE, [Plan, Level], []).
 
 %% @private
 %% @doc Start the process.
@@ -145,8 +166,8 @@ start(Plan, _Level) ->
 -spec start_link(plan(), non_neg_integer()) ->
   {ok, pid()} | {error, term()}.
 
-start_link(Plan, _Level) ->
-  gen_server:start_link(?MODULE, [Plan], []).
+start_link(Plan, Level) ->
+  gen_server:start_link(?MODULE, [Plan, Level], []).
 
 %%%---------------------------------------------------------------------------
 %%% gen_server callbacks
@@ -158,12 +179,12 @@ start_link(Plan, _Level) ->
 %% @private
 %% @doc Initialize event handler.
 
-init([Plan] = _Args) ->
+init([Plan, Level] = _Args) ->
+  State = #state{plan = Plan, level = Level},
   case Plan of
     no_plan -> skip;
-    C when is_integer(C) -> io:fwrite("1..~B~n", [C])
+    C when is_integer(C) -> print("1..~B", [C], State)
   end,
-  State = #state{plan = Plan},
   {ok, State}.
 
 %% @private
@@ -182,8 +203,10 @@ terminate(_Arg, _State) ->
 handle_call(done = _Request, _From,
             State = #state{plan = Plan, test = {LastTestNo, _}}) ->
   case Plan of
-    no_plan -> io:fwrite("1..~B~n", [LastTestNo]);
-    C when is_integer(C) -> io:fwrite("# ran ~B of ~B tests~n", [LastTestNo, C])
+    no_plan ->
+      print("1..~B", [LastTestNo], State);
+    C when is_integer(C) ->
+      print("# ran ~B of ~B tests", [LastTestNo, C], State)
   end,
   {stop, normal, ok, State};
 
@@ -197,40 +220,53 @@ handle_call({next, Desc} = _Request, _From,
   {reply, ok, NewState};
 
 handle_call({result, TestResult} = _Request, _From,
-            State = #state{test = {TestNo, TestDesc}}) ->
+            State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
   case TestResult of
     success ->
-      io:fwrite("ok ~B - ~s~n", [TestNo, TestDesc]);
+      print("ok ~B - ~s", [TestNo, TestDesc], State);
     {failure, Reason} ->
-      io:fwrite("not ok ~B - ~s # result: ~s~n",
-                [TestNo, TestDesc, format(Reason)]);
+      print("not ok ~B - ~s # result: ~s",
+            [TestNo, TestDesc, format(Reason)], State);
     {dubious, Value} ->
-      io:fwrite("not ok ~B - ~s # dubious result: ~s~n",
-                [TestNo, TestDesc, format(Value)]);
+      print("not ok ~B - ~s # dubious result: ~s",
+            [TestNo, TestDesc, format(Value)], State);
     {died, Reason} ->
-      io:fwrite("not ok ~B - ~s # died: ~s~n",
-                [TestNo, TestDesc, format(Reason)])
+      print("not ok ~B - ~s # died: ~s",
+            [TestNo, TestDesc, format(Reason)], State)
   end,
-  {reply, ok, State};
+  NewState = State#state{counters = add_result(TestResult, Counters)},
+  {reply, ok, NewState};
 
 handle_call({todo, TestResult, Why} = _Request, _From,
-            State = #state{test = {TestNo, TestDesc}}) ->
+            State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
   case TestResult of
     success ->
-      io:fwrite("ok ~B - ~s # TODO ~s~n", [TestNo, TestDesc, Why]);
+      print("ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
     {failure, _Reason} ->
-      io:fwrite("not ok ~B - ~s # TODO ~s~n", [TestNo, TestDesc, Why]);
+      print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
     {dubious, _Value} ->
-      io:fwrite("not ok ~B - ~s # TODO ~s~n", [TestNo, TestDesc, Why]);
+      print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State);
     {died, _Reason} ->
-      io:fwrite("not ok ~B - ~s # TODO ~s~n", [TestNo, TestDesc, Why])
+      print("not ok ~B - ~s # TODO ~s", [TestNo, TestDesc, Why], State)
   end,
-  {reply, ok, State};
+  NewState = State#state{counters = add_todo(TestResult, Counters)},
+  {reply, ok, NewState};
 
 handle_call({skipped, Reason} = _Request, _From,
-            State = #state{test = {TestNo, TestDesc}}) ->
-  io:fwrite("ok ~B - ~s # SKIP ~s~n", [TestNo, TestDesc, Reason]),
-  {reply, ok, State};
+            State = #state{test = {TestNo, TestDesc}, counters = Counters}) ->
+  print("ok ~B - ~s # SKIP ~s", [TestNo, TestDesc, Reason], State),
+  NewState = State#state{counters = add_skipped(Counters)},
+  {reply, ok, NewState};
+
+handle_call(status = _Request, _From, State = #state{counters = Counters}) ->
+  Planned = case State of
+    #state{plan = no_plan} -> undefined;
+    #state{plan = C} -> C
+  end,
+  Total = Counters#counters.tests,
+  Failed = Counters#counters.failures,
+  TODO = Counters#counters.todo_failures,
+  {reply, {Planned, Total, Failed, TODO}, State};
 
 %% unknown calls
 handle_call(_Request, _From, State) ->
@@ -272,6 +308,63 @@ format(Term) ->
   % no term should weigh 1MB
   io_lib:print(Term, 1, 1024 * 1024, -1).
 
+%% @doc Print message to screen.
+%%   The message doesn't need to end with NL character.
+
+-spec print(string(), [term()], #state{}) ->
+  ok.
+
+print(Format, Args, _State = #state{level = Level}) ->
+  Indent = ["    " || _ <- lists:seq(1, Level)],
+  Text = iolist_to_binary(io_lib:format(Format, Args)),
+  Lines = binary:split(Text, <<"\n">>, [global, trim]),
+  [io:put_chars([Indent, L, "\n"]) || L <- Lines],
+  ok.
+
+%%----------------------------------------------------------
+
+%% @doc Add 1 to skipped tests counter.
+
+add_skipped(Counters = #counters{tests = T, skipped = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, skipped = N + 1}.
+
+%% @doc Add 1 to successes or failures counter, depending on the `Result'.
+
+add_result(success = _Result,
+           Counters = #counters{tests = T, successes = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, successes = N + 1};
+
+add_result({failure, _} = _Result,
+           Counters = #counters{tests = T, failures = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
+
+add_result({dubious, _} = _Result,
+           Counters = #counters{tests = T, failures = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, failures = N + 1};
+
+add_result({died, _} = _Result,
+           Counters = #counters{tests = T, failures = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, failures = N + 1}.
+
+%% @doc Add 1 to TODO successes or failures counter, depending on the
+%%   `Result'.
+
+add_todo(success = _Result,
+         Counters = #counters{tests = T, todo_successes = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, todo_successes = N + 1};
+
+add_todo({failure, _} = _Result,
+         Counters = #counters{tests = T, todo_failures = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
+
+add_todo({dubious, _} = _Result,
+         Counters = #counters{tests = T, todo_failures = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1};
+
+add_todo({died, _} = _Result,
+         Counters = #counters{tests = T, todo_failures = N}) ->
+  _NewCounters = Counters#counters{tests = T + 1, todo_failures = N + 1}.
+
 %%----------------------------------------------------------
 
 %%%---------------------------------------------------------------------------