diff options
author | Tristan Sloughter <tristan.sloughter@gmail.com> | 2015-03-03 07:32:57 -0600 |
---|---|---|
committer | Tristan Sloughter <tristan.sloughter@gmail.com> | 2015-03-03 07:32:57 -0600 |
commit | 358046b0957fc7211f5dab7d76f0bc365d00c439 (patch) | |
tree | f13157852e061f5184ce9727dd6051d1a85e56d9 /src | |
parent | 4c70d16e505c05695e902bea502855d8383fbe82 (diff) | |
parent | 6c421e543373aaf41a6ed10719f5da19b0cafd93 (diff) |
Merge pull request #202 from talentdeficit/cover
`cover` task
Diffstat (limited to 'src')
-rw-r--r-- | src/rebar.app.src | 1 | ||||
-rw-r--r-- | src/rebar_cover_utils.erl | 261 | ||||
-rw-r--r-- | src/rebar_erlc_compiler.erl | 6 | ||||
-rw-r--r-- | src/rebar_otp_app.erl | 30 | ||||
-rw-r--r-- | src/rebar_prv_common_test.erl | 174 | ||||
-rw-r--r-- | src/rebar_prv_cover.erl | 343 | ||||
-rw-r--r-- | src/rebar_prv_eunit.erl | 153 | ||||
-rw-r--r-- | src/rebar_state.erl | 10 |
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]) -> |