%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%

%%% @doc Common Test Framework test execution control module.
%%%
%%% <p>This module exports functions for installing and running tests
%%% withing the Common Test Framework.</p>

-module(ct_run).


%% Script interface
-export([script_start/0,script_usage/0]).

%% User interface
-export([install/1,install/2,run/1,run/2,run/3,run_test/1,
	 run_testspec/1,step/3,step/4,refresh_logs/1]).


%% Exported for VTS
-export([run_make/3,do_run/3,tests/1,tests/2,tests/3]).


%% Misc internal functions
-export([variables_file_name/1,script_start1/2,run_test1/1]).

-include("ct_event.hrl").
-include("ct_util.hrl").

-define(abs(Name), filename:absname(Name)).
-define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)).

%%%-----------------------------------------------------------------
%%% @spec script_start() -> void()
%%%
%%% @doc Start tests via the run_test script.
%%% 
%%% <p>Example:<br/><code>./run_test -config config.ctc -dir
%%% $TEST_DIR</code></p>
%%%
%%% <p>Example:<br/><code>./run_test -config config.ctc -suite
%%% $SUITE_PATH/$SUITE_NAME [-case $CASE_NAME]</code></p>
%%%
script_start() ->
    process_flag(trap_exit, true),
    Args = merge_arguments(init:get_arguments()),
    Tracing = start_trace(Args),
    Res = 
	case ct_repeat:loop_test(script, Args) of
	    false ->	    
		{ok,Cwd} = file:get_cwd(),
		CTVsn = 
		    case filename:basename(code:lib_dir(common_test)) of
			CTBase when is_list(CTBase) ->
			    case string:tokens(CTBase, "-") of
				["common_test",Vsn] -> " v"++Vsn;
				_ -> ""
			    end
		    end,
		io:format("~nCommon Test~s starting (cwd is ~s)~n~n", [CTVsn,Cwd]),
		Self = self(),
		Pid = spawn_link(fun() -> script_start1(Self, Args) end),
	        receive 
		    {'EXIT',Pid,Reason} ->
			case Reason of
			    {user_error,What} ->
				io:format("\nTest run failed!\nReason: ~p\n\n", [What]),
				{error,What};
			    _ ->
				io:format("Test run crashed! This could be an internal error "
					  "- please report!\n\n"
					  "~p\n\n", [Reason]),
				{error,Reason}				
			end;
		    {Pid,{error,Reason}} ->
			io:format("\nTest run failed! Reason:\n~p\n\n",[Reason]),
			{error,Reason};
		    {Pid,Result} ->
			Result
		end;
	    Result ->
		Result
	end,
    stop_trace(Tracing),
    Res.

script_start1(Parent, Args) ->
    case lists:keymember(preload, 1, Args) of
	true -> preload();
	false -> ok
    end,

    VtsOrShell = 
	case lists:keymember(vts, 1, Args) of
	    true -> 
		vts;
	    false ->
		case lists:keymember(shell, 1, Args) of
		    true -> shell;
		    false -> false
		end
	end,
    LogDir =
	case lists:keysearch(logdir, 1, Args) of
	    {value,{logdir,[LogD]}} -> LogD;
	    false -> "."
	end,
    EvHandlers =	
	case lists:keysearch(event_handler, 1, Args) of
	    {value,{event_handler,Handlers}} -> 
		lists:map(fun(H) -> {list_to_atom(H),[]} end, Handlers);
	    false -> 
		[]
	end,
    Cover = 
	case lists:keysearch(cover, 1, Args) of
	    {value,{cover,CoverFile}} -> 
		{cover,?abs(CoverFile)};
	    false -> 
		false
	end,

    case lists:keysearch(ct_decrypt_key, 1, Args) of
	{value,{_,[DecryptKey]}} ->
	    application:set_env(common_test, decrypt, {key,DecryptKey});
	false ->
	    case lists:keysearch(ct_decrypt_file, 1, Args) of
		{value,{_,[DecryptFile]}} ->
		    application:set_env(common_test, decrypt, 
					{file,filename:absname(DecryptFile)});
		false ->
		    application:unset_env(common_test, decrypt)
	    end
    end,

    case lists:keysearch(no_auto_compile, 1, Args) of
	{value,_} ->
	    application:set_env(common_test, auto_compile, false);
	false ->
	    application:set_env(common_test, auto_compile, true),

	    InclDirs =
		case lists:keysearch(include,1,Args) of
		    {value,{include,Incl}} when is_list(hd(Incl)) ->
			Incl;		    
		    {value,{include,Incl}} when is_list(Incl) ->
			[Incl];
		    false ->
			[]
		end,
	    case os:getenv("CT_INCLUDE_PATH") of
		false ->
		    application:set_env(common_test, include, InclDirs);
		CtInclPath ->
		    InclDirs1 = string:tokens(CtInclPath,[$:,$ ,$,]),
		    application:set_env(common_test, include, InclDirs1++InclDirs)
	    end
    end,

    case lists:keysearch(basic_html, 1, Args) of
	{value,_} ->
	    application:set_env(common_test, basic_html, true);
	false ->
	    application:set_env(common_test, basic_html, false)
    end,

    Result =
	case lists:keysearch(refresh_logs, 1, Args) of
	    {value,{refresh_logs,Refresh}} ->
		LogDir1 = case Refresh of
			      [] -> LogDir;
			      [RefreshDir] -> ?abs(RefreshDir)
			  end,
		{ok,Cwd} = file:get_cwd(),
		file:set_cwd(LogDir1),
		timer:sleep(500),		% give the shell time to print version etc
		io:nl(),
		case catch ct_logs:make_all_suites_index(refresh) of
		    {'EXIT',ASReason} ->
			file:set_cwd(Cwd),
			{error,{all_suites_index,ASReason}};
		    _ ->
			case catch ct_logs:make_all_runs_index(refresh) of
			    {'EXIT',ARReason} ->
				file:set_cwd(Cwd),
				{error,{all_runs_index,ARReason}};
			    _ ->
				file:set_cwd(Cwd),
				io:format("Logs in ~s refreshed!~n~n", [LogDir1]),
				timer:sleep(500), % time to flush io before quitting
				ok
			end
		end;
	    false ->    
		case lists:keysearch(ct_config, 1, Args) of
		    {value,{ct_config,ConfigFiles}} ->
			case lists:keysearch(spec, 1, Args) of
			    false ->
				case get_configfiles(ConfigFiles, [], LogDir,
						     EvHandlers) of
				    ok ->
					script_start2(VtsOrShell, ConfigFiles,
						      EvHandlers, Args, LogDir,
						      Cover);
				    Error ->
					Error
				end;
			    _ ->
				script_start2(VtsOrShell, ConfigFiles,
					      EvHandlers, Args, LogDir, Cover)
			end;
		    false ->
			case install([{config,[]},
				      {event_handler,EvHandlers}],
				     LogDir) of
			    ok ->
				script_start2(VtsOrShell, [], EvHandlers,
					      Args, LogDir, Cover);
			    Error ->
				Error
			end
		end
	end,
    Parent ! {self(), Result}.

get_configfiles([File|Files], Acc, LogDir, EvHandlers) ->
    case filelib:is_file(File) of
	true ->
	    get_configfiles(Files, [?abs(File)|Acc],
			    LogDir, EvHandlers);
	false ->
	    {error,{cant_read_config_file,File}}
    end;
get_configfiles([], Acc, LogDir, EvHandlers) ->
    install([{config,lists:reverse(Acc)}, {event_handler,EvHandlers}], LogDir).

script_start2(false, ConfigFiles, EvHandlers, Args, LogDir, Cover) ->
    case lists:keysearch(spec, 1, Args) of
	{value,{spec,[]}} ->
	    {error,no_testspec_specified};
	{value,{spec,Specs}} ->
	    Relaxed = lists:keymember(allow_user_terms, 1, Args),
	    %% using testspec as input for test
	    case catch ct_testspec:collect_tests_from_file(Specs, Relaxed) of
		{error,Reason} ->
		    {error,Reason};
		TS ->
		    {LogDir1,TSCoverFile,ConfigFiles1,EvHandlers1,Include1} = 
			get_data_for_node(TS,node()),
		    UserInclude =
			case application:get_env(common_test, include) of
			    {ok,Include} -> Include++Include1;
			    _ -> Include1
			end,
		    application:set_env(common_test, include, UserInclude),
		    LogDir2 = which_logdir(LogDir,LogDir1),
		    CoverOpt = case {Cover,TSCoverFile} of
				   {false,undef} -> [];
				   {_,undef} ->     [Cover];
				   {false,_} ->     [{cover,TSCoverFile}]
			       end,
		    case get_configfiles(ConfigFiles++ConfigFiles1,
					 [], LogDir2,
					 EvHandlers++EvHandlers1) of
			ok ->
			    {Run,Skip} = ct_testspec:prepare_tests(TS, node()),
			    do_run(Run, Skip, CoverOpt, Args, LogDir2);
			Error ->
			    Error
		    end
	    end;
	false ->
	    script_start3(false, ConfigFiles, EvHandlers, Args, LogDir, Cover)
    end;
