From b77d3e5083e281101a2e7820388bb7aa51143ddf Mon Sep 17 00:00:00 2001 From: alisdair sullivan Date: Tue, 10 Mar 2015 09:31:04 -0700 Subject: modify `ct` provider to copy selected directories and compile them alongside their source (ie, if `some_tests` is a directory that contains test suites beams resulting from compiling them will be placed in `some_tests` in the appropriate `_build` directory --- src/rebar_prv_common_test.erl | 602 ++++++++++++++++++++++++------------------ src/rebar_prv_cover.erl | 22 +- test/rebar_ct_SUITE.erl | 520 ++++++++++++++++++++++++++++++++++++ 3 files changed, 871 insertions(+), 273 deletions(-) create mode 100644 test/rebar_ct_SUITE.erl diff --git a/src/rebar_prv_common_test.erl b/src/rebar_prv_common_test.erl index f4085c5..97f0637 100644 --- a/src/rebar_prv_common_test.erl +++ b/src/rebar_prv_common_test.erl @@ -2,12 +2,13 @@ %% ex: ts=4 sw=4 et -module(rebar_prv_common_test). - -behaviour(provider). -export([init/1, do/1, format_error/1]). +%% exported for test purposes, consider private +-export([setup_ct/1]). -include("rebar.hrl"). -include_lib("providers/include/providers.hrl"). @@ -37,54 +38,352 @@ init(State) -> -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), - Opts = transform_opts(RawOpts), - TestApps = filter_checkouts(rebar_state:project_apps(State)), - ok = create_dirs(Opts), - InDirs = in_dirs(State, RawOpts), - ok = compile_tests(State, TestApps, InDirs), - case resolve_ct_opts(State, TestApps, Opts) of - {ok, CTOpts} -> - run_test(State, RawOpts, CTOpts); - {error, Reason} -> - ?PRV_ERROR(Reason) - end. - -run_test(State, RawOpts, CTOpts) -> - ok = maybe_cover_compile(State, RawOpts), - Verbose = proplists:get_value(verbose, RawOpts, false), - Result = run_test(CTOpts, Verbose), - ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER), - case Result of - {error, Reason} -> - {error, {?MODULE, Reason}}; - ok -> - {ok, State} + try + case setup_ct(State) of + {error, {no_tests_specified, Opts}} -> + ?WARN("No tests specified in opts: ~p", [Opts]), + {ok, State}; + Opts -> + Opts1 = setup_logdir(State, Opts), + ?DEBUG("common test opts: ~p", [Opts1]), + run_test(State, Opts1) + end + catch error:Reason -> ?PRV_ERROR(Reason) end. -spec format_error(any()) -> iolist(). +format_error({multiple_dirs_and_suites, Opts}) -> + io_lib:format("Multiple dirs declared alongside suite in opts: ~p", [Opts]); +format_error({bad_dir_or_suite, Opts}) -> + io_lib:format("Bad value for dir or suite in opts: ~p", [Opts]); format_error({failures_running_tests, {Failed, AutoSkipped}}) -> io_lib:format("Failures occured running tests: ~b", [Failed+AutoSkipped]); format_error({error_running_tests, Reason}) -> io_lib:format("Error running tests: ~p", [Reason]); -format_error({error_processing_options, Reason}) -> - io_lib:format("Error processing options: ~p", [Reason]). +format_error({error, Reason}) -> + io_lib:format("Unknown error: ~p", [Reason]). + +%% =================================================================== +%% Internal functions +%% =================================================================== + +run_test(State, Opts) -> + {RawOpts, _} = rebar_state:command_parsed_args(State), + Result = case proplists:get_value(verbose, RawOpts, false) of + true -> run_test(Opts); + false -> run_test_quiet(Opts) + end, + ok = rebar_prv_cover:maybe_write_coverdata(State, ?PROVIDER), + case Result of + ok -> {ok, State}; + Error -> Error + end. + +run_test(Opts) -> handle_results(ct:run_test(Opts)). -run_test(CTOpts, true) -> - handle_results(ct:run_test(CTOpts)); -run_test(CTOpts, false) -> +run_test_quiet(Opts) -> Pid = self(), - LogDir = proplists:get_value(logdir, CTOpts), + LogDir = proplists:get_value(logdir, Opts), erlang:spawn_monitor(fun() -> {ok, F} = file:open(filename:join([LogDir, "ct.latest.log"]), [write]), true = group_leader(F, self()), - Pid ! ct:run_test(CTOpts) + Pid ! ct:run_test(Opts) end), - receive Result -> handle_quiet_results(CTOpts, Result) end. + receive Result -> handle_quiet_results(Opts, Result) end. + +handle_results(Results) when is_list(Results) -> + Result = lists:foldl(fun sum_results/2, {0, 0, {0,0}}, Results), + handle_results(Result); +handle_results({_, Failed, {_, AutoSkipped}}) + when Failed > 0 orelse AutoSkipped > 0 -> + ?PRV_ERROR({failures_running_tests, {Failed, AutoSkipped}}); +handle_results({error, Reason}) -> + ?PRV_ERROR({error_running_tests, Reason}); +handle_results(_) -> + ok. + +sum_results({Passed, Failed, {UserSkipped, AutoSkipped}}, + {Passed2, Failed2, {UserSkipped2, AutoSkipped2}}) -> + {Passed+Passed2, Failed+Failed2, + {UserSkipped+UserSkipped2, AutoSkipped+AutoSkipped2}}. + +handle_quiet_results(_, {error, _} = Result) -> + handle_results(Result); +handle_quiet_results(_, {'DOWN', _, _, _, Reason}) -> + handle_results(?PRV_ERROR(Reason)); +handle_quiet_results(CTOpts, Results) when is_list(Results) -> + _ = [format_result(Result) || Result <- Results], + case handle_results(Results) of + ?PRV_ERROR({failures_running_tests, _}) = Error -> + LogDir = proplists:get_value(logdir, CTOpts), + Index = filename:join([LogDir, "index.html"]), + ?CONSOLE("Results written to ~p.", [Index]), + Error; + Other -> + Other + end; +handle_quiet_results(CTOpts, Result) -> + handle_quiet_results(CTOpts, [Result]). + +format_result({Passed, 0, {0, 0}}) -> + ?CONSOLE("All ~p tests passed.", [Passed]); +format_result({Passed, Failed, Skipped}) -> + Format = [format_failed(Failed), format_skipped(Skipped), + format_passed(Passed)], + ?CONSOLE("~s", [Format]). + +format_failed(0) -> + []; +format_failed(Failed) -> + io_lib:format("Failed ~p tests. ", [Failed]). + +format_passed(Passed) -> + io_lib:format("Passed ~p tests. ", [Passed]). + +format_skipped({0, 0}) -> + []; +format_skipped({User, Auto}) -> + io_lib:format("Skipped ~p (~p, ~p) tests. ", [User+Auto, User, Auto]). + +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) -> + CTFirst = rebar_state:get(State, ct_first_files, []), + {erl_first_files, CTFirst}. + +setup_ct(State) -> + Opts = resolve_ct_opts(State), + Opts1 = discover_tests(State, Opts), + copy_and_compile_tests(State, Opts1). + +resolve_ct_opts(State) -> + {RawOpts, _} = rebar_state:command_parsed_args(State), + CmdOpts = transform_opts(RawOpts), + CfgOpts = rebar_state:get(State, ct_opts, []), + Merged = lists:ukeymerge(1, + lists:ukeysort(1, CmdOpts), + lists:ukeysort(1, CfgOpts)), + %% make sure `dir` and/or `suite` from command line go in as + %% a pair overriding both `dir` and `suite` from config if + %% they exist + case {proplists:get_value(suite, CmdOpts), proplists:get_value(dir, CmdOpts)} of + {undefined, undefined} -> Merged; + {_Suite, undefined} -> lists:keydelete(dir, 1, Merged); + {undefined, _Dir} -> lists:keydelete(suite, 1, Merged); + {_Suite, _Dir} -> Merged + end. + +discover_tests(State, Opts) -> + case proplists:get_value(spec, Opts) of + undefined -> discover_dirs_and_suites(State, Opts); + TestSpec -> discover_testspec(TestSpec, Opts) + end. + +discover_dirs_and_suites(State, Opts) -> + case {proplists:get_value(dir, Opts), proplists:get_value(suite, Opts)} of + %% no dirs or suites defined, try using `$APP/test` and `$ROOT/test` + %% as suites + {undefined, undefined} -> test_dirs(State, Opts); + %% no dirs defined + {undefined, _} -> Opts; + %% no suites defined + {_, undefined} -> Opts; + %% a single dir defined, this is ok + {Dirs, Suites} when is_integer(hd(Dirs)), is_list(Suites) -> Opts; + %% still a single dir defined, adjust to make acceptable to ct + {[Dir], Suites} when is_integer(hd(Dir)), is_list(Suites) -> + [{dir, Dir}|lists:keydelete(dir, 1, Opts)]; + %% multiple dirs and suites, error now to simplify later steps + {_, _} -> erlang:error({multiple_dirs_and_suites, Opts}) + end. + +discover_testspec(_TestSpec, Opts) -> + lists:keydelete(auto_compile, 1, Opts). + +copy_and_compile_tests(State, Opts) -> + %% possibly enable cover + {RawOpts, _} = rebar_state:command_parsed_args(State), + State1 = case proplists:get_value(cover, RawOpts, false) of + true -> rebar_state:set(State, cover_enabled, true); + false -> State + end, + copy_and_compile_test_suites(State1, Opts). + +copy_and_compile_test_suites(State, Opts) -> + case proplists:get_value(suite, Opts) of + %% no suites, try dirs + undefined -> copy_and_compile_test_dirs(State, Opts); + Suites -> + Dir = proplists:get_value(dir, Opts, undefined), + AllSuites = join(Dir, Suites), + Dirs = find_suite_dirs(AllSuites), + lists:foreach(fun(S) -> + NewPath = copy(State, S), + compile_dir(State, NewPath) + end, Dirs), + NewSuites = lists:map(fun(S) -> retarget_path(State, S) end, AllSuites), + [{suite, NewSuites}|lists:keydelete(suite, 1, Opts)] + end. + +copy_and_compile_test_dirs(State, Opts) -> + case proplists:get_value(dir, Opts) of + undefined -> {error, {no_tests_specified, Opts}}; + %% dir is a single directory + Dir when is_list(Dir), is_integer(hd(Dir)) -> + NewPath = copy(State, Dir), + [{dir, compile_dir(State, NewPath)}|lists:keydelete(dir, 1, Opts)]; + %% dir is a list of directories + Dirs when is_list(Dirs) -> + NewDirs = lists:map(fun(Dir) -> + NewPath = copy(State, Dir), + compile_dir(State, NewPath) + end, Dirs), + [{dir, NewDirs}|lists:keydelete(dir, 1, Opts)] + end. + +join(undefined, Suites) -> Suites; +join(Dir, Suites) when is_list(Dir), is_integer(hd(Dir)) -> + lists:map(fun(S) -> filename:join([Dir, S]) end, Suites); +%% multiple dirs or a bad dir argument, try to continue anyways +join(_, Suites) -> Suites. + +find_suite_dirs(Suites) -> + AllDirs = lists:map(fun(S) -> filename:dirname(filename:absname(S)) end, Suites), + %% eliminate duplicates + lists:usort(AllDirs). + +copy(State, Target) -> + case retarget_path(State, Target) of + %% directory lies outside of our project's file structure so + %% don't copy it + Target -> Target; + NewTarget -> + %% unlink the directory if it's a symlink + case ec_file:is_symlink(NewTarget) of + true -> ok = ec_file:remove(NewTarget); + false -> ok + end, + ok = ec_file:copy(Target, NewTarget, [recursive]), + NewTarget + end. + +compile_dir(State, Dir) -> + NewState = replace_src_dirs(State, [Dir]), + ok = rebar_erlc_compiler:compile(NewState, rebar_dir:base_dir(State), Dir), + ok = maybe_cover_compile(State, Dir), + Dir. + +retarget_path(State, Path) -> + ProjectApps = rebar_state:project_apps(State), + retarget_path(State, Path, ProjectApps). + +%% not relative to any apps in project, check to see it's relative to +%% project root +retarget_path(State, Path, []) -> + case relative_path(reduce_path(Path), rebar_state:dir(State)) of + {ok, NewPath} -> filename:join([rebar_dir:base_dir(State), NewPath]); + %% not relative to project root, don't modify + {error, not_relative} -> Path + end; +%% relative to current app, retarget to the same dir relative to +%% the app's out_dir +retarget_path(State, Path, [App|Rest]) -> + case relative_path(reduce_path(Path), rebar_app_info:dir(App)) of + {ok, NewPath} -> filename:join([rebar_app_info:out_dir(App), NewPath]); + {error, not_relative} -> retarget_path(State, Path, Rest) + end. + +relative_path(Target, To) -> + relative_path1(filename:split(filename:absname(Target)), + filename:split(filename:absname(To))). + +relative_path1([Part|Target], [Part|To]) -> relative_path1(Target, To); +relative_path1([], []) -> {ok, ""}; +relative_path1(Target, []) -> {ok, filename:join(Target)}; +relative_path1(_, _) -> {error, not_relative}. + +reduce_path(Dir) -> reduce_path([], filename:split(filename:absname(Dir))). + +reduce_path([], []) -> filename:nativename("/"); +reduce_path(Acc, []) -> filename:join(lists:reverse(Acc)); +reduce_path(Acc, ["."|Rest]) -> reduce_path(Acc, Rest); +reduce_path([_|Acc], [".."|Rest]) -> reduce_path(Acc, Rest); +reduce_path([], [".."|Rest]) -> reduce_path([], Rest); +reduce_path(Acc, [Component|Rest]) -> reduce_path([Component|Acc], Rest). + +replace_src_dirs(State, Dirs) -> + %% replace any `src_dirs` with the test dirs + ErlOpts = rebar_state:get(State, erl_opts, []), + StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts), + rebar_state:set(State, erl_opts, [{src_dirs, Dirs}|StrippedOpts]). + +test_dirs(State, Opts) -> + BareTest = filename:join([rebar_state:dir(State), "test"]), + F = fun(App) -> rebar_app_info:dir(App) == rebar_state:dir(State) end, + TestApps = project_apps(State), + case filelib:is_dir(BareTest) andalso not lists:any(F, TestApps) of + %% `test` dir at root of project is already scheduled to be + %% included or `test` does not exist + false -> application_dirs(TestApps, Opts, []); + %% need to add `test` dir at root to dirs to be included + true -> application_dirs(TestApps, Opts, [BareTest]) + end. + +project_apps(State) -> + filter_checkouts(rebar_state:project_apps(State)). + +filter_checkouts(Apps) -> filter_checkouts(Apps, []). + +filter_checkouts([], Acc) -> lists:reverse(Acc); +filter_checkouts([App|Rest], Acc) -> + case rebar_app_info:is_checkout(App) of + true -> filter_checkouts(Rest, Acc); + false -> filter_checkouts(Rest, [App|Acc]) + end. + +application_dirs([], Opts, []) -> Opts; +application_dirs([], Opts, [Acc]) -> [{dir, Acc}|Opts]; +application_dirs([], Opts, Acc) -> [{dir, lists:reverse(Acc)}|Opts]; +application_dirs([App|Rest], Opts, Acc) -> + TestDir = filename:join([rebar_app_info:dir(App), "test"]), + case filelib:is_dir(TestDir) of + true -> application_dirs(Rest, Opts, [TestDir|Acc]); + false -> application_dirs(Rest, Opts, Acc) + end. + +setup_logdir(State, Opts) -> + Logdir = case proplists:get_value(logdir, Opts) of + undefined -> filename:join([rebar_dir:base_dir(State), "logs"]); + Dir -> Dir + end, + ensure_dir([Logdir]), + [{logdir, Logdir}|lists:keydelete(logdir, 1, Opts)]. + +ensure_dir([]) -> ok; +ensure_dir([Dir|Rest]) -> + case ec_file:is_dir(Dir) of + true -> + ok; + false -> + ec_file:mkdir_path(Dir) + end, + ensure_dir(Rest). + +maybe_cover_compile(State, Dir) -> + {Opts, _} = rebar_state:command_parsed_args(State), + 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, [Dir]). ct_opts(_State) -> - DefaultLogsDir = filename:join([rebar_dir:get_cwd(), "_build", "logs"]), [{dir, undefined, "dir", string, help(dir)}, %% comma-seperated list {suite, undefined, "suite", string, help(suite)}, %% comma-seperated list {group, undefined, "group", string, help(group)}, %% comma-seperated list @@ -95,13 +394,13 @@ ct_opts(_State) -> {config, undefined, "config", string, help(config)}, %% comma-seperated list {userconfig, undefined, "userconfig", string, help(userconfig)}, %% [{CallbackMod, CfgStrings}] | {CallbackMod, CfgStrings} {allow_user_terms, undefined, "allow_user_terms", boolean, help(allow_user_terms)}, %% Bool - {logdir, undefined, "logdir", {string, DefaultLogsDir}, help(logdir)}, %% dir + {logdir, undefined, "logdir", string, help(logdir)}, %% dir {logopts, undefined, "logopts", string, help(logopts)}, %% enum, no_nl | no_src {verbosity, undefined, "verbosity", string, help(verbosity)}, %% Integer OR [{Category, VLevel}] {silent_connections, undefined, "silent_connections", string, help(silent_connections)}, % all OR %% comma-seperated list {stylesheet, undefined, "stylesheet", string, help(stylesheet)}, %% file - {cover, $c, "cover", boolean, help(cover)}, + {cover, $c, "cover", {boolean, false}, 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} @@ -118,6 +417,7 @@ ct_opts(_State) -> {force_stop, undefined, "force_stop", string, help(force_stop)}, % enum: skip_rest, bool {basic_html, undefined, "basic_html", boolean, help(basic_html)}, %% Booloean {ct_hooks, undefined, "ct_hooks", string, help(ct_hooks)}, %% List: [CTHModule | {CTHModule, CTHInitArgs}] where CTHModule is atom CthInitArgs is term + {auto_compile, undefined, "auto_compile", {boolean, false}, help(auto_compile)}, {verbose, $v, "verbose", boolean, help(verbose)} ]. @@ -131,42 +431,26 @@ help(testcase) -> "List of test cases to run"; help(spec) -> "List of test specs to run"; -help(join_specs) -> - ""; %% ?? help(label) -> "Test label"; help(config) -> "List of config files"; -help(allow_user_terms) -> - ""; %% ?? help(logdir) -> "Log folder"; -help(logopts) -> - ""; %% ?? help(verbosity) -> "Verbosity"; -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) -> - ""; %% ?? help(event_handler) -> "Event handlers to attach to the runner"; help(include) -> "Include folder"; help(abort_if_missing_suites) -> "Abort if suites are missing"; -help(multiply_timetraps) -> - ""; %% ?? -help(scale_timetraps) -> - ""; %% ?? -help(create_priv_dir) -> - ""; %% ?? help(repeat) -> "How often to repeat tests"; help(duration) -> @@ -177,12 +461,10 @@ help(force_stop) -> "Force stop after time"; help(basic_html) -> "Show basic HTML"; -help(ct_hooks) -> - ""; -help(userconfig) -> - ""; help(verbose) -> - "Verbose output". + "Verbose output"; +help(_) -> + "". transform_opts(Opts) -> transform_opts(Opts, []). @@ -255,210 +537,4 @@ parse_term(String) -> Terms; Term -> Term - end. - -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. - -create_dirs(Opts) -> - LogDir = proplists:get_value(logdir, Opts), - ensure_dir([LogDir]), - ok. - -ensure_dir([]) -> ok; -ensure_dir([Dir|Rest]) -> - case ec_file:is_dir(Dir) of - true -> - ok; - false -> - ec_file:mkdir_path(Dir) - end, - ensure_dir(Rest). - -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) -> - CTFirst = rebar_state:get(State, ct_first_files, []), - {erl_first_files, CTFirst}. - -resolve_ct_opts(State, TestApps, CmdLineOpts) -> - CTOpts = rebar_state:get(State, ct_opts, []), - Opts = lists:ukeymerge(1, - lists:ukeysort(1, CmdLineOpts), - lists:ukeysort(1, CTOpts)), - TestDirs = test_dirs(State, TestApps), - try resolve_ct_opts(TestDirs, Opts) of - Opts2 -> - %% disable `auto_compile` - {ok, [{auto_compile, false} | Opts2]} - catch - throw:{error, Reason}-> - {error, {error_processing_options, Reason}} - end. - -resolve_ct_opts(Dirs, Opts) -> - Opts2 = lists:keydelete(dir, 1, Opts), - case lists:keytake(suite, 1, Opts2) of - {value, {suite, Suites}, Opts3} -> - %% Find full path to suites so that test names are consistent with - %% names when testing all dirs. - Suites2 = [resolve_suite(Dirs, Suite) || Suite <- Suites], - [{suite, Suites2} | Opts3]; - false -> - %% No suites, test all dirs. - [{dir, Dirs} | Opts2] - end. - -resolve_suite(Dirs, Suite) -> - File = Suite ++ code:objfile_extension(), - case [Path || Dir <- Dirs, - Path <- [filename:join(Dir, File)], - filelib:is_file(Path)] of - [Suite2] -> - Suite2; - [] -> - throw({error, {unknown_suite, File}}); - Suites -> - throw({error, {duplicate_suites, Suites}}) - end. - -compile_tests(State, TestApps, 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(State, C, AppDir); - AppState -> - AppState - end, - ok = rebar_erlc_compiler:compile(replace_src_dirs(S, ["test"]), - ec_cnv:to_list(rebar_app_info:out_dir(AppInfo))) - end, - lists:foreach(F, TestApps), - compile_extra_tests(State, TestApps, InDirs). - -%% extra directories containing tests can be passed to ct via the `dir` option -compile_extra_tests(State, TestApps, InDirs) -> - F = fun(App) -> rebar_app_info:dir(App) == rebar_dir:get_cwd() end, - TestDirs = case lists:filter(F, TestApps) of - %% add `test` to indirs if it exists at the root of the project and - %% it hasn't already been compiled - [] -> ["test"|InDirs]; - %% already compiled `./test` so do nothing - _ -> InDirs - end, - %% symlink each of the extra dirs - lists:foreach(fun(Dir) -> - Source = filename:join([rebar_dir:get_cwd(), Dir]), - Target = filename:join([rebar_dir:base_dir(State), Dir]), - ok = rebar_file_utils:symlink_or_copy(Source, Target) - end, TestDirs), - rebar_erlc_compiler:compile(replace_src_dirs(State, TestDirs), - rebar_dir:base_dir(State), - filename:join([rebar_dir:base_dir(State), "ebin"])). - -replace_src_dirs(State, Dirs) -> - %% replace any `src_dirs` with the test dirs - ErlOpts = rebar_state:get(State, erl_opts, []), - StrippedOpts = lists:keydelete(src_dirs, 1, ErlOpts), - rebar_state:set(State, erl_opts, [{src_dirs, Dirs}|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(Results) when is_list(Results) -> - Result = lists:foldl(fun sum_results/2, {0, 0, {0,0}}, Results), - handle_results(Result); -handle_results({_, Failed, {_, AutoSkipped}}) - when Failed > 0 orelse AutoSkipped > 0 -> - {error, {failures_running_tests, {Failed, AutoSkipped}}}; -handle_results({error, Reason}) -> - {error, {error_running_tests, Reason}}; -handle_results(_) -> - ok. - -sum_results({Passed, Failed, {UserSkipped, AutoSkipped}}, - {Passed2, Failed2, {UserSkipped2, AutoSkipped2}}) -> - {Passed+Passed2, Failed+Failed2, - {UserSkipped+UserSkipped2, AutoSkipped+AutoSkipped2}}. - -handle_quiet_results(_, {error, _} = Result) -> - handle_results(Result); -handle_quiet_results(_, {'DOWN', _, _, _, Reason}) -> - handle_results({error, Reason}); -handle_quiet_results(CTOpts, Results) when is_list(Results) -> - _ = [format_result(Result) || Result <- Results], - case handle_results(Results) of - {error, {failures_running_tests, _}} = Error -> - LogDir = proplists:get_value(logdir, CTOpts), - Index = filename:join([LogDir, "index.html"]), - ?CONSOLE("Results written to ~p.", [Index]), - Error; - Other -> - Other - end; -handle_quiet_results(CTOpts, Result) -> - handle_quiet_results(CTOpts, [Result]). - -format_result({Passed, 0, {0, 0}}) -> - ?CONSOLE("All ~p tests passed.", [Passed]); -format_result({Passed, Failed, Skipped}) -> - Format = [format_failed(Failed), format_skipped(Skipped), - format_passed(Passed)], - ?CONSOLE("~s", [Format]). - -format_failed(0) -> - []; -format_failed(Failed) -> - io_lib:format("Failed ~p tests. ", [Failed]). - -format_passed(Passed) -> - io_lib:format("Passed ~p tests. ", [Passed]). - -format_skipped({0, 0}) -> - []; -format_skipped({User, Auto}) -> - io_lib:format("Skipped ~p (~p, ~p) tests. ", [User+Auto, User, Auto]). + end. \ No newline at end of file diff --git a/src/rebar_prv_cover.erl b/src/rebar_prv_cover.erl index 13c12d1..39aa212 100644 --- a/src/rebar_prv_cover.erl +++ b/src/rebar_prv_cover.erl @@ -8,6 +8,7 @@ -export([init/1, do/1, maybe_cover_compile/1, + maybe_cover_compile/2, maybe_write_coverdata/2, format_error/1]). @@ -43,8 +44,12 @@ do(State) -> -spec maybe_cover_compile(rebar_state:t()) -> ok. maybe_cover_compile(State) -> + maybe_cover_compile(State, []). + +-spec maybe_cover_compile(rebar_state:t(), [file:name()]) -> ok. +maybe_cover_compile(State, ExtraDirs) -> case rebar_state:get(State, cover_enabled, false) of - true -> cover_compile(State); + true -> cover_compile(State, ExtraDirs); false -> ok end. @@ -269,7 +274,7 @@ strip_coverdir(File) -> filename:join(lists:reverse(lists:sublist(lists:reverse(filename:split(File)), 2))). -cover_compile(State) -> +cover_compile(State, ExtraDirs) -> %% start the cover server if necessary {ok, CoverPid} = start_cover(), %% redirect cover output @@ -277,7 +282,7 @@ cover_compile(State) -> %% 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), + compile_extras(ExtraDirs, []), %% print any warnings about modules that failed to cover compile lists:foreach(fun print_cover_warnings/1, CompileResult). @@ -298,13 +303,10 @@ compile_beam_directories([App|Rest], Acc) -> "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. +compile_extras([], Acc) -> Acc; +compile_extras([Dir|Rest], Acc) -> + Result = cover:compile_beam_directory(Dir), + compile_extras(Rest, Acc ++ Result). start_cover() -> case cover:start() of diff --git a/test/rebar_ct_SUITE.erl b/test/rebar_ct_SUITE.erl new file mode 100644 index 0000000..3c4aed3 --- /dev/null +++ b/test/rebar_ct_SUITE.erl @@ -0,0 +1,520 @@ +-module(rebar_ct_SUITE). + +-export([all/0, + groups/0, + init_per_group/2, + end_per_group/2]). +-export([basic_app_default_dirs/1, + basic_app_default_beams/1, + multi_app_default_dirs/1, + multi_app_default_beams/1, + single_app_dir/1, + single_extra_dir/1, + single_unmanaged_dir/1, + single_suite/1, + single_extra_suite/1, + single_unmanaged_suite/1, + multi_suite/1, + all_suite/1, + single_dir_and_single_suite/1]). + +-include_lib("common_test/include/ct.hrl"). + +all() -> [{group, basic_app}, + {group, multi_app}, + {group, dirs_and_suites}]. + +groups() -> [{basic_app, [], [basic_app_default_dirs, + basic_app_default_beams]}, + {multi_app, [], [multi_app_default_dirs, + multi_app_default_beams]}, + {dirs_and_suites, [], [single_app_dir, + single_extra_dir, + single_unmanaged_dir, + single_suite, + single_extra_suite, + single_unmanaged_suite, + multi_suite, + all_suite, + single_dir_and_single_suite]}]. + +init_per_group(basic_app, Config) -> + C = rebar_test_utils:init_rebar_state(Config, "ct_"), + + AppDir = ?config(apps, C), + + Name = rebar_test_utils:create_random_name(atom_to_list(basic_app) ++ "_"), + Vsn = rebar_test_utils:create_random_vsn(), + rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]), + + Suite = filename:join([AppDir, "test", Name ++ "_SUITE.erl"]), + ok = filelib:ensure_dir(Suite), + ok = file:write_file(Suite, test_suite(Name)), + + {ok, State} = rebar_test_utils:run_and_check(C, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, []), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + [{result, Result}, {appnames, [Name]}|C]; +init_per_group(multi_app, Config) -> + C = rebar_test_utils:init_rebar_state(Config, "ct_"), + + AppDir = ?config(apps, C), + + Name1 = rebar_test_utils:create_random_name(atom_to_list(multi_app) ++ "_"), + Vsn1 = rebar_test_utils:create_random_vsn(), + AppDir1 = filename:join([AppDir, "apps", Name1]), + rebar_test_utils:create_app(AppDir1, Name1, Vsn1, [kernel, stdlib]), + + Suite1 = filename:join([AppDir1, "test", Name1 ++ "_SUITE.erl"]), + ok = filelib:ensure_dir(Suite1), + ok = file:write_file(Suite1, test_suite(Name1)), + + Name2 = rebar_test_utils:create_random_name(atom_to_list(multi_app) ++ "_"), + Vsn2 = rebar_test_utils:create_random_vsn(), + AppDir2 = filename:join([AppDir, "apps", Name2]), + rebar_test_utils:create_app(AppDir2, Name2, Vsn2, [kernel, stdlib]), + + Suite2 = filename:join([AppDir2, "test", Name2 ++ "_SUITE.erl"]), + ok = filelib:ensure_dir(Suite2), + ok = file:write_file(Suite2, test_suite(Name2)), + + Suite3 = filename:join([AppDir, "test", "extras_SUITE.erl"]), + ok = filelib:ensure_dir(Suite3), + ok = file:write_file(Suite3, test_suite("extras")), + + {ok, State} = rebar_test_utils:run_and_check(C, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, []), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + [{result, Result}, {appnames, [Name1, Name2]}|C]; +init_per_group(dirs_and_suites, Config) -> + C = rebar_test_utils:init_rebar_state(Config, "ct_"), + + AppDir = ?config(apps, C), + + Name1 = rebar_test_utils:create_random_name(atom_to_list(dirs_and_suites) ++ "_"), + Vsn1 = rebar_test_utils:create_random_vsn(), + AppDir1 = filename:join([AppDir, "apps", Name1]), + rebar_test_utils:create_app(AppDir1, Name1, Vsn1, [kernel, stdlib]), + + Suite1 = filename:join([AppDir1, "test", Name1 ++ "_SUITE.erl"]), + ok = filelib:ensure_dir(Suite1), + ok = file:write_file(Suite1, test_suite(Name1)), + + Name2 = rebar_test_utils:create_random_name(atom_to_list(dir_and_suites) ++ "_"), + Vsn2 = rebar_test_utils:create_random_vsn(), + AppDir2 = filename:join([AppDir, "apps", Name2]), + rebar_test_utils:create_app(AppDir2, Name2, Vsn2, [kernel, stdlib]), + + Suite2 = filename:join([AppDir2, "test", Name2 ++ "_SUITE.erl"]), + ok = filelib:ensure_dir(Suite2), + ok = file:write_file(Suite2, test_suite(Name2)), + + Suite3 = filename:join([AppDir, "test", "extras_SUITE.erl"]), + ok = filelib:ensure_dir(Suite3), + ok = file:write_file(Suite3, test_suite("extras")), + + [{appnames, [Name1, Name2]}|C]. + +end_per_group(_Group, _Config) -> ok. + +basic_app_default_dirs(Config) -> + AppDir = ?config(apps, Config), + [Name] = ?config(appnames, Config), + Result = ?config(result, Config), + + Expect = filename:absname(filename:join([AppDir, "_build", "test", "lib", Name, "test"])), + Dir = proplists:get_value(dir, Result), + + Expect = Dir. + +basic_app_default_beams(Config) -> + AppDir = ?config(apps, Config), + [Name] = ?config(appnames, Config), + + File = filename:join([AppDir, + "_build", + "test", + "lib", + Name, + "test", + Name ++ "_SUITE.beam"]), + + true = filelib:is_file(File). + +multi_app_default_dirs(Config) -> + AppDir = ?config(apps, Config), + [Name1, Name2] = ?config(appnames, Config), + Result = ?config(result, Config), + + Expect1 = filename:absname(filename:join([AppDir, "_build", "test", "lib", Name1, "test"])), + Expect2 = filename:absname(filename:join([AppDir, "_build", "test", "lib", Name2, "test"])), + Expect3 = filename:absname(filename:join([AppDir, "_build", "test", "test"])), + Dirs = proplists:get_value(dir, Result), + + true = (lists:sort([Expect1, Expect2, Expect3]) == lists:sort(Dirs)). + +multi_app_default_beams(Config) -> + AppDir = ?config(apps, Config), + [Name1, Name2] = ?config(appnames, Config), + + File1 = filename:join([AppDir, + "_build", + "test", + "lib", + Name1, + "test", + Name1 ++ "_SUITE.beam"]), + File2 = filename:join([AppDir, + "_build", + "test", + "lib", + Name2, + "test", + Name2 ++ "_SUITE.beam"]), + File3 = filename:join([AppDir, + "_build", + "test", + "test", + "extras_SUITE.beam"]), + + true = filelib:is_file(File1), + true = filelib:is_file(File2), + true = filelib:is_file(File3). + +single_app_dir(Config) -> + AppDir = ?config(apps, Config), + [Name1, _Name2] = ?config(appnames, Config), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, + ["--dir=" ++ filename:join([AppDir, + "apps", + Name1, + "test"])]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect = filename:absname(filename:join([AppDir, "_build", "test", "lib", Name1, "test"])), + Dir = proplists:get_value(dir, Result), + + Expect = Dir. + +single_extra_dir(Config) -> + AppDir = ?config(apps, Config), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, ["--dir=" ++ filename:join([AppDir, + "test"])]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect = filename:absname(filename:join([AppDir, "_build", "test", "test"])), + Dir = proplists:get_value(dir, Result), + + Expect = Dir. + +single_unmanaged_dir(Config) -> + PrivDir = ?config(priv_dir, Config), + + Suite = filename:join([PrivDir, "unmanaged_dir", "unmanaged_dir_SUITE.erl"]), + ok = filelib:ensure_dir(Suite), + ok = file:write_file(Suite, test_suite("unmanaged_dir")), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, ["--dir=" ++ filename:absname(filename:join([PrivDir, + "unmanaged_dir"]))]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect = filename:absname(filename:join([PrivDir, "unmanaged_dir"])), + Dir = proplists:get_value(dir, Result), + + Expect = Dir. + +single_suite(Config) -> + AppDir = ?config(apps, Config), + [Name1, _Name2] = ?config(appnames, Config), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, + ["--suite=" ++ filename:join([AppDir, + "apps", + Name1, + "test", + Name1 ++ "_SUITE"])]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect = [filename:absname(filename:join([AppDir, + "_build", + "test", + "lib", + Name1, + "test", + Name1 ++ "_SUITE"]))], + Suite = proplists:get_value(suite, Result), + + Expect = Suite. + +single_extra_suite(Config) -> + AppDir = ?config(apps, Config), + [_Name1, _Name2] = ?config(appnames, Config), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, + ["--suite=" ++ filename:join([AppDir, + "test", + "extra_SUITE"])]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect = [filename:absname(filename:join([AppDir, + "_build", + "test", + "test", + "extra_SUITE"]))], + Suite = proplists:get_value(suite, Result), + + Expect = Suite. + +single_unmanaged_suite(Config) -> + PrivDir = ?config(priv_dir, Config), + [_Name1, _Name2] = ?config(appnames, Config), + + Suite = filename:join([PrivDir, "unmanaged", "unmanaged_SUITE.erl"]), + ok = filelib:ensure_dir(Suite), + ok = file:write_file(Suite, test_suite("unmanaged")), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, + ["--suite=" ++ filename:absname(filename:join([PrivDir, + "unmanaged", + "unmanaged_SUITE"]))]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect = [filename:absname(filename:join([PrivDir, + "unmanaged", + "unmanaged_SUITE"]))], + SuitePath = proplists:get_value(suite, Result), + + Expect = SuitePath. + +multi_suite(Config) -> + AppDir = ?config(apps, Config), + [Name1, Name2] = ?config(appnames, Config), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, + ["--suite=" ++ filename:join([AppDir, + "apps", + Name1, + "test", + Name1 ++ "_SUITE," ++ AppDir, + "apps", + Name2, + "test", + Name2 ++ "_SUITE"])]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect1 = filename:absname(filename:join([AppDir, + "_build", + "test", + "lib", + Name1, + "test", + Name1 ++ "_SUITE"])), + Expect2 = filename:absname(filename:join([AppDir, + "_build", + "test", + "lib", + Name2, + "test", + Name2 ++ "_SUITE"])), + Suites = proplists:get_value(suite, Result), + + true = (lists:sort([Expect1, Expect2]) == lists:sort(Suites)). + +all_suite(Config) -> + AppDir = ?config(apps, Config), + [Name1, Name2] = ?config(appnames, Config), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, + ["--suite=" ++ filename:join([AppDir, + "apps", + Name1, + "test", + Name1 ++ "_SUITE," ++ AppDir, + "apps", + Name2, + "test", + Name2 ++ "_SUITE," ++ AppDir, + "test", + "extra_SUITE"])]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect1 = filename:absname(filename:join([AppDir, + "_build", + "test", + "lib", + Name1, + "test", + Name1 ++ "_SUITE"])), + Expect2 = filename:absname(filename:join([AppDir, + "_build", + "test", + "lib", + Name2, + "test", + Name2 ++ "_SUITE"])), + Expect3 = filename:absname(filename:join([AppDir, + "_build", + "test", + "test", + "extra_SUITE"])), + Suites = proplists:get_value(suite, Result), + + true = (lists:sort([Expect1, Expect2, Expect3]) == lists:sort(Suites)). + +single_dir_and_single_suite(Config) -> + AppDir = ?config(apps, Config), + [_Name1, _Name2] = ?config(appnames, Config), + + {ok, State} = rebar_test_utils:run_and_check(Config, [], ["as", "test", "compile"], return), + + LibDirs = rebar_dir:lib_dirs(State), + State1 = rebar_app_discover:do(State, LibDirs), + + Providers = rebar_state:providers(State1), + Namespace = rebar_state:namespace(State1), + CommandProvider = providers:get_provider(ct, Providers, Namespace), + GetOptSpec = providers:opts(CommandProvider), + {ok, GetOptResult} = getopt:parse(GetOptSpec, + ["--dir=" ++ filename:join([AppDir, "test"]), + "--suite=extra_SUITE"]), + + State2 = rebar_state:command_parsed_args(State1, GetOptResult), + + Result = rebar_prv_common_test:setup_ct(State2), + + Expect = [filename:absname(filename:join([AppDir, + "_build", + "test", + "test", + "extra_SUITE"]))], + Suite = proplists:get_value(suite, Result), + + Expect = Suite. + +test_suite(Name) -> + io_lib:format("-module(~ts_SUITE).\n" + "-compile(export_all).\n" + "all() -> [some_test].\n" + "some_test(_) -> ok.\n", [Name]). \ No newline at end of file -- cgit v1.1