%%
%% %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.
%%%
%%% <p>This module contains CT internal help functions for searching
%%% through groups specification trees and producing resulting
%%% tests.</p>
-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).