script_start2(VtsOrShell, ConfigFiles, EvHandlers, Args, LogDir, Cover) ->
    script_start3(VtsOrShell, ConfigFiles, EvHandlers, Args, LogDir, Cover).

script_start3(VtsOrShell, ConfigFiles, EvHandlers, Args, LogDir, Cover) ->
    case lists:keysearch(dir, 1, Args) of
	{value,{dir,[]}} ->
	    {error,no_dir_specified};
	{value,{dir,Dirs}} ->
	    script_start4(VtsOrShell, ConfigFiles, EvHandlers, tests(Dirs),
			  Cover, Args, LogDir);
	false ->
	    case lists:keysearch(suite, 1, Args) of
		{value,{suite,[]}} ->
		    {error,no_suite_specified};
		{value,{suite,Suites}} ->
		    StepOrCover =
			case lists:keysearch(step, 1, Args) of
			    {value,Step} -> Step;
			    false -> Cover
			end,
		    S2M = fun(S) ->
				  {filename:dirname(S),
				   list_to_atom(
				     filename:rootname(filename:basename(S)))}
			  end,
		    DirMods = lists:map(S2M, Suites),
		    {Specified,GroupsAndCases} =
			case {lists:keysearch(group, 1, Args),
			      lists:keysearch('case', 1, Args)} of
			    {{value,{_,Gs}},{value,{_,Cs}}} -> {true,Gs++Cs};
			    {{value,{_,Gs}},_}              -> {true,Gs};
			    {_,{value,{_,Cs}}}              -> {true,Cs};
			    _                               -> {false,[]}
			end,
		    if Specified, length(GroupsAndCases) == 0 ->
			    {error,no_case_or_group_specified};
		       Specified, length(DirMods) > 1 ->
			    {error,multiple_suites_and_cases};
		       length(GroupsAndCases) > 0, length(DirMods) == 1 ->
			    GsAndCs = lists:map(fun(C) -> list_to_atom(C) end, 
						GroupsAndCases),
			    script_start4(VtsOrShell, ConfigFiles, EvHandlers,
					  tests(DirMods, GsAndCs),
					  StepOrCover, Args, LogDir);
		       not Specified, length(DirMods) > 0 ->
			    script_start4(VtsOrShell, ConfigFiles, EvHandlers,
					  tests(DirMods), 
					  StepOrCover, Args, LogDir);
		       true ->
			    {error,incorrect_suite_and_case_options}
		    end;
		false when VtsOrShell=/=false ->
		    script_start4(VtsOrShell, ConfigFiles, EvHandlers,
				  [], Cover, Args, LogDir);
		false ->
		    script_usage(),
		    {error,incorrect_usage}
	    end
    end.

script_start4(vts, ConfigFiles, EvHandlers, Tests, false, _Args, LogDir) ->
    vts:init_data(ConfigFiles, EvHandlers, ?abs(LogDir), Tests);
script_start4(shell, ConfigFiles, EvHandlers, _Tests, false, Args, LogDir) ->
    Opts = [{config,ConfigFiles},{event_handler,EvHandlers}],
    if ConfigFiles == [] ->
	    ok;
       true ->
	    io:format("\nInstalling: ~p\n\n", [ConfigFiles])
    end,
    case install(Opts) of
	ok ->
	    ct_util:start(interactive, LogDir),
	    log_ts_names(Args),
	    io:nl(),
	    ok;
	Error ->
	    Error
    end;
script_start4(vts, _CfgFs, _EvHs, _Tests, _Cover={cover,_}, _Args, _LogDir) ->
    %% Add support later (maybe).
    script_usage(),
    erlang:halt();
script_start4(shell, _CfgFs, _EvHs, _Tests, _Cover={cover,_}, _Args, _LogDir) ->
    %% Add support later (maybe).
    script_usage();
script_start4(false, _CfgFs, _EvHs, Tests, Cover={cover,_}, Args, LogDir) ->
    do_run(Tests, [], [Cover], Args, LogDir);
script_start4(false, _ConfigFiles, _EvHandlers, Tests, false, Args, LogDir) ->
    do_run(Tests, [], [], Args, LogDir);
script_start4(false, _ConfigFiles, _EvHandlers, Test, Step, Args, LogDir) ->
    do_run(Test, [], [Step], Args, LogDir);
script_start4(vts, _ConfigFiles, _EvHandlers, _Test, _Step, _Args, _LogDir) ->
    script_usage(),
    erlang:halt();
script_start4(shell, _ConfigFiles, _EvHandlers, _Test, _Step, _Args, _LogDir) ->
    script_usage().

%%%-----------------------------------------------------------------
%%% @spec script_usage() -> ok
%%% @doc Print script usage information for <code>run_test</code>.
script_usage() ->
    io:format("\n\nUsage:\n\n"),
    io:format("Run tests in web based GUI:\n\n"
	      "\trun_test -vts [-browser Browser]"
	      "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
	      "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
	      "\n\t[-dir TestDir1 TestDir2 .. TestDirN] |"
	      "\n\t[-suite Suite [-case Case]]"
	      "\n\t[-include InclDir1 InclDir2 .. InclDirN]" 
	      "\n\t[-no_auto_compile]"
	      "\n\t[-basic_html]\n\n"),
    io:format("Run tests from command line:\n\n"
	      "\trun_test [-dir TestDir1 TestDir2 .. TestDirN] |"
	      "\n\t[-suite Suite1 Suite2 .. SuiteN [-case Case1 Case2 .. CaseN]]"
	      "\n\t[-step [config | keep_inactive]]"
	      "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
	      "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
	      "\n\t[-logdir LogDir]"
	      "\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
	      "\n\t[-stylesheet CSSFile]"
	      "\n\t[-cover CoverCfgFile]"
	      "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
	      "\n\t[-include InclDir1 InclDir2 .. InclDirN]" 
	      "\n\t[-no_auto_compile]"
	      "\n\t[-basic_html]" 
	      "\n\t[-repeat N [-force_stop]] |" 
	      "\n\t[-duration HHMMSS [-force_stop]] |"
	      "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop]]\n\n"),
    io:format("Run tests using test specification:\n\n"
	      "\trun_test -spec TestSpec1 TestSpec2 .. TestSpecN"
	      "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
	      "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]"
	      "\n\t[-logdir LogDir]"
	      "\n\t[-allow_user_terms]"
	      "\n\t[-silent_connections [ConnType1 ConnType2 .. ConnTypeN]]"
	      "\n\t[-stylesheet CSSFile]"
	      "\n\t[-cover CoverCfgFile]"
	      "\n\t[-event_handler EvHandler1 EvHandler2 .. EvHandlerN]"
	      "\n\t[-include InclDir1 InclDir2 .. InclDirN]" 
	      "\n\t[-no_auto_compile]"
	      "\n\t[-basic_html]" 
	      "\n\t[-repeat N [-force_stop]] |" 
	      "\n\t[-duration HHMMSS [-force_stop]] |"
	      "\n\t[-until [YYMoMoDD]HHMMSS [-force_stop]]\n\n"),
    io:format("Refresh the HTML index files:\n\n"
	      "\trun_test -refresh_logs [LogDir]"
	      "[-logdir LogDir] "
	      "[-basic_html]\n\n"),
    io:format("Run CT in interactive mode:\n\n"
	      "\trun_test -shell"
	      "\n\t[-config ConfigFile1 ConfigFile2 .. ConfigFileN]"
	      "\n\t[-decrypt_key Key] | [-decrypt_file KeyFile]\n\n").
	      

%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:install/1
install(Opts) ->
    install(Opts, ".").

