summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md31
-rw-r--r--dialyzer_reference2
-rw-r--r--rebar.config.sample5
-rw-r--r--src/rebar_deps.erl49
-rw-r--r--src/rebar_eunit.erl171
-rw-r--r--src/rebar_utils.erl20
-rw-r--r--test/rebar_eunit_tests.erl40
7 files changed, 245 insertions, 73 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 30693d8..e0de0eb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,6 +36,37 @@ Do not commit to master in your fork.
Provide a clean branch without merge commits.
+Tests
+-----
+
+As a general rule, any behavioral change to rebar requires a test to go with it. If there's
+already a test case, you may have to modify that one. If there isn't a test case or a test
+suite, add a new test case or suite in `inttest/`. [retest](https://github.com/dizzyd/retest) based tests are preferred, but
+we also have EUnit tests in `test/`.
+
+Say you've added a new test case in `inttest/erlc`. To only execute the modified suite,
+you would do the following:
+```sh
+# First we build rebar and its deps to also get `deps/retest/retest`
+$ make debug deps
+# Now we can test the modified erlc suite
+$ deps/retest/retest -v inttest/erlc
+```
+
+To test EUnit tests, you would do:
+```sh
+$ make debug
+$ ./rebar -v eunit
+```
+
+You can also run `make test` to execute both EUnit and [retest](https://github.com/dizzyd/retest) tests as `make check` does.
+
+Credit
+------
+
+To give everyone proper credit in addition to the git history, please feel free to append
+your name to `THANKS` in your first contribution.
+
Committing your changes
-----------------------
diff --git a/dialyzer_reference b/dialyzer_reference
index 7fbe609..88909a6 100644
--- a/dialyzer_reference
+++ b/dialyzer_reference
@@ -1,3 +1,3 @@
-rebar_eunit.erl:434: Call to missing or unexported function eunit_test:function_wrapper/2
+rebar_eunit.erl:469: Call to missing or unexported function eunit_test:function_wrapper/2
rebar_utils.erl:164: Call to missing or unexported function escript:foldl/3
diff --git a/rebar.config.sample b/rebar.config.sample
index 515ed00..47812c1 100644
--- a/rebar.config.sample
+++ b/rebar.config.sample
@@ -155,7 +155,7 @@
%% name as an atom, eg. mochiweb, a name and a version (from the .app file), or
%% an application name, a version and the SCM details on how to fetch it (SCM
%% type, location and revision).
-%% Rebar currently supports git, hg, bzr, svn, rsync, and fossil.
+%% Rebar currently supports git, hg, bzr, svn, rsync, fossil, and p4.
{deps, [app_name,
{rebar, "1.0.*"},
{rebar, ".*",
@@ -188,7 +188,8 @@
{app_name, ".*", {svn, "svn://svn.example.org/url"}},
{app_name, ".*", {bzr, "https://www.example.org/url", "Rev"}},
{app_name, ".*", {fossil, "https://www.example.org/url"}},
- {app_name, ".*", {fossil, "https://www.example.org/url", "Vsn"}}]}.
+ {app_name, ".*", {fossil, "https://www.example.org/url", "Vsn"}},
+ {app_name, ".*", {p4, "//depot/subdir/app_dir"}}]}.
%% == Subdirectories ==
diff --git a/src/rebar_deps.erl b/src/rebar_deps.erl
index 43bde04..392882c 100644
--- a/src/rebar_deps.erl
+++ b/src/rebar_deps.erl
@@ -277,7 +277,8 @@ info_help(Description) ->
{app_name, ".*", {svn, "svn://svn.example.org/url"}},
{app_name, ".*", {bzr, "https://www.example.org/url", "Rev"}},
{app_name, ".*", {fossil, "https://www.example.org/url"}},
- {app_name, ".*", {fossil, "https://www.example.org/url", "Vsn"}}]}
+ {app_name, ".*", {fossil, "https://www.example.org/url", "Vsn"}},
+ {app_name, ".*", {p4, "//depot/subdir/app_dir"}}]}
]).
%% Added because of trans deps,
@@ -507,6 +508,40 @@ use_source(Config, Dep, Count) ->
use_source(Config, Dep#dep { dir = TargetDir }, Count-1)
end.
+-record(p4_settings, {
+ client=undefined,
+ transport="tcp4:perforce:1666",
+ username,
+ password
+ }).
+init_p4_settings(Basename) ->
+ #p4_settings{client =
+ case inet:gethostname() of
+ {ok,HostName} ->
+ HostName ++ "-"
+ ++ os:getenv("USER") ++ "-"
+ ++ Basename
+ ++ "-Rebar-automated-download"
+ end}.
+
+download_source(AppDir, {p4, Url}) ->
+ download_source(AppDir, {p4, Url, "#head"});
+download_source(AppDir, {p4, Url, Rev}) ->
+ download_source(AppDir, {p4, Url, Rev, init_p4_settings(filename:basename(AppDir))});
+download_source(AppDir, {p4, Url, _Rev, Settings}) ->
+ ok = filelib:ensure_dir(AppDir),
+ rebar_utils:sh_send("p4 client -i",
+ ?FMT("Client: ~s~n"
+ ++"Description: generated by Rebar~n"
+ ++"Root: ~s~n"
+ ++"View:~n"
+ ++" ~s/... //~s/...~n",
+ [Settings#p4_settings.client,
+ AppDir,
+ Url,
+ Settings#p4_settings.client]),
+ []),
+ rebar_utils:sh(?FMT("p4 -c ~s sync -f", [Settings#p4_settings.client]), []);
download_source(AppDir, {hg, Url, Rev}) ->
ok = filelib:ensure_dir(AppDir),
rebar_utils:sh(?FMT("hg clone -U ~s ~s", [Url, filename:basename(AppDir)]),
@@ -573,6 +608,8 @@ update_source(Config, Dep) ->
Dep
end.
+update_source1(AppDir, Args) when element(1, Args) =:= p4 ->
+ download_source(AppDir, Args);
update_source1(AppDir, {git, Url}) ->
update_source1(AppDir, {git, Url, {branch, "HEAD"}});
update_source1(AppDir, {git, Url, ""}) ->
@@ -696,7 +733,7 @@ source_engine_avail(Source) ->
source_engine_avail(Name, Source)
when Name == hg; Name == git; Name == svn; Name == bzr; Name == rsync;
- Name == fossil ->
+ Name == fossil; Name == p4 ->
case vcs_client_vsn(Name) >= required_vcs_client_vsn(Name) of
true ->
true;
@@ -717,6 +754,7 @@ vcs_client_vsn(Path, VsnArg, VsnRegex) ->
false
end.
+required_vcs_client_vsn(p4) -> {2013, 1};
required_vcs_client_vsn(hg) -> {1, 1};
required_vcs_client_vsn(git) -> {1, 5};
required_vcs_client_vsn(bzr) -> {2, 0};
@@ -724,6 +762,9 @@ required_vcs_client_vsn(svn) -> {1, 6};
required_vcs_client_vsn(rsync) -> {2, 0};
required_vcs_client_vsn(fossil) -> {1, 0}.
+vcs_client_vsn(p4) ->
+ vcs_client_vsn(rebar_utils:find_executable("p4"), " -V",
+ "Rev\\. .*/(\\d+)\\.(\\d)/");
vcs_client_vsn(hg) ->
vcs_client_vsn(rebar_utils:find_executable("hg"), " --version",
"version (\\d+).(\\d+)");
@@ -743,6 +784,8 @@ vcs_client_vsn(fossil) ->
vcs_client_vsn(rebar_utils:find_executable("fossil"), " version",
"version (\\d+).(\\d+)").
+has_vcs_dir(p4, _) ->
+ true;
has_vcs_dir(git, Dir) ->
filelib:is_dir(filename:join(Dir, ".git"));
has_vcs_dir(hg, Dir) ->
@@ -760,6 +803,8 @@ has_vcs_dir(_, _) ->
print_source(#dep{app=App, source=Source}) ->
?CONSOLE("~s~n", [format_source(App, Source)]).
+format_source(App, {p4, Url}) ->
+ format_source(App, {p4, Url, "#head"});
format_source(App, {git, Url}) ->
?FMT("~p BRANCH ~s ~s", [App, "HEAD", Url]);
format_source(App, {git, Url, ""}) ->
diff --git a/src/rebar_eunit.erl b/src/rebar_eunit.erl
index d969f96..8532af1 100644
--- a/src/rebar_eunit.erl
+++ b/src/rebar_eunit.erl
@@ -155,12 +155,10 @@ run_eunit(Config, CodePath, SrcErls) ->
[filename:rootname(N) ++ "_tests.beam" || N <- AllBeamFiles],
ModuleBeamFiles = randomize_suites(Config, BeamFiles ++ OtherBeamFiles),
- %% Get modules to be run in eunit
+ %% Get matching tests and modules
AllModules = [rebar_utils:beam_to_mod(?EUNIT_DIR, N) || N <- AllBeamFiles],
- {SuitesProvided, FilteredModules} = filter_suites(Config, AllModules),
-
- %% Get matching tests
- Tests = get_tests(Config, SuitesProvided, ModuleBeamFiles, FilteredModules),
+ {Tests, CoverageModules} =
+ get_tests_and_modules(Config, ModuleBeamFiles, AllModules),
SrcModules = [rebar_utils:erl_to_mod(M) || M <- SrcErls],
@@ -169,7 +167,7 @@ run_eunit(Config, CodePath, SrcErls) ->
StatusBefore = status_before_eunit(),
EunitResult = perform_eunit(Config, Tests),
- perform_cover(Config, FilteredModules, SrcModules),
+ perform_cover(Config, CoverageModules, SrcModules),
cover_close(CoverLog),
case proplists:get_value(reset_after_eunit, get_eunit_opts(Config),
@@ -214,18 +212,23 @@ setup_code_path() ->
CodePath.
%%
-%% == filter suites ==
+%% == get matching tests ==
%%
+get_tests_and_modules(Config, ModuleBeamFiles, AllModules) ->
+ SelectedSuites = get_selected_suites(Config, AllModules),
+ {Tests, QualifiedTests} = get_qualified_and_unqualified_tests(Config),
+ Modules = get_test_modules(SelectedSuites, Tests,
+ QualifiedTests, ModuleBeamFiles),
+ CoverageModules = get_coverage_modules(AllModules, Modules, QualifiedTests),
+ MatchedTests = get_matching_tests(Modules, Tests, QualifiedTests),
+ {MatchedTests, CoverageModules}.
-filter_suites(Config, Modules) ->
+%%
+%% == get suites specified via 'suites' option ==
+%%
+get_selected_suites(Config, Modules) ->
RawSuites = get_suites(Config),
- SuitesProvided = RawSuites =/= "",
Suites = [list_to_atom(Suite) || Suite <- string:tokens(RawSuites, ",")],
- {SuitesProvided, filter_suites1(Modules, Suites)}.
-
-filter_suites1(Modules, []) ->
- Modules;
-filter_suites1(Modules, Suites) ->
[M || M <- Suites, lists:member(M, Modules)].
get_suites(Config) ->
@@ -236,6 +239,32 @@ get_suites(Config) ->
Suites
end.
+get_qualified_and_unqualified_tests(Config) ->
+ RawFunctions = rebar_utils:get_experimental_global(Config, tests, ""),
+ FunctionNames = [FunctionName ||
+ FunctionName <- string:tokens(RawFunctions, ",")],
+ get_qualified_and_unqualified_tests1(FunctionNames, [], []).
+
+get_qualified_and_unqualified_tests1([], Functions, QualifiedFunctions) ->
+ {Functions, QualifiedFunctions};
+get_qualified_and_unqualified_tests1([TestName|TestNames], Functions,
+ QualifiedFunctions) ->
+ case string:tokens(TestName, ":") of
+ [TestName] ->
+ Function = list_to_atom(TestName),
+ get_qualified_and_unqualified_tests1(
+ TestNames, [Function|Functions], QualifiedFunctions);
+ [ModuleName, FunctionName] ->
+ M = list_to_atom(ModuleName),
+ F = list_to_atom(FunctionName),
+ get_qualified_and_unqualified_tests1(TestNames, Functions,
+ [{M, F}|QualifiedFunctions]);
+ _ ->
+ ?ABORT("Unsupported test function specification: ~s~n", [TestName])
+ end.
+
+%% Provide modules which are to be searched for tests.
+%% Several scenarios are possible:
%%
%% == randomize suites ==
%%
@@ -265,60 +294,66 @@ randomize_suites1(Modules, Seed) ->
%%
%% == get matching tests ==
+%% 1) Specific tests have been provided and/or
+%% no unqualified tests have been specified and
+%% there were some qualified tests, then we can search for
+%% functions in specified suites (or in empty set of suites).
%%
-get_tests(Config, SuitesProvided, ModuleBeamFiles, FilteredModules) ->
- Modules = 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
- %% filtered modules
- FilteredModules
- end,
- get_matching_tests(Config, Modules).
-
-get_tests(Config) ->
- case rebar_config:get_global(Config, tests, "") of
- "" ->
- rebar_config:get_global(Config, test, "");
- Suites ->
- Suites
+%% 2) Neither specific suites nor qualified test names 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)."
+get_test_modules(SelectedSuites, Tests, QualifiedTests, ModuleBeamFiles) ->
+ SuitesProvided = SelectedSuites =/= [],
+ OnlyQualifiedTestsProvided = QualifiedTests =/= [] andalso Tests =:= [],
+ if
+ SuitesProvided orelse OnlyQualifiedTestsProvided ->
+ SelectedSuites;
+ true ->
+ [rebar_utils:beam_to_mod(?EUNIT_DIR, N) ||
+ N <- ModuleBeamFiles]
end.
-get_matching_tests(Config, Modules) ->
- RawFunctions = get_tests(Config),
- Tests = [list_to_atom(F1) || F1 <- string:tokens(RawFunctions, ",")],
- case Tests of
- [] ->
- Modules;
- Functions ->
- case get_matching_tests1(Modules, Functions, []) of
- [] ->
- [];
- RawTests ->
- make_test_primitives(RawTests)
- end
- end.
+get_coverage_modules(AllModules, Modules, QualifiedTests) ->
+ ModuleFilterMapper =
+ fun({M, _}) ->
+ case lists:member(M, AllModules) of
+ true -> {true, M};
+ _-> false
+ end
+ end,
+ ModulesFromQualifiedTests = lists:zf(ModuleFilterMapper, QualifiedTests),
+ lists:usort(Modules ++ ModulesFromQualifiedTests).
+
+get_matching_tests(Modules, [], []) ->
+ Modules;
+get_matching_tests(Modules, [], QualifiedTests) ->
+ FilteredQualifiedTests = filter_qualified_tests(Modules, QualifiedTests),
+ lists:merge(Modules, make_test_primitives(FilteredQualifiedTests));
+get_matching_tests(Modules, Tests, QualifiedTests) ->
+ AllTests = lists:merge(QualifiedTests,
+ get_matching_tests1(Modules, Tests, [])),
+ make_test_primitives(AllTests).
+
+filter_qualified_tests(Modules, QualifiedTests) ->
+ TestsFilter = fun({Module, _Function}) ->
+ lists:all(fun(M) -> M =/= Module end, Modules) end,
+ lists:filter(TestsFilter, QualifiedTests).
get_matching_tests1([], _Functions, TestFunctions) ->
TestFunctions;
@@ -608,9 +643,9 @@ align_notcovered_count(Module, Covered, NotCovered, true) ->
cover_write_index(Coverage, SrcModules) ->
{ok, F} = file:open(filename:join([?EUNIT_DIR, "index.html"]), [write]),
ok = file:write(F, "<!DOCTYPE HTML><html>\n"
- "<head><meta charset=\"utf-8\">"
- "<title>Coverage Summary</title></head>\n"
- "<body>\n"),
+ "<head><meta charset=\"utf-8\">"
+ "<title>Coverage Summary</title></head>\n"
+ "<body>\n"),
IsSrcCoverage = fun({Mod,_C,_N}) -> lists:member(Mod, SrcModules) end,
{SrcCoverage, TestCoverage} = lists:partition(IsSrcCoverage, Coverage),
cover_write_index_section(F, "Source", SrcCoverage),
diff --git a/src/rebar_utils.erl b/src/rebar_utils.erl
index 517ac33..c02d200 100644
--- a/src/rebar_utils.erl
+++ b/src/rebar_utils.erl
@@ -31,6 +31,7 @@
get_arch/0,
wordsize/0,
sh/2,
+ sh_send/3,
find_files/2, find_files/3,
now_str/0,
ensure_dir/1,
@@ -87,6 +88,24 @@ wordsize() ->
integer_to_list(8 * erlang:system_info(wordsize))
end.
+sh_send(Command0, String, Options0) ->
+ ?INFO("sh_send info:\n\tcwd: ~p\n\tcmd: ~s < ~s\n", [get_cwd(), Command0, String]),
+ ?DEBUG("\topts: ~p\n", [Options0]),
+
+ DefaultOptions = [use_stdout, abort_on_error],
+ Options = [expand_sh_flag(V)
+ || V <- proplists:compact(Options0 ++ DefaultOptions)],
+
+ Command = patch_on_windows(Command0, proplists:get_value(env, Options, [])),
+ PortSettings = proplists:get_all_values(port_settings, Options) ++
+ [exit_status, {line, 16384}, use_stdio, stderr_to_stdout, hide],
+ Port = open_port({spawn, Command}, PortSettings),
+
+ %% allow us to send some data to the shell command's STDIN
+ %% Erlang doesn't let us get any reply after sending an EOF, though...
+ Port ! {self(), {command, String}},
+ port_close(Port).
+
%%
%% Options = [Option] -- defaults to [use_stdout, abort_on_error]
%% Option = ErrorOption | OutputOption | {cd, string()} | {env, Env}
@@ -480,6 +499,7 @@ vcs_vsn_1(Vcs, Dir) ->
end.
vcs_vsn_cmd(git) -> "git describe --always --tags";
+vcs_vsn_cmd(p4) -> "echo #head";
vcs_vsn_cmd(hg) -> "hg identify -i";
vcs_vsn_cmd(bzr) -> "bzr revno";
vcs_vsn_cmd(svn) -> "svnversion";
diff --git a/test/rebar_eunit_tests.erl b/test/rebar_eunit_tests.erl
index 61a9bbf..bb64507 100644
--- a/test/rebar_eunit_tests.erl
+++ b/test/rebar_eunit_tests.erl
@@ -191,6 +191,46 @@ eunit_with_suites_and_tests_test_() ->
{"Selected suite tests is run once",
?_assert(string:str(RebarOut, "All 2 tests passed") =/= 0)}]
+ end},
+ {"Ensure EUnit runs a specific test by qualified function name",
+ setup,
+ fun() ->
+ setup_project_with_multiple_modules(),
+ rebar("-v eunit tests=myapp_mymod:myprivate_test")
+ end,
+ fun teardown/1,
+ fun(RebarOut) ->
+ [{"Selected test is run",
+ [?_assert(string:str(RebarOut,
+ "myapp_mymod:myprivate_test/0")
+ =/= 0)]},
+
+ {"Only selected test is run",
+ [?_assert(string:str(RebarOut,
+ "Test passed.") =/= 0)]}]
+ end},
+ {"Ensure EUnit runs a specific test by qualified function "
+ ++ "name and tests from other module",
+ setup,
+ fun() ->
+ setup_project_with_multiple_modules(),
+ rebar("-v eunit suites=myapp_mymod3 "
+ ++ "tests=myapp_mymod:myprivate_test")
+ end,
+ fun teardown/1,
+ fun(RebarOut) ->
+ [{"Selected test is run",
+ [?_assert(string:str(RebarOut,
+ "myapp_mymod:myprivate_test/0") =/= 0)]},
+
+ {"Tests from module are run",
+ [?_assert(string:str(RebarOut,
+ "myapp_mymod3:") =/= 0)]},
+
+ {"Only selected tests are run",
+ [?_assert(string:str(RebarOut,
+ "Failed: 1. Skipped: 0. Passed: 1")
+ =/= 0)]}]
end}].
cover_test_() ->