%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ts=4 sw=4 et %% ------------------------------------------------------------------- %% %% rebar: Erlang Build Tools %% %% Copyright (c) 2009 Dave Smith (dizzyd@dizzyd.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. %% ------------------------------------------------------------------- %% %% Targets: %% test - run common test suites in ./test %% int_test - run suites in ./int_test %% perf_test - run suites inm ./perf_test %% %% Global options: %% verbose=1 - show output from the common_test run as it goes %% suites="foo,bar" - run /foo_SUITE and /bar_SUITE %% case="mycase" - run individual test case foo_SUITE:mycase %% ------------------------------------------------------------------- -module(rebar_ct). -export([ct/2]). %% for internal use only -export([info/2]). -include("rebar.hrl"). %% =================================================================== %% Public API %% =================================================================== ct(Config, File) -> TestDir = rebar_config:get_local(Config, ct_dir, "test"), LogDir = rebar_config:get_local(Config, ct_log_dir, "logs"), run_test_if_present(TestDir, LogDir, Config, File). %% =================================================================== %% Internal functions %% =================================================================== info(help, ct) -> ?CONSOLE( "Run common_test suites.~n" "~n" "Valid rebar.config options:~n" " ~p~n" " ~p~n" " ~p~n" " ~p~n" " ~p~n" "Valid command line options:~n" " suites=Suite1,Suite2,...,SuiteN~n" " - run Suite1_SUITE, Suite2_SUITE, ..., SuiteN_SUITE~n" " in the test folder.~n" " groups=Group1,Group2,...,GroupN~n" " - run test groups Group1, Group2, ..., GroupN of specified suites.~n" " cases=Case1,Case2,...,CaseM~n" " - run test cases Case1, Case2, ..., CaseN of specified suites.~n" " case=\"mycase\" - run individual test case Suite1_SUITE:mycase.~n" " This option is deprecated and remains for backward compability.~n" " It is recommended to use 'cases' instead.~n", [ {ct_dir, "itest"}, {ct_log_dir, "test/logs"}, {ct_extra_params, "-boot start_sasl -s myapp"}, {ct_use_short_names, true}, {ct_search_specs_from_test_dir, false} ]). run_test_if_present(TestDir, LogDir, Config, File) -> case filelib:is_dir(TestDir) of false -> ?WARN("~s directory not present - skipping\n", [TestDir]), ok; true -> case filelib:wildcard(TestDir ++ "/*_SUITE.{beam,erl}") of [] -> ?WARN("~s directory present, but no common_test" ++ " SUITES - skipping\n", [TestDir]), ok; _ -> try run_test(TestDir, LogDir, Config, File) catch throw:skip -> ok end end end. run_test(TestDir, LogDir, Config, _File) -> {Cmd, RawLog} = make_cmd(TestDir, LogDir, Config), ?DEBUG("ct_run cmd:~n~p~n", [Cmd]), clear_log(LogDir, RawLog), Output = case rebar_log:is_verbose(Config) of false -> " >> " ++ RawLog ++ " 2>&1"; true -> case os:type() of {win32, nt} -> " >> " ++ RawLog ++ " 2>&1"; _ -> " 2>&1 | tee -a " ++ RawLog end end, ShOpts = [{env,[{"TESTDIR", TestDir}]}, return_on_error], case rebar_utils:sh(Cmd ++ Output, ShOpts) of {ok,_} -> %% in older versions of ct_run, this could have been a failure %% that returned a non-0 code. Check for that! check_success_log(Config, RawLog); {error,Res} -> %% In newer ct_run versions, this may be a sign of a good compile %% that failed cases. In older version, it's a worse error. check_fail_log(Config, RawLog, Cmd ++ Output, Res) end. clear_log(LogDir, RawLog) -> case filelib:ensure_dir(filename:join(LogDir, "index.html")) of ok -> NowStr = rebar_utils:now_str(), LogHeader = "--- Test run on " ++ NowStr ++ " ---\n", ok = file:write_file(RawLog, LogHeader); {error, Reason} -> ?ERROR("Could not create log dir - ~p\n", [Reason]), ?FAIL end. %% calling ct with erl does not return non-zero on failure - have to check %% log results check_success_log(Config, RawLog) -> check_log(Config, RawLog, fun(Msg) -> ?CONSOLE("DONE.\n~s\n", [Msg]) end). -type err_handler() :: fun((string()) -> no_return()). -spec failure_logger(string(), {integer(), string()}) -> err_handler(). failure_logger(Command, {Rc, Output}) -> fun(_Msg) -> ?ABORT("~s failed with error: ~w and output:~n~s~n", [Command, Rc, Output]) end. check_fail_log(Config, RawLog, Command, Result) -> check_log(Config, RawLog, failure_logger(Command, Result)). check_log(Config,RawLogFilename,Fun) -> %% read the file and split into a list separated by newlines {ok, RawLog} = file:read_file(RawLogFilename), Msg = string:tokens(binary_to_list(RawLog), "\n"), %% now filter out all the list entries that do not have test %% completion strings CompleteRuns = lists:filter(fun(M) -> string:str(M, "TEST COMPLETE") =/= 0 end, Msg), MakeFailed = lists:filter(fun(M) -> string:str(M, "{error,make_failed}") =/= 0 end, Msg), %% the run has failed if at least one of the tests failed RunFailed = lists:foldl(fun(M, Acc) -> %% the "0 failed" string must be present for %% the test to be considered successful TestFailed = string:str(M, "0 failed") =:= 0, TestFailed orelse Acc end, false, CompleteRuns), if MakeFailed =/= [] -> show_log(Config, RawLog), ?ERROR("Building tests failed\n",[]), ?FAIL; RunFailed -> show_log(Config, RawLog), ?ERROR("One or more tests failed\n",[]), ?FAIL; true -> Fun(string:join(Msg, "\n")) end. %% Show the log if it hasn't already been shown because verbose was on show_log(Config, RawLog) -> ?CONSOLE("Showing log\n", []), case rebar_log:is_verbose(Config) of false -> ?CONSOLE("~s", [RawLog]); true -> ok end. make_cmd(TestDir, RawLogDir, Config) -> Cwd = rebar_utils:get_cwd(), LogDir = filename:join(Cwd, RawLogDir), EbinDir = filename:absname(filename:join(Cwd, "ebin")), IncludeDir = filename:join(Cwd, "include"), Include = case filelib:is_dir(IncludeDir) of true -> " -include \"" ++ IncludeDir ++ "\""; false -> "" end, %% Check for the availability of ct_run; if we can't find it, generate a %% warning and use the old school, less reliable approach to running CT. BaseCmd = case os:find_executable("ct_run") of false -> "erl -noshell -s ct_run script_start -s erlang halt"; _ -> "ct_run -noshell" end, %% Add the code path of the rebar process to the code path. This %% includes the dependencies in the code path. The directories %% that are part of the root Erlang install are filtered out to %% avoid duplication R = code:root_dir(), NonLibCodeDirs = [P || P <- code:get_path(), not lists:prefix(R, P)], CodeDirs = [io_lib:format("\"~s\"", [Dir]) || Dir <- [EbinDir|NonLibCodeDirs]], CodePathString = string:join(CodeDirs, " "), Cmd = case get_ct_specs(Config, search_ct_specs_from(Cwd, TestDir, Config)) of undefined -> ?FMT("~s" " -pa ~s" " ~s" " ~s" " -logdir \"~s\"" " -env TEST_DIR \"~s\"", [BaseCmd, CodePathString, Include, build_name(Config), LogDir, filename:join(Cwd, TestDir)]) ++ get_cover_config(Config, Cwd) ++ get_ct_config_file(TestDir) ++ get_suites(Config, TestDir) ++ get_groups(Config) ++ get_cases(Config) ++ get_extra_params(Config) ++ get_config_file(TestDir); SpecFlags -> ?FMT("~s" " -pa ~s" " ~s" " ~s" " -logdir \"~s\"" " -env TEST_DIR \"~s\"", [BaseCmd, CodePathString, Include, build_name(Config), LogDir, filename:join(Cwd, TestDir)]) ++ SpecFlags ++ get_cover_config(Config, Cwd) ++ get_extra_params(Config) end, RawLog = filename:join(LogDir, "raw.log"), {Cmd, RawLog}. search_ct_specs_from(Cwd, TestDir, Config) -> case rebar_config:get_local(Config, ct_search_specs_from_test_dir, false) of true -> filename:join(Cwd, TestDir); false -> Cwd end. build_name(Config) -> %% generate a unique name for our test node, we want %% to make sure the odds of name clashing are low Random = integer_to_list(crypto:rand_uniform(0, 10000)), case rebar_config:get_local(Config, ct_use_short_names, false) of true -> "-sname test" ++ Random; false -> " -name test" ++ Random ++ "@" ++ net_adm:localhost() end. get_extra_params(Config) -> case rebar_config:get_local(Config, ct_extra_params, undefined) of undefined -> ""; Defined -> " " ++ Defined end. get_ct_specs(Config, Cwd) -> case collect_glob(Config, Cwd, ".*\.test\.spec\$") of [] -> undefined; [Spec] -> " -spec " ++ Spec; Specs -> " -spec " ++ lists:flatten([io_lib:format("~s ", [Spec]) || Spec <- Specs]) end. get_cover_config(Config, Cwd) -> case rebar_config:get_local(Config, cover_enabled, false) of false -> ""; true -> case collect_glob(Config, Cwd, ".*cover\.spec\$") of [] -> ?DEBUG("No cover spec found: ~s~n", [Cwd]), ""; [Spec] -> ?DEBUG("Found cover file ~s~n", [Spec]), " -cover " ++ Spec; Specs -> ?ABORT("Multiple cover specs found: ~p~n", [Specs]) end end. collect_glob(Config, Cwd, Glob) -> {true, Deps} = rebar_deps:get_deps_dir(Config), DepsDir = filename:basename(Deps), CwdParts = filename:split(Cwd), filelib:fold_files( Cwd, Glob, true, fun(F, Acc) -> %% Ignore any specs under the deps/ directory. Do this pulling %% the dirname off the F and then splitting it into a list. Parts = filename:split(filename:dirname(F)), Parts2 = remove_common_prefix(Parts, CwdParts), case lists:member(DepsDir, Parts2) of true -> %% There is a directory named "deps" in path Acc; false -> %% No "deps" directory in path [F | Acc] end end, []). remove_common_prefix([H1|T1], [H1|T2]) -> remove_common_prefix(T1, T2); remove_common_prefix(L1, _) -> L1. get_ct_config_file(TestDir) -> Config = filename:join(TestDir, "test.config"), case filelib:is_regular(Config) of false -> " "; true -> " -ct_config " ++ Config end. get_config_file(TestDir) -> Config = filename:join(TestDir, "app.config"), case filelib:is_regular(Config) of false -> " "; true -> " -erl_args -config " ++ Config end. get_suites(Config, TestDir) -> case get_suites(Config) of undefined -> " -dir " ++ TestDir; Suites -> Suites1 = [find_suite_path(Suite, TestDir) || Suite <- Suites], string:join([" -suite"] ++ Suites1, " ") end. get_suites(Config) -> case rebar_config:get_global(Config, suites, undefined) of undefined -> %% The option 'suite' is deprecated and remains %% for backward compatibility. %% It is recommended to use 'suites' instead. case get_deprecated_global(Config, suite, suites) of undefined -> undefined; Suite -> [Suite] end; Suites -> string:tokens(Suites, ",") end. find_suite_path(Suite, TestDir) -> Path = filename:join(TestDir, Suite ++ "_SUITE.erl"), case filelib:is_regular(Path) of false -> ?WARN("Suite ~s not found\n", [Suite]), %% Note - this throw is caught in run_test_if_present/3; %% this solution was easier than refactoring the entire module. throw(skip); true -> Path end. get_groups(Config) -> case rebar_config:get_global(Config, groups, undefined) of undefined -> %% The option 'group' was added only for consistency %% because there are options 'suite' and 'case'. case get_deprecated_global(Config, group, groups) of undefined -> ""; Group -> " -group " ++ Group end; Groups -> Groups1 = string:tokens(Groups, ","), string:join([" -group"] ++ Groups1, " ") end. get_cases(Config) -> case rebar_config:get_global(Config, cases, undefined) of undefined -> %% The option 'case' is deprecated and remains %% for backward compatibility. %% It is recommended to use 'cases' instead. case get_deprecated_global(Config, 'case', cases) of undefined -> ""; Case -> " -case " ++ Case end; Cases -> Cases1 = string:tokens(Cases, ","), string:join([" -case"] ++ Cases1, " ") end. get_deprecated_global(Config, OldOpt, NewOpt) -> rebar_utils:get_deprecated_global( Config, OldOpt, NewOpt, undefined, "in the future").