install(Opts, LogDir) ->
    case application:get_env(common_test, decrypt) of
	{ok,_} ->
	    ok;
	_ ->
	    case lists:keysearch(decrypt, 1, Opts) of
		{value,{_,KeyOrFile}} ->
		    application:set_env(common_test, decrypt, KeyOrFile);
		false ->
		    application:unset_env(common_test, decrypt)
	    end
    end,
    case whereis(ct_util_server) of
	undefined ->
	    VarFile = variables_file_name(LogDir),
	    case file:open(VarFile, [write]) of
		{ok,Fd} ->
		    [io:format(Fd, "~p.\n", [Opt]) || Opt <- Opts],
		    file:close(Fd),
		    ok;
		{error,Reason} -> 
		    io:format("CT failed to install configuration data. Please "
			      "verify that the log directory exists and that "
			      "write permission is set.\n\n", []),
		    {error,{VarFile,Reason}}
	    end;
	_ ->
	    io:format("It is not possible to install CT while running "
		      "in interactive mode.\n"
		      "To exit this mode, run ct:stop_interactive().\n"
		      "To enter the interactive mode again, "
		      "run ct:start_interactive()\n\n", []),
	    {error,interactive_mode}
    end.

variables_file_name(Dir) ->
    filename:join(Dir, "variables-"++atom_to_list(node())).

%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:run_test/1

%%   Opts = [OptTuples]
%%   OptTuples = {config,CfgFiles} | {dir,TestDirs} | {suite,Suites} |
%%               {testcase,Cases} | {spec,TestSpecs} | {allow_user_terms,Bool} |
%%               {logdir,LogDir} | {cover,CoverSpecFile} | {step,StepOpts} |
%%               {silent_connections,Conns} | {event_handler,EventHandlers} | 
%%               {include,InclDirs} | {auto_compile,Bool} | 
%%               {repeat,N} | {duration,DurTime} | {until,StopTime} | {force_stop,Bool} |
%%               {decrypt,KeyOrFile}

run_test(Opt) when is_tuple(Opt) ->
    run_test([Opt]);

run_test(Opts) when is_list(Opts) ->
    case lists:keysearch(refresh_logs, 1, Opts) of
	{value,{_,RefreshDir}} ->
	    refresh_logs(?abs(RefreshDir)),
	    ok;
	false ->
	    Tracing = start_trace(Opts),
	    {ok,Cwd} = file:get_cwd(),
	    io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]),	    
	    Res =
		case ct_repeat:loop_test(func, Opts) of
		    false ->
			case catch run_test1(Opts) of
			    {'EXIT',Reason} -> 
				file:set_cwd(Cwd),
				{error,Reason};
			    Result -> 
				Result
			end;
		    Result ->
			Result
		end,
	    stop_trace(Tracing),
	    Res
    end.

run_test1(Opts) ->
    LogDir =
	case lists:keysearch(logdir, 1, Opts) of
	    {value,{_,LD}} when is_list(LD) -> LD;
	    false -> "."
	end,
    CfgFiles =
	case lists:keysearch(config, 1, Opts) of
	    {value,{_,Files=[File|_]}} when is_list(File) ->
		Files;
	    {value,{_,File=[C|_]}} when is_integer(C) ->
		[File];
	    {value,{_,[]}} ->
		[];
	    false ->
		[]
	end,
    EvHandlers =
	case lists:keysearch(event_handler, 1, Opts) of
	    {value,{_,H}} when is_atom(H) ->
		[{H,[]}];
	    {value,{_,H}} ->
		Hs =
		    if is_tuple(H) -> [H];
		       is_list(H) -> H;
		       true -> []
		    end,
		lists:flatten(
		  lists:map(fun(EH) when is_atom(EH) ->
				    {EH,[]};
			       ({HL,Args}) when is_list(HL) ->
				    [{EH,Args} || EH <- HL];
			       ({EH,Args}) when is_atom(EH) ->
				    {EH,Args};
			       (_) ->
				    []
			    end, Hs));
	    _ ->
		[]
	end,
    SilentConns =
	case lists:keysearch(silent_connections, 1, Opts) of
	    {value,{_,all}} ->
		[];
	    {value,{_,Conns}} ->
		Conns;
	    _ ->
		undefined
	end,
    Cover = 
	case lists:keysearch(cover, 1, Opts) of
	    {value,{_,CoverFile}} ->
		[{cover,?abs(CoverFile)}];
	    _ ->
		[]
	end,
    Include =
	case lists:keysearch(auto_compile, 1, Opts) of
	    {value,{auto_compile,ACBool}} ->
		application:set_env(common_test, auto_compile, ACBool),
		[];
	    _ ->
		application:set_env(common_test, auto_compile, true),		
		InclDirs =
		    case lists:keysearch(include, 1, Opts) of
			{value,{include,Incl}}  when is_list(hd(Incl)) ->
			    Incl;		    
			{value,{include,Incl}} when is_list(Incl) ->
			    [Incl];
			false ->
			    []
		    end,
		case os:getenv("CT_INCLUDE_PATH") of
		    false ->
			application:set_env(common_test, include, InclDirs),
			InclDirs;
		    CtInclPath ->
			InclDirs1 = string:tokens(CtInclPath, [$:,$ ,$,]),
			AllInclDirs = InclDirs1++InclDirs,
			application:set_env(common_test, include, AllInclDirs),
			AllInclDirs
		end
	end,

    case lists:keysearch(decrypt, 1, Opts) of
	{value,{_,Key={key,_}}} ->
	    application:set_env(common_test, decrypt, Key);
	{value,{_,{file,KeyFile}}} ->
	    application:set_env(common_test, decrypt, {file,filename:absname(KeyFile)});
	false ->
	    application:unset_env(common_test, decrypt)
    end,

    case lists:keysearch(basic_html, 1, Opts) of
	{value,{basic_html,BasicHtmlBool}} ->
	    application:set_env(common_test, basic_html, BasicHtmlBool);
	_ ->
	    application:set_env(common_test, basic_html, false)
    end,

    case lists:keysearch(spec, 1, Opts) of
	{value,{_,Specs}} ->
	    Relaxed =
		case lists:keysearch(allow_user_terms, 1, Opts) of
		    {value,{_,true}} -> true;
		    _ -> false
		end,	    
	    %% using testspec(s) as input for test
	    run_spec_file(LogDir, CfgFiles, EvHandlers, Include, Specs, Relaxed, Cover,
			 replace_opt([{silent_connections,SilentConns}], Opts));
	false ->
	    case lists:keysearch(prepared_tests, 1, Opts) of
		{value,{_,{Run,Skip},Specs}} ->	% use prepared tests
		    run_prepared(LogDir, CfgFiles, EvHandlers,
				 Run, Skip, Cover,
				 replace_opt([{silent_connections,SilentConns},
					      {spec,Specs}],Opts));
		false ->		% use dir|suite|case
		    StepOrCover =
			case lists:keysearch(step, 1, Opts) of
			    {value,Step} -> [Step];
			    false -> Cover
			end,
		    run_dir(LogDir, CfgFiles, EvHandlers, StepOrCover,
			    replace_opt([{silent_connections,SilentConns}], Opts))
	    end
    end.

replace_opt([O={Key,_Val}|Os], Opts) ->
    [O | replace_opt(Os, lists:keydelete(Key, 1, Opts))];
replace_opt([], Opts) ->
    Opts.

run_spec_file(LogDir, CfgFiles, EvHandlers, Include, Specs, Relaxed, Cover, Opts) ->
    Specs1 = case Specs of
		 [X|_] when is_integer(X) -> [Specs];
		 _ -> Specs
	     end,
    AbsSpecs = lists:map(fun(SF) -> ?abs(SF) end, Specs1), 
    log_ts_names(AbsSpecs),
    case catch ct_testspec:collect_tests_from_file(AbsSpecs, Relaxed) of
	{error,CTReason} ->
	    exit(CTReason);
	TS ->
	    {LogDir1,TSCoverFile,CfgFiles1,EvHandlers1,Include1} = 
		get_data_for_node(TS, node()),
	    application:set_env(common_test, include, Include++Include1),
	    LogDir2 = which_logdir(LogDir, LogDir1),
	    CoverOpt = case {Cover,TSCoverFile} of
			   {[],undef} -> [];
			   {_,undef} ->  Cover;
			   {[],_} ->     [{cover,TSCoverFile}]
		       end,
	    case get_configfiles(CfgFiles++CfgFiles1, [], LogDir2,
				 EvHandlers++EvHandlers1) of
		ok ->
		    {Run,Skip} = ct_testspec:prepare_tests(TS, node()),
		    do_run(Run, Skip, CoverOpt,
			   replace_opt([{spec,AbsSpecs}], Opts),
			   LogDir2);
		{error,GCFReason} ->
		    exit(GCFReason)
	    end
    end.

