%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %%% Purpose : Supervises running of test cases. -module(ts_run). -export([run/4,ct_run_test/2]). -define(DEFAULT_MAKE_TIMETRAP_MINUTES, 60). -define(DEFAULT_UNMAKE_TIMETRAP_MINUTES, 15). -include("ts.hrl"). -import(lists, [member/2,filter/2]). -record(state, {file, % File given. mod, % Module to run. test_server_args, % Arguments to test server. command, % Command to run. test_dir, % Directory for test suite. makefiles, % List of all makefiles. makefile, % Current makefile. batch, % Are we running in batch mode? data_wc, % Wildcard for data dirs. topcase, % Top case specification. all % Set if we have all_SUITE_data }). -define(tracefile,"traceinfo"). %% Options is a slightly modified version of the options given to %% ts:run. Vars0 are from the variables file. run(File, Args0, Options, Vars0) -> Vars= case lists:keysearch(vars, 1, Options) of {value, {vars, Vars1}} -> Vars1++Vars0; _ -> Vars0 end, {Batch,Runner} = case {member(interactive, Options), member(batch, Options)} of {false, true} -> {true, fun run_batch/3}; _ -> {false, fun run_interactive/3} end, Hooks = [fun init_state/3, fun run_preinits/3, fun make_command/3, Runner], Args = make_common_test_args(Args0,Options,Vars), St = #state{file=File,test_server_args=Args,batch=Batch}, R = execute(Hooks, Vars, [], St), case R of {ok,_,_,_} -> ok; Error -> Error end. execute([Hook|Rest], Vars0, Spec0, St0) -> case Hook(Vars0, Spec0, St0) of ok -> execute(Rest, Vars0, Spec0, St0); {ok, Vars, Spec, St} -> execute(Rest, Vars, Spec, St); Error -> Error end; execute([], Vars, Spec, St) -> {ok, Vars, Spec, St}. %% Wrapper to run tests using ct:run_test/1 and handle any errors. ct_run_test(Dir, CommonTestArgs) -> try ok = file:set_cwd(Dir), case ct:run_test(CommonTestArgs) of {_,_,_} -> ok; {error,Error} -> io:format("ERROR: ~P\n", [Error,20]); Other -> io:format("~P\n", [Other,20]) end catch _:Crash -> io:format("CRASH: ~P\n", [Crash,20]) end. %% %% Deletes File from Files when File is on the form .../_data/ %% when all of has been skipped in Spec, i.e. there %% exists a {skip, {, _}} tuple in Spec. %% del_skipped_suite_data_dir(Files, Spec) -> SkipDirNames = lists:foldl(fun ({skip, {SS, _C}}, SSs) -> [atom_to_list(SS) ++ "_data" | SSs]; (_, SSs) -> SSs end, [], Spec), filter(fun (File) -> not member(filename:basename(filename:dirname(File)), SkipDirNames) end, Files). %% Initialize our internal state. init_state(Vars, [], St0) -> {FileBase,Wc0,Mod} = case St0#state.file of {Fil,Mod0} -> {Fil, atom_to_list(Mod0) ++ "*_data",Mod0}; Fil -> {Fil,"*_SUITE_data",[]} end, {ok,Cwd} = file:get_cwd(), TestDir = filename:join(filename:dirname(Cwd), FileBase++"_test"), case filelib:is_dir(TestDir) of true -> Wc = filename:join(TestDir, Wc0), {ok,Vars,[],St0#state{file=FileBase,mod=Mod, test_dir=TestDir,data_wc=Wc}}; false -> {error,{no_test_directory,TestDir}} end. %% Run any "Makefile.first" files first. %% XXX We should fake a failing test case if the make fails. run_preinits(Vars, Spec, St) -> Wc = filename:join(St#state.data_wc, "Makefile.first"), run_pre_makefiles(del_skipped_suite_data_dir(filelib:wildcard(Wc), Spec), Vars, Spec, St), {ok,Vars,Spec,St}. run_pre_makefiles([Makefile|Ms], Vars0, Spec0, St0) -> Hooks = [fun run_pre_makefile/3], case execute(Hooks, Vars0, Spec0, St0#state{makefile=Makefile}) of {error,_Reason}=Error -> Error; {ok,Vars,Spec,St} -> run_pre_makefiles(Ms, Vars, Spec, St) end; run_pre_makefiles([], Vars, Spec, St) -> {ok,Vars,Spec,St}. run_pre_makefile(Vars, Spec, St) -> Makefile = St#state.makefile, Shortname = filename:basename(Makefile), DataDir = filename:dirname(Makefile), Make = ts_lib:var(make_command, Vars), case ts_make:make(Make,DataDir, Shortname) of ok -> {ok,Vars,Spec,St}; {error,_Reason}=Error -> Error end. get_config_files() -> TSConfig = "ts.config", [TSConfig | case os:type() of {unix,_} -> ["ts.unix.config"]; {win32,_} -> ["ts.win32.config"]; _ -> [] end]. %% Makes the command to start up the Erlang node to run the tests. backslashify([$\\, $" | T]) -> [$\\, $" | backslashify(T)]; backslashify([$" | T]) -> [$\\, $" | backslashify(T)]; backslashify([H | T]) -> [H | backslashify(T)]; backslashify([]) -> []. make_command(Vars, Spec, State) -> {ok,Cwd} = file:get_cwd(), TestDir = State#state.test_dir, TestPath = filename:nativename(TestDir), Erl = case os:getenv("TS_RUN_VALGRIND") of false -> atom_to_list(lib:progname()); _ -> case State#state.file of Dir when is_list(Dir) -> os:putenv("VALGRIND_LOGFILE_PREFIX", Dir++"-"); _ -> ok end, "cerl -valgrind" ++ case erlang:system_info(smp_support) of true -> " -smp"; false -> "" end end, Naming = case ts_lib:var(longnames, Vars) of true -> " -name "; false -> " -sname " end, ExtraArgs = case lists:keysearch(erl_start_args,1,Vars) of {value,{erl_start_args,Args}} -> Args; false -> "" end, CrashFile = filename:join(Cwd,State#state.file ++ "_erl_crash.dump"), case filelib:is_file(CrashFile) of true -> io:format("ts_run: Deleting dump: ~ts\n",[CrashFile]), file:delete(CrashFile); false -> ok end, %% If Common Test specific variables are needed, add them here %% on form: "{key1,value1}" "{key2,value2}" ... NetDir = ts_lib:var(ts_net_dir, Vars), TestVars = [ "\"{net_dir,\\\"",NetDir,"\\\"}\"" ], %% NOTE: Do not use ' in these commands as it wont work on windows Cmd = [Erl, Naming, "test_server" " -rsh ", ts_lib:var(rsh_name, Vars), " -env PATH \"", backslashify(lists:flatten([TestPath, path_separator(), remove_path_spaces()])), "\"", " -env ERL_CRASH_DUMP ", CrashFile, %% uncomment the line below to disable exception formatting %% " -test_server_format_exception false", " -boot start_sasl -sasl errlog_type error", " -pz \"",Cwd,"\"", " -ct_test_vars ",TestVars, " -eval \"ts_run:ct_run_test(\\\"",TestDir,"\\\", ", backslashify(lists:flatten(State#state.test_server_args)),")\"" " ", ExtraArgs], {ok, Vars, Spec, State#state{command=lists:flatten(Cmd)}}. run_batch(Vars, _Spec, State) -> process_flag(trap_exit, true), Command = State#state.command ++ " -noinput -eval \"erlang:halt(0,[{flush,false}]).\"", ts_lib:progress(Vars, 1, "Command: ~ts~n", [Command]), io:format(user, "Command: ~ts~n",[Command]), Port = open_port({spawn, Command}, [stream, in, eof, exit_status]), Timeout = 30000 * case os:getenv("TS_RUN_VALGRIND") of false -> 1; _ -> 100 end, tricky_print_data(Port, Timeout). tricky_print_data(Port, Timeout) -> receive {Port, {data, Bytes}} -> io:put_chars(Bytes), tricky_print_data(Port, Timeout); {Port, eof} -> Port ! {self(), close}, receive {Port, closed} -> true end, receive {'EXIT', Port, _} -> ok after 1 -> % force context switch ok end, receive {Port, {exit_status, 0}} -> ok; {Port, {exit_status, N}} -> io:format(user, "Test run exited with status ~p~n", [N]) after 1 -> %% This shouldn't happen, but better safe then hanging ok end after Timeout -> case erl_epmd:names() of {ok,Names} -> case is_testnode_dead(Names) of true -> io:put_chars("WARNING: No EOF, but " "test_server node is down!\n"); false -> tricky_print_data(Port, Timeout) end; _ -> tricky_print_data(Port, Timeout) end end. is_testnode_dead([]) -> true; is_testnode_dead([{"test_server",_}|_]) -> false; is_testnode_dead([_|T]) -> is_testnode_dead(T). run_interactive(Vars, _Spec, State) -> Command = State#state.command, ts_lib:progress(Vars, 1, "Command: ~s~n", [Command]), case ts_lib:var(os, Vars) of "Windows 95" -> %% Windows 95 strikes again! We must redirect standard %% input and output for the `start' command, to force %% standard input and output to the Erlang shell to be %% connected to the newly started console. %% Without these redirections, the Erlang shell would be %% connected to the pipes provided by the port program %% and there would be an inactive console window. os:cmd("start < nul > nul w" ++ Command), ok; "Windows 98" -> os:cmd("start < nul > nul w" ++ Command), ok; "Windows"++_ -> os:cmd("start w" ++ Command), ok; _Other -> %% Assuming ts and controller always run on solaris start_xterm(Command) end. start_xterm(Command) -> case os:find_executable("xterm") of false -> io:format("The `xterm' program was not found.\n"), {error, no_xterm}; _Xterm -> case os:getenv("DISPLAY") of false -> io:format("DISPLAY is not set.\n"), {error, display_not_set}; Display -> io:format("Starting xterm (DISPLAY=~s)...\n", [Display]), os:cmd("xterm -sl 10000 -e " ++ Command ++ "&"), ok end end. path_separator() -> case os:type() of {win32, _} -> ";"; {unix, _} -> ":" end. make_common_test_args(Args0, Options0, _Vars) -> Trace = case lists:keysearch(trace,1,Options0) of {value,{trace,TI}} when is_tuple(TI); is_tuple(hd(TI)) -> ok = file:write_file(?tracefile,io_lib:format("~p.~n",[TI])), [{ct_trace,?tracefile}]; {value,{trace,TIFile}} when is_atom(TIFile) -> [{ct_trace,atom_to_list(TIFile)}]; {value,{trace,TIFile}} -> [{ct_trace,TIFile}]; false -> [] end, Cover = case lists:keysearch(cover,1,Options0) of {value,{cover, App, none, _Analyse}} -> io:format("No cover file found for ~p~n",[App]), []; {value,{cover,_App,File,_Analyse}} -> [{cover,to_list(File)},{cover_stop,false}]; false -> [] end, Logdir = case lists:keysearch(logdir, 1, Options0) of {value,{logdir, _}} -> []; false -> [{logdir,"../test_server"}] end, TimeTrap = [{scale_timetraps, true}], {ConfigPath, Options} = case {os:getenv("TEST_CONFIG_PATH"), lists:keysearch(config, 1, Options0)} of {_,{value, {config, Path}}} -> {Path,lists:keydelete(config, 1, Options0)}; {false,false} -> {"../test_server",Options0}; {Path,_} -> {Path,Options0} end, ConfigFiles = [{config,[filename:join(ConfigPath,File) || File <- get_config_files()]}], io_lib:format("~100000p",[[{abort_if_missing_suites,true} | Args0++Trace++Cover++Logdir++ ConfigFiles++Options++TimeTrap]]). to_list(X) when is_atom(X) -> atom_to_list(X); to_list(X) when is_list(X) -> X. %% %% Paths and spaces handling for w2k and XP %% remove_path_spaces() -> Path = os:getenv("PATH"), case os:type() of {win32,nt} -> remove_path_spaces(Path); _ -> Path end. remove_path_spaces(Path) -> SPath = split_path(Path), [NSHead|NSTail] = lists:map(fun(X) -> filename:nativename( filename:join( translate_path(split_one(X)))) end, SPath), NSHead ++ lists:flatten([[$;|X] || X <- NSTail]). translate_path(PList) -> %io:format("translate_path([~p|~p]~n",[Base,PList]), translate_path(PList,[]). translate_path([],_) -> []; translate_path([PC | T],BaseList) -> FullPath = filename:nativename(filename:join(BaseList ++ [PC])), NewPC = case catch file:altname(FullPath) of {ok,X} -> X; _ -> PC end, %io:format("NewPC:~s, DirList:~p~n",[NewPC,DirList]), NewBase = BaseList ++ [NewPC], [NewPC | translate_path(T,NewBase)]. split_one(Path) -> filename:split(Path). split_path(Path) -> string:tokens(Path,";").