Added functions for compiling test suite and loading it to Erlang.
authorStanislaw Klekot <dozzie@jarowit.net>
Sun, 14 Jun 2015 20:03:33 +0000 (22:03 +0200)
committerStanislaw Klekot <dozzie@jarowit.net>
Sun, 14 Jun 2015 20:03:33 +0000 (22:03 +0200)
src/estap_file.erl

index e1837bc..2c3c5b9 100644 (file)
@@ -8,19 +8,38 @@
 
 %% public interface
 -export([tempdir/0, tempdir/1]).
--export([load_file/3]).
+-export([read_file/2, read_file/3]).
+
+-export([load_code/1]).
 
 %%%---------------------------------------------------------------------------
 %%% types and definitions {{{
 
 -define(STEM_PREFIX, "estap").
 -define(STEM_LEN, 8).
+-define(DEFAULT_TMP, "/tmp").
+
+-record(test, {
+  %% TODO: `-prep(fun/0)', `-cleanup(fun/1)'
+  name :: atom(),
+  desc :: string(),
+  todo = false :: {true, Reason :: string()} | false,
+  skip = false :: {true, Reason :: string()} | false
+}).
+
+-type test() :: {Func :: {module(), atom()}, Description :: string(),
+                  Status :: run | {todo | skip, Why :: string()}}.
+
+-type test_plan() :: {plan, pos_integer()} | no_plan.
 
 %%% }}}
 %%%---------------------------------------------------------------------------
 %%% public interface
 %%%---------------------------------------------------------------------------
 