run_prepared(LogDir, CfgFiles, EvHandlers, Run, Skip, Cover, Opts) ->
    case get_configfiles(CfgFiles, [], LogDir, EvHandlers) of
	ok ->
	    do_run(Run, Skip, Cover, Opts, LogDir);
	{error,Reason} ->
	    exit(Reason)
    end.    

run_dir(LogDir, CfgFiles, EvHandlers, StepOrCover, Opts) ->
    AbsCfgFiles = 
	lists:map(fun(F) -> 
			  AbsName = ?abs(F),
			  case filelib:is_file(AbsName) of
			      true -> AbsName;
			      false -> exit({no_such_file,AbsName})
			  end
		  end, CfgFiles), 

    case install([{config,AbsCfgFiles},{event_handler,EvHandlers}], LogDir) of
	ok -> ok;
	{error,IReason} -> exit(IReason)
    end,
    case lists:keysearch(dir,1,Opts) of
	{value,{_,Dirs=[Dir|_]}} when not is_integer(Dir),
	                              length(Dirs)>1 ->
	    %% multiple dirs (no suite)
	    do_run(tests(Dirs), [], StepOrCover, Opts, LogDir);
	false ->				% no dir
	    %% fun for converting suite name to {Dir,Mod} tuple
	    S2M = fun(S) when is_list(S) ->
			  {filename:dirname(S),
			   list_to_atom(filename:rootname(filename:basename(S)))};
		     (A) ->
			  {".",A}
		  end,
	    case lists:keysearch(suite, 1, Opts) of
		{value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) ->
		    {Dir,Mod} = S2M(Suite),
		    case listify(proplists:get_value(group, Opts, [])) ++
			 listify(proplists:get_value(testcase, Opts, [])) of
			[] ->
			    do_run(tests(Dir, listify(Mod)), [], StepOrCover, Opts, LogDir);
			GsAndCs ->
			    do_run(tests(Dir, Mod, GsAndCs), [], StepOrCover, Opts, LogDir)
		    end;
		{value,{_,Suites}} ->
		    do_run(tests(lists:map(S2M, Suites)), [], StepOrCover, Opts, LogDir);
		_ ->
		    exit(no_tests_specified)
	    end;		   
	{value,{_,Dir}} ->
	    case lists:keysearch(suite, 1, Opts) of
		{value,{_,Suite}} when is_integer(hd(Suite)) ; is_atom(Suite) ->
		    Mod = if is_atom(Suite) -> Suite; 
			     true -> list_to_atom(Suite) 
			  end,
		    case listify(proplists:get_value(group, Opts, [])) ++
			 listify(proplists:get_value(testcase, Opts, [])) of
			[] ->
			    do_run(tests(Dir, listify(Mod)), [], StepOrCover, Opts, LogDir);
			GsAndCs ->
			    do_run(tests(Dir, Mod, GsAndCs), [], StepOrCover, Opts, LogDir)
		    end;
		{value,{_,Suites=[Suite|_]}} when is_list(Suite) ->
		    Mods = lists:map(fun(Str) -> list_to_atom(Str) end, Suites),
		    do_run(tests(delistify(Dir), Mods), [], StepOrCover, Opts, LogDir);		    
		{value,{_,Suites}} ->
		    do_run(tests(delistify(Dir), Suites), [], StepOrCover, Opts, LogDir);		    
	        false ->			% no suite, only dir
		    do_run(tests(listify(Dir)), [], StepOrCover, Opts, LogDir)
	    end   
    end.

%%%-----------------------------------------------------------------
%%% @hidden
%%%

%% using testspec(s) as input for test
run_testspec(TestSpec) ->
    {ok,Cwd} = file:get_cwd(),
    io:format("~nCommon Test starting (cwd is ~s)~n~n", [Cwd]),
    case catch run_testspec1(TestSpec) of
	{'EXIT',Reason} -> 
	    file:set_cwd(Cwd),
	    {error,Reason};
	Result -> 
	    Result
    end.

run_testspec1(TestSpec) ->
    case ct_testspec:collect_tests_from_list(TestSpec,false) of
	{error,CTReason} ->
	    exit(CTReason);
	TS ->
	    {LogDir,TSCoverFile,CfgFiles,EvHandlers,Include} = 
		get_data_for_node(TS,node()),
	    case os:getenv("CT_INCLUDE_PATH") of
		false ->
		    application:set_env(common_test, include, Include);
		CtInclPath ->
		    EnvInclude = string:tokens(CtInclPath, [$:,$ ,$,]),
		    application:set_env(common_test, include, EnvInclude++Include)
	    end,
	    CoverOpt = if TSCoverFile == undef -> [];
			  true -> [{cover,TSCoverFile}]
		       end,
	    case get_configfiles(CfgFiles,[],LogDir,EvHandlers) of
		ok ->
		    {Run,Skip} = ct_testspec:prepare_tests(TS,node()),
		    do_run(Run,Skip,CoverOpt,[],LogDir);
		{error,GCFReason} ->
		    exit(GCFReason)
	    end
    end.


get_data_for_node(#testspec{logdir=LogDirs,
			    cover=CoverFs,
			    config=Cfgs,
			    event_handler=EvHs,
			    include=Incl}, Node) ->
    LogDir = case lists:keysearch(Node,1,LogDirs) of
		 {value,{Node,Dir}} -> Dir;
		 false -> "."
	     end,
    Cover = case lists:keysearch(Node,1,CoverFs) of
		{value,{Node,CovFile}} -> CovFile;
		false -> undef
	    end,
    ConfigFiles = [F || {N,F} <- Cfgs, N==Node],
    EvHandlers =  [{H,A} || {N,H,A} <- EvHs, N==Node],
    Include =  [I || {N,I} <- Incl, N==Node],
    {LogDir,Cover,ConfigFiles,EvHandlers,Include}.


refresh_logs(LogDir) ->
    {ok,Cwd} = file:get_cwd(),
    case file:set_cwd(LogDir) of
	E = {error,_Reason} ->
	    E;
	_ ->
	    case catch ct_logs:make_all_suites_index(refresh) of
		{'EXIT',ASReason} ->
		    file:set_cwd(Cwd),
		    {error,{all_suites_index,ASReason}};
		_ ->
		    case catch ct_logs:make_all_runs_index(refresh) of
			{'EXIT',ARReason} ->
			    file:set_cwd(Cwd),
			    {error,{all_runs_index,ARReason}};
			_ ->
			    file:set_cwd(Cwd),
			    io:format("Logs in ~s refreshed!~n",[LogDir]),
			    ok
		    end
	    end
    end.

which_logdir(".",Dir) ->
    Dir;
which_logdir(Dir,_) ->
    Dir.
	   
listify([C|_]=Str) when is_integer(C) -> [Str];
listify(L) when is_list(L) -> L;
listify(E) -> [E].

delistify([E]) -> E;
delistify(E)   -> E.


%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:run/3
run(TestDir, Suite, Cases) ->
    install([]),
    do_run(tests(TestDir, Suite, Cases), []).

%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:run/2
run(TestDir, Suite) when is_list(TestDir), is_integer(hd(TestDir)) ->
    install([]),
    do_run(tests(TestDir, Suite), []).

%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:run/1
run(TestDirs) ->
    install([]),
    do_run(tests(TestDirs), []).


tests(TestDir, Suites, []) when is_list(TestDir), is_integer(hd(TestDir)) ->
    [{?testdir(TestDir,Suites),ensure_atom(Suites),all}];
tests(TestDir, Suite, Cases) when is_list(TestDir), is_integer(hd(TestDir)) ->
    [{?testdir(TestDir,Suite),ensure_atom(Suite),Cases}].
tests([{Dir,Suite}],Cases) ->
    [{?testdir(Dir,Suite),ensure_atom(Suite),Cases}];
tests(TestDir, Suite) when is_list(TestDir), is_integer(hd(TestDir)) ->
    tests(TestDir, ensure_atom(Suite), all).
tests(DirSuites) when is_list(DirSuites), is_tuple(hd(DirSuites)) ->
    [{?testdir(Dir,Suite),ensure_atom(Suite),all} || {Dir,Suite} <- DirSuites];
