From 0139043d48c692c0f0835d389f4e6ecaa38e8692 Mon Sep 17 00:00:00 2001 From: Peter Andersson Date: Sat, 3 Nov 2012 00:01:53 +0100 Subject: Implement new group search functionality OTP-10466 --- lib/common_test/src/ct_groups.erl | 500 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 lib/common_test/src/ct_groups.erl (limited to 'lib/common_test/src/ct_groups.erl') diff --git a/lib/common_test/src/ct_groups.erl b/lib/common_test/src/ct_groups.erl new file mode 100644 index 0000000000..29fbc97d3e --- /dev/null +++ b/lib/common_test/src/ct_groups.erl @@ -0,0 +1,500 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2004-2012. 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 callback module. +%%% +%%%

This module contains CT internal help functions for searching +%%% through groups specification trees and producing resulting +%%% tests.

+ +-module(ct_groups). + +-export([find_groups/4]). +-export([make_all_conf/3, make_conf/5]). +-export([delete_subs/2]). +-export([expand_groups/3, search_and_override/3]). + +-define(val(Key, List), proplists:get_value(Key, List)). +-define(val(Key, List, Def), proplists:get_value(Key, List, Def)). +-define(rev(L), lists:reverse(L)). + +find_groups(Mod, GrNames, TCs, GroupDefs) when is_atom(GrNames) ; + (length(GrNames) == 1) -> + find_groups1(Mod, GrNames, TCs, GroupDefs); + +find_groups(Mod, Groups, TCs, GroupDefs) when Groups /= [] -> + [find_groups1(Mod, [GrNames], TCs, GroupDefs) || GrNames <- Groups]; + +find_groups(_Mod, [], _TCs, _GroupDefs) -> + []. + +%% GrNames == atom: Single group name, perform full search +%% GrNames == list: List of groups, find all matching paths +%% GrNames == [list]: Search path terminated by last group in GrNames +find_groups1(Mod, GrNames, TCs, GroupDefs) -> + {GrNames1,FindAll} = + case GrNames of + Name when is_atom(Name), Name /= all -> + {[Name],true}; + [Path] when is_list(Path) -> + {Path,false}; + Path -> + {Path,true} + end, + TCs1 = if is_atom(TCs), TCs /= all -> [TCs]; + true -> TCs end, + Found = find(Mod, GrNames1, TCs1, GroupDefs, [], + GroupDefs, FindAll), + [Conf || Conf <- Found, Conf /= 'NOMATCH']. + +%% Locate all groups +find(Mod, all, all, [{Name,Props,Tests} | Gs], Known, Defs, _) + when is_atom(Name), is_list(Props), is_list(Tests) -> + cyclic_test(Mod, Name, Known), + trim(make_conf(Mod, Name, Props, + find(Mod, all, all, Tests, [Name | Known], + Defs, true))) ++ + find(Mod, all, all, Gs, Known, Defs, true); + +%% Locate particular TCs in all groups +find(Mod, all, TCs, [{Name,Props,Tests} | Gs], Known, Defs, _) + when is_atom(Name), is_list(Props), is_list(Tests) -> + cyclic_test(Mod, Name, Known), + Tests1 = rm_unwanted_tcs(Tests, TCs, []), + trim(make_conf(Mod, Name, Props, + find(Mod, all, TCs, Tests1, [Name | Known], + Defs, true))) ++ + find(Mod, all, TCs, Gs, Known, Defs, true); + +%% Found next group is in search path +find(Mod, [Name|GrNames]=SPath, TCs, [{Name,Props,Tests} | Gs], Known, + Defs, FindAll) when is_atom(Name), is_list(Props), is_list(Tests) -> + cyclic_test(Mod, Name, Known), + Tests1 = rm_unwanted_tcs(Tests, TCs, GrNames), + trim(make_conf(Mod, Name, Props, + find(Mod, GrNames, TCs, Tests1, [Name|Known], + Defs, FindAll))) ++ + find(Mod, SPath, TCs, Gs, Known, Defs, FindAll); + +%% Group path terminated, stop the search +find(Mod, [], TCs, Tests, _Known, _Defs, false) -> + case [{Mod,TC} || TC <- Tests, + (((TCs == all) and is_atom(TC)) + or ((catch lists:member(TC, TCs)) == true))] of + [] -> ['NOMATCH']; + Cases -> Cases + end; + +%% No more groups +find(_Mod, [_|_], _TCs, [], _Known, _Defs, _) -> + ['NOMATCH']; + +%% Found group not next in search path +find(Mod, GrNames, TCs, [{Name,Props,Tests} | Gs], Known, + Defs, FindAll) when is_atom(Name), is_list(Props), is_list(Tests) -> + cyclic_test(Mod, Name, Known), + Tests1 = rm_unwanted_tcs(Tests, TCs, GrNames), + trim(make_conf(Mod, Name, Props, + find(Mod, GrNames, TCs, Tests1, [Name|Known], + Defs, FindAll))) ++ + find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll); + +%% A nested group defined on top level found +find(Mod, GrNames, TCs, [{group,Name1} | Gs], Known, Defs, FindAll) + when is_atom(Name1) -> + find(Mod, GrNames, TCs, [expand(Mod, Name1, Defs) | Gs], Known, + Defs, FindAll); + +%% Undocumented remote group feature, use with caution +find(Mod, GrNames, TCs, [{group, ExtMod, ExtGrp} | Gs], Known, + Defs, FindAll) when is_atom(ExtMod), is_atom(ExtGrp) -> + ExternalDefs = ExtMod:groups(), + ExternalTCs = find(ExtMod, ExtGrp, TCs, [{group, ExtGrp}], + [], ExternalDefs, FindAll), + ExternalTCs ++ find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll); + +%% Group definition without properties, add an empty property list +find(Mod, GrNames, TCs, [{Name1,Tests} | Gs], Known, Defs, FindAll) + when is_atom(Name1), is_list(Tests) -> + find(Mod, GrNames, TCs, [{Name1,[],Tests} | Gs], Known, Defs, FindAll); + +%% Save, and keep searching +find(Mod, GrNames, TCs, [{ExternalTC, Case} = TC | Gs], Known, + Defs, FindAll) when is_atom(ExternalTC), + is_atom(Case) -> + [TC | find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll)]; + +%% Save test case +find(Mod, GrNames, all, [TC | Gs], Known, + Defs, FindAll) when is_atom(TC) -> + [{Mod, TC} | find(Mod, GrNames, all, Gs, Known, Defs, FindAll)]; + +%% Check if test case should be saved +find(Mod, GrNames, TCs, [TC | Gs], Known, + Defs, FindAll) when is_atom(TC) -> + case lists:member(TC, TCs) of + true -> + [{Mod, TC} | find(Mod, GrNames, TCs, Gs, Known, + Defs, FindAll)]; + false -> + find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll) + end; + +%% Unexpeted term in group list +find(Mod, _GrNames, _TCs, [BadTerm | _Gs], Known, _Defs, _FindAll) -> + Where = if length(Known) == 0 -> + atom_to_list(Mod)++":groups/0"; + true -> + "group "++atom_to_list(lists:last(Known))++ + " in "++atom_to_list(Mod)++":groups/0" + end, + Term = io_lib:format("~p", [BadTerm]), + E = "Bad term "++lists:flatten(Term)++" in "++Where, + throw({error,list_to_atom(E)}); + +%% No more groups +find(_Mod, _GrNames, _TCs, [], _Known, _Defs, _) -> + []. + +%%%----------------------------------------------------------------- + +%% We have to always search bottom up to only remove a branch +%% if there's 'NOMATCH' in the leaf (otherwise, the branch should +%% be kept) + +trim({conf,Props,Init,Tests,End}) -> + try trim(Tests) of + [] -> []; + Tests1 -> [{conf,Props,Init,Tests1,End}] + catch + throw:_ -> [] + end; + +trim(Tests) when is_list(Tests) -> + %% we need to compare the result of trimming each test on this + %% level, and only let a 'NOMATCH' fail the level if no + %% successful sub group can be found + Tests1 = + lists:flatmap(fun(Test) -> + IsConf = case Test of + {conf,_,_,_,_} -> + true; + _ -> + false + end, + try trim_test(Test) of + [] -> []; + Test1 when IsConf -> [{conf,Test1}]; + Test1 -> [Test1] + catch + throw:_ -> ['NOMATCH'] + end + end, Tests), + case lists:keymember(conf, 1, Tests1) of + true -> % at least one successful group + lists:flatmap(fun({conf,Test}) -> [Test]; + ('NOMATCH') -> []; % ignore any 'NOMATCH' + (Test) -> [Test] + end, Tests1); + false -> + case lists:member('NOMATCH', Tests1) of + true -> + throw('NOMATCH'); + false -> + Tests1 + end + end. + +trim_test({conf,Props,Init,Tests,End}) -> + case trim(Tests) of + [] -> + []; + Tests1 -> + {conf,Props,Init,Tests1,End} + end; + +trim_test('NOMATCH') -> + throw('NOMATCH'); + +trim_test(Test) -> + Test. + +%% GrNames is [] if the terminating group has been found. From +%% that point, all specified test should be included (as well as +%% sub groups for deeper search). +rm_unwanted_tcs(Tests, all, []) -> + Tests; + +rm_unwanted_tcs(Tests, TCs, []) -> + [Test || Test <- Tests, + ((is_atom(Test) and (lists:member(Test, TCs) == true)) + or (not is_atom(Test)))]; + +rm_unwanted_tcs(Tests, _TCs, _) -> + [Test || Test <- Tests, not is_atom(Test)]. + +delete_subs([{conf, _,_,_,_} = Conf | Confs], All) -> + All1 = delete_conf(Conf, All), + case is_sub(Conf, All1) of + true -> + delete_subs(Confs, All1); + false -> + delete_subs(Confs, All) + end; +delete_subs([_Else | Confs], All) -> + delete_subs(Confs, All); +delete_subs([], All) -> + All. + +delete_conf({conf,Props,_,_,_}, Confs) -> + Name = ?val(name, Props), + [Conf || Conf = {conf,Props0,_,_,_} <- Confs, + Name =/= ?val(name, Props0)]. + +is_sub({conf,Props,_,_,_}=Conf, [{conf,_,_,Tests,_} | Confs]) -> + Name = ?val(name, Props), + case lists:any(fun({conf,Props0,_,_,_}) -> + case ?val(name, Props0) of + N when N == Name -> + true; + _ -> + false + end; + (_) -> + false + end, Tests) of + true -> + true; + false -> + is_sub(Conf, Tests) or is_sub(Conf, Confs) + end; + +is_sub(Conf, [_TC | Tests]) -> + is_sub(Conf, Tests); + +is_sub(_Conf, []) -> + false. + + +cyclic_test(Mod, Name, Names) -> + case lists:member(Name, Names) of + true -> + E = "Cyclic reference to group "++atom_to_list(Name)++ + " in "++atom_to_list(Mod)++":groups/0", + throw({error,list_to_atom(E)}); + false -> + ok + end. + +expand(Mod, Name, Defs) -> + case lists:keysearch(Name, 1, Defs) of + {value,Def} -> + Def; + false -> + E = "Invalid group "++atom_to_list(Name)++ + " in "++atom_to_list(Mod)++":groups/0", + throw({error,list_to_atom(E)}) + end. + +make_all_conf(Dir, Mod, _Props) -> + case code:is_loaded(Mod) of + false -> + code:load_abs(filename:join(Dir,atom_to_list(Mod))); + _ -> + ok + end, + make_all_conf(Mod). + +make_all_conf(Mod) -> + case catch apply(Mod, groups, []) of + {'EXIT',_} -> + {error,{invalid_group_definition,Mod}}; + GroupDefs when is_list(GroupDefs) -> + case catch find_groups(Mod, all, all, GroupDefs) of + {error,_} = Error -> + %% this makes test_server call error_in_suite as first + %% (and only) test case so we can report Error properly + [{ct_framework,error_in_suite,[[Error]]}]; + [] -> + {error,{invalid_group_spec,Mod}}; + ConfTests -> + [{conf,Props,Init,all,End} || + {conf,Props,Init,_,End} + <- delete_subs(ConfTests, ConfTests)] + end + end. + +make_conf(Dir, Mod, Name, Props, TestSpec) -> + case code:is_loaded(Mod) of + false -> + code:load_abs(filename:join(Dir,atom_to_list(Mod))); + _ -> + ok + end, + make_conf(Mod, Name, Props, TestSpec). + +make_conf(Mod, Name, Props, TestSpec) -> + case code:is_loaded(Mod) of + false -> + code:load_file(Mod); + _ -> + ok + end, + {InitConf,EndConf,ExtraProps} = + case erlang:function_exported(Mod,init_per_group,2) of + true -> + {{Mod,init_per_group},{Mod,end_per_group},[]}; + false -> + ct_logs:log("TEST INFO", "init_per_group/2 and " + "end_per_group/2 missing for group " + "~p in ~p, using default.", + [Name,Mod]), + {{ct_framework,init_per_group}, + {ct_framework,end_per_group}, + [{suite,Mod}]} + end, + {conf,[{name,Name}|Props++ExtraProps],InitConf,TestSpec,EndConf}. + +%%%----------------------------------------------------------------- + +expand_groups([H | T], ConfTests, Mod) -> + [expand_groups(H, ConfTests, Mod) | expand_groups(T, ConfTests, Mod)]; +expand_groups([], _ConfTests, _Mod) -> + []; +expand_groups({group,Name}, ConfTests, Mod) -> + expand_groups({group,Name,default,[]}, ConfTests, Mod); +expand_groups({group,Name,default}, ConfTests, Mod) -> + expand_groups({group,Name,default,[]}, ConfTests, Mod); +expand_groups({group,Name,ORProps}, ConfTests, Mod) when is_list(ORProps) -> + expand_groups({group,Name,ORProps,[]}, ConfTests, Mod); +expand_groups({group,Name,ORProps,SubORSpec}, ConfTests, Mod) -> + FindConf = + fun(Conf = {conf,Props,Init,Ts,End}) -> + case ?val(name, Props) of + Name when ORProps == default -> + [Conf]; + Name -> + Props1 = case ?val(suite, Props) of + undefined -> + ORProps; + SuiteName -> + [{suite,SuiteName}|ORProps] + end, + [{conf,[{name,Name}|Props1],Init,Ts,End}]; + _ -> + [] + end + end, + case lists:flatmap(FindConf, ConfTests) of + [] -> + throw({error,invalid_ref_msg(Name, Mod)}); + Matching when SubORSpec == [] -> + Matching; + Matching -> + override_props(Matching, SubORSpec, Name,Mod) + end; +expand_groups(SeqOrTC, _ConfTests, _Mod) -> + SeqOrTC. + +%% search deep for the matching conf test and modify it and any +%% sub tests according to the override specification +search_and_override([Conf = {conf,Props,Init,Tests,End}], ORSpec, Mod) -> + InsProps = fun(GrName, undefined, Ps) -> + [{name,GrName} | Ps]; + (GrName, Suite, Ps) -> + [{name,GrName}, {suite,Suite} | Ps] + end, + Name = ?val(name, Props), + Suite = ?val(suite, Props), + case lists:keysearch(Name, 1, ORSpec) of + {value,{Name,default}} -> + [Conf]; + {value,{Name,ORProps}} -> + [{conf,InsProps(Name,Suite,ORProps),Init,Tests,End}]; + {value,{Name,default,[]}} -> + [Conf]; + {value,{Name,default,SubORSpec}} -> + override_props([Conf], SubORSpec, Name,Mod); + {value,{Name,ORProps,SubORSpec}} -> + override_props([{conf,InsProps(Name,Suite,ORProps), + Init,Tests,End}], SubORSpec, Name,Mod); + _ -> + [{conf,Props,Init,search_and_override(Tests,ORSpec,Mod),End}] + end. + +%% Modify the Tests element according to the override specification +override_props([{conf,Props,Init,Tests,End} | Confs], SubORSpec, Name,Mod) -> + {Subs,SubORSpec1} = override_sub_props(Tests, [], SubORSpec, Mod), + [{conf,Props,Init,Subs,End} | override_props(Confs, SubORSpec1, Name,Mod)]; +override_props([], [], _,_) -> + []; +override_props([], SubORSpec, Name,Mod) -> + Es = [invalid_ref_msg(Name, element(1,Spec), Mod) || Spec <- SubORSpec], + throw({error,Es}). + +override_sub_props([], New, ORSpec, _) -> + {?rev(New),ORSpec}; +override_sub_props([T = {conf,Props,Init,Tests,End} | Ts], + New, ORSpec, Mod) -> + Name = ?val(name, Props), + Suite = ?val(suite, Props), + case lists:keysearch(Name, 1, ORSpec) of + {value,Spec} -> % group found in spec + Props1 = + case element(2, Spec) of + default -> Props; + ORProps when Suite == undefined -> [{name,Name} | ORProps]; + ORProps -> [{name,Name}, {suite,Suite} | ORProps] + end, + case catch element(3, Spec) of + Undef when Undef == [] ; 'EXIT' == element(1, Undef) -> + override_sub_props(Ts, [{conf,Props1,Init,Tests,End} | New], + lists:keydelete(Name, 1, ORSpec), Mod); + SubORSpec when is_list(SubORSpec) -> + case override_sub_props(Tests, [], SubORSpec, Mod) of + {Subs,[]} -> + override_sub_props(Ts, [{conf,Props1,Init, + Subs,End} | New], + lists:keydelete(Name, 1, ORSpec), + Mod); + {_,NonEmptySpec} -> + Es = [invalid_ref_msg(Name, element(1, GrRef), + Mod) || GrRef <- NonEmptySpec], + throw({error,Es}) + end; + BadGrSpec -> + throw({error,{invalid_form,BadGrSpec}}) + end; + _ -> % not a group in spec + override_sub_props(Ts, [T | New], ORSpec, Mod) + end; +override_sub_props([TC | Ts], New, ORSpec, Mod) -> + override_sub_props(Ts, [TC | New], ORSpec, Mod). + +invalid_ref_msg(Name, Mod) -> + E = "Invalid reference to group "++ + atom_to_list(Name)++" in "++ + atom_to_list(Mod)++":all/0", + list_to_atom(E). + +invalid_ref_msg(Name0, Name1, Mod) -> + E = "Invalid reference to group "++ + atom_to_list(Name1)++" from "++atom_to_list(Name0)++ + " in "++atom_to_list(Mod)++":all/0", + list_to_atom(E). -- cgit v1.2.3