From 69dc9ec933ad8468cd94a0b3e1d62fa2a4c72527 Mon Sep 17 00:00:00 2001 From: Roberto Ostinelli Date: Sat, 28 Jul 2012 17:04:50 -0700 Subject: [PATCH] Add experimental tests= filter for eunit suites --- src/rebar.erl | 4 +- src/rebar_eunit.erl | 168 +++++++++++++++++++++++++++++++------ test/rebar_eunit_tests.erl | 118 ++++++++++++++++++++++++++ 3 files changed, 263 insertions(+), 27 deletions(-) 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: %% %% 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").