tests(TestDir) when is_list(TestDir), is_integer(hd(TestDir)) ->
    tests([TestDir]);
tests(TestDirs) when is_list(TestDirs), is_list(hd(TestDirs)) ->
    [{?testdir(TestDir,all),all,all} || TestDir <- TestDirs].

do_run(Tests, Opt) ->
    do_run(Tests, [], Opt, [], ".").

do_run(Tests, Opt, LogDir) ->
    do_run(Tests, [], Opt, [], LogDir).

do_run(Tests, Skip, Opt, Args, LogDir) ->
    case code:which(test_server) of
	non_existing ->
	    exit({error,no_path_to_test_server});
	_ ->
	    Opt1 = 
		case lists:keysearch(cover, 1, Opt) of
		    {value,{_,CoverFile}} ->
			case ct_cover:get_spec(CoverFile) of
			    {error,Reason} ->
				exit({error,Reason});
			    Spec ->
				[{cover_spec,Spec} |
				 lists:keydelete(cover, 1, Opt)]
			end;
		    _ ->
			Opt
		end,
	    %% This env variable is used by test_server to determine
	    %% which framework it runs under.
	    case os:getenv("TEST_SERVER_FRAMEWORK") of
		false ->
		    os:putenv("TEST_SERVER_FRAMEWORK", "ct_framework");
		"ct_framework" ->
		    ok;
		Other ->
		    erlang:display(list_to_atom("Note: TEST_SERVER_FRAMEWORK = " ++ Other))
	    end,
	    case ct_util:start(LogDir) of
		{error,interactive_mode} ->
		    io:format("CT is started in interactive mode. "
			      "To exit this mode, run ct:stop_interactive().\n"
			      "To enter the interactive mode again, "
			      "run ct:start_interactive()\n\n",[]),
		    {error,interactive_mode};

		_Pid ->
		    %% save style sheet info
		    case lists:keysearch(stylesheet, 1, Args) of
			{value,{_,SSFile}} ->
			    ct_util:set_testdata({stylesheet,SSFile});
			_ ->
			    ct_util:set_testdata({stylesheet,undefined})
		    end,

		    case lists:keysearch(silent_connections, 1, Args) of
			{value,{silent_connections,undefined}} ->
			    ok;
			{value,{silent_connections,[]}} ->
			    Conns = ct_util:override_silence_all_connections(),
			    ct_logs:log("Silent connections", "~p", [Conns]);
			{value,{silent_connections,Cs}} ->
			    Conns = lists:map(fun(S) when is_list(S) ->
						      list_to_atom(S);
						 (A) -> A
					      end, Cs),
			    ct_util:override_silence_connections(Conns),
			    ct_logs:log("Silent connections", "~p", [Conns]);
			_ ->
			    ok
		    end,
		    log_ts_names(Args),
		    TestSuites = suite_tuples(Tests),

		    {SuiteMakeErrors,AllMakeErrors} =
			case application:get_env(common_test, auto_compile) of
			    {ok,false} ->
				SuitesNotFound = verify_suites(TestSuites),
				{SuitesNotFound,SuitesNotFound};
			    _ ->
				{SuiteErrs,HelpErrs} = auto_compile(TestSuites),
				{SuiteErrs,SuiteErrs++HelpErrs}
			end,

		    case continue(AllMakeErrors) of
			true ->
			    SavedErrors = save_make_errors(SuiteMakeErrors),
			    ct_repeat:log_loop_info(Args),
			    {Tests1,Skip1} = final_tests(Tests,[],Skip,SavedErrors),
			    R = do_run_test(Tests1, Skip1, Opt1),
			    ct_util:stop(normal),
			    R;
			false ->
			    io:nl(),
			    ct_util:stop(clean),
			    BadMods =
				lists:foldr(
				  fun({{_,_},Ms}, Acc) ->
					  Ms ++ lists:foldl(
						  fun(M, Acc1) ->
							  lists:delete(M, Acc1)
						  end, Acc, Ms)
				  end, [], AllMakeErrors),
			    {error,{make_failed,BadMods}}
		    end
	    end
    end.

%% attempt to compile the modules specified in TestSuites
auto_compile(TestSuites) ->
    io:format("~nCommon Test: Running make in test directories...~n"),
    UserInclude =
	case application:get_env(common_test, include) of
	    {ok,UserInclDirs} when length(UserInclDirs) > 0 ->
		io:format("Including the following directories:~n"),
		[begin io:format("~p~n",[UserInclDir]), {i,UserInclDir} end || 
		 UserInclDir <- UserInclDirs];
	    _ ->
		[]
	end,
    SuiteMakeErrors =
	lists:flatmap(fun({TestDir,Suite} = TS) ->
			      case run_make(suites, TestDir, Suite, UserInclude) of
				  {error,{make_failed,Bad}} -> 
				      [{TS,Bad}];
				  {error,_} -> 
				      [{TS,[filename:join(TestDir,"*_SUITE")]}];
				  _ -> 
				      []
			      end
		      end, TestSuites),

    %% try to compile other modules than SUITEs in the test directories
    {_,HelpMakeErrors} =
	lists:foldl(
	  fun({Dir,Suite}, {Done,Failed}) ->
		  case lists:member(Dir, Done) of
		      false ->
			  Failed1 =
			      case run_make(helpmods, Dir, Suite, UserInclude) of
				  {error,{make_failed,BadMods}} ->
				      [{{Dir,all},BadMods}|Failed];
				  {error,_} ->
				      [{{Dir,all},[Dir]}|Failed];
				  _ ->
				      Failed
			      end,
			  {[Dir|Done],Failed1};
		      true ->		    % already visited
			  {Done,Failed}
		  end
	  end, {[],[]}, TestSuites),    
    {SuiteMakeErrors,lists:reverse(HelpMakeErrors)}.

%% verify that specified test suites exist (if auto compile is disabled)
verify_suites(TestSuites) ->
    io:nl(),
    Verify = 
	fun({Dir,Suite},NotFound) ->
		case locate_test_dir(Dir, Suite) of
		    {ok,TestDir} ->
			if Suite == all ->
				NotFound;
			   true ->
				Beam = filename:join(TestDir, atom_to_list(Suite)++".beam"),
				case filelib:is_regular(Beam) of
				    true  -> 
					NotFound;
				    false -> 
					Name = filename:join(TestDir, atom_to_list(Suite)),
					io:format("Suite ~w not found in directory ~s~n", 
						  [Suite,TestDir]),
					[{{Dir,Suite},[Name]} | NotFound]
				end
			end;
		    {error,_Reason} ->
			io:format("Directory ~s is invalid~n", [Dir]),
			Name = filename:join(Dir, atom_to_list(Suite)),
			[{{Dir,Suite},[Name]} | NotFound]
		end
	end,
    lists:reverse(lists:foldl(Verify, [], TestSuites)).
		     
    
save_make_errors([]) ->
    [];
save_make_errors(Errors) ->
    Suites = get_bad_suites(Errors,[]),
    ct_logs:log("MAKE RESULTS",
		"Error compiling or locating the following suites: ~n~p",[Suites]),
    %% save the info for logger
    file:write_file(?missing_suites_info,term_to_binary(Errors)),
    Errors.

get_bad_suites([{{_TestDir,_Suite},Failed}|Errors], BadSuites) ->
    get_bad_suites(Errors,BadSuites++Failed);
get_bad_suites([], BadSuites) ->
    BadSuites.

    

%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:step/3
step(TestDir, Suite, Case) ->
    step(TestDir, Suite, Case, []).

%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:step/4
step(TestDir, Suite, Case, Opts) when is_list(TestDir), is_atom(Suite), is_atom(Case), 
				   Suite =/= all, Case =/= all ->
    do_run([{TestDir,Suite,Case}], [{step,Opts}]).


%%%-----------------------------------------------------------------
%%% Internal
suite_tuples([{TestDir,Suites,_} | Tests]) when is_list(Suites) ->
    lists:map(fun(S) -> {TestDir,S} end, Suites) ++ suite_tuples(Tests);
suite_tuples([{TestDir,Suite,_} | Tests]) when is_atom(Suite) ->
    [{TestDir,Suite} | suite_tuples(Tests)];
suite_tuples([]) ->
    [].

final_tests([{TestDir,Suites,_}|Tests],
	    Final, Skip, Bad) when is_list(Suites), is_atom(hd(Suites)) ->
