%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2006-2010. 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 functions handlig test specifikations. %%% %%%
This module exports functions that are used within CT to %%% scan and parse test specifikations.
-module(ct_testspec). -export([prepare_tests/1, prepare_tests/2, collect_tests_from_list/2, collect_tests_from_list/3, collect_tests_from_file/2, collect_tests_from_file/3]). -include("ct_util.hrl"). %%%------------------------------------------------------------------ %%% NOTE: %%% Multiple testspecs may be used as input with the result that %%% the data is merged. It's in this case up to the user to ensure %%% there are no clashes in any "global" variables, such as logdir. %%%------------------------------------------------------------------- %%%------------------------------------------------------------------- %%% prepare_tests/2 compiles the testspec data into a list of tests %%% to be run and a list of tests to be skipped, either for one %%% particular node or for all nodes. %%%------------------------------------------------------------------- %%%------------------------------------------------------------------- %%% Version 1 - extract and return all tests and skips for Node %%% (incl all_nodes) %%%------------------------------------------------------------------- prepare_tests(TestSpec,Node) when is_record(TestSpec,testspec), is_atom(Node) -> case lists:keysearch(Node,1,prepare_tests(TestSpec)) of {value,{Node,Run,Skip}} -> {Run,Skip}; false -> {[],[]} end. %%%------------------------------------------------------------------- %%% Version 2 - create and return a list of {Node,Run,Skip} tuples, %%% one for each node specified in the test specification. %%% The tuples in the Run list will have the form {Dir,Suites,Cases} %%% and the tuples in the Skip list will have the form %%% {Dir,Suites,Comment} or {Dir,Suite,Cases,Comment}. %%%------------------------------------------------------------------- prepare_tests(TestSpec) when is_record(TestSpec,testspec) -> Tests = TestSpec#testspec.tests, %% Sort Tests into "flat" Run and Skip lists (not sorted per node). {Run,Skip} = get_run_and_skip(Tests,[],[]), %% Create initial list of {Node,{Run,Skip}} tuples NodeList = lists:map(fun(N) -> {N,{[],[]}} end, list_nodes(TestSpec)), %% Get all Run tests sorted per node basis. NodeList1 = run_per_node(Run,NodeList), %% Get all Skip entries sorted per node basis. NodeList2 = skip_per_node(Skip,NodeList1), %% Change representation. Result= lists:map(fun({Node,{Run1,Skip1}}) -> Run2 = lists:map(fun({D,{Ss,Cs}}) -> {D,Ss,Cs} end, Run1), Skip2 = lists:map(fun({D,{Ss,Cmt}}) -> {D,Ss,Cmt}; ({D,{S,Cs,Cmt}}) -> {D,S,Cs,Cmt} end, Skip1), {Node,Run2,Skip2} end, NodeList2), Result. %% run_per_node/2 takes the Run list as input and returns a list %% of {Node,RunPerNode,[]} tuples where the tests have been sorted %% on a per node basis. run_per_node([{{Node,Dir},Test}|Ts],Result) -> {value,{Node,{Run,Skip}}} = lists:keysearch(Node,1,Result), Run1 = merge_tests(Dir,Test,Run), run_per_node(Ts,insert_in_order({Node,{Run1,Skip}},Result)); run_per_node([],Result) -> Result. merge_tests(Dir,Test={all,_},TestDirs) -> %% overwrite all previous entries for Dir TestDirs1 = lists:filter(fun({D,_}) when D==Dir -> false; (_) -> true end,TestDirs), insert_in_order({Dir,Test},TestDirs1); merge_tests(Dir,Test={Suite,all},TestDirs) -> TestDirs1 = lists:filter(fun({D,{S,_}}) when D==Dir,S==Suite -> false; (_) -> true end,TestDirs), TestDirs1++[{Dir,Test}]; merge_tests(Dir,Test,TestDirs) -> merge_suites(Dir,Test,TestDirs). merge_suites(Dir,{Suite,Cases},[{Dir,{Suite,Cases0}}|Dirs]) -> Cases1 = insert_in_order(Cases,Cases0), [{Dir,{Suite,Cases1}}|Dirs]; merge_suites(Dir,Test,[Other|Dirs]) -> [Other|merge_suites(Dir,Test,Dirs)]; merge_suites(Dir,Test,[]) -> [{Dir,Test}]. %% skip_per_node/2 takes the Skip list as input and returns a list %% of {Node,RunPerNode,SkipPerNode} tuples where the skips have been %% sorted on a per node basis. skip_per_node([{{Node,Dir},Test}|Ts],Result) -> {value,{Node,{Run,Skip}}} = lists:keysearch(Node,1,Result), Skip1 = [{Dir,Test}|Skip], skip_per_node(Ts,insert_in_order({Node,{Run,Skip1}},Result)); skip_per_node([],Result) -> Result. %% get_run_and_skip/3 takes a list of test terms as input and sorts %% them into a list of Run tests and a list of Skip entries. The %% elements all have the form %% %% {{Node,Dir},TestData} %% %% TestData has the form: %% %% Run entry: {Suite,Cases} %% %% Skip entry: {Suites,Comment} or {Suite,Cases,Comment} %% get_run_and_skip([{{Node,Dir},Suites}|Tests],Run,Skip) -> TestDir = ct_util:get_testdir(Dir,catch element(1,hd(Suites))), case lists:keysearch(all,1,Suites) of {value,_} -> % all Suites in Dir Skipped = get_skipped_suites(Node,TestDir,Suites), %% note: this adds an 'all' test even if only skip is specified, %% probably a good thing cause it gets logged as skipped then get_run_and_skip(Tests, [[{{Node,TestDir},{all,all}}]|Run], [Skipped|Skip]); false -> {R,S} = prepare_suites(Node,TestDir,Suites,[],[]), get_run_and_skip(Tests,[R|Run],[S|Skip]) end; get_run_and_skip([],Run,Skip) -> {lists:flatten(lists:reverse(Run)), lists:flatten(lists:reverse(Skip))}. prepare_suites(Node,Dir,[{Suite,Cases}|Suites],Run,Skip) -> case lists:member(all,Cases) of true -> % all Cases in Suite Skipped = get_skipped_cases(Node,Dir,Suite,Cases), %% note: this adds an 'all' test even if only skip is specified prepare_suites(Node,Dir,Suites, [[{{Node,Dir},{Suite,all}}]|Run], [Skipped|Skip]); false -> {RL,SL} = prepare_cases(Node,Dir,Suite,Cases), prepare_suites(Node,Dir,Suites,[RL|Run],[SL|Skip]) end; prepare_suites(_Node,_Dir,[],Run,Skip) -> {lists:flatten(lists:reverse(Run)), lists:flatten(lists:reverse(Skip))}. prepare_cases(Node,Dir,Suite,Cases) -> case get_skipped_cases(Node,Dir,Suite,Cases) of SkipAll=[{{Node,Dir},{Suite,_Cmt}}] -> % all cases to be skipped %% note: this adds an 'all' test even if only skip is specified {[{{Node,Dir},{Suite,all}}],SkipAll}; Skipped -> %% note: this adds a test even if only skip is specified PrepC = lists:foldr(fun({C,{skip,_Cmt}},Acc) -> case lists:member(C,Cases) of true -> Acc; false -> [C|Acc] end; (C,Acc) -> [C|Acc] end, [], Cases), {{{Node,Dir},{Suite,PrepC}},Skipped} end. get_skipped_suites(Node,Dir,Suites) -> lists:flatten(get_skipped_suites1(Node,Dir,Suites)). get_skipped_suites1(Node,Dir,[{Suite,Cases}|Suites]) -> SkippedCases = get_skipped_cases(Node,Dir,Suite,Cases), [SkippedCases|get_skipped_suites1(Node,Dir,Suites)]; get_skipped_suites1(_,_,[]) -> []. get_skipped_cases(Node,Dir,Suite,Cases) -> case lists:keysearch(all,1,Cases) of {value,{all,{skip,Cmt}}} -> [{{Node,Dir},{Suite,Cmt}}]; false -> get_skipped_cases1(Node,Dir,Suite,Cases) end. get_skipped_cases1(Node,Dir,Suite,[{Case,{skip,Cmt}}|Cs]) -> [{{Node,Dir},{Suite,Case,Cmt}}|get_skipped_cases1(Node,Dir,Suite,Cs)]; get_skipped_cases1(Node,Dir,Suite,[_Case|Cs]) -> get_skipped_cases1(Node,Dir,Suite,Cs); get_skipped_cases1(_,_,_,[]) -> []. %%% collect_tests_from_file reads a testspec file and returns a record %%% containing the data found. collect_tests_from_file(Specs, Relaxed) -> collect_tests_from_file(Specs,[node()],Relaxed). collect_tests_from_file(Specs,Nodes,Relaxed) when is_list(Nodes) -> NodeRefs = lists:map(fun(N) -> {undefined,N} end, Nodes), catch collect_tests_from_file1(Specs,#testspec{nodes=NodeRefs},Relaxed). collect_tests_from_file1([Spec|Specs],TestSpec,Relaxed) -> SpecDir = filename:dirname(filename:absname(Spec)), case file:consult(Spec) of {ok,Terms} -> TestSpec1 = collect_tests(Terms,TestSpec#testspec{spec_dir=SpecDir}, Relaxed), collect_tests_from_file1(Specs,TestSpec1,Relaxed); {error,Reason} -> throw({error,{Spec,Reason}}) end; collect_tests_from_file1([],TS=#testspec{config=Cfgs,event_handler=EvHs, include=Incl,tests=Tests},_) -> TS#testspec{config=lists:reverse(Cfgs), event_handler=lists:reverse(EvHs), include=lists:reverse(Incl), tests=lists:flatten(Tests)}. collect_tests_from_list(Terms,Relaxed) -> collect_tests_from_list(Terms,[node()],Relaxed). collect_tests_from_list(Terms,Nodes,Relaxed) when is_list(Nodes) -> {ok,Cwd} = file:get_cwd(), NodeRefs = lists:map(fun(N) -> {undefined,N} end, Nodes), case catch collect_tests(Terms,#testspec{nodes=NodeRefs, spec_dir=Cwd}, Relaxed) of E = {error,_} -> E; TS -> #testspec{config=Cfgs,event_handler=EvHs,include=Incl,tests=Tests} = TS, TS#testspec{config=lists:reverse(Cfgs), event_handler=lists:reverse(EvHs), include=lists:reverse(Incl), tests=lists:flatten(Tests)} end. collect_tests(Terms,TestSpec,Relaxed) -> put(relaxed,Relaxed), TestSpec1 = get_global(Terms,TestSpec), TestSpec2 = get_all_nodes(Terms,TestSpec1), % filter out node_start options and save them into the specification {Terms2, TestSpec3} = filter_nodestart_specs(Terms, [], TestSpec2), % only save the 'global' evals and evals for nodes which have no node_start {Terms3, TestSpec4} = filter_evals(Terms2, [], TestSpec3), % after evaluation, only valid terms exist in the specification list Terms4 = case catch evaluate(Terms3, [], TestSpec4) of {error,{Node,{M,F,A},Reason}} -> io:format("Error! Common Test failed to evaluate ~w:~w/~w on ~w. " "Reason: ~p~n~n", [M,F,A,Node,Reason]), Terms3; NewTerms -> NewTerms end, add_tests(Terms4,TestSpec4). evaluate([{eval,Node,[{_,_,_}|_]=Mfas}|Ts],NewTerms,Spec)-> EvalTerms = lists:map(fun(Mfa)-> {eval, Node, Mfa} end, Mfas), evaluate([EvalTerms|Ts], NewTerms, Spec); evaluate([{eval,[{_,_,_}|_]=Mfas}|Ts],NewTerms,Spec)-> EvalTerms = lists:map(fun(Mfa)-> {eval, Mfa} end, Mfas), evaluate([EvalTerms|Ts], NewTerms, Spec); evaluate([{eval,Node,{M,F,Args}}|Ts],NewTerms,Spec) -> case rpc:call(Node,M,F,Args) of {badrpc,Reason} -> throw({error,{Node,{M,F,length(Args)},Reason}}); _ -> ok end, evaluate(Ts,NewTerms,Spec); evaluate([{eval,{M,F,Args}}|Ts],NewTerms,Spec) -> case catch apply(M,F,Args) of {'EXIT',Reason} -> throw({error,{node(),{M,F,length(Args)},Reason}}); _ -> ok end, evaluate(Ts,NewTerms,Spec); evaluate([Term|Ts], NewTerms, Spec)-> evaluate(Ts, [Term|NewTerms], Spec); evaluate([], NewTerms, _Spec) -> NewTerms. get_global([{alias,Ref,Dir}|Ts],Spec=#testspec{alias=Refs}) -> get_global(Ts,Spec#testspec{alias=[{Ref,get_absdir(Dir,Spec)}|Refs]}); get_global([{node,Ref,Node}|Ts],Spec=#testspec{nodes=Refs}) -> get_global(Ts,Spec#testspec{nodes=[{Ref,Node}|lists:keydelete(Node,2,Refs)]}); get_global([_|Ts],Spec) -> get_global(Ts,Spec); get_global([],Spec) -> Spec. get_absfile(Callback, FullName,#testspec{spec_dir=SpecDir}) -> % we need to temporary switch to new cwd here, because % otherwise config files cannot be found {ok, OldWd} = file:get_cwd(), ok = file:set_cwd(SpecDir), R = Callback:check_parameter(FullName), ok = file:set_cwd(OldWd), case R of {ok, {file, FullName}}-> File = filename:basename(FullName), Dir = get_absname(filename:dirname(FullName),SpecDir), filename:join(Dir,File); {ok, {config, FullName}}-> FullName; {error, {nofile, FullName}}-> FullName; {error, {wrong_config, FullName}}-> FullName end. get_absfile(FullName,#testspec{spec_dir=SpecDir}) -> File = filename:basename(FullName), Dir = get_absname(filename:dirname(FullName),SpecDir), filename:join(Dir,File). get_absdir(Dir,#testspec{spec_dir=SpecDir}) -> get_absname(Dir,SpecDir). get_absname(TestDir,SpecDir) -> AbsName = filename:absname(TestDir,SpecDir), TestDirName = filename:basename(AbsName), Path = filename:dirname(AbsName), TopDir = filename:basename(Path), Path1 = case TopDir of "." -> [_|Rev] = lists:reverse(filename:split(Path)), filename:join(lists:reverse(Rev)); ".." -> [_,_|Rev] = lists:reverse(filename:split(Path)), filename:join(lists:reverse(Rev)); _ -> Path end, filename:join(Path1,TestDirName). %% go through all tests and register all nodes found get_all_nodes([{suites,Nodes,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{suites,Node,_,_}|Ts],Spec) -> get_all_nodes(Ts,save_nodes([Node],Spec)); get_all_nodes([{cases,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{cases,Node,_,_,_}|Ts],Spec) -> get_all_nodes(Ts,save_nodes([Node],Spec)); get_all_nodes([{skip_suites,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{skip_suites,Node,_,_,_}|Ts],Spec) -> get_all_nodes(Ts,save_nodes([Node],Spec)); get_all_nodes([{skip_cases,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{skip_cases,Node,_,_,_,_}|Ts],Spec) -> get_all_nodes(Ts,save_nodes([Node],Spec)); get_all_nodes([_|Ts],Spec) -> get_all_nodes(Ts,Spec); get_all_nodes([],Spec) -> Spec. filter_nodestart_specs([{node_start, Options}|Ts], NewTerms, Spec) -> filter_nodestart_specs([{node_start, list_nodes(Spec), Options}|Ts], NewTerms, Spec); filter_nodestart_specs([{node_start, NodeRef, Options}|Ts], NewTerms, Spec) when is_atom(NodeRef) -> filter_nodestart_specs([{node_start, [NodeRef], Options}|Ts], NewTerms, Spec); filter_nodestart_specs([{node_start, NodeRefs, Options}|Ts], NewTerms, Spec=#testspec{node_start=NodeStart})-> Options2 = case lists:keyfind(callback_module, 1, Options) of false-> [{callback_module, ct_slave}|Options]; {callback_module, _Callback}-> Options end, NSSAdder = fun(NodeRef, NodeStartAcc)-> Node=ref2node(NodeRef,Spec#testspec.nodes), case lists:keyfind(Node, 1, NodeStartAcc) of false-> [{Node, Options2}|NodeStartAcc]; {Node, OtherOptions}-> io:format("~nWarning: There are other options defined for node ~p:" "~n~w, skipping ~n~w...~n", [Node, OtherOptions, Options2]), NodeStartAcc end end, NodeStart2 = lists:foldl(NSSAdder, NodeStart, NodeRefs), filter_nodestart_specs(Ts, NewTerms, Spec#testspec{node_start=NodeStart2}); filter_nodestart_specs([Term|Ts], NewTerms, Spec)-> filter_nodestart_specs(Ts, [Term|NewTerms], Spec); filter_nodestart_specs([], NewTerms, Spec) -> {NewTerms, Spec}. filter_evals([{eval,NodeRefs,Mfa}|Ts], NewTerms, Spec) when is_list(NodeRefs)-> EvalTerms = lists:map(fun(NodeRef)-> {eval, NodeRef, Mfa} end, NodeRefs), filter_evals(EvalTerms++Ts, NewTerms, Spec); filter_evals([{eval,NodeRef,{_,_,_}=Mfa}|Ts],NewTerms,Spec)-> filter_evals([{eval,NodeRef,[Mfa]}|Ts],NewTerms,Spec); filter_evals([{eval,NodeRef,[{_,_,_}|_]=Mfas}=EvalTerm|Ts], NewTerms,Spec=#testspec{node_start=NodeStart})-> Node=ref2node(NodeRef,Spec#testspec.nodes), case lists:keyfind(Node, 1, NodeStart) of false-> filter_evals(Ts, [EvalTerm|NewTerms], Spec); {Node, Options}-> Options2 = case lists:keyfind(startup_functions, 1, Options) of false-> [{startup_functions, Mfas}|Options]; {startup_functions, StartupFunctions}-> lists:keyreplace(startup_functions, 1, Options, {startup_functions, StartupFunctions ++ Mfas}) end, NodeStart2 = lists:keyreplace(Node, 1, NodeStart, {Node, Options2}), filter_evals(Ts, NewTerms, Spec#testspec{node_start=NodeStart2}) end; filter_evals([Term|Ts], NewTerms, Spec)-> filter_evals(Ts, [Term|NewTerms], Spec); filter_evals([], NewTerms, Spec)-> {NewTerms, Spec}. save_nodes(Nodes,Spec=#testspec{nodes=NodeRefs}) -> NodeRefs1 = lists:foldr(fun(all_nodes,NR) -> NR; (Node,NR) -> case lists:keymember(Node,1,NR) of true -> NR; false -> case lists:keymember(Node,2,NR) of true -> NR; false -> [{undefined,Node}|NR] end end end,NodeRefs,Nodes), Spec#testspec{nodes=NodeRefs1}. list_nodes(#testspec{nodes=NodeRefs}) -> lists:map(fun({_Ref,Node}) -> Node end, NodeRefs). %% Associate a "global" logdir with all nodes %% except those with specific logdir, e.g: %% ["/tmp/logdir",{ct1@finwe,"/tmp/logdir2"}] %% means all nodes should write to /tmp/logdir %% except ct1@finwe that should use /tmp/logdir2. %% --- logdir --- add_tests([{logdir,all_nodes,Dir}|Ts],Spec) -> Dirs = Spec#testspec.logdir, Tests = [{logdir,N,get_absdir(Dir,Spec)} || N <- list_nodes(Spec), lists:keymember(ref2node(N,Spec#testspec.nodes), 1,Dirs) == false], add_tests(Tests++Ts,Spec); add_tests([{logdir,Nodes,Dir}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,logdir,[Dir],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{logdir,Node,Dir}|Ts],Spec) -> Dirs = Spec#testspec.logdir, Dirs1 = [{ref2node(Node,Spec#testspec.nodes),get_absdir(Dir,Spec)} | lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,Dirs)], add_tests(Ts,Spec#testspec{logdir=Dirs1}); add_tests([{logdir,Dir}|Ts],Spec) -> add_tests([{logdir,all_nodes,Dir}|Ts],Spec); %% --- cover --- add_tests([{cover,all_nodes,File}|Ts],Spec) -> Tests = lists:map(fun(N) -> {cover,N,File} end, list_nodes(Spec)), add_tests(Tests++Ts,Spec); add_tests([{cover,Nodes,File}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,cover,[File],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{cover,Node,File}|Ts],Spec) -> CoverFs = Spec#testspec.cover, CoverFs1 = [{ref2node(Node,Spec#testspec.nodes),get_absfile(File,Spec)} | lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,CoverFs)], add_tests(Ts,Spec#testspec{cover=CoverFs1}); add_tests([{cover,File}|Ts],Spec) -> add_tests([{cover,all_nodes,File}|Ts],Spec); %% --- config --- add_tests([{config,all_nodes,Files}|Ts],Spec) -> Tests = lists:map(fun(N) -> {config,N,Files} end, list_nodes(Spec)), add_tests(Tests++Ts,Spec); add_tests([{config,Nodes,Files}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,config,[Files],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{config,Node,[F|Fs]}|Ts],Spec) when is_list(F) -> Cfgs = Spec#testspec.config, Node1 = ref2node(Node,Spec#testspec.nodes), add_tests([{config,Node,Fs}|Ts], Spec#testspec{config=[{Node1,get_absfile(F,Spec)}|Cfgs]}); add_tests([{config,_Node,[]}|Ts],Spec) -> add_tests(Ts,Spec); add_tests([{config,Node,F}|Ts],Spec) -> add_tests([{config,Node,[F]}|Ts],Spec); add_tests([{config,Files}|Ts],Spec) -> add_tests([{config,all_nodes,Files}|Ts],Spec); %% --- userconfig --- add_tests([{userconfig,all_nodes,CBF}|Ts],Spec) -> Tests = lists:map(fun(N) -> {userconfig,N,CBF} end, list_nodes(Spec)), add_tests(Tests++Ts,Spec); add_tests([{userconfig,Nodes,CBF}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,userconfig,[CBF],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{userconfig,Node,[{Callback, Config}|CBF]}|Ts],Spec) -> Cfgs = Spec#testspec.userconfig, Node1 = ref2node(Node,Spec#testspec.nodes), add_tests([{userconfig,Node,CBF}|Ts], Spec#testspec{userconfig=[{Node1,{Callback, get_absfile(Callback, Config ,Spec)}}|Cfgs]}); add_tests([{userconfig,_Node,[]}|Ts],Spec) -> add_tests(Ts,Spec); add_tests([{userconfig,Node,CBF}|Ts],Spec) -> add_tests([{userconfig,Node,[CBF]}|Ts],Spec); add_tests([{userconfig,CBF}|Ts],Spec) -> add_tests([{userconfig,all_nodes,CBF}|Ts],Spec); %% --- event_handler --- add_tests([{event_handler,all_nodes,Hs}|Ts],Spec) -> Tests = lists:map(fun(N) -> {event_handler,N,Hs,[]} end, list_nodes(Spec)), add_tests(Tests++Ts,Spec); add_tests([{event_handler,all_nodes,Hs,Args}|Ts],Spec) when is_list(Args) -> Tests = lists:map(fun(N) -> {event_handler,N,Hs,Args} end, list_nodes(Spec)), add_tests(Tests++Ts,Spec); add_tests([{event_handler,Hs}|Ts],Spec) -> add_tests([{event_handler,all_nodes,Hs,[]}|Ts],Spec); add_tests([{event_handler,HsOrNodes,HsOrArgs}|Ts],Spec) -> case is_noderef(HsOrNodes,Spec#testspec.nodes) of true -> % HsOrNodes == Nodes, HsOrArgs == Hs case {HsOrNodes,HsOrArgs} of {Nodes,Hs} when is_list(Nodes) -> Ts1 = separate(Nodes,event_handler,[Hs,[]],Ts, Spec#testspec.nodes), add_tests(Ts1,Spec); {_Node,[]} -> add_tests(Ts,Spec); {Node,HOrHs} -> EvHs = Spec#testspec.event_handler, Node1 = ref2node(Node,Spec#testspec.nodes), case HOrHs of [H|Hs] when is_atom(H) -> add_tests([{event_handler,Node,Hs}|Ts], Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]}); H when is_atom(H) -> add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]}) end end; false -> % HsOrNodes == Hs, HsOrArgs == Args add_tests([{event_handler,all_nodes,HsOrNodes,HsOrArgs}|Ts],Spec) end; add_tests([{event_handler,Nodes,Hs,Args}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,event_handler,[Hs,Args],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{event_handler,Node,[H|Hs],Args}|Ts],Spec) when is_atom(H) -> EvHs = Spec#testspec.event_handler, Node1 = ref2node(Node,Spec#testspec.nodes), add_tests([{event_handler,Node,Hs,Args}|Ts], Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]}); add_tests([{event_handler,_Node,[],_Args}|Ts],Spec) -> add_tests(Ts,Spec); add_tests([{event_handler,Node,H,Args}|Ts],Spec) when is_atom(H) -> EvHs = Spec#testspec.event_handler, Node1 = ref2node(Node,Spec#testspec.nodes), add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]}); %% --- include --- add_tests([{include,all_nodes,InclDirs}|Ts],Spec) -> Tests = lists:map(fun(N) -> {include,N,InclDirs} end, list_nodes(Spec)), add_tests(Tests++Ts,Spec); add_tests([{include,Nodes,InclDirs}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,include,[InclDirs],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{include,Node,[D|Ds]}|Ts],Spec) when is_list(D) -> Dirs = Spec#testspec.include, Node1 = ref2node(Node,Spec#testspec.nodes), add_tests([{include,Node,Ds}|Ts], Spec#testspec{include=[{Node1,get_absdir(D,Spec)}|Dirs]}); add_tests([{include,_Node,[]}|Ts],Spec) -> add_tests(Ts,Spec); add_tests([{include,Node,D}|Ts],Spec) -> add_tests([{include,Node,[D]}|Ts],Spec); add_tests([{include,InclDirs}|Ts],Spec) -> add_tests([{include,all_nodes,InclDirs}|Ts],Spec); %% --- suites --- add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec) -> add_tests([{suites,list_nodes(Spec),Dir,Ss}|Ts],Spec); add_tests([{suites,Dir,Ss}|Ts],Spec) -> add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec); add_tests([{suites,Nodes,Dir,Ss}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,suites,[Dir,Ss],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{suites,Node,Dir,Ss}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = insert_suites(ref2node(Node,Spec#testspec.nodes), ref2dir(Dir,Spec#testspec.alias), Ss,Tests), add_tests(Ts,Spec#testspec{tests=Tests1}); %% --- cases --- add_tests([{cases,all_nodes,Dir,Suite,Cs}|Ts],Spec) -> add_tests([{cases,list_nodes(Spec),Dir,Suite,Cs}|Ts],Spec); add_tests([{cases,Dir,Suite,Cs}|Ts],Spec) -> add_tests([{cases,all_nodes,Dir,Suite,Cs}|Ts],Spec); add_tests([{cases,Nodes,Dir,Suite,Cs}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,cases,[Dir,Suite,Cs],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{cases,Node,Dir,Suite,Cs}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = insert_cases(ref2node(Node,Spec#testspec.nodes), ref2dir(Dir,Spec#testspec.alias), Suite,Cs,Tests), add_tests(Ts,Spec#testspec{tests=Tests1}); %% --- skip_suites --- add_tests([{skip_suites,all_nodes,Dir,Ss,Cmt}|Ts],Spec) -> add_tests([{skip_suites,list_nodes(Spec),Dir,Ss,Cmt}|Ts],Spec); add_tests([{skip_suites,Dir,Ss,Cmt}|Ts],Spec) -> add_tests([{skip_suites,all_nodes,Dir,Ss,Cmt}|Ts],Spec); add_tests([{skip_suites,Nodes,Dir,Ss,Cmt}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,skip_suites,[Dir,Ss,Cmt],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = skip_suites(ref2node(Node,Spec#testspec.nodes), ref2dir(Dir,Spec#testspec.alias), Ss,Cmt,Tests), add_tests(Ts,Spec#testspec{tests=Tests1}); %% --- skip_cases --- add_tests([{skip_cases,all_nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) -> add_tests([{skip_cases,list_nodes(Spec),Dir,Suite,Cs,Cmt}|Ts],Spec); add_tests([{skip_cases,Dir,Suite,Cs,Cmt}|Ts],Spec) -> add_tests([{skip_cases,all_nodes,Dir,Suite,Cs,Cmt}|Ts],Spec); add_tests([{skip_cases,Nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,skip_cases,[Dir,Suite,Cs,Cmt],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{skip_cases,Node,Dir,Suite,Cs,Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = skip_cases(ref2node(Node,Spec#testspec.nodes), ref2dir(Dir,Spec#testspec.alias), Suite,Cs,Cmt,Tests), add_tests(Ts,Spec#testspec{tests=Tests1}); %% --- handled/errors --- add_tests([{alias,_,_}|Ts],Spec) -> % handled add_tests(Ts,Spec); add_tests([{node,_,_}|Ts],Spec) -> % handled add_tests(Ts,Spec); %% check if it's a CT term that has bad format or if the user seems to %% have added something of his/her own, which we'll let pass if relaxed %% mode is enabled. add_tests([Other|Ts],Spec) when is_tuple(Other) -> [Name|_] = tuple_to_list(Other), case lists:keymember(Name,1,valid_terms()) of true -> % halt throw({error,{bad_term_in_spec,Other}}); false -> % ignore case get(relaxed) of true -> %% warn if name resembles a CT term case resembles_ct_term(Name,size(Other)) of true -> io:format("~nSuspicious term, please check:~n" "~p~n", [Other]); false -> ok end, add_tests(Ts,Spec); false -> throw({error,{undefined_term_in_spec,Other}}) end end; add_tests([Other|Ts],Spec) -> case get(relaxed) of true -> add_tests(Ts,Spec); false -> throw({error,{undefined_term_in_spec,Other}}) end; add_tests([],Spec) -> % done Spec. separate(Nodes,Tag,Data,Tests,Refs) -> Separated = separate(Nodes,Tag,Data,Refs), Separated ++ Tests. separate([N|Ns],Tag,Data,Refs) -> [list_to_tuple([Tag,ref2node(N,Refs)|Data])|separate(Ns,Tag,Data,Refs)]; separate([],_,_,_) -> []. %% Representation: %% {{Node,Dir},[{Suite1,[case11,case12,...]},{Suite2,[case21,case22,...]},...]} %% {{Node,Dir},[{Suite1,{skip,Cmt}},{Suite2,[{case21,{skip,Cmt}},case22,...]},...]} insert_suites(Node,Dir,[S|Ss],Tests) -> Tests1 = insert_cases(Node,Dir,S,all,Tests), insert_suites(Node,Dir,Ss,Tests1); insert_suites(_Node,_Dir,[],Tests) -> Tests; insert_suites(Node,Dir,S,Tests) -> insert_suites(Node,Dir,[S],Tests). insert_cases(Node,Dir,Suite,Cases,Tests) when is_list(Cases) -> case lists:keysearch({Node,Dir},1,Tests) of {value,{{Node,Dir},[{all,_}]}} -> Tests; {value,{{Node,Dir},Suites0}} -> Suites1 = insert_cases1(Suite,Cases,Suites0), insert_in_order({{Node,Dir},Suites1},Tests); false -> insert_in_order({{Node,Dir},[{Suite,Cases}]},Tests) end; insert_cases(Node,Dir,Suite,Case,Tests) when is_atom(Case) -> insert_cases(Node,Dir,Suite,[Case],Tests). insert_cases1(_Suite,_Cases,all) -> all; insert_cases1(Suite,Cases,Suites0) -> case lists:keysearch(Suite,1,Suites0) of {value,{Suite,all}} -> Suites0; {value,{Suite,Cases0}} -> Cases1 = insert_in_order(Cases,Cases0), insert_in_order({Suite,Cases1},Suites0); false -> insert_in_order({Suite,Cases},Suites0) end. skip_suites(Node,Dir,[S|Ss],Cmt,Tests) -> Tests1 = skip_cases(Node,Dir,S,all,Cmt,Tests), skip_suites(Node,Dir,Ss,Cmt,Tests1); skip_suites(_Node,_Dir,[],_Cmt,Tests) -> Tests; skip_suites(Node,Dir,S,Cmt,Tests) -> skip_suites(Node,Dir,[S],Cmt,Tests). skip_cases(Node,Dir,Suite,Cases,Cmt,Tests) when is_list(Cases) -> Suites = case lists:keysearch({Node,Dir},1,Tests) of {value,{{Node,Dir},Suites0}} -> Suites0; false -> [] end, Suites1 = skip_cases1(Suite,Cases,Cmt,Suites), insert_in_order({{Node,Dir},Suites1},Tests); skip_cases(Node,Dir,Suite,Case,Cmt,Tests) when is_atom(Case) -> skip_cases(Node,Dir,Suite,[Case],Cmt,Tests). skip_cases1(Suite,Cases,Cmt,Suites0) -> SkipCases = lists:map(fun(C) -> {C,{skip,Cmt}} end,Cases), case lists:keysearch(Suite,1,Suites0) of {value,{Suite,Cases0}} -> Cases1 = Cases0 ++ SkipCases, insert_in_order({Suite,Cases1},Suites0); false -> insert_in_order({Suite,SkipCases},Suites0) end. insert_in_order([E|Es],List) -> List1 = insert_elem(E,List,[]), insert_in_order(Es,List1); insert_in_order([],List) -> List; insert_in_order(E,List) -> insert_elem(E,List,[]). %% replace an existing entry (same key) or add last in list insert_elem({Key,_}=E,[{Key,_}|Rest],SoFar) -> lists:reverse([E|SoFar]) ++ Rest; insert_elem({E,_},[E|Rest],SoFar) -> lists:reverse([E|SoFar]) ++ Rest; insert_elem(E,[E|Rest],SoFar) -> lists:reverse([E|SoFar]) ++ Rest; insert_elem(E,[E1|Rest],SoFar) -> insert_elem(E,Rest,[E1|SoFar]); insert_elem(E,[],SoFar) -> lists:reverse([E|SoFar]). ref2node(all_nodes,_Refs) -> all_nodes; ref2node(master,_Refs) -> master; ref2node(RefOrNode,Refs) -> case string:chr(atom_to_list(RefOrNode),$@) of 0 -> % a ref case lists:keysearch(RefOrNode,1,Refs) of {value,{RefOrNode,Node}} -> Node; false -> throw({error,{noderef_missing,RefOrNode}}) end; _ -> % a node RefOrNode end. ref2dir(Ref,Refs) when is_atom(Ref) -> case lists:keysearch(Ref,1,Refs) of {value,{Ref,Dir}} -> Dir; false -> throw({error,{alias_missing,Ref}}) end; ref2dir(Dir,_) when is_list(Dir) -> Dir. is_noderef(What,Nodes) when is_atom(What) -> is_noderef([What],Nodes); is_noderef([master|_],_Nodes) -> true; is_noderef([What|_],Nodes) -> case lists:keymember(What,1,Nodes) or lists:keymember(What,2,Nodes) of true -> true; false -> false end; is_noderef([],_) -> false. valid_terms() -> [ {node,3}, {cover,2}, {cover,3}, {config,2}, {config,3}, {userconfig, 2}, {userconfig, 3}, {alias,3}, {logdir,2}, {logdir,3}, {event_handler,2}, {event_handler,3}, {event_handler,4}, {include,2}, {include,3}, {suites,3}, {suites,4}, {cases,4}, {cases,5}, {skip_suites,4}, {skip_suites,5}, {skip_cases,5}, {skip_cases,6} ]. %% this function "guesses" if the user has misspelled a term name resembles_ct_term(Name,Size) when is_atom(Name) -> resembles_ct_term2(atom_to_list(Name),Size); resembles_ct_term(_Name,_) -> false. resembles_ct_term2(Name,Size) when length(Name) > 3 -> CTTerms = [{atom_to_list(Tag),Sz} || {Tag,Sz} <- valid_terms()], compare_names(Name,Size,CTTerms); resembles_ct_term2(_,_) -> false. compare_names(Name,Size,[{Term,Sz}|Ts]) -> if abs(Size-Sz) > 0 -> compare_names(Name,Size,Ts); true -> Diff = abs(length(Name)-length(Term)), if Diff > 1 -> compare_names(Name,Size,Ts); true -> Common = common_letters(Name,Term,0), Bad = abs(length(Name)-Common), if Bad > 2 -> compare_names(Name,Size,Ts); true -> true end end end; compare_names(_,_,[]) -> false. common_letters(_,[],Count) -> Count; common_letters([L|Ls],Term,Count) -> case lists:member(L,Term) of true -> Term1 = lists:delete(L,Term), common_letters(Ls,Term1,Count+1); false -> common_letters(Ls,Term,Count) end; common_letters([],_,Count) -> Count.