1%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
2%% ex: ts=4 sw=4 et
3
4-module(rebar_prv_eunit).
5
6-behaviour(provider).
7
8-export([init/1,
9         do/1,
10         format_error/1]).
11%% exported solely for tests
12-export([prepare_tests/1, eunit_opts/1, validate_tests/2]).
13
14-include("rebar.hrl").
15-include_lib("providers/include/providers.hrl").
16
17-define(PROVIDER, eunit).
18%% we need to modify app_info state before compile
19-define(DEPS, [lock]).
20
21-define(DEFAULT_TEST_REGEX, "^(?!\\._).*\\.erl\$").
22
23%% ===================================================================
24%% Public API
25%% ===================================================================
26
27-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
28init(State) ->
29    Provider = providers:create([{name, ?PROVIDER},
30                                 {module, ?MODULE},
31                                 {deps, ?DEPS},
32                                 {bare, true},
33                                 {example, "rebar3 eunit"},
34                                 {short_desc, "Run EUnit Tests."},
35                                 {desc, "Run EUnit Tests."},
36                                 {opts, eunit_opts(State)},
37                                 {profiles, [test]}]),
38    State1 = rebar_state:add_provider(State, Provider),
39    {ok, State1}.
40
41-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
42do(State) ->
43    Tests = prepare_tests(State),
44    %% inject `eunit_first_files`, `eunit_compile_opts` and any
45    %% directories required by tests into the applications
46    NewState = inject_eunit_state(State, Tests),
47    case compile(NewState) of
48        %% successfully compiled apps
49        {ok, S} -> do(S, Tests);
50        Error   -> Error
51    end.
52
53do(State, Tests) ->
54    ?INFO("Performing EUnit tests...", []),
55
56    setup_name(State),
57    rebar_paths:set_paths([deps, plugins], State),
58
59    %% Run eunit provider prehooks
60    Providers = rebar_state:providers(State),
61    Cwd = rebar_dir:get_cwd(),
62    rebar_hooks:run_project_and_app_hooks(Cwd, pre, ?PROVIDER, Providers, State),
63
64    case validate_tests(State, Tests) of
65        {ok, T} ->
66            case run_tests(State, T) of
67                {ok, State1} ->
68                    %% Run eunit provider posthooks
69                    rebar_hooks:run_project_and_app_hooks(Cwd, post, ?PROVIDER, Providers, State1),
70                    rebar_paths:set_paths([plugins, deps], State),
71                    {ok, State1};
72                Error ->
73                    rebar_paths:set_paths([plugins, deps], State),
74                    Error
75            end;
76        Error ->
77            rebar_paths:set_paths([plugins, deps], State),
78            Error
79    end.
80
81run_tests(State, Tests) ->
82    T = translate_paths(State, Tests),
83    EUnitOpts = resolve_eunit_opts(State),
84    ?DEBUG("finding tests in:~n\t{eunit_tests, ~p}.", [T]),
85    ?DEBUG("with options:~n\t{eunit_opts, ~p}.", [EUnitOpts]),
86    apply_sys_config(State),
87    try eunit:test(T, EUnitOpts) of
88      Result ->
89        ok = maybe_write_coverdata(State),
90        case handle_results(Result) of
91            {error, Reason} ->
92                ?PRV_ERROR(Reason);
93            ok ->
94                {ok, State}
95        end
96    catch error:badarg -> ?PRV_ERROR({error, badarg})
97    end.
98
99-spec format_error(any()) -> iolist().
100format_error(unknown_error) ->
101    io_lib:format("Error running tests", []);
102format_error({error_running_tests, Reason}) ->
103    io_lib:format("Error running tests: ~p", [Reason]);
104format_error({eunit_test_errors, Errors}) ->
105    io_lib:format(lists:concat(["Error Running EUnit Tests:"] ++
106                               lists:map(fun(Error) -> "~n  " ++ Error end, Errors)), []);
107format_error({badconfig, {Msg, {Value, Key}}}) ->
108    io_lib:format(Msg, [Value, Key]);
109format_error({generator, Value}) ->
110    io_lib:format("Generator ~p has an invalid format", [Value]);
111format_error({error, Error}) ->
112    format_error({error_running_tests, Error}).
113
114%% ===================================================================
115%% Internal functions
116%% ===================================================================
117
118setup_name(State) ->
119    {Long, Short, Opts} = rebar_dist_utils:find_options(State),
120    rebar_dist_utils:either(Long, Short, Opts).
121
122prepare_tests(State) ->
123    %% parse and translate command line tests
124    CmdTests = resolve_tests(State),
125    CfgTests = cfg_tests(State),
126    ProjectApps = rebar_state:project_apps(State),
127    %% prioritize tests to run first trying any command line specified
128    %% tests falling back to tests specified in the config file finally
129    %% running a default set if no other tests are present
130    select_tests(State, ProjectApps, CmdTests, CfgTests).
131
132resolve_tests(State) ->
133    {RawOpts, _} = rebar_state:command_parsed_args(State),
134    Apps         = resolve(app, application, RawOpts),
135    Applications = resolve(application, RawOpts),
136    Dirs         = resolve(dir, RawOpts),
137    Files        = resolve(file, RawOpts),
138    Modules      = resolve(module, RawOpts),
139    Suites       = resolve(suite, module, RawOpts),
140    Generator    = resolve(generator, RawOpts),
141    Apps ++ Applications ++ Dirs ++ Files ++ Modules ++ Suites ++ Generator.
142
143resolve(Flag, RawOpts) -> resolve(Flag, Flag, RawOpts).
144
145resolve(Flag, EUnitKey, RawOpts) ->
146    case proplists:get_value(Flag, RawOpts) of
147        undefined -> [];
148        Args      -> normalize(EUnitKey,
149                               rebar_string:lexemes(Args, [$,]))
150    end.
151
152normalize(generator, Args) ->
153    lists:flatmap(fun(Value) -> normalize_(generator, Value) end, Args);
154normalize(EUnitKey, Args) ->
155    lists:map(fun(Arg) -> normalize_(EUnitKey, Arg) end, Args).
156
157normalize_(generator, Value) ->
158    case string:tokens(Value, [$:]) of
159        [Module0, Functions] ->
160            Module = list_to_atom(Module0),
161            lists:map(fun(F) -> {generator, Module, list_to_atom(F)} end,
162                      string:tokens(Functions, [$;]));
163        _ ->
164            ?PRV_ERROR({generator, Value})
165    end;
166normalize_(Key, Value) when Key == dir; Key == file -> {Key, Value};
167normalize_(Key, Value) -> {Key, list_to_atom(Value)}.
168
169cfg_tests(State) ->
170    case rebar_state:get(State, eunit_tests, []) of
171        Tests when is_list(Tests) ->
172            lists:map(fun({app, App}) -> {application, App}; (T) -> T end, Tests);
173        Wrong ->
174            %% probably a single non list term
175            ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, eunit_tests}}})
176    end.
177
178select_tests(_State, _ProjectApps, _, {error, _} = Error) -> Error;
179select_tests(State, ProjectApps, [], []) -> {ok, default_tests(State, ProjectApps)};
180select_tests(_State, _ProjectApps, [], Tests)  -> {ok, Tests};
181select_tests(_State, _ProjectApps, Tests, _) -> {ok, Tests}.
182
183default_tests(State, Apps) ->
184    %% use `{application, App}` for each app in project
185    AppTests = set_apps(Apps),
186    %% additional test modules in `test` dir of each app
187    ModTests = set_modules(Apps, State),
188    AppTests ++ ModTests.
189
190set_apps(Apps) -> set_apps(Apps, []).
191
192set_apps([], Acc) -> Acc;
193set_apps([App|Rest], Acc) ->
194    AppName = list_to_atom(binary_to_list(rebar_app_info:name(App))),
195    set_apps(Rest, [{application, AppName}|Acc]).
196
197set_modules(Apps, State) -> set_modules(Apps, State, {[], []}).
198
199set_modules([], State, {AppAcc, TestAcc}) ->
200    Regex = rebar_state:get(State, eunit_test_regex, ?DEFAULT_TEST_REGEX),
201    BareTestDir = [filename:join([rebar_state:dir(State), "test"])],
202    TestSrc = gather_src(BareTestDir, Regex),
203    dedupe_tests({AppAcc, TestAcc ++ TestSrc});
204set_modules([App|Rest], State, {AppAcc, TestAcc}) ->
205    F = fun(Dir) -> filename:join([rebar_app_info:dir(App), Dir]) end,
206    AppDirs = lists:map(F, rebar_dir:src_dirs(rebar_app_info:opts(App), ["src"])),
207    Regex = rebar_state:get(State, eunit_test_regex, ?DEFAULT_TEST_REGEX),
208    AppSrc = gather_src(AppDirs, Regex),
209    TestDirs = [filename:join([rebar_app_info:dir(App), "test"])],
210    TestSrc = gather_src(TestDirs, Regex),
211    set_modules(Rest, State, {AppSrc ++ AppAcc, TestSrc ++ TestAcc}).
212
213gather_src(Dirs, Regex) -> gather_src(Dirs, Regex, []).
214
215gather_src([], _Regex, Srcs) -> Srcs;
216gather_src([Dir|Rest], Regex, Srcs) ->
217    gather_src(Rest, Regex, Srcs ++ rebar_utils:find_files(Dir, Regex, true)).
218
219dedupe_tests({AppMods, TestMods}) ->
220    UniqueTestMods = lists:usort(TestMods) -- AppMods,
221    %% for each modules in TestMods create a test if there is not a module
222    %% in AppMods that will trigger it
223    F = fun(TestMod) ->
224        M = filename:rootname(filename:basename(TestMod)),
225        MatchesTest = fun(AppMod) -> filename:rootname(filename:basename(AppMod)) ++ "_tests" == M end,
226        case lists:any(MatchesTest, AppMods) of
227            false -> {true, {module, list_to_atom(M)}};
228            true  -> false
229        end
230    end,
231    rebar_utils:filtermap(F, UniqueTestMods).
232
233inject_eunit_state(State, {ok, Tests}) ->
234    Apps = rebar_state:project_apps(State),
235    case inject_eunit_state(State, Apps, []) of
236        {ok, {NewState, ModdedApps}} ->
237            test_dirs(NewState, ModdedApps, Tests);
238        {error, _} = Error           -> Error
239    end;
240inject_eunit_state(_State, Error) -> Error.
241
242inject_eunit_state(State, [App|Rest], Acc) ->
243    case inject(rebar_app_info:opts(App)) of
244        {error, _} = Error -> Error;
245        NewOpts            ->
246            NewApp = rebar_app_info:opts(App, NewOpts),
247            inject_eunit_state(State, Rest, [NewApp|Acc])
248    end;
249inject_eunit_state(State, [], Acc) ->
250    case inject(rebar_state:opts(State)) of
251        {error, _} = Error -> Error;
252        NewOpts            -> {ok, {rebar_state:opts(State, NewOpts), lists:reverse(Acc)}}
253    end.
254
255opts(Opts, Key, Default) ->
256    case rebar_opts:get(Opts, Key, Default) of
257        Vs when is_list(Vs) -> Vs;
258        Wrong ->
259            ?PRV_ERROR({badconfig, {"Value `~p' of option `~p' must be a list", {Wrong, Key}}})
260    end.
261
262inject(Opts) -> erl_opts(Opts).
263
264erl_opts(Opts) ->
265    %% append `eunit_compile_opts` to app defined `erl_opts`
266    ErlOpts = opts(Opts, erl_opts, []),
267    EUnitOpts = opts(Opts, eunit_compile_opts, []),
268    case append(EUnitOpts, ErlOpts) of
269        {error, _} = Error -> Error;
270        NewErlOpts         -> first_files(rebar_opts:set(Opts, erl_opts, NewErlOpts))
271    end.
272
273first_files(Opts) ->
274    %% append `eunit_first_files` to app defined `erl_first_files`
275    FirstFiles = opts(Opts, erl_first_files, []),
276    EUnitFirstFiles = opts(Opts, eunit_first_files, []),
277    case append(EUnitFirstFiles, FirstFiles) of
278        {error, _} = Error -> Error;
279        NewFirstFiles  -> eunit_macro(rebar_opts:set(Opts, erl_first_files, NewFirstFiles))
280    end.
281
282eunit_macro(Opts) ->
283    ErlOpts = opts(Opts, erl_opts, []),
284    NewOpts = safe_define_eunit_macro(ErlOpts),
285    rebar_opts:set(Opts, erl_opts, NewOpts).
286
287safe_define_eunit_macro(Opts) ->
288    %% defining a compile macro twice results in an exception so
289    %% make sure 'EUNIT' is only defined once
290    case test_defined(Opts) of
291       true  -> Opts;
292       false -> [{d, 'EUNIT'}|Opts]
293    end.
294
295test_defined([{d, 'EUNIT'}|_]) -> true;
296test_defined([{d, 'EUNIT', true}|_]) -> true;
297test_defined([_|Rest]) -> test_defined(Rest);
298test_defined([]) -> false.
299
300append({error, _} = Error, _) -> Error;
301append(_, {error, _} = Error) -> Error;
302append(A, B) -> A ++ B.
303
304test_dirs(State, Apps, []) -> rebar_state:project_apps(State, Apps);
305test_dirs(State, Apps, [{dir, Dir}|Rest]) ->
306    %% insert `Dir` into an app if relative, or the base state if not
307    %% app relative but relative to the root or not at all if outside
308    %% project scope
309    {NewState, NewApps} = maybe_inject_test_dir(State, [], Apps, Dir),
310    test_dirs(NewState, NewApps, Rest);
311test_dirs(State, Apps, [{file, File}|Rest]) ->
312    Dir = filename:dirname(File),
313    {NewState, NewApps} = maybe_inject_test_dir(State, [], Apps, Dir),
314    test_dirs(NewState, NewApps, Rest);
315test_dirs(State, Apps, [_|Rest]) -> test_dirs(State, Apps, Rest).
316
317maybe_inject_test_dir(State, AppAcc, [App|Rest], Dir) ->
318    case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
319        {ok, Path} ->
320            Opts = inject_test_dir(rebar_app_info:opts(App), Path),
321            {State, AppAcc ++ [rebar_app_info:opts(App, Opts)] ++ Rest};
322        {error, badparent} ->
323            maybe_inject_test_dir(State, AppAcc ++ [App], Rest, Dir)
324    end;
325maybe_inject_test_dir(State, AppAcc, [], Dir) ->
326    case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of
327        {ok, Path} ->
328            Opts = inject_test_dir(rebar_state:opts(State), Path),
329            {rebar_state:opts(State, Opts), AppAcc};
330        {error, badparent} ->
331            {State, AppAcc}
332    end.
333
334inject_test_dir(Opts, Dir) ->
335    %% append specified test targets to app defined `extra_src_dirs`
336    ExtraSrcDirs = rebar_opts:get(Opts, extra_src_dirs, []),
337    rebar_opts:set(Opts, extra_src_dirs, ExtraSrcDirs ++ [Dir]).
338
339compile({error, _} = Error) -> Error;
340compile(State) ->
341    {ok, S} = rebar_prv_compile:do(State),
342    ok = maybe_cover_compile(S),
343    {ok, S}.
344
345validate_tests(State, {ok, Tests}) ->
346    gather_tests(fun(Elem) -> validate(State, Elem) end, Tests, []);
347validate_tests(_State, Error) -> Error.
348
349gather_tests(_F, [], Acc) -> {ok, lists:reverse(Acc)};
350gather_tests(F, [Test|Rest], Acc) ->
351    case F(Test) of
352        ok             -> gather_tests(F, Rest, [Test|Acc]);
353        %% failure mode, switch to gathering errors
354        {error, Error} -> gather_errors(F, Rest, [Error])
355    end.
356
357gather_errors(_F, [], Acc) -> ?PRV_ERROR({eunit_test_errors, lists:reverse(Acc)});
358gather_errors(F, [Test|Rest], Acc) ->
359    case F(Test) of
360        ok             -> gather_errors(F, Rest, Acc);
361        {error, Error} -> gather_errors(F, Rest, [Error|Acc])
362    end.
363
364validate(State, {application, App}) ->
365    validate_app(State, App);
366validate(State, {dir, Dir}) ->
367    validate_dir(State, Dir);
368validate(State, {file, File}) ->
369    validate_file(State, File);
370validate(State, {module, Module}) ->
371    validate_module(State, Module);
372validate(State, {suite, Module}) ->
373    validate_module(State, Module);
374validate(State, {generator, Module, Function}) ->
375    validate_generator(State, Module, Function);
376validate(State, Module) when is_atom(Module) ->
377    validate_module(State, Module);
378validate(State, Path) when is_list(Path) ->
379    case ec_file:is_dir(Path) of
380        true  -> validate(State, {dir, Path});
381        false -> validate(State, {file, Path})
382    end;
383%% unrecognized tests should be included. if they're invalid eunit will error
384%% and rebar.config may contain arbitrarily complex tests that are effectively
385%% unvalidatable
386validate(_State, _Test) -> ok.
387
388validate_app(State, AppName) ->
389    ProjectApps = rebar_state:project_apps(State),
390    validate_app(State, ProjectApps, AppName).
391
392validate_app(_State, [], AppName) ->
393    {error, lists:concat(["Application `", AppName, "' not found in project."])};
394validate_app(State, [App|Rest], AppName) ->
395    case AppName == binary_to_atom(rebar_app_info:name(App), unicode) of
396        true  -> ok;
397        false -> validate_app(State, Rest, AppName)
398    end.
399
400validate_dir(State, Dir) ->
401    case ec_file:is_dir(filename:join([rebar_state:dir(State), Dir])) of
402        true  -> ok;
403        false -> {error, lists:concat(["Directory `", Dir, "' not found."])}
404    end.
405
406validate_file(State, File) ->
407    case ec_file:exists(filename:join([rebar_state:dir(State), File])) of
408        true  -> ok;
409        false -> {error, lists:concat(["File `", File, "' not found."])}
410    end.
411
412validate_module(_State, Module) ->
413    case code:which(Module) of
414        non_existing -> {error, lists:concat(["Module `", Module, "' not found in project."])};
415        _            -> ok
416    end.
417
418validate_generator(State, Module, _Function) ->
419    validate_module(State, Module).
420
421resolve_eunit_opts(State) ->
422    {Opts, _} = rebar_state:command_parsed_args(State),
423    EUnitOpts = rebar_state:get(State, eunit_opts, []),
424    EUnitOpts1 = case proplists:get_value(verbose, Opts, false) of
425                    true  -> set_verbose(EUnitOpts);
426                    false -> EUnitOpts
427                 end,
428    EUnitOpts2 = case proplists:get_value(profile, Opts, false) of
429                    true  -> set_profile(EUnitOpts1);
430                    false -> EUnitOpts1
431                 end,
432    IsVerbose = lists:member(verbose, EUnitOpts2),
433    case proplists:get_value(eunit_formatters, Opts, not IsVerbose) of
434        true  -> custom_eunit_formatters(EUnitOpts2);
435        false -> EUnitOpts2
436    end.
437
438custom_eunit_formatters(Opts) ->
439    ReportOpts = custom_eunit_report_options(Opts),
440    %% If `report` is already set then treat that like `eunit_formatters` is false
441    case lists:keymember(report, 1, Opts) of
442        true -> Opts;
443        false -> [no_tty, {report, {eunit_progress, ReportOpts}} | Opts]
444    end.
445
446custom_eunit_report_options(Opts) ->
447    case lists:member(profile, Opts) of
448        true -> [colored, profile];
449        false -> [colored]
450    end.
451
452set_profile(Opts) ->
453    %% if `profile` is already set don't set it again
454    case lists:member(profile, Opts) of
455        true  -> Opts;
456        false -> [profile] ++ Opts
457    end.
458
459set_verbose(Opts) ->
460    %% if `verbose` is already set don't set it again
461    case lists:member(verbose, Opts) of
462        true  -> Opts;
463        false -> [verbose] ++ Opts
464    end.
465
466translate_paths(State, Tests) -> translate_paths(State, Tests, []).
467
468translate_paths(_State, [], Acc) -> lists:reverse(Acc);
469translate_paths(State, [{K, _} = Path|Rest], Acc) when K == file; K == dir ->
470    Apps = rebar_state:project_apps(State),
471    translate_paths(State, Rest, [translate(State, Apps, Path)|Acc]);
472translate_paths(State, [Test|Rest], Acc) ->
473    translate_paths(State, Rest, [Test|Acc]).
474
475translate(State, [App|Rest], {dir, Dir}) ->
476    case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
477        {ok, Path}         -> {dir, filename:join([rebar_app_info:out_dir(App), Path])};
478        {error, badparent} -> translate(State, Rest, {dir, Dir})
479    end;
480translate(State, [App|Rest], {file, FilePath}) ->
481    Dir = filename:dirname(FilePath),
482    File = filename:basename(FilePath),
483    case rebar_file_utils:path_from_ancestor(Dir, rebar_app_info:dir(App)) of
484        {ok, Path}         -> {file, filename:join([rebar_app_info:out_dir(App), Path, File])};
485        {error, badparent} -> translate(State, Rest, {file, FilePath})
486    end;
487translate(State, [], {dir, Dir}) ->
488    case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of
489        {ok, Path}         -> {dir, filename:join([rebar_dir:base_dir(State), "extras", Path])};
490        %% not relative, leave as is
491        {error, badparent} -> {dir, Dir}
492    end;
493translate(State, [], {file, FilePath}) ->
494    Dir = filename:dirname(FilePath),
495    File = filename:basename(FilePath),
496    case rebar_file_utils:path_from_ancestor(Dir, rebar_state:dir(State)) of
497        {ok, Path}         -> {file, filename:join([rebar_dir:base_dir(State), "extras", Path, File])};
498        %% not relative, leave as is
499        {error, badparent} -> {file, FilePath}
500    end.
501
502apply_sys_config(State) ->
503    CfgSysCfg = case rebar_state:get(State, eunit_opts, []) of
504        Opts when is_list(Opts) ->
505            case proplists:get_value(sys_config, Opts, []) of
506                [] -> [];
507                L when is_list(hd(L)) -> L;
508                S when is_list(S) -> [S]
509            end;
510        _ ->
511            []
512    end,
513    {RawOpts, _} = rebar_state:command_parsed_args(State),
514    SysCfgs = rebar_string:lexemes(
515        proplists:get_value(sys_config, RawOpts, ""),
516        [$,]
517    ) ++ CfgSysCfg,
518    Configs = lists:flatmap(
519        fun(Filename) -> rebar_file_utils:consult_config(State, Filename) end,
520        SysCfgs
521    ),
522    %% NB: load the applications (from user directories too) to support OTP < 17
523    %% to our best ability.
524    rebar_paths:set_paths([deps, plugins], State),
525    [application:load(Application) || Config <- Configs, {Application, _} <- Config],
526    rebar_utils:reread_config(Configs, [update_logger]),
527    ok.
528
529maybe_cover_compile(State) ->
530    {RawOpts, _} = rebar_state:command_parsed_args(State),
531    State1 = case proplists:get_value(cover, RawOpts, false) of
532        true  -> rebar_state:set(State, cover_enabled, true);
533        false -> State
534    end,
535    rebar_prv_cover:maybe_cover_compile(State1).
536
537maybe_write_coverdata(State) ->
538    {RawOpts, _} = rebar_state:command_parsed_args(State),
539    State1 = case proplists:get_value(cover, RawOpts, false) of
540        true  -> rebar_state:set(State, cover_enabled, true);
541        false -> State
542    end,
543    Name = proplists:get_value(cover_export_name, RawOpts, ?PROVIDER),
544    rebar_prv_cover:maybe_write_coverdata(State1, Name).
545
546handle_results(ok) -> ok;
547handle_results(error) ->
548    {error, unknown_error};
549handle_results({error, Reason}) ->
550    {error, {error_running_tests, Reason}}.
551
552eunit_opts(_State) ->
553    [{app, undefined, "app", string, help(app)},
554     {application, undefined, "application", string, help(app)},
555     {cover, $c, "cover", boolean, help(cover)},
556     {cover_export_name, undefined, "cover_export_name", string, help(cover_export_name)},
557     {profile, $p, "profile", boolean, help(profile)},
558     {dir, $d, "dir", string, help(dir)},
559     {file, $f, "file", string, help(file)},
560     {module, $m, "module", string, help(module)},
561     {suite, $s, "suite", string, help(module)},
562     {generator, $g, "generator", string, help(generator)},
563     {verbose, $v, "verbose", boolean, help(verbose)},
564     {name, undefined, "name", atom, help(name)},
565     {sname, undefined, "sname", atom, help(sname)},
566     {sys_config, undefined, "sys_config", string, help(sys_config)}, %% comma-separated list
567     {setcookie, undefined, "setcookie", atom, help(setcookie)}].
568
569help(app)       -> "Comma separated list of application test suites to run. Equivalent to `[{application, App}]`.";
570help(cover)     -> "Generate cover data. Defaults to false.";
571help(cover_export_name) -> "Base name of the coverdata file to write";
572help(profile)   -> "Show the slowest tests. Defaults to false.";
573help(dir)       -> "Comma separated list of dirs to load tests from. Equivalent to `[{dir, Dir}]`.";
574help(file)      -> "Comma separated list of files to load tests from. Equivalent to `[{file, File}]`.";
575help(module)    -> "Comma separated list of modules to load tests from. Equivalent to `[{module, Module}]`.";
576help(generator) -> "Comma separated list of generators (the format is `module:function`) to load tests from. Equivalent to `[{generator, Module, Function}]`.";
577help(verbose)   -> "Verbose output. Defaults to false.";
578help(name)      -> "Gives a long name to the node";
579help(sname)     -> "Gives a short name to the node";
580help(setcookie) -> "Sets the cookie if the node is distributed";
581help(sys_config) -> "List of application config files".
582