%     Separate =
% 	fun(S,{DoSuite,Dont}) ->		
% 		case lists:keymember({TestDir,S},1,Bad) of
% 		    false ->	
% 			{[S|DoSuite],Dont};
% 		    true ->	
% 			SkipIt = {TestDir,S,"Make failed"},
% 			{DoSuite,Dont++[SkipIt]}
% 		end
% 	end,
	
%     {DoSuites,Skip1} =
% 	lists:foldl(Separate,{[],Skip},Suites),
%     Do = {TestDir,lists:reverse(DoSuites),all},

    Skip1 = [{TD,S,"Make failed"} || {{TD,S},_} <- Bad, S1 <- Suites,
				     S == S1, TD == TestDir],
    Final1 = [{TestDir,S,all} || S <- Suites], 
    final_tests(Tests, lists:reverse(Final1)++Final, Skip++Skip1, Bad);

final_tests([{TestDir,all,all}|Tests], Final, Skip, Bad) ->
    MissingSuites =
	case lists:keysearch({TestDir,all}, 1, Bad) of
	    {value,{_,Failed}} ->
		[list_to_atom(filename:basename(F)) || F <- Failed];
	    false ->
		[]
	end,
    Missing = [{TestDir,S,"Make failed"} || S <- MissingSuites],
    Final1 = [{TestDir,all,all}|Final],
    final_tests(Tests, Final1, Skip++Missing, Bad);

final_tests([{TestDir,Suite,Cases}|Tests],
	    Final, Skip, Bad) when Cases==[]; Cases==all  ->
    final_tests([{TestDir,[Suite],all}|Tests], Final, Skip, Bad);

final_tests([{TestDir,Suite,Cases}|Tests], Final, Skip, Bad) ->
    case lists:keymember({TestDir,Suite}, 1, Bad) of
	false ->
	    Do = {TestDir,Suite,Cases},
	    final_tests(Tests, [Do|Final], Skip, Bad);
	true ->
	    Do = {TestDir,Suite,Cases},
	    Skip1 = Skip ++ [{TestDir,Suite,Cases,"Make failed"}],
	    final_tests(Tests, [Do|Final], Skip1, Bad)
    end;

final_tests([], Final, Skip, _Bad) ->
    {lists:reverse(Final),Skip}.

continue([]) ->    
    true;
continue(_MakeErrors) ->
    io:nl(),
    OldGl = group_leader(),
    case set_group_leader_same_as_shell() of
	true ->
	    S = self(),
	    io:format("Failed to compile or locate one or more test suites\n"
		      "Press \'c\' to continue or \'a\' to abort.\n"
		      "Will continue in 15 seconds if no answer is given!\n"),
	    Pid = spawn(fun() ->
				case io:get_line('(c/a) ') of
				    "c\n" ->
					S ! true;
				    _ ->
					S ! false
				end
			end),
	    group_leader(OldGl, self()),
	    receive R when R==true; R==false ->
		    R
	    after 15000 ->
		    exit(Pid, kill),
		    io:format("... timeout - continuing!!\n"),
		    true
	    end;
	false ->				% no shell process to use
	    true
    end.

set_group_leader_same_as_shell() ->
    %%! Locate the shell process... UGLY!!!
    GS2or3 = fun(P) ->
		     case process_info(P,initial_call) of
			 {initial_call,{group,server,X}} when X == 2 ; X == 3 ->
			     true;
			 _ ->
			     false
		     end
	     end,	
    case [P || P <- processes(), GS2or3(P), 
	       true == lists:keymember(shell,1,element(2,process_info(P,dictionary)))] of
	[GL|_] ->
	    group_leader(GL, self());
	[] ->
	    false
    end.

check_and_add([{TestDir0,M,_} | Tests], Added) ->
    case locate_test_dir(TestDir0, M) of
	{ok,TestDir} ->
	    case lists:member(TestDir, Added) of
		true ->
		    check_and_add(Tests, Added);
		false ->
		    true = code:add_patha(TestDir),
		    check_and_add(Tests, [TestDir|Added])
	    end;
	{error,_} ->
	    {error,{invalid_directory,TestDir0}}
    end;
check_and_add([], _) ->
    ok.

do_run_test(Tests, Skip, Opt) ->
    case check_and_add(Tests, []) of
	ok ->
	    ct_util:set_testdata({stats,{0,0,{0,0}}}),
	    ct_util:set_testdata({cover,undefined}),
	    test_server_ctrl:start_link(local),
	    case lists:keysearch(cover_spec, 1, Opt) of
		{value,{_,CovData={CovFile,
				   CovNodes,
				   _CovImport,
				   CovExport,
				   #cover{app        = CovApp,
					  level      = CovLevel,
					  excl_mods  = CovExcl,
					  incl_mods  = CovIncl,
					  cross      = CovCross,
					  src        = _CovSrc}}}} ->
		    ct_logs:log("COVER INFO","Using cover specification file: ~s~n"
				"App: ~w~n"
				"Cross cover: ~w~n"
				"Including ~w modules~n"
				"Excluding ~w modules",
				[CovFile,CovApp,CovCross,length(CovIncl),length(CovExcl)]), 

		    %% cover export file will be used for export and import
		    %% between tests so make sure it doesn't exist initially
		    case filelib:is_file(CovExport) of
			true ->
			    DelResult = file:delete(CovExport),
			    ct_logs:log("COVER INFO",
					"Warning! Export file ~s already exists. "
					"Deleting with result: ~p",
					[CovExport,DelResult]);
			false ->
			    ok
		    end,

		    %% tell test_server which modules should be cover compiled
		    %% note that actual compilation is done when tests start
		    test_server_ctrl:cover(CovApp, CovFile, CovExcl, CovIncl,
					   CovCross, CovExport, CovLevel),
		    %% save cover data (used e.g. to add nodes dynamically)
		    ct_util:set_testdata({cover,CovData}),
		    %% start cover on specified nodes
		    if (CovNodes /= []) and (CovNodes /= undefined) ->
			    ct_logs:log("COVER INFO",
					"Nodes included in cover session: ~w",
					[CovNodes]),
			    cover:start(CovNodes);
		       true ->
			    ok
		    end,
		    true;
		_ ->
		    false
	    end,	 
	    %% let test_server expand the test tuples and count no of cases
	    {Suites,NoOfCases} = count_test_cases(Tests, Skip),
	    Suites1 = delete_dups(Suites),
	    NoOfTests = length(Tests),
	    NoOfSuites = length(Suites1),
	    ct_util:warn_duplicates(Suites1),	    
	    {ok,Cwd} = file:get_cwd(),
	    io:format("~nCWD set to: ~p~n", [Cwd]),
	    if NoOfCases == unknown ->
		    io:format("~nTEST INFO: ~w test(s), ~w suite(s)~n~n", 
			      [NoOfTests,NoOfSuites]),
		    ct_logs:log("TEST INFO","~w test(s), ~w suite(s)", 
				[NoOfTests,NoOfSuites]);
	       true ->
		    io:format("~nTEST INFO: ~w test(s), ~w case(s) in ~w suite(s)~n~n", 
			      [NoOfTests,NoOfCases,NoOfSuites]),
		    ct_logs:log("TEST INFO","~w test(s), ~w case(s) in ~w suite(s)", 
				[NoOfTests,NoOfCases,NoOfSuites])
	    end,
	    ct_event:notify(#event{name=start_info,
				   node=node(),
				   data={NoOfTests,NoOfSuites,NoOfCases}}),
	    CleanUp = add_jobs(Tests, Skip, Opt, []),
	    unlink(whereis(test_server_ctrl)),
	    catch test_server_ctrl:wait_finish(), 
	    %% check if last testcase has left a "dead" trace window 
	    %% behind, and if so, kill it
	    case ct_util:get_testdata(interpret) of
		{_What,kill,{TCPid,AttPid}} ->
		    ct_util:kill_attached(TCPid, AttPid);
		_ ->
		    ok
	    end,
	    lists:foreach(fun(Suite) -> 
				  maybe_cleanup_interpret(Suite, Opt) 
			  end, CleanUp);
	Error ->
	    Error
    end.

delete_dups([S | Suites]) ->
    Suites1 = lists:delete(S, Suites),
    [S | delete_dups(Suites1)];
delete_dups([]) ->
    [].

