summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTristan Sloughter <tristan.sloughter@gmail.com>2015-03-03 07:32:57 -0600
committerTristan Sloughter <tristan.sloughter@gmail.com>2015-03-03 07:32:57 -0600
commit358046b0957fc7211f5dab7d76f0bc365d00c439 (patch)
treef13157852e061f5184ce9727dd6051d1a85e56d9 /src
parent4c70d16e505c05695e902bea502855d8383fbe82 (diff)
parent6c421e543373aaf41a6ed10719f5da19b0cafd93 (diff)
Merge pull request #202 from talentdeficit/cover
`cover` task
Diffstat (limited to 'src')
-rw-r--r--src/rebar.app.src1
-rw-r--r--src/rebar_cover_utils.erl261
-rw-r--r--src/rebar_erlc_compiler.erl6
-rw-r--r--src/rebar_otp_app.erl30
-rw-r--r--src/rebar_prv_common_test.erl174
-rw-r--r--src/rebar_prv_cover.erl343
-rw-r--r--src/rebar_prv_eunit.erl153
-rw-r--r--src/rebar_state.erl10
8 files changed, 546 insertions, 432 deletions
diff --git a/src/rebar.app.src b/src/rebar.app.src
index e5f21ea..8c544aa 100644
--- a/src/rebar.app.src
+++ b/src/rebar.app.src
@@ -25,6 +25,7 @@
{providers, [rebar_prv_as,
rebar_prv_clean,
+ rebar_prv_cover,
rebar_prv_deps,
rebar_prv_dialyzer,
rebar_prv_do,
diff --git a/src/rebar_cover_utils.erl b/src/rebar_cover_utils.erl
deleted file mode 100644
index 0439b8a..0000000
--- a/src/rebar_cover_utils.erl
+++ /dev/null
@@ -1,261 +0,0 @@
-%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
-%% ex: ts=4 sw=4 et
-%% -------------------------------------------------------------------
-%%
-%% rebar: Erlang Build Tools
-%%
-%% Copyright (c) 2009, 2010 Dave Smith (dizzyd@dizzyd.com)
-%% Copyright (c) 2013 Andras Horvath (andras.horvath@erlang-solutions.com)
-%%
-%% Permission is hereby granted, free of charge, to any person obtaining a copy
-%% of this software and associated documentation files (the "Software"), to deal
-%% in the Software without restriction, including without limitation the rights
-%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-%% copies of the Software, and to permit persons to whom the Software is
-%% furnished to do so, subject to the following conditions:
-%%
-%% The above copyright notice and this permission notice shall be included in
-%% all copies or substantial portions of the Software.
-%%
-%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-%% THE SOFTWARE.
-%% -------------------------------------------------------------------
--module(rebar_cover_utils).
-
-%% for internal use only
--export([init/3,
- perform_cover/4,
- close/1,
- exit/0]).
-
--include("rebar.hrl").
-
-%% ====================================================================
-%% Internal functions
-%% ====================================================================
-
-perform_cover(Config, BeamFiles, SrcModules, TargetDir) ->
- perform_cover(rebar_state:get(Config, cover_enabled, false),
- Config, BeamFiles, SrcModules, TargetDir).
-
-perform_cover(false, _Config, _BeamFiles, _SrcModules, _TargetDir) ->
- ok;
-perform_cover(true, Config, BeamFiles, SrcModules, TargetDir) ->
- analyze(Config, BeamFiles, SrcModules, TargetDir).
-
-close(not_enabled) ->
- ok;
-close(F) ->
- ok = file:close(F).
-
-exit() ->
- cover:stop().
-
-init(false, _BeamFiles, _TargetDir) ->
- {ok, not_enabled};
-init(true, BeamFiles, TargetDir) ->
- %% Attempt to start the cover server, then set its group leader to
- %% TargetDir/cover.log, so all cover log messages will go there instead of
- %% to stdout. If the cover server is already started, we'll kill that
- %% server and start a new one in order not to inherit a polluted
- %% cover_server state.
- {ok, CoverPid} = case whereis(cover_server) of
- undefined ->
- cover:start();
- _ ->
- cover:stop(),
- cover:start()
- end,
-
- {ok, F} = OkOpen = file:open(
- filename:join([TargetDir, "cover.log"]),
- [write]),
-
- group_leader(F, CoverPid),
-
- ?INFO("Cover compiling ~s\n", [rebar_dir:get_cwd()]),
-
- Compiled = [{Beam, cover:compile_beam(Beam)} || Beam <- BeamFiles],
- case [Module || {_, {ok, Module}} <- Compiled] of
- [] ->
- %% No modules compiled successfully...fail
- ?ERROR("Cover failed to compile any modules; aborting.", []),
- ?FAIL;
- _ ->
- %% At least one module compiled successfully
-
- %% It's not an error for cover compilation to fail partially,
- %% but we do want to warn about them
- PrintWarning =
- fun(Beam, Desc) ->
- ?CONSOLE("Cover compilation warning for ~p: ~p",
- [Beam, Desc])
- end,
- _ = [PrintWarning(Beam, Desc) || {Beam, {error, Desc}} <- Compiled],
- OkOpen
- end;
-init(Config, BeamFiles, TargetDir) ->
- init(rebar_state:get(Config, cover_enabled, false), BeamFiles, TargetDir).
-
-analyze(_Config, [], _SrcModules, _TargetDir) ->
- ok;
-analyze(Config, FilteredModules, SrcModules, TargetDir) ->
- %% Generate coverage info for all the cover-compiled modules
- Coverage = lists:flatten([analyze_mod(M)
- || M <- FilteredModules,
- cover:is_compiled(M) =/= false]),
-
- %% Write index of coverage info
- write_index(lists:sort(Coverage), SrcModules, TargetDir),
-
- %% Write coverage details for each file
- lists:foreach(
- fun({M, _, _}) ->
- {ok, _} = cover:analyze_to_file(M,
- cover_file(M, TargetDir),
- [html])
- end, Coverage),
-
- Index = filename:join([rebar_dir:get_cwd(), TargetDir, "index.html"]),
- ?CONSOLE("Cover analysis: ~s\n", [Index]),
-
- %% Export coverage data, if configured
- case rebar_state:get(Config, cover_export_enabled, false) of
- true ->
- export_coverdata(TargetDir);
- false ->
- ok
- end,
-
- %% Print coverage report, if configured
- case rebar_state:get(Config, cover_print_enabled, false) of
- true ->
- print_coverage(lists:sort(Coverage));
- false ->
- ok
- end.
-
-analyze_mod(Module) ->
- case cover:analyze(Module, coverage, module) of
- {ok, {Module, {Covered, NotCovered}}} ->
- %% Modules that include the eunit header get an implicit
- %% test/0 fun, which cover considers a runnable line, but
- %% eunit:test(TestRepresentation) never calls. Decrement
- %% NotCovered in this case.
- [align_notcovered_count(Module, Covered, NotCovered,
- is_eunitized(Module))];
- {error, Reason} ->
- ?ERROR("Cover analyze failed for ~p: ~p ~p\n",
- [Module, Reason, code:which(Module)]),
- []
- end.
-
-is_eunitized(Mod) ->
- has_eunit_test_fun(Mod) andalso
- has_header(Mod, "include/eunit.hrl").
-
-has_eunit_test_fun(Mod) ->
- [F || {exports, Funs} <- Mod:module_info(),
- {F, 0} <- Funs, F =:= test] =/= [].
-
-has_header(Mod, Header) ->
- Mod1 = case code:which(Mod) of
- cover_compiled ->
- {file, File} = cover:is_compiled(Mod),
- File;
- non_existing -> Mod;
- preloaded -> Mod;
- L -> L
- end,
- {ok, {_, [{abstract_code, {_, AC}}]}} =
- beam_lib:chunks(Mod1, [abstract_code]),
- [F || {attribute, 1, file, {F, 1}} <- AC,
- string:str(F, Header) =/= 0] =/= [].
-
-align_notcovered_count(Module, Covered, NotCovered, false) ->
- {Module, Covered, NotCovered};
-align_notcovered_count(Module, Covered, NotCovered, true) ->
- {Module, Covered, NotCovered - 1}.
-
-write_index(Coverage, SrcModules, TargetDir) ->
- {ok, F} = file:open(filename:join([TargetDir, "index.html"]), [write]),
- ok = file:write(F, "<!DOCTYPE HTML><html>\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),
- write_index_section(F, "Source", SrcCoverage),
- write_index_section(F, "Test", TestCoverage),
- ok = file:write(F, "</body></html>"),
- ok = file:close(F).
-
-write_index_section(_F, _SectionName, []) ->
- ok;
-write_index_section(F, SectionName, Coverage) ->
- %% Calculate total coverage
- {Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
- {CAcc + C, NAcc + N}
- end, {0, 0}, Coverage),
- TotalCoverage = percentage(Covered, NotCovered),
-
- %% Write the report
- ok = file:write(F, ?FMT("<h1>~s Summary</h1>\n", [SectionName])),
- ok = file:write(F, ?FMT("<h3>Total: ~s</h3>\n", [TotalCoverage])),
- ok = file:write(F, "<table><tr><th>Module</th><th>Coverage %</th></tr>\n"),
-
- FmtLink =
- fun(Module, Cov, NotCov) ->
- ?FMT("<tr><td><a href='~s.COVER.html'>~s</a></td><td>~s</td>\n",
- [Module, Module, percentage(Cov, NotCov)])
- end,
- lists:foreach(fun({Module, Cov, NotCov}) ->
- ok = file:write(F, FmtLink(Module, Cov, NotCov))
- end, Coverage),
- ok = file:write(F, "</table>\n").
-
-print_coverage(Coverage) ->
- {Covered, NotCovered} = lists:foldl(fun({_Mod, C, N}, {CAcc, NAcc}) ->
- {CAcc + C, NAcc + N}
- end, {0, 0}, Coverage),
- TotalCoverage = percentage(Covered, NotCovered),
-
- %% Determine the longest module name for right-padding
- Width = lists:foldl(fun({Mod, _, _}, Acc) ->
- case length(atom_to_list(Mod)) of
- N when N > Acc ->
- N;
- _ ->
- Acc
- end
- end, 0, Coverage) * -1,
-
- %% Print the output the console
- ?CONSOLE("~nCode Coverage:", []),
- lists:foreach(fun({Mod, C, N}) ->
- ?CONSOLE("~*s : ~4s",
- [Width, Mod, percentage(C, N)])
- end, Coverage),
- ?CONSOLE("~n~*s : ~s", [Width, "Total", TotalCoverage]).
-
-cover_file(Module, TargetDir) ->
- filename:join([TargetDir, atom_to_list(Module) ++ ".COVER.html"]).
-
-export_coverdata(TargetDir) ->
- ExportFile = filename:join(TargetDir, "cover.coverdata"),
- case cover:export(ExportFile) of
- ok ->
- ?CONSOLE("Coverdata export: ~s", [ExportFile]);
- {error, Reason} ->
- ?ERROR("Coverdata export failed: ~p", [Reason])
- end.
-
-percentage(0, 0) ->
- "not executed";
-percentage(Cov, NotCov) ->
- integer_to_list(trunc((Cov / (Cov + NotCov)) * 100)) ++ "%".
diff --git a/src/rebar_erlc_compiler.erl b/src/rebar_erlc_compiler.erl
index 9157e2a..c757511 100644
--- a/src/rebar_erlc_compiler.erl
+++ b/src/rebar_erlc_compiler.erl
@@ -260,10 +260,10 @@ opts_changed(Opts, Target) ->
Basename = filename:basename(Target, ".beam"),
Dirname = filename:dirname(Target),
ObjectFile = filename:join([Dirname, Basename]),
+ _ = purge(list_to_atom(Basename)),
case code:load_abs(ObjectFile) of
{module, Mod} ->
Compile = Mod:module_info(compile),
- _ = purge(Mod),
lists:sort(Opts) =/= lists:sort(proplists:get_value(options,
Compile));
{error, nofile} -> false
@@ -273,9 +273,7 @@ purge(Mod) ->
%% remove old code if necessary
_ = code:purge(Mod),
%% move current code to old
- true = code:delete(Mod),
- %% remove new old code
- _ = code:purge(Mod).
+ _ = code:delete(Mod).
check_erlcinfo(_Config, #erlcinfo{vsn=?ERLCINFO_VSN}) ->
ok;
diff --git a/src/rebar_otp_app.erl b/src/rebar_otp_app.erl
index 278d7e5..e850e58 100644
--- a/src/rebar_otp_app.erl
+++ b/src/rebar_otp_app.erl
@@ -100,7 +100,7 @@ preprocess(State, AppInfo, AppSrcFile) ->
%% substitute. Note that we include the list of modules available in
%% ebin/ and update the app data accordingly.
OutDir = rebar_app_info:out_dir(AppInfo),
- AppVars = load_app_vars(State) ++ [{modules, ebin_modules(OutDir)}],
+ AppVars = load_app_vars(State) ++ [{modules, ebin_modules(AppInfo, OutDir)}],
A1 = apply_app_vars(AppVars, AppData),
%% AppSrcFile may contain instructions for generating a vsn number
@@ -157,9 +157,31 @@ validate_name(AppName, File) ->
?PRV_ERROR({invalid_name, File, AppName})
end.
-ebin_modules(Dir) ->
- lists:sort([rebar_utils:beam_to_mod(N) ||
- N <- rebar_utils:beams(filename:join(Dir, "ebin"))]).
+ebin_modules(App, Dir) ->
+ Beams = lists:sort(rebar_utils:beams(filename:join(Dir, "ebin"))),
+ F = fun(Beam) -> not lists:prefix(filename:join([rebar_app_info:dir(App), "test"]),
+ beam_src(Beam))
+ end,
+ Filtered = lists:filter(F, Beams),
+ [rebar_utils:beam_to_mod(N) || N <- Filtered].
+
+beam_src(Beam) ->
+ try
+ Mod = list_to_atom(filename:basename(Beam, ".beam")),
+ _ = purge(Mod),
+ {module, Mod} = code:load_abs(filename:rootname(Beam, ".beam")),
+ Compile = Mod:module_info(compile),
+ proplists:get_value(source, Compile, [])
+ catch
+ error:undef -> [];
+ error:nofile -> []
+ end.
+
+purge(Mod) ->
+ %% remove old code if necessary
+ _ = code:purge(Mod),
+ %% move current code to old
+ _ = code:delete(Mod).
ensure_registered(AppData) ->
case lists:keyfind(registered, 1, AppData) of
diff --git a/src/rebar_prv_common_test.erl b/src/rebar_prv_common_test.erl
index c3f9163..032a8a6 100644
--- a/src/rebar_prv_common_test.erl
+++ b/src/rebar_prv_common_test.erl
@@ -30,35 +30,24 @@ init(State) ->
{opts, ct_opts(State)},
{profiles, [test]}]),
State1 = rebar_state:add_provider(State, Provider),
- {ok, State1}.
+ State2 = rebar_state:add_to_profile(State1, test, test_state(State1)),
+ {ok, State2}.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
?INFO("Running Common Test suites...", []),
{RawOpts, _} = rebar_state:command_parsed_args(State),
- {InDirs, OutDir} = split_ct_dirs(State, RawOpts),
Opts = transform_opts(RawOpts),
TestApps = filter_checkouts(rebar_state:project_apps(State)),
ok = create_dirs(Opts),
- ?DEBUG("Compiling Common Test suites in: ~p", [OutDir]),
- lists:foreach(fun(App) ->
- AppDir = rebar_app_info:dir(App),
- AppOutDir = rebar_app_info:out_dir(App),
- C = rebar_config:consult(AppDir),
- S = rebar_state:new(State, C, AppDir),
- %% combine `erl_first_files` and `common_test_first_files` and
- %% adjust compile opts to include `common_test_compile_opts`
- %% and `{src_dirs, "test"}`
- TestState = test_state(S, InDirs, OutDir),
- ok = rebar_erlc_compiler:compile(TestState, AppDir, AppOutDir)
- end, TestApps),
- ok = maybe_compile_extra_tests(TestApps, State, InDirs, OutDir),
- Path = code:get_path(),
- true = code:add_patha(OutDir),
- CTOpts = resolve_ct_opts(State, Opts, OutDir),
- Verbose = proplists:get_value(verbose, Opts, false),
- Result = run_test(CTOpts, Verbose),
- true = code:set_path(Path),
+ InDirs = in_dirs(State, RawOpts),
+ ok = compile_tests(State, TestApps, InDirs),
+ ok = maybe_cover_compile(State, RawOpts),
+ CTOpts = resolve_ct_opts(State, Opts),
+ Verbose = proplists:get_value(verbose, RawOpts, false),
+ TestDirs = test_dirs(State, TestApps),
+ Result = run_test([{dir, TestDirs}|CTOpts], Verbose),
+ ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER),
case Result of
{error, Reason} ->
{error, {?MODULE, Reason}};
@@ -86,9 +75,8 @@ run_test(CTOpts, false) ->
receive Result -> handle_quiet_results(CTOpts, Result) end.
ct_opts(State) ->
- DefaultLogsDir = filename:join([rebar_state:dir(State), "logs"]),
+ DefaultLogsDir = filename:join([rebar_state:dir(State), "_logs"]),
[{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list
- {outdir, undefined, "outdir", string, help(outdir)}, %% string
{suite, undefined, "suite", string, help(suite)}, %% comma-seperated list
{group, undefined, "group", string, help(group)}, %% comma-seperated list
{testcase, undefined, "case", string, help(testcase)}, %% comma-seperated list
@@ -104,7 +92,8 @@ ct_opts(State) ->
{silent_connections, undefined, "silent_connections", string,
help(silent_connections)}, % all OR %% comma-seperated list
{stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% file
- {cover, undefined, "cover", string, help(cover)}, %% file
+ {cover, $c, "cover", boolean, help(cover)},
+ {cover_spec, undefined, "cover_spec", string, help(cover_spec)}, %% file
{cover_stop, undefined, "cover_stop", boolean, help(cover_stop)}, %% Boolean
{event_handler, undefined, "event_handler", string, help(event_handler)}, %% EH | [EH] WHERE EH atom() | {atom(), InitArgs} | {[atom()], InitArgs}
{include, undefined, "include", string, help(include)}, % comma-seperated list
@@ -123,8 +112,6 @@ ct_opts(State) ->
{verbose, $v, "verbose", boolean, help(verbose)}
].
-help(outdir) ->
- "Output directory for compiled modules";
help(dir) ->
"List of additional directories containing test suites";
help(suite) ->
@@ -154,6 +141,8 @@ help(silent_connections) ->
help(stylesheet) ->
"Stylesheet to use for test results";
help(cover) ->
+ "Generate cover data";
+help(cover_spec) ->
"Cover file to use";
help(cover_stop) ->
""; %% ??
@@ -186,33 +175,14 @@ help(userconfig) ->
help(verbose) ->
"Verbose output".
-split_ct_dirs(State, RawOpts) ->
- %% preserve the override nature of command line opts by only checking
- %% `rebar.config` defined additional test dirs if none are defined via
- %% command line flag
- InDirs = case proplists:get_value(dir, RawOpts) of
- undefined ->
- CTOpts = rebar_state:get(State, common_test_opts, []),
- proplists:get_value(dir, CTOpts, []);
- Dirs -> split_string(Dirs)
- end,
- OutDir = proplists:get_value(outdir, RawOpts, default_test_dir(State)),
- {InDirs, OutDir}.
-
-default_test_dir(State) ->
- Tmp = rebar_file_utils:system_tmpdir(),
- Root = filename:join([rebar_state:dir(State), Tmp]),
- Project = filename:basename(rebar_state:dir(State)),
- OutDir = filename:join([Root, Project ++ "_rebar3_ct"]),
- ok = rebar_file_utils:reset_dir(OutDir),
- OutDir.
-
transform_opts(Opts) ->
transform_opts(Opts, []).
transform_opts([], Acc) -> Acc;
-%% drop `outdir` so it's not passed to common_test
-transform_opts([{outdir, _}|Rest], Acc) ->
+%% drop `cover` and `verbose` so they're not passed as an option to common_test
+transform_opts([{cover, _}|Rest], Acc) ->
+ transform_opts(Rest, Acc);
+transform_opts([{verbose, _}|Rest], Acc) ->
transform_opts(Rest, Acc);
transform_opts([{ct_hooks, CtHooks}|Rest], Acc) ->
transform_opts(Rest, [{ct_hooks, parse_term(CtHooks)}|Acc]);
@@ -302,52 +272,92 @@ ensure_dir([Dir|Rest]) ->
end,
ensure_dir(Rest).
-test_state(State, InDirs, OutDir) ->
- ErlOpts = rebar_state:get(State, common_test_compile_opts, []) ++
- rebar_utils:erl_opts(State),
- TestOpts = [{outdir, OutDir}] ++
- add_test_dir(ErlOpts, InDirs),
- first_files(rebar_state:set(State, erl_opts, TestOpts)).
-
-add_test_dir(Opts, InDirs) ->
- %% if no src_dirs are set we have to specify `src` or it won't
- %% be built
- case proplists:append_values(src_dirs, Opts) of
- [] -> [{src_dirs, ["src", "test" | InDirs]} | Opts];
- _ -> [{src_dirs, ["test" | InDirs]} | Opts]
+in_dirs(State, Opts) ->
+ %% preserve the override nature of command line opts by only checking
+ %% `rebar.config` defined additional test dirs if none are defined via
+ %% command line flag
+ case proplists:get_value(dir, Opts) of
+ undefined ->
+ CTOpts = rebar_state:get(State, ct_opts, []),
+ proplists:get_value(dir, CTOpts, []);
+ Dirs -> split_string(Dirs)
end.
+test_dirs(State, TestApps) ->
+ %% we need to add "./ebin" if it exists but only if it's not already
+ %% due to be added
+ F = fun(App) -> rebar_app_info:dir(App) =/= rebar_dir:get_cwd() end,
+ BareEbin = filename:join([rebar_dir:base_dir(State), "ebin"]),
+ case lists:any(F, TestApps) andalso filelib:is_dir(BareEbin) of
+ false -> application_dirs(TestApps, []);
+ true -> [BareEbin|application_dirs(TestApps, [])]
+ end.
+
+application_dirs([], Acc) -> lists:reverse(Acc);
+application_dirs([App|Rest], Acc) ->
+ application_dirs(Rest, [rebar_app_info:ebin_dir(App)|Acc]).
+
+test_state(State) ->
+ TestOpts = case rebar_state:get(State, ct_compile_opts, []) of
+ [] -> [];
+ Opts -> [{erl_opts, Opts}]
+ end,
+ [first_files(State)|TestOpts].
+
first_files(State) ->
- BaseFirst = rebar_state:get(State, erl_first_files, []),
- CTFirst = rebar_state:get(State, common_test_first_files, []),
- rebar_state:set(State, erl_first_files, BaseFirst ++ CTFirst).
+ CTFirst = rebar_state:get(State, ct_first_files, []),
+ {erl_first_files, CTFirst}.
-resolve_ct_opts(State, CmdLineOpts, OutDir) ->
- CTOpts = rebar_state:get(State, common_test_opts, []),
+resolve_ct_opts(State, CmdLineOpts) ->
+ CTOpts = rebar_state:get(State, ct_opts, []),
Opts = lists:ukeymerge(1,
lists:ukeysort(1, CmdLineOpts),
lists:ukeysort(1, CTOpts)),
- %% rebar has seperate input and output directories whereas `common_test`
- %% uses only a single directory so set `dir` to our precompiled `OutDir`
- %% and disable `auto_compile`
- [{auto_compile, false}, {dir, OutDir}] ++ lists:keydelete(dir, 1, Opts).
+ %% disable `auto_compile` and remove `dir` from the opts
+ [{auto_compile, false}|lists:keydelete(dir, 1, Opts)].
+
+compile_tests(State, TestApps, InDirs) ->
+ State1 = replace_src_dirs(State, InDirs),
+ F = fun(AppInfo) ->
+ AppDir = rebar_app_info:dir(AppInfo),
+ S = case rebar_app_info:state(AppInfo) of
+ undefined ->
+ C = rebar_config:consult(AppDir),
+ rebar_state:new(State1, C, AppDir);
+ AppState ->
+ AppState
+ end,
+ ok = rebar_erlc_compiler:compile(S,
+ ec_cnv:to_list(rebar_app_info:dir(AppInfo)),
+ ec_cnv:to_list(rebar_app_info:out_dir(AppInfo)))
+ end,
+ lists:foreach(F, TestApps),
+ compile_bare_tests(State1, TestApps).
-maybe_compile_extra_tests(TestApps, State, InDirs, OutDir) ->
+compile_bare_tests(State, TestApps) ->
F = fun(App) -> rebar_app_info:dir(App) == rebar_dir:get_cwd() end,
case lists:filter(F, TestApps) of
- %% compile just the `test` and extra test directories of the base dir
- [] ->
- ErlOpts = rebar_state:get(State, common_test_compile_opts, []) ++
- rebar_utils:erl_opts(State),
- TestOpts = [{outdir, OutDir}] ++
- [{src_dirs, ["test"|InDirs]}] ++
- lists:keydelete(src_dirs, 1, ErlOpts),
- TestState = first_files(rebar_state:set(State, erl_opts, TestOpts)),
- rebar_erlc_compiler:compile(TestState, rebar_dir:get_cwd(), rebar_dir:get_cwd());
+ %% compile just the `test` directory of the base dir
+ [] -> rebar_erlc_compiler:compile(State,
+ rebar_dir:get_cwd(),
+ rebar_dir:base_dir(State));
%% already compiled `./test` so do nothing
- _ -> ok
+ _ -> ok
end.
+replace_src_dirs(State, InDirs) ->
+ %% replace any `src_dirs` with just the `test` dir and any `InDirs`
+ ErlOpts = rebar_state:get(State, erl_opts, []),
+ StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts),
+ rebar_state:set(State, erl_opts, [{src_dirs, ["test"|InDirs]}|StrippedOpts]).
+
+maybe_cover_compile(State, Opts) ->
+ State1 = case proplists:get_value(cover, Opts, false) of
+ true -> rebar_state:set(State, cover_enabled, true);
+ false -> State
+ end,
+ rebar_prv_cover:maybe_cover_compile(State1).
+
handle_results([Result]) ->
handle_results(Result);
handle_results([Result|Results]) when is_list(Results) ->
diff --git a/src/rebar_prv_cover.erl b/src/rebar_prv_cover.erl
new file mode 100644
index 0000000..765e2ac
--- /dev/null
+++ b/src/rebar_prv_cover.erl
@@ -0,0 +1,343 @@
+%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
+%% ex: ts=4 sw=4 et
+
+-module(rebar_prv_cover).
+
+-behaviour(provider).
+
+-export([init/1,
+ do/1,
+ maybe_cover_compile/1,
+ maybe_write_coverdata/2,
+ format_error/1]).
+
+-include("rebar.hrl").
+
+-define(PROVIDER, cover).
+-define(DEPS, []).
+
+%% ===================================================================
+%% Public API
+%% ===================================================================
+
+-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
+init(State) ->
+ State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER},
+ {module, ?MODULE},
+ {bare, false},
+ {deps, ?DEPS},
+ {example, "rebar cover"},
+ {short_desc, "Perform coverage analysis."},
+ {desc, ""},
+ {opts, cover_opts(State)}])),
+ {ok, State1}.
+
+-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
+do(State) ->
+ {Opts, _} = rebar_state:command_parsed_args(State),
+ case proplists:get_value(reset, Opts, false) of
+ true -> reset(State);
+ false -> analyze(State)
+ end.
+
+-spec maybe_cover_compile(rebar_state:t()) -> ok.
+maybe_cover_compile(State) ->
+ case rebar_state:get(State, cover_enabled, false) of
+ true -> cover_compile(State);
+ false -> ok
+ end.
+
+-spec maybe_write_coverdata(rebar_state:t(), atom()) -> ok.
+maybe_write_coverdata(State, Task) ->
+ case cover:modules() of
+ %% no coverdata collected, skip writing anything out
+ [] -> ok;
+ _ -> write_coverdata(State, Task)
+ end.
+
+-spec format_error(any()) -> iolist().
+format_error(Reason) ->
+ io_lib:format("~p", [Reason]).
+
+%% ===================================================================
+%% Internal functions
+%% ===================================================================
+
+reset(State) ->
+ ?INFO("Resetting collected cover data...", []),
+ CoverDir = cover_dir(State),
+ CoverFiles = get_all_coverdata(CoverDir),
+ F = fun(File) ->
+ case file:delete(File) of
+ {error, Reason} ->
+ ?WARN("Error deleting ~p: ~p", [Reason, File]);
+ _ -> ok
+ end
+ end,
+ ok = lists:foreach(F, CoverFiles),
+ {ok, State}.
+
+analyze(State) ->
+ ?INFO("Performing cover analysis...", []),
+ %% figure out what coverdata we have
+ CoverDir = cover_dir(State),
+ CoverFiles = get_all_coverdata(CoverDir),
+ %% start the cover server if necessary
+ {ok, CoverPid} = start_cover(),
+ %% redirect cover output
+ true = redirect_cover_output(State, CoverPid),
+ %% analyze!
+ ok = case analyze(State, CoverFiles) of
+ [] -> ok;
+ Analysis ->
+ print_analysis(Analysis, verbose(State)),
+ write_index(State, Analysis)
+ end,
+ {ok, State}.
+
+get_all_coverdata(CoverDir) ->
+ ok = filelib:ensure_dir(filename:join([CoverDir, "dummy.log"])),
+ {ok, Files} = file:list_dir(CoverDir),
+ rebar_utils:filtermap(fun(FileName) ->
+ case filename:extension(FileName) == ".coverdata" of
+ true -> {true, filename:join([CoverDir, FileName])};
+ false -> false
+ end
+ end, Files).
+
+analyze(_State, []) ->
+ ?WARN("No coverdata found", []),
+ [];
+analyze(State, CoverFiles) ->
+ %% reset any existing cover data
+ ok = cover:reset(),
+ %% import all coverdata files
+ ok = lists:foreach(fun(M) -> import(M) end, CoverFiles),
+ [{"aggregate", CoverFiles, analysis(State, "aggregate")}] ++
+ analyze(State, CoverFiles, []).
+
+analyze(_State, [], Acc) -> lists:reverse(Acc);
+analyze(State, [F|Rest], Acc) ->
+ %% reset any existing cover data
+ ok = cover:reset(),
+ %% extract taskname from the CoverData file
+ Task = filename:basename(F, ".coverdata"),
+ %% import task cover data and process it
+ ok = import(F),
+ analyze(State, Rest, [{Task, [F], analysis(State, Task)}] ++ Acc).
+
+import(CoverData) ->
+ case cover:import(CoverData) of
+ {error, {cant_open_file, F, _Reason}} ->
+ ?WARN("Can't import cover data from ~ts.", [F]),
+ error;
+ ok -> ok
+ end.
+
+analysis(State, Task) ->
+ Mods = cover:imported_modules(),
+ lists:map(
+ fun(Mod) ->
+ {ok, Answer} = cover:analyze(Mod, coverage, line),
+ {ok, File} = analyze_to_file(Mod, State, Task),
+ {Mod, process(Answer), File}
+ end,
+ Mods).
+
+analyze_to_file(Mod, State, Task) ->
+ CoverDir = cover_dir(State),
+ TaskDir = filename:join([CoverDir, Task]),
+ ok = filelib:ensure_dir(filename:join([TaskDir, "dummy.html"])),
+ case code:ensure_loaded(Mod) of
+ {module, _} ->
+ write_file(Mod, mod_to_filename(TaskDir, Mod));
+ {error, _} ->
+ ?WARN("Can't load module ~ts.", [Mod]),
+ {ok, []}
+ end.
+
+write_file(Mod, FileName) ->
+ case cover:analyze_to_file(Mod, FileName, [html]) of
+ {ok, File} -> {ok, File};
+ {error, Reason} ->
+ ?WARN("Couldn't write annotated file for module ~p for reason ~p", [Mod, Reason]),
+ {ok, []}
+ end.
+
+mod_to_filename(TaskDir, M) ->
+ filename:join([TaskDir, atom_to_list(M) ++ ".html"]).
+
+process(Coverage) -> process(Coverage, {0, 0}).
+
+process([], {0, 0}) ->
+ "0%";
+process([], {Cov, Not}) ->
+ integer_to_list(trunc((Cov / (Cov + Not)) * 100)) ++ "%";
+%% line 0 is a line added by eunit and never executed so ignore it
+process([{{_, 0}, _}|Rest], Acc) -> process(Rest, Acc);
+process([{_, {Cov, Not}}|Rest], {Covered, NotCovered}) ->
+ process(Rest, {Covered + Cov, NotCovered + Not}).
+
+print_analysis(_, false) -> ok;
+print_analysis(Analysis, true) ->
+ {_, CoverFiles, Stats} = lists:keyfind("aggregate", 1, Analysis),
+ ConsoleStats = [ {atom_to_list(M), C} || {M, C, _} <- Stats ],
+ Table = format_table(ConsoleStats, CoverFiles),
+ io:format("~ts", [Table]).
+
+format_table(Stats, CoverFiles) ->
+ MaxLength = max(lists:foldl(fun max_length/2, 0, Stats), 20),
+ Header = header(MaxLength),
+ Seperator = seperator(MaxLength),
+ [io_lib:format("~ts~n~ts~n~ts~n", [Seperator, Header, Seperator]),
+ lists:map(fun({Mod, Coverage}) ->
+ Name = format(Mod, MaxLength),
+ Cov = format(Coverage, 8),
+ io_lib:format(" | ~ts | ~ts |~n", [Name, Cov])
+ end, Stats),
+ io_lib:format("~ts~n", [Seperator]),
+ io_lib:format(" coverage calculated from:~n", []),
+ lists:map(fun(File) ->
+ io_lib:format(" ~ts~n", [File])
+ end, CoverFiles)].
+
+max_length({ModName, _}, Min) ->
+ Length = length(lists:flatten(ModName)),
+ case Length > Min of
+ true -> Length;
+ false -> Min
+ end.
+
+header(Width) ->
+ [" | ", format("module", Width), " | ", format("coverage", 8), " |"].
+
+seperator(Width) ->
+ [" |--", io_lib:format("~*c", [Width, $-]), "--|------------|"].
+
+format(String, Width) -> io_lib:format("~*.ts", [Width, String]).
+
+write_index(State, Coverage) ->
+ CoverDir = cover_dir(State),
+ FileName = filename:join([CoverDir, "index.html"]),
+ {ok, F} = file:open(FileName, [write]),
+ ok = file:write(F, "<!DOCTYPE HTML><html>\n"
+ "<head><meta charset=\"utf-8\">"
+ "<title>Coverage Summary</title></head>\n"
+ "<body>\n"),
+ {Aggregate, Rest} = lists:partition(fun({"aggregate", _, _}) -> true; (_) -> false end,
+ Coverage),
+ ok = write_index_section(F, Aggregate),
+ ok = write_index_section(F, Rest),
+ ok = file:write(F, "</body></html>"),
+ ok = file:close(F),
+ io:format(" cover summary written to: ~ts~n", [filename:absname(FileName)]).
+
+write_index_section(_F, []) -> ok;
+write_index_section(F, [{Section, DataFile, Mods}|Rest]) ->
+ %% Write the report
+ ok = file:write(F, ?FMT("<h1>~s summary</h1>\n", [Section])),
+ ok = file:write(F, "coverage calculated from:\n<ul>"),
+ ok = lists:foreach(fun(D) -> ok = file:write(F, io_lib:format("<li>~ts</li>", [D])) end,
+ DataFile),
+ ok = file:write(F, "</ul>\n"),
+ ok = file:write(F, "<table><tr><th>module</th><th>coverage %</th></tr>\n"),
+ FmtLink =
+ fun({Mod, Cov, Report}) ->
+ ?FMT("<tr><td><a href='~ts'>~ts</a></td><td>~ts</td>\n",
+ [strip_coverdir(Report), Mod, Cov])
+ end,
+ lists:foreach(fun(M) -> ok = file:write(F, FmtLink(M)) end, Mods),
+ ok = file:write(F, "</table>\n"),
+ write_index_section(F, Rest).
+
+%% fix for r15b which doesn't put the correct path in the `source` section
+%% of `module_info(compile)`
+strip_coverdir([]) -> "";
+strip_coverdir(File) ->
+ filename:join(lists:reverse(lists:sublist(lists:reverse(filename:split(File)),
+ 2))).
+
+cover_compile(State) ->
+ %% start the cover server if necessary
+ {ok, CoverPid} = start_cover(),
+ %% redirect cover output
+ true = redirect_cover_output(State, CoverPid),
+ %% cover compile the modules we just compiled
+ Apps = filter_checkouts(rebar_state:project_apps(State)),
+ CompileResult = compile_beam_directories(Apps, []) ++
+ compile_bare_test_directory(State),
+ %% print any warnings about modules that failed to cover compile
+ lists:foreach(fun print_cover_warnings/1, CompileResult).
+
+filter_checkouts(Apps) -> filter_checkouts(Apps, []).
+
+filter_checkouts([], Acc) -> lists:reverse(Acc);
+filter_checkouts([App|Rest], Acc) ->
+ AppDir = filename:absname(rebar_app_info:dir(App)),
+ CheckoutsDir = filename:absname("_checkouts"),
+ case lists:prefix(CheckoutsDir, AppDir) of
+ true -> filter_checkouts(Rest, Acc);
+ false -> filter_checkouts(Rest, [App|Acc])
+ end.
+
+compile_beam_directories([], Acc) -> Acc;
+compile_beam_directories([App|Rest], Acc) ->
+ Result = cover:compile_beam_directory(filename:join([rebar_app_info:out_dir(App),
+ "ebin"])),
+ compile_beam_directories(Rest, Acc ++ Result).
+
+compile_bare_test_directory(State) ->
+ case cover:compile_beam_directory(filename:join([rebar_dir:base_dir(State),
+ "ebin"])) of
+ %% if directory doesn't exist just return empty result set
+ {error, enoent} -> [];
+ Result -> Result
+ end.
+
+start_cover() ->
+ case cover:start() of
+ {ok, Pid} -> {ok, Pid};
+ {error, {already_started, Pid}} -> {ok, Pid}
+ end.
+
+redirect_cover_output(State, CoverPid) ->
+ %% redirect cover console output to file
+ DataDir = cover_dir(State),
+ ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
+ {ok, F} = file:open(filename:join([DataDir, "cover.log"]),
+ [append]),
+ group_leader(F, CoverPid).
+
+print_cover_warnings({ok, _}) -> ok;
+print_cover_warnings({error, File}) ->
+ ?WARN("Cover compilation of ~p failed, module is not included in cover data.",
+ [File]).
+
+write_coverdata(State, Task) ->
+ DataDir = cover_dir(State),
+ ok = filelib:ensure_dir(filename:join([DataDir, "dummy.log"])),
+ ExportFile = filename:join([DataDir, atom_to_list(Task) ++ ".coverdata"]),
+ case cover:export(ExportFile) of
+ ok ->
+ ?DEBUG("Cover data written to ~p.", [ExportFile]),
+ ok;
+ {error, Reason} ->
+ ?WARN("Cover data export failed: ~p", [Reason])
+ end.
+
+verbose(State) ->
+ {Opts, _} = rebar_state:command_parsed_args(State),
+ case proplists:get_value(verbose, Opts, missing) of
+ missing -> rebar_state:get(State, cover_print_enabled, false);
+ Else -> Else
+ end.
+
+cover_dir(State) ->
+ rebar_state:get(State, cover_data_dir, "_cover").
+
+cover_opts(_State) ->
+ [{reset, $r, "reset", boolean, help(reset)},
+ {verbose, $v, "verbose", boolean, help(verbose)}].
+
+help(reset) -> "Reset all coverdata.";
+help(verbose) -> "Print coverage analysis.". \ No newline at end of file
diff --git a/src/rebar_prv_eunit.erl b/src/rebar_prv_eunit.erl
index cd1b0f8..d841f26 100644
--- a/src/rebar_prv_eunit.erl
+++ b/src/rebar_prv_eunit.erl
@@ -30,35 +30,20 @@ init(State) ->
{opts, eunit_opts(State)},
{profiles, [test]}]),
State1 = rebar_state:add_provider(State, Provider),
- {ok, State1}.
+ State2 = rebar_state:add_to_profile(State1, test, test_state(State1)),
+ {ok, State2}.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
?INFO("Performing EUnit tests...", []),
- {RawOpts, _} = rebar_state:command_parsed_args(State),
- Opts = transform_opts(RawOpts, State),
- TestApps = filter_checkouts(rebar_state:project_apps(State)),
- OutDir = proplists:get_value(outdir, Opts, default_test_dir(State)),
- ?DEBUG("Compiling EUnit instrumented modules in: ~p", [OutDir]),
- lists:foreach(fun(App) ->
- AppDir = rebar_app_info:dir(App),
- AppOutDir = rebar_app_info:out_dir(App),
- C = rebar_config:consult(AppDir),
- S = rebar_state:new(State, C, AppDir),
- %% combine `erl_first_files` and `eunit_first_files` and adjust
- %% compile opts to include `eunit_compile_opts`, `{d, 'TEST'}`
- %% and `{src_dirs, "test"}`
- TestState = first_files(test_state(S, OutDir)),
- ok = rebar_erlc_compiler:compile(TestState, AppDir, AppOutDir)
- end, TestApps),
- ok = maybe_compile_extra_tests(TestApps, State, OutDir),
- Path = code:get_path(),
- true = code:add_patha(OutDir),
+ {Opts, _} = rebar_state:command_parsed_args(State),
EUnitOpts = resolve_eunit_opts(State, Opts),
- AppsToTest = [{application, erlang:binary_to_atom(rebar_app_info:name(App), unicode)}
- || App <- TestApps],
+ TestApps = filter_checkouts(rebar_state:project_apps(State)),
+ ok = compile_tests(State, TestApps),
+ ok = maybe_cover_compile(State, Opts),
+ AppsToTest = test_dirs(State, TestApps),
Result = eunit:test(AppsToTest, EUnitOpts),
- true = code:set_path(Path),
+ ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER),
case handle_results(Result) of
{error, Reason} ->
{error, {?MODULE, Reason}};
@@ -73,24 +58,12 @@ format_error({error_running_tests, Reason}) ->
io_lib:format("Error running tests: ~p", [Reason]).
eunit_opts(_State) ->
- [{outdir, $o, "outdir", string, help(outdir)},
+ [{cover, $c, "cover", boolean, help(cover)},
{verbose, $v, "verbose", boolean, help(verbose)}].
-help(outdir) -> "Output directory for EUnit compiled modules";
+help(cover) -> "Generate cover data";
help(verbose) -> "Verbose output".
-transform_opts(Opts, State) -> transform_opts(Opts, State, []).
-
-transform_opts([], _State, Acc) -> Acc;
-transform_opts([{outdir, Path}|Rest], State, Acc) ->
- NewAcc = case filename:pathtype(Path) of
- absolute -> [{outdir, Path}] ++ Acc;
- _ -> [{outdir, filename:join([rebar_state:dir(State), Path])}] ++ Acc
- end,
- transform_opts(Rest, State, NewAcc);
-transform_opts([{Key, Val}|Rest], State, Acc) ->
- transform_opts(Rest, State, [{Key, Val}|Acc]).
-
filter_checkouts(Apps) -> filter_checkouts(Apps, []).
filter_checkouts([], Acc) -> lists:reverse(Acc);
@@ -102,30 +75,33 @@ filter_checkouts([App|Rest], Acc) ->
false -> filter_checkouts(Rest, [App|Acc])
end.
-default_test_dir(State) ->
- Tmp = rebar_file_utils:system_tmpdir(),
- Root = filename:join([rebar_state:dir(State), Tmp]),
- Project = filename:basename(rebar_state:dir(State)),
- OutDir = filename:join([Root, Project ++ "_rebar3_eunit"]),
- ok = rebar_file_utils:reset_dir(OutDir),
- OutDir.
-
-test_state(State, TmpDir) ->
- ErlOpts = rebar_state:get(State, eunit_compile_opts, []) ++
- rebar_utils:erl_opts(State),
- ErlOpts1 = [{outdir, TmpDir}] ++
- add_test_dir(ErlOpts),
- TestOpts = safe_define_test_macro(ErlOpts1),
- rebar_state:set(State, erl_opts, TestOpts).
-
-add_test_dir(Opts) ->
- %% if no src_dirs are set we have to specify `src` or it won't
- %% be built
- case proplists:append_values(src_dirs, Opts) of
- [] -> [{src_dirs, ["src", "test"]} | Opts];
- _ -> [{src_dirs, ["test"]} | Opts]
+resolve_eunit_opts(State, Opts) ->
+ EUnitOpts = rebar_state:get(State, eunit_opts, []),
+ case proplists:get_value(verbose, Opts, false) of
+ true -> set_verbose(EUnitOpts);
+ false -> EUnitOpts
+ end.
+
+test_dirs(State, TestApps) ->
+ %% we need to add "./ebin" if it exists but only if it's not already
+ %% due to be added
+ F = fun(App) -> rebar_app_info:dir(App) =/= rebar_dir:get_cwd() end,
+ BareEbin = filename:join([rebar_dir:base_dir(State), "ebin"]),
+ case lists:any(F, TestApps) andalso filelib:is_dir(BareEbin) of
+ false -> application_dirs(TestApps, []);
+ true -> [{dir, BareEbin}|application_dirs(TestApps, [])]
end.
+application_dirs([], Acc) -> lists:reverse(Acc);
+application_dirs([App|Rest], Acc) ->
+ AppName = list_to_atom(binary_to_list(rebar_app_info:name(App))),
+ application_dirs(Rest, [{application, AppName}|Acc]).
+
+test_state(State) ->
+ ErlOpts = rebar_state:get(State, eunit_compile_opts, []),
+ TestOpts = safe_define_test_macro(ErlOpts),
+ first_files(State) ++ [{erl_opts, TestOpts}].
+
safe_define_test_macro(Opts) ->
%% defining a compile macro twice results in an exception so
%% make sure 'TEST' is only defined once
@@ -140,39 +116,58 @@ test_defined([_|Rest]) -> test_defined(Rest);
test_defined([]) -> false.
first_files(State) ->
- BaseFirst = rebar_state:get(State, erl_first_files, []),
EUnitFirst = rebar_state:get(State, eunit_first_files, []),
- rebar_state:set(State, erl_first_files, BaseFirst ++ EUnitFirst).
-
-resolve_eunit_opts(State, Opts) ->
- EUnitOpts = rebar_state:get(State, eunit_opts, []),
- case lists:member({verbose, true}, Opts) of
- true -> set_verbose(EUnitOpts);
- false -> EUnitOpts
- end.
+ [{erl_first_files, EUnitFirst}].
set_verbose(Opts) ->
+ %% if `verbose` is already set don't set it again
case lists:member(verbose, Opts) of
true -> Opts;
false -> [verbose] ++ Opts
end.
-maybe_compile_extra_tests(TestApps, State, OutDir) ->
+compile_tests(State, TestApps) ->
+ State1 = replace_src_dirs(State),
+ F = fun(AppInfo) ->
+ AppDir = rebar_app_info:dir(AppInfo),
+ S = case rebar_app_info:state(AppInfo) of
+ undefined ->
+ C = rebar_config:consult(AppDir),
+ rebar_state:new(State1, C, AppDir);
+ AppState ->
+ AppState
+ end,
+ ok = rebar_erlc_compiler:compile(S,
+ ec_cnv:to_list(rebar_app_info:dir(AppInfo)),
+ ec_cnv:to_list(rebar_app_info:out_dir(AppInfo)))
+ end,
+ lists:foreach(F, TestApps),
+ compile_bare_tests(State1, TestApps).
+
+compile_bare_tests(State, TestApps) ->
F = fun(App) -> rebar_app_info:dir(App) == rebar_dir:get_cwd() end,
case lists:filter(F, TestApps) of
- %% compile just the `test` and extra test directories of the base dir
- [] ->
- ErlOpts = rebar_state:get(State, common_test_compile_opts, []) ++
- rebar_utils:erl_opts(State),
- TestOpts = [{outdir, OutDir}] ++
- [{src_dirs, ["test"]}] ++
- safe_define_test_macro(lists:keydelete(src_dirs, 1, ErlOpts)),
- TestState = first_files(rebar_state:set(State, erl_opts, TestOpts)),
- rebar_erlc_compiler:compile(TestState, rebar_dir:get_cwd(), rebar_dir:get_cwd());
+ %% compile just the `test` directory of the base dir
+ [] -> rebar_erlc_compiler:compile(State,
+ rebar_dir:get_cwd(),
+ rebar_dir:base_dir(State));
%% already compiled `./test` so do nothing
- _ -> ok
+ _ -> ok
end.
+replace_src_dirs(State) ->
+ %% replace any `src_dirs` with just the `test` dir
+ ErlOpts = rebar_state:get(State, erl_opts, []),
+ StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts),
+ rebar_state:set(State, erl_opts, [{src_dirs, ["test"]}|StrippedOpts]).
+
+maybe_cover_compile(State, Opts) ->
+ State1 = case proplists:get_value(cover, Opts, false) of
+ true -> rebar_state:set(State, cover_enabled, true);
+ false -> State
+ end,
+ rebar_prv_cover:maybe_cover_compile(State1).
+
handle_results(ok) -> ok;
handle_results(error) ->
{error, unknown_error};
diff --git a/src/rebar_state.erl b/src/rebar_state.erl
index 39e0e88..8170b8d 100644
--- a/src/rebar_state.erl
+++ b/src/rebar_state.erl
@@ -15,7 +15,7 @@
command_args/1, command_args/2,
command_parsed_args/1, command_parsed_args/2,
- apply_profiles/2,
+ add_to_profile/3, apply_profiles/2,
dir/1, dir/2,
create_logic_providers/2,
@@ -97,7 +97,6 @@ new(ParentState, Config, Dir) ->
D = proplists:get_value(deps, Config, []),
dict:from_list([{{deps, default}, D} | Config])
end,
-
NewOpts = dict:merge(fun(_Key, Value1, _Value2) ->
Value1
end, LocalOpts, Opts),
@@ -188,6 +187,13 @@ apply_overrides(State=#state_t{overrides=Overrides}, AppName) ->
StateAcc
end, State2, Overrides).
+add_to_profile(State, Profile, KVs) when is_atom(Profile), is_list(KVs) ->
+ Profiles = rebar_state:get(State, profiles, []),
+ ProfileOpts = dict:from_list(proplists:get_value(Profile, Profiles, [])),
+ NewOpts = merge_opts(Profile, dict:from_list(KVs), ProfileOpts),
+ NewProfiles = [{Profile, dict:to_list(NewOpts)}|lists:keydelete(Profile, 1, Profiles)],
+ rebar_state:set(State, profiles, NewProfiles).
+
apply_profiles(State, Profile) when not is_list(Profile) ->
apply_profiles(State, [Profile]);
apply_profiles(State, [default]) ->