diff --git a/src/rebar.erl b/src/rebar.erl
index 44ba00b..aec516b 100644
--- a/src/rebar.erl
+++ b/src/rebar.erl
@@ -297,7 +297,9 @@ generate-upgrade previous_release=path Build an upgrade package
generate-appups previous_release=path Generate appup files
-eunit [suites=foo] Run eunit [test/foo_tests.erl] tests
+eunit [suites=foo] Run eunit tests [foo.erl and test/foo_tests.erl]
+ [suites=foo] [tests=bar] Run specific eunit tests [first test name starting
+ with 'bar' in foo.erl and test/foo_tests.erl]
ct [suites=] [case=] Run common_test suites
qc Test QuickCheck properties
diff --git a/src/rebar_eunit.erl b/src/rebar_eunit.erl
index 85eca3a..8e09452 100644
--- a/src/rebar_eunit.erl
+++ b/src/rebar_eunit.erl
@@ -43,7 +43,14 @@
%% The following Global options are supported:
%%
%% - verbose=1 - show extra output from the eunit test
-%% - suites="foo,bar" - runs test/foo_tests.erl and test/bar_tests.erl
+%% -
+%% suites="foo,bar" - runs tests in foo.erl, test/foo_tests.erl and
+%% tests in bar.erl, test/bar_tests.erl
+%%
+%% -
+%% suites="foo,bar" tests="baz"- runs first test with name starting
+%% with 'baz' in foo.erl, test/foo_tests.erl and tests in bar.erl,
+%% test/bar_tests.erl
%%
%% Additionally, for projects that have separate folders for the core
%% implementation, and for the unit tests, then the following
@@ -92,21 +99,7 @@ run_eunit(Config, CodePath, SrcErls) ->
%% cover and eunit testing. Normally you can just tell cover
%% and/or eunit to scan the directory for you, but eunit does a
%% code:purge in conjunction with that scan and causes any cover
- %% compilation info to be lost. Filter out "*_tests" modules so
- %% eunit won't doubly run them and so cover only calculates
- %% coverage on production code. However, keep "*_tests" modules
- %% that are not automatically included by eunit.
- %%
- %% From 'Primitives' in the EUnit User's Guide
- %% http://www.erlang.org/doc/apps/eunit/chapter.html
- %% "In addition, EUnit will also look for another module whose
- %% name is ModuleName plus the suffix _tests, and if it exists,
- %% all the tests from that module will also be added. (If
- %% ModuleName already contains the suffix _tests, this is not
- %% done.) E.g., the specification {module, mymodule} will run all
- %% tests in the modules mymodule and mymodule_tests. Typically,
- %% the _tests module should only contain test cases that use the
- %% public interface of the main module (and no other code)."
+ %% compilation info to be lost.
AllBeamFiles = rebar_utils:beams(?EUNIT_DIR),
{BeamFiles, TestBeamFiles} =
@@ -115,16 +108,20 @@ run_eunit(Config, CodePath, SrcErls) ->
OtherBeamFiles = TestBeamFiles --
[filename:rootname(N) ++ "_tests.beam" || N <- AllBeamFiles],
ModuleBeamFiles = BeamFiles ++ OtherBeamFiles,
- Modules = [rebar_utils:beam_to_mod(?EUNIT_DIR, N) || N <- ModuleBeamFiles],
+
+ %% Get modules to be run in eunit
+ AllModules = [rebar_utils:beam_to_mod(?EUNIT_DIR, N) || N <- AllBeamFiles],
+ {SuitesProvided, FilteredModules} = filter_suites(Config, AllModules),
+ Tests = get_tests(Config, SuitesProvided, ModuleBeamFiles, FilteredModules),
+
SrcModules = [rebar_utils:erl_to_mod(M) || M <- SrcErls],
- FilteredModules = filter_modules(Config, Modules),
{ok, CoverLog} = cover_init(Config, ModuleBeamFiles),
StatusBefore = status_before_eunit(),
- EunitResult = perform_eunit(Config, FilteredModules),
- perform_cover(Config, FilteredModules, SrcModules),
+ EunitResult = perform_eunit(Config, Tests),
+ perform_cover(Config, FilteredModules, SrcModules),
cover_close(CoverLog),
case proplists:get_value(reset_after_eunit, get_eunit_opts(Config),
@@ -164,17 +161,136 @@ setup_code_path() ->
true = code:add_pathz(rebar_utils:ebin_dir()),
CodePath.
-filter_modules(Config, Modules) ->
+filter_suites(Config, Modules) ->
RawSuites = rebar_config:get_global(Config, suites, ""),
+ SuitesProvided = RawSuites =/= "",
Suites = [list_to_atom(Suite) || Suite <- string:tokens(RawSuites, ",")],
- filter_modules1(Modules, Suites).
+ {SuitesProvided, filter_suites1(Modules, Suites)}.
-filter_modules1(Modules, []) ->
+filter_suites1(Modules, []) ->
Modules;
-filter_modules1(Modules, Suites) ->
+filter_suites1(Modules, Suites) ->
[M || M <- Modules, lists:member(M, Suites)].
-perform_eunit(Config, FilteredModules) ->
+get_tests(Config, SuitesProvided, ModuleBeamFiles, FilteredModules) ->
+ case SuitesProvided of
+ false ->
+ %% No specific suites have been provided, use ModuleBeamFiles
+ %% which filters out "*_tests" modules so eunit won't doubly run
+ %% them and cover only calculates coverage on production code.
+ %% However, keep "*_tests" modules that are not automatically
+ %% included by eunit.
+ %%
+ %% From 'Primitives' in the EUnit User's Guide
+ %% http://www.erlang.org/doc/apps/eunit/chapter.html
+ %% "In addition, EUnit will also look for another module whose
+ %% name is ModuleName plus the suffix _tests, and if it exists,
+ %% all the tests from that module will also be added. (If
+ %% ModuleName already contains the suffix _tests, this is not
+ %% done.) E.g., the specification {module, mymodule} will run all
+ %% tests in the modules mymodule and mymodule_tests. Typically,
+ %% the _tests module should only contain test cases that use the
+ %% public interface of the main module (and no other code)."
+ [rebar_utils:beam_to_mod(?EUNIT_DIR, N) || N <- ModuleBeamFiles];
+ true ->
+ %% Specific suites have been provided, return the existing modules
+ build_tests(Config, FilteredModules)
+ end.
+
+build_tests(Config, SuitesModules) ->
+ RawFunctions = rebar_utils:get_experimental_global(Config, tests, ""),
+ Tests = [list_to_atom(F1) || F1 <- string:tokens(RawFunctions, ",")],
+ case Tests of
+ [] ->
+ SuitesModules;
+ Functions ->
+ case build_tests1(SuitesModules, Functions, []) of
+ [] ->
+ [];
+ RawTests ->
+ ?CONSOLE(" Running test function(s):~n", []),
+ F = fun({M, F2}, Acc) ->
+ ?CONSOLE(" ~p:~p/0~n", [M, F2]),
+ [eunit_test:function_wrapper(M, F2)|Acc]
+ end,
+ lists:foldl(F, [], RawTests)
+ end
+ end.
+
+build_tests1([], _Functions, TestFunctions) ->
+ TestFunctions;
+
+build_tests1([Module|TModules], Functions, TestFunctions) ->
+ %% Get module exports
+ ModuleStr = atom_to_list(Module),
+ ModuleExports = get_beam_test_exports(ModuleStr),
+ %% Get module _tests exports
+ TestModuleStr = string:concat(ModuleStr, "_tests"),
+ TestModuleExports = get_beam_test_exports(TestModuleStr),
+ %% Build tests {M, F} list
+ Tests = build_tests2(Functions, {Module, ModuleExports},
+ {list_to_atom(TestModuleStr), TestModuleExports}),
+ build_tests1(TModules, Functions, lists:merge([TestFunctions, Tests])).
+
+build_tests2(Functions, {Mod, ModExports}, {TestMod, TestModExports}) ->
+ %% Look for matching functions into ModExports
+ ModExportsStr = [atom_to_list(E1) || E1 <- ModExports],
+ TestModExportsStr = [atom_to_list(E2) || E2 <- TestModExports],
+ get_matching_exports(Functions, {Mod, ModExportsStr},
+ {TestMod, TestModExportsStr}, []).
+
+get_matching_exports([], _, _, Matched) ->
+ Matched;
+get_matching_exports([Function|TFunctions], {Mod, ModExportsStr},
+ {TestMod, TestModExportsStr}, Matched) ->
+
+ FunctionStr = atom_to_list(Function),
+ %% Get matching Function in module, otherwise look in _tests module
+ NewMatch = case get_matching_export(FunctionStr, ModExportsStr) of
+ [] ->
+ {TestMod, get_matching_export(FunctionStr,
+ TestModExportsStr)};
+ MatchingExport ->
+ {Mod, MatchingExport}
+ end,
+ case NewMatch of
+ {_, []} ->
+ get_matching_exports(TFunctions, {Mod, ModExportsStr},
+ {TestMod, TestModExportsStr}, Matched);
+ _ ->
+ get_matching_exports(TFunctions, {Mod, ModExportsStr},
+ {TestMod, TestModExportsStr},
+ [NewMatch|Matched])
+ end.
+
+get_matching_export(_FunctionStr, []) ->
+ [];
+get_matching_export(FunctionStr, [ExportStr|TExportsStr]) ->
+ case string:str(ExportStr, FunctionStr) of
+ 1 ->
+ list_to_atom(ExportStr);
+ _ ->
+ get_matching_export(FunctionStr, TExportsStr)
+ end.
+
+get_beam_test_exports(ModuleStr) ->
+ FilePath = filename:join(eunit_dir(),
+ string:concat(ModuleStr, ".beam")),
+ case filelib:is_regular(FilePath) of
+ true ->
+ {beam_file, _, Exports0, _, _, _} = beam_disasm:file(FilePath),
+ Exports1 = [FunName || {FunName, FunArity, _} <- Exports0,
+ FunArity =:= 0],
+ F = fun(FName) ->
+ FNameStr = atom_to_list(FName),
+ re:run(FNameStr, "_test(_)?") =/= nomatch
+ end,
+ lists:filter(F, Exports1);
+ _ ->
+ []
+ end.
+
+perform_eunit(Config, Tests) ->
EunitOpts = get_eunit_opts(Config),
%% Move down into ?EUNIT_DIR while we run tests so any generated files
@@ -182,7 +298,7 @@ perform_eunit(Config, FilteredModules) ->
Cwd = rebar_utils:get_cwd(),
ok = file:set_cwd(?EUNIT_DIR),
- EunitResult = (catch eunit:test(FilteredModules, EunitOpts)),
+ EunitResult = (catch eunit:test(Tests, EunitOpts)),
%% Return to original working dir
ok = file:set_cwd(Cwd),
diff --git a/test/rebar_eunit_tests.erl b/test/rebar_eunit_tests.erl
index f8ca97f..d70d4b7 100644
--- a/test/rebar_eunit_tests.erl
+++ b/test/rebar_eunit_tests.erl
@@ -59,6 +59,104 @@ eunit_test_() ->
?_assert(string:str(RebarOut, "All 2 tests passed") =/= 0)}]
end}.
+eunit_with_suites_and_tests_test_() ->
+ [{"Ensure EUnit runs selected suites",
+ setup, fun() ->
+ setup_project_with_multiple_modules(),
+ rebar("-v eunit suites=myapp_mymod2")
+ end,
+ fun teardown/1,
+ fun(RebarOut) ->
+ [{"Selected suite tests in 'test' directory are found and run",
+ ?_assert(string:str(RebarOut, "myapp_mymod2_tests:") =/= 0)},
+
+ {"Selected suite tests in 'src' directory are found and run",
+ ?_assert(string:str(RebarOut, "myapp_mymod2:") =/= 0)},
+
+ {"Unselected suite tests in 'test' directory are not run",
+ ?_assert(string:str(RebarOut, "myapp_mymod_tests:") =:= 0)},
+
+ {"Unselected suite tests in 'src' directory are not run",
+ ?_assert(string:str(RebarOut, "myapp_mymod:") =:= 0)},
+
+ {"Selected suite tests are only run once",
+ ?_assert(string:str(RebarOut, "All 4 tests passed") =/= 0)}]
+ end},
+ {"Ensure EUnit runs selected _tests suites",
+ setup, fun() ->
+ setup_project_with_multiple_modules(),
+ rebar("-v eunit suites=myapp_mymod2_tests")
+ end,
+ fun teardown/1,
+ fun(RebarOut) ->
+ [{"Selected suite tests in 'test' directory are found and run",
+ ?_assert(string:str(RebarOut, "myapp_mymod2_tests:") =/= 0)},
+
+ {"Selected suite tests in 'src' directory are not run",
+ ?_assert(string:str(RebarOut, "myapp_mymod2:") =:= 0)},
+
+ {"Unselected suite tests in 'test' directory are not run",
+ ?_assert(string:str(RebarOut, "myapp_mymod_tests:") =:= 0)},
+
+ {"Unselected suite tests in 'src' directory are not run",
+ ?_assert(string:str(RebarOut, "myapp_mymod:") =:= 0)},
+
+ {"Selected suite tests are only run once",
+ ?_assert(string:str(RebarOut, "All 2 tests passed") =/= 0)}]
+ end},
+ {"Ensure EUnit runs a specific test defined in a selected suite",
+ setup, fun() ->
+ setup_project_with_multiple_modules(),
+ rebar("-v eunit suites=myapp_mymod2 tests=myprivate2")
+ end,
+ fun teardown/1,
+ fun(RebarOut) ->
+ [{"Selected suite tests are found and run",
+ ?_assert(string:str(RebarOut,
+ "myapp_mymod2:myprivate2_test/0") =/= 0)},
+
+ {"Selected suite tests is run once",
+ ?_assert(string:str(RebarOut, "Test passed") =/= 0)}]
+ end},
+ {"Ensure EUnit runs specific tests defined in selected suites",
+ setup, fun() ->
+ setup_project_with_multiple_modules(),
+ rebar("-v eunit suites=myapp_mymod,myapp_mymod2"
+ " tests=myprivate,myfunc2")
+ end,
+ fun teardown/1,
+ fun(RebarOut) ->
+ [{"Selected suite tests are found and run",
+ [?_assert(string:str(RebarOut,
+ "myapp_mymod:myprivate_test/0") =/= 0),
+ ?_assert(string:str(RebarOut,
+ "myapp_mymod2:myprivate2_test/0") =/= 0),
+ ?_assert(
+ string:str(RebarOut,
+ "myapp_mymod2_tests:myfunc2_test/0") =/= 0)]},
+
+ {"Selected suite tests are run once",
+ ?_assert(string:str(RebarOut, "All 3 tests passed") =/= 0)}]
+ end},
+ {"Ensure EUnit runs specific test in _tests suites",
+ setup,
+ fun() ->
+ setup_project_with_multiple_modules(),
+ rebar("-v eunit suites=myapp_mymod2_tests tests=common_name_test")
+ end,
+ fun teardown/1,
+ fun(RebarOut) ->
+ [{"Only selected suite tests are found and run",
+ [?_assert(string:str(RebarOut,
+ "myapp_mymod2:common_name_test/0") =:= 0),
+ ?_assert(string:str(RebarOut,
+ "myapp_mymod2_tests:common_name_test/0")
+ =/= 0)]},
+
+ {"Selected suite tests is run once",
+ ?_assert(string:str(RebarOut, "Test passed") =/= 0)}]
+ end}].
+
cover_test_() ->
{"Ensure Cover runs with tests in a test dir and no defined suite",
setup, fun() -> setup_cover_project(), rebar("-v eunit") end,
@@ -158,6 +256,21 @@ basic_setup_test_() ->
"-include_lib(\"eunit/include/eunit.hrl\").\n",
"myfunc_test() -> ?assertMatch(ok, myapp_mymod:myfunc()).\n"]).
+-define(myapp_mymod2,
+ ["-module(myapp_mymod2).\n",
+ "-export([myfunc2/0]).\n",
+ "-include_lib(\"eunit/include/eunit.hrl\").\n",
+ "myfunc2() -> ok.\n",
+ "myprivate2_test() -> ?assert(true).\n",
+ "common_name_test() -> ?assert(true).\n"]).
+
+-define(myapp_mymod2_tests,
+ ["-module(myapp_mymod2_tests).\n",
+ "-compile([export_all]).\n",
+ "-include_lib(\"eunit/include/eunit.hrl\").\n",
+ "myfunc2_test() -> ?assertMatch(ok, myapp_mymod2:myfunc2()).\n",
+ "common_name_test() -> ?assert(true).\n"]).
+
-define(mysuite,
["-module(mysuite).\n",
"-export([all_test_/0]).\n",
@@ -186,6 +299,11 @@ setup_basic_project() ->
ok = file:write_file("test/myapp_mymod_tests.erl", ?myapp_mymod_tests),
ok = file:write_file("src/myapp_mymod.erl", ?myapp_mymod).
+setup_project_with_multiple_modules() ->
+ setup_basic_project(),
+ ok = file:write_file("test/myapp_mymod2_tests.erl", ?myapp_mymod2_tests),
+ ok = file:write_file("src/myapp_mymod2.erl", ?myapp_mymod2).
+
setup_cover_project() ->
setup_basic_project(),
ok = file:write_file("rebar.config", "{cover_enabled, true}.\n").