count_test_cases(Tests, Skip) ->
    SendResult = fun(Me, Result) -> Me ! {no_of_cases,Result} end,
    TSPid = test_server_ctrl:start_get_totals(SendResult),
    Ref = erlang:monitor(process, TSPid),
    add_jobs(Tests, Skip, [], []),
    {Suites,NoOfCases} = count_test_cases1(length(Tests), 0, [], Ref),
    erlang:demonitor(Ref),
    test_server_ctrl:stop_get_totals(),
    {Suites,NoOfCases}.

count_test_cases1(0, N, Suites, _) ->
    {lists:flatten(Suites), N};
count_test_cases1(Jobs, N, Suites, Ref) ->
    receive
	{no_of_cases,{Ss,N1}} -> 
	    count_test_cases1(Jobs-1, add_known(N,N1), [Ss|Suites], Ref);
	{'DOWN', Ref, _, _, _} -> 
	    {[],0}
    end.				      

add_known(unknown, _) ->
    unknown;
add_known(_, unknown) ->
    unknown;
add_known(N, N1) ->
    N+N1.

add_jobs([{TestDir,all,_}|Tests], Skip, Opt, CleanUp) ->
    Name = get_name(TestDir),
    case catch test_server_ctrl:add_dir_with_skip(Name, TestDir,
						  skiplist(TestDir,Skip)) of
	{'EXIT',_} -> 
	    CleanUp;
	_ ->
	    wait_for_idle(),
	    add_jobs(Tests, Skip, Opt, CleanUp)
    end;
add_jobs([{TestDir,[Suite],all}|Tests], Skip, Opt, CleanUp) when is_atom(Suite) ->
    add_jobs([{TestDir,Suite,all}|Tests], Skip, Opt, CleanUp);
add_jobs([{TestDir,Suites,all}|Tests], Skip, Opt, CleanUp) when is_list(Suites) ->
    Name = get_name(TestDir) ++ ".suites",
    case catch test_server_ctrl:add_module_with_skip(Name, Suites,
						     skiplist(TestDir,Skip)) of
	{'EXIT',_} -> 
	    CleanUp;
	_ ->
	    wait_for_idle(),
	    add_jobs(Tests, Skip, Opt, CleanUp)
    end;
add_jobs([{TestDir,Suite,all}|Tests], Skip, Opt, CleanUp) ->
    case maybe_interpret(Suite, all, Opt) of
	ok ->
	    Name =  get_name(TestDir) ++ "." ++ atom_to_list(Suite),
	    case catch test_server_ctrl:add_module_with_skip(Name, [Suite],
							     skiplist(TestDir,Skip)) of
		{'EXIT',_} -> 
		    CleanUp;
		_ ->
		    wait_for_idle(),
		    add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
	    end;
	Error ->
	    Error
    end;
add_jobs([{TestDir,Suite,[Case]}|Tests], Skip, Opt, CleanUp) when is_atom(Case) ->
    add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opt, CleanUp);
add_jobs([{TestDir,Suite,Cases}|Tests], Skip, Opt, CleanUp) when is_list(Cases) ->
    case maybe_interpret(Suite, Cases, Opt) of
	ok ->
	    Name =  get_name(TestDir) ++ "." ++	atom_to_list(Suite) ++ ".cases",
	    case catch test_server_ctrl:add_cases_with_skip(Name, Suite, Cases,
							    skiplist(TestDir,Skip)) of
		{'EXIT',_} -> 
		    CleanUp;
		_ ->
		    wait_for_idle(),
		    add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
	    end;
	Error ->
	    Error
    end;
add_jobs([{TestDir,Suite,Case}|Tests], Skip, Opt, CleanUp) when is_atom(Case) ->
    case maybe_interpret(Suite, Case, Opt) of
	ok ->
	    Name = get_name(TestDir) ++	"." ++ atom_to_list(Suite) ++ "." ++ 
		atom_to_list(Case),
	    case catch test_server_ctrl:add_case_with_skip(Name, Suite, Case,
							   skiplist(TestDir,Skip)) of
		{'EXIT',_} -> 
		    CleanUp;
		_ ->
		    wait_for_idle(),
		    add_jobs(Tests, Skip, Opt, [Suite|CleanUp])
	    end;
	Error ->
	    Error
    end;
add_jobs([], _, _, CleanUp) ->
    CleanUp.

wait_for_idle() ->
    ct_util:update_last_run_index(),
    Notify = fun(Me) -> Me ! idle end,
    case catch test_server_ctrl:idle_notify(Notify) of
	{'EXIT',_} ->
	    error;
	TSPid ->
	    %% so we don't hang forever if test_server dies
	    Ref = erlang:monitor(process, TSPid),
	    Result = receive
			 idle -> ok;
			 {'DOWN', Ref, _, _, _} -> error
		     end,
	    erlang:demonitor(Ref),
	    ct_util:update_last_run_index(),
	    Result
    end.

skiplist(Dir, [{Dir,all,Cmt}|Skip]) ->
    %% we need to turn 'all' into list of modules since
    %% test_server doesn't do skips on Dir level
    Ss = filelib:wildcard(filename:join(Dir, "*_SUITE.beam")),
    [{list_to_atom(filename:basename(S,".beam")),Cmt} || S <- Ss] ++ skiplist(Dir,Skip);
skiplist(Dir, [{Dir,S,Cmt}|Skip]) ->
    [{S,Cmt} | skiplist(Dir, Skip)];
skiplist(Dir, [{Dir,S,C,Cmt}|Skip]) ->
    [{S,C,Cmt} | skiplist(Dir, Skip)];
skiplist(Dir, [_|Skip]) ->
    skiplist(Dir, Skip);
skiplist(_Dir, []) ->
    [].

get_name(Dir) ->
    TestDir =
	case filename:basename(Dir) of
	    "test" ->
		filename:dirname(Dir);
	    _ ->
		Dir
	end,
    Base = filename:basename(TestDir),
    case filename:basename(filename:dirname(TestDir)) of
	"" -> 
	    Base;
	TopDir ->
	    TopDir ++ "." ++ Base
    end.


run_make(TestDir, Mod, UserInclude) ->
    run_make(suites, TestDir, Mod, UserInclude).

run_make(Targets, TestDir0, Mod, UserInclude) when is_list(Mod) ->
    run_make(Targets, TestDir0, list_to_atom(Mod), UserInclude);

run_make(Targets, TestDir0, Mod, UserInclude) ->
    case locate_test_dir(TestDir0, Mod) of
	{ok,TestDir} ->
	    %% send a start_make notification which may suspend
	    %% the process if some other node is compiling files
	    %% in the same directory
	    ct_event:sync_notify(#event{name=start_make,
					node=node(),
					data=TestDir}),
	    {ok,Cwd} = file:get_cwd(),
	    ok = file:set_cwd(TestDir),
	    TestServerInclude = get_dir(test_server, "include"),
	    CtInclude = get_dir(common_test, "include"),
	    XmerlInclude = get_dir(xmerl, "include"),
	    ErlFlags = UserInclude ++ [{i,TestServerInclude},
				       {i,CtInclude},
				       {i,XmerlInclude},
				       debug_info],
	    Result = 
		if Mod == all ; Targets == helpmods ->
			case (catch ct_make:all([noexec])) of
			    {'EXIT',_} = Failure -> 
				Failure;
			    MakeInfo ->
				FileTest = fun(F, suites) -> is_suite(F);
					      (F, helpmods) -> not is_suite(F);
					      (_, _) -> true end, 
				Files = lists:flatmap(fun({F,out_of_date}) ->
							      case FileTest(F, Targets) of
								  true -> [F];
								  false -> []
							      end;
							 (_) ->
							      []
						      end, MakeInfo),
				(catch ct_make:files(Files, [load|ErlFlags]))
			end;
		   true ->
			(catch ct_make:files([Mod], [load|ErlFlags]))
		end,
	    
	    ok = file:set_cwd(Cwd),
	    %% send finished_make notification
	    ct_event:notify(#event{name=finished_make,
				   node=node(),
				   data=TestDir}),
	    case Result of
		{up_to_date,_} ->
		    ok;
		{'EXIT',Reason} ->
		    io:format("{error,{make_crashed,~p}\n", [Reason]),
		    {error,{make_crashed,TestDir,Reason}};
		{error,ModInfo} ->
		    io:format("{error,make_failed}\n", []),
		    Bad = [filename:join(TestDir, M) || {M,R} <- ModInfo, 
							R == error],
		    {error,{make_failed,Bad}}
	    end;
	{error,_} ->
	    io:format("{error,{invalid_directory,~p}}\n", [TestDir0]),
	    {error,{invalid_directory,TestDir0}}
    end.