+%%----------------------------------------------------------
+%% temporary directories {{{
+
 %% @doc Create a temporary directory of unique name.
 %%   The directory is created under `$TMP', or `/tmp' if the variable is not
 %%   set.
@@ -33,7 +52,7 @@ tempdir() ->
     Dir when is_list(Dir) ->
       tempdir(Dir);
     false ->
-      tempdir("/tmp")
+      tempdir(?DEFAULT_TMP)
   end.
 
 %% @doc Create a temporary directory of unique name under specified directory.
@@ -55,12 +74,24 @@ tempdir(TempDir) ->
       {error, Reason}
   end.
 
+%% }}}
+%%----------------------------------------------------------
+%% loading test files {{{
+
+%% @doc Load estap file as ABF forms.
+
+-spec read_file(file:name(), [file:name()]) ->
+  {ok, {module(), [erl_parse:abstract_form()]}} | {error, term()}.
+
+read_file(File, IncludePath) ->
+  read_file(File, IncludePath, ?DEFAULT_TMP).
+
 %% @doc Load estap file as ABF forms.
 
--spec load_file(file:name(), [file:name()], file:name()) ->
+-spec read_file(file:name(), [file:name()], file:name()) ->
   {ok, {module(), [erl_parse:abstract_form()]}} | {error, term()}.
 
-load_file(File, IncludePath, TempDir) ->
+read_file(File, IncludePath, TempDir) ->
   case tempdir(TempDir) of
     {ok, DirName} ->
       case copy_source(File, DirName) of
@@ -127,5 +158,123 @@ parse_file(File, IncludePath) ->
       {error, Reason}
   end.
 
+%% }}}
+%%----------------------------------------------------------
+%% ABF handling functions {{{
+
+%% @doc Load ABFs as a callable module.
+%%   Function returns list of tests to run, in order of their appearance.
+
+-spec load_code([erl_parse:abstract_form()]) ->
+  {ok, {test_plan(), [test()]}} | {error, sticky_directory | not_purged}.
+
+load_code(Forms) ->
+  Exports = sets:from_list(exports(Forms)),
+  Tests = tests(Forms),
+  MissingTestExports = [
+    {Fun, 0} ||
+    #test{name = Fun} <- Tests,
+    not sets:is_element({Fun, 0}, Exports)
+  ],
+  % drop all occurrences of `-test()', `-todo()', and `-skip()'
+  ToCompile = lists:filter(
+    fun
+      ({attribute, _, A, _}) when A == test; A == todo; A == skip -> false;
+      (_) -> true
+    end,
+    insert_exports(MissingTestExports, Forms)
+  ),
+  case compile:forms(ToCompile) of
+    {ok, Module, Binary} ->
+      case code:load_binary(Module, "", Binary) of
+        {module, Module} ->
+          % TODO: indicate whether anything uses the old code
+          code:soft_purge(Module),
+          TestsToReturn = lists:map(
+            fun
+              (#test{name = Name, desc = Desc, todo = {true, Why}}) ->
+                {{Module, Name}, Desc, {todo, Why}};
+              (#test{name = Name, desc = Desc, skip = {true, Why}}) ->
+                {{Module, Name}, Desc, {skip, Why}};
+              (#test{name = Name, desc = Desc, todo = false, skip = false}) ->
+                {{Module, Name}, Desc, run}
+            end,
+            Tests
+          ),
+          case proplists:get_value(plan, Module:module_info(attributes)) of
+            [TestCount] when is_integer(TestCount), TestCount > 0 ->
+              Plan = {plan, TestCount};
+            _ ->
+              Plan = no_plan
+          end,
+          {ok, {Plan, TestsToReturn}};
+        {error, Reason} ->
+          {error, Reason}
+      end;
+    {error, Reason} ->
+      {error, Reason}
+  end.
+
+%% @doc Insert specified exports in list of ABFs for the module.
+
+insert_exports(Exports, [{attribute,_,module,_} = Attr | Rest] = _Forms) ->
+  [Attr, {attribute, 0, export, Exports} | Rest];
+insert_exports(Exports, [Attr | Rest] = _Forms) ->
+  [Attr | insert_exports(Exports, Rest)].
+
+%% @doc Extract from list of ABFs functions that are tests to be run.
+
+-spec tests([erl_parse:abstract_form()]) ->
+  [#test{}].
+
+tests(Forms) ->
+  tests(Forms, #test{}).
+
+%% @doc Extract from list of ABFs functions that are tests to be run.
+%%   Worker function for {@link tests/1}.
+
+-spec tests([erl_parse:abstract_form()], #test{}) ->
+  [#test{}].
+
+tests([] = _Forms, _Test) ->
+  [];
+
+tests([{attribute, _Line, test, Desc} | Rest] = _Forms, Test) ->
+  tests(Rest, Test#test{desc = Desc});
+
+tests([{attribute, _Line, todo, Reason} | Rest] = _Forms, Test) ->
+  tests(Rest, Test#test{todo = {true, Reason}});
+
+tests([{attribute, _Line, skip, Reason} | Rest] = _Forms, Test) ->
+  tests(Rest, Test#test{skip = {true, Reason}});
+
+tests([{function, _Line, FName, 0, _Body} | Rest] = _Forms, Test) ->
+  case Test of
+    #test{desc = undefined} ->
+      % TODO: check if `FName' ends with `"_test"'
+      tests(Rest, Test);
+    #test{desc = Desc} when is_list(Desc) ->
+      [Test#test{name = FName} | tests(Rest, #test{})]
+  end;
+
+tests([{function, _Line, _FName, _Arity, _Body} | Rest] = _Forms, _Test) ->
+  % reset attributes
+  tests(Rest, #test{});
+
+tests([_Any | Rest] = _Forms, Test) ->
+  tests(Rest, Test).
+
+%% @doc Extract exports from the module.
+
+-spec exports([erl_parse:abstract_form()]) ->
+  [{atom(), byte()}].
+
+exports(Forms) ->
+  Exports = [Fs || {attribute, _Line, export, Fs} <- Forms],
+  lists:flatten(Exports).
+
+%% }}}
+%%----------------------------------------------------------
+
 %%%---------------------------------------------------------------------------
 %%% vim:ft=erlang:foldmethod=marker