get_dir(App, Dir) ->
    filename:join(code:lib_dir(App), Dir).

maybe_interpret(Suite, Cases, [{step,StepOpts}]) ->
    %% if other suite has run before this one, check if last testcase 
    %% has left a "dead" trace window behind, and if so, kill it
    case ct_util:get_testdata(interpret) of
	{_What,kill,{TCPid,AttPid}} ->
	    ct_util:kill_attached(TCPid, AttPid);
	_ ->
	    ok
    end,
    maybe_interpret1(Suite, Cases, StepOpts);
maybe_interpret(_, _, _) ->
    ok.

maybe_interpret1(Suite, all, StepOpts) ->
    case i:ii(Suite) of
	{module,_} ->
	    i:iaa([break]),
	    case get_all_testcases(Suite) of
		{error,_} ->
		    {error,no_testcases_found};
		Cases ->
		    maybe_interpret2(Suite, Cases, StepOpts)
	    end;
	error ->
	    {error,could_not_interpret_module}
    end;
maybe_interpret1(Suite, Case, StepOpts) when is_atom(Case) ->
    maybe_interpret1(Suite, [Case], StepOpts);
maybe_interpret1(Suite, Cases, StepOpts) when is_list(Cases) ->
    case i:ii(Suite) of
	{module,_} ->
	    i:iaa([break]),
	    maybe_interpret2(Suite, Cases, StepOpts);
	error ->
	    {error,could_not_interpret_module}
    end.

maybe_interpret2(Suite, Cases, StepOpts) ->
    set_break_on_config(Suite, StepOpts),
    [i:ib(Suite, Case, 1) || Case <- Cases],
    test_server_ctrl:multiply_timetraps(infinity),
    WinOp = case lists:member(keep_inactive, ensure_atom(StepOpts)) of
		true -> no_kill;
		false -> kill
	    end,		    
    ct_util:set_testdata({interpret,{{Suite,Cases},WinOp,
				     {undefined,undefined}}}),
    ok.

set_break_on_config(Suite, StepOpts) ->
    case lists:member(config, ensure_atom(StepOpts)) of
	true ->
	    i:ib(Suite, init_per_suite, 1),
	    i:ib(Suite, init_per_testcase, 2),
	    i:ib(Suite, end_per_testcase, 2),
	    i:ib(Suite, end_per_suite, 1);
	false ->
	    ok
    end.

maybe_cleanup_interpret(Suite, [{step,_}]) ->
    i:iq(Suite);
maybe_cleanup_interpret(_, _) ->
    ok.

log_ts_names(Args) ->
    case lists:keysearch(spec, 1, Args) of
	{value,{_,Specs}} ->
	    List = lists:map(fun(Name) ->
				     Name ++ " "
			     end, Specs),
	    ct_logs:log("Test Specification file(s)", "~s",
			[lists:flatten(List)]);
	_ ->
	    ok
    end.
			  
merge_arguments(Args) ->
    merge_arguments(Args, []).

merge_arguments([LogDir={logdir,_}|Args], Merged) ->
    merge_arguments(Args, handle_arg(replace, LogDir, Merged));
merge_arguments([CoverFile={cover,_}|Args], Merged) ->
    merge_arguments(Args, handle_arg(replace, CoverFile, Merged));
merge_arguments([Arg={_,_}|Args], Merged) ->
    merge_arguments(Args, handle_arg(merge, Arg, Merged));
merge_arguments([], Merged) ->
    Merged.

handle_arg(replace, {Key,Elems}, [{Key,_}|Merged]) ->
    [{Key,Elems}|Merged];
handle_arg(merge, {Key,Elems}, [{Key,PrevElems}|Merged]) ->
    [{Key,PrevElems++Elems}|Merged];
handle_arg(Op, Arg, [Other|Merged]) ->
    [Other|handle_arg(Op, Arg, Merged)];
handle_arg(_,Arg,[]) ->
    [Arg].

locate_test_dir(Dir, Suite) ->
    TestDir = case ct_util:is_test_dir(Dir) of
		  true  -> Dir;
		  false -> ct_util:get_testdir(Dir, Suite)
	      end,
    case filelib:is_dir(TestDir) of
	true  -> {ok,TestDir};
	false -> {error,invalid}
    end.

is_suite(Mod) when is_atom(Mod) ->
    is_suite(atom_to_list(Mod));
is_suite(ModOrFile) when is_list(ModOrFile) ->
    case lists:reverse(filename:basename(ModOrFile, ".erl")) of
	[$E,$T,$I,$U,$S,$_|_] ->
	    true;
	_ ->
	    case lists:reverse(filename:basename(ModOrFile, ".beam")) of
		[$E,$T,$I,$U,$S,$_|_] ->
		    true;
		_ ->
		    false
	    end
    end.

get_all_testcases(Suite) ->
    %%! this needs to be updated to handle testcase groups later!!
    case catch Suite:all() of
	{'EXIT',Why} ->
	    {error,Why};
	{skip,_} ->
	    [];
	Cases ->
	    AllCases =
		lists:foldl(fun({sequence,SeqName}, All) ->
				    case catch Suite:sequences() of
					{'EXIT',_} ->
					    All;
					Seqs ->
					    case proplists:get_value(SeqName, Seqs) of
						undefined ->
						    All;
						SeqCases ->
						    lists:reverse(SeqCases) ++ All
					    end
				    end;
			       (Case,All) ->
				    [Case|All]
			    end, [], Cases),
	    lists:reverse(AllCases)
    end.
					

%% Internal tracing support. If {ct_trace,TraceSpec} is present, the
%% TraceSpec file will be consulted and dbg used to trace function
%% calls during test run. Expected terms in TraceSpec:
%% {m,Mod} or {f,Mod,Func}.
start_trace(Args) ->
    case lists:keysearch(ct_trace,1,Args) of
	{value,{ct_trace,File}} ->
	    TraceSpec = delistify(File),
	    case file:consult(TraceSpec) of
		{ok,Terms} ->
		    case catch do_trace(Terms) of
			ok -> 
			    true;
			{_,Error} ->
			    io:format("Warning! Tracing not started. Reason: ~p~n~n",
				      [Error]),
			    false
		    end;		           
		{_,Error} ->
		    io:format("Warning! Tracing not started. Reason: ~p~n~n",
			      [Error]),
		    false
	    end;	    			
	false ->
	    false		
    end.

do_trace(Terms) ->
    dbg:tracer(),
    dbg:p(self(), [sos,call]),
    lists:foreach(fun({m,M}) ->
			  case dbg:tpl(M,[{'_',[],[{return_trace}]}]) of
			      {error,What} -> exit({error,{tracing_failed,What}});
			      _ -> ok
			  end;			  
		     ({f,M,F}) ->
			  case dbg:tpl(M,F,[{'_',[],[{return_trace}]}]) of
			      {error,What} -> exit({error,{tracing_failed,What}});
			      _ -> ok
			  end;			  
		     (Huh) ->
			  exit({error,{unrecognized_trace_term,Huh}})
		  end, Terms),
    ok.
			 
stop_trace(true) ->
    dbg:stop_clear();
stop_trace(false) ->
    ok.

preload() ->
    io:format("~nLoading Common Test and Test Server modules...~n~n"),
    preload_mod([ct_logs,
		 ct_make,
		 ct_telnet,
		 ct,
		 ct_master,
		 ct_testspec,
		 ct_cover,
		 ct_master_event,
		 ct_util,
		 ct_event,           
		 ct_master_logs,
		 ct_framework,
		 teln,
		 ct_ftp,
		 ct_rpc,
		 unix_telnet,
		 ct_gen_conn,
		 ct_line,
		 ct_snmp,
		 test_server_sup,
		 test_server,
		 test_server_ctrl,
		 test_server_h,
		 test_server_line,
		 test_server_node]).

preload_mod([M|Ms]) ->
    case code:is_loaded(M) of
	false ->
	    {module,M} = code:load_file(M),
	    preload_mod(Ms);
	_ ->
	    ok
    end;
preload_mod([]) ->
    ok.
    
ensure_atom(Atom) when is_atom(Atom) ->
    Atom;
ensure_atom(String) when is_list(String), is_integer(hd(String)) ->
    list_to_atom(String);
ensure_atom(List) when is_list(List) ->
    [ensure_atom(Item) || Item <- List];
ensure_atom(Other) ->				
    Other.
		  
