1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2004-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%% This module contains CT internal help functions for searching
22%%% through groups specification trees and producing resulting
23%%% tests.
24
25-module(ct_groups).
26
27-export([find_groups/4]).
28-export([make_all_conf/3, make_all_conf/4, make_conf/5]).
29-export([delete_subs/2]).
30-export([expand_groups/3, search_and_override/3]).
31
32-define(val(Key, List), proplists:get_value(Key, List)).
33-define(val(Key, List, Def), proplists:get_value(Key, List, Def)).
34-define(rev(L), lists:reverse(L)).
35
36find_groups(Mod, GrNames, TCs, GroupDefs) when is_atom(GrNames) ;
37					       (length(GrNames) == 1) ->
38    find_groups1(Mod, GrNames, TCs, GroupDefs);
39
40find_groups(Mod, Groups, TCs, GroupDefs) when Groups /= [] ->
41    lists:append([find_groups1(Mod, [GrNames], TCs, GroupDefs) ||
42		     GrNames <- Groups]);
43
44find_groups(_Mod, [], _TCs, _GroupDefs) ->
45    [].
46
47%% GrNames == atom(): Single group name, perform full search
48%% GrNames == list(): List of groups, find all matching paths
49%% GrNames == [list()]: Search path terminated by last group in GrNames
50find_groups1(Mod, GrNames, TCs, GroupDefs) ->
51    {GrNames1,FindAll} =
52	case GrNames of
53	    Name when is_atom(Name), Name /= all ->
54		{[Name],true};
55	    [Path] when is_list(Path) ->
56		{Path,false};
57	    Path ->
58		{Path,true}
59	end,
60    TCs1 = if (is_atom(TCs) and (TCs /= all)) or is_tuple(TCs) ->
61		   [TCs];
62	      true ->
63		   TCs
64	   end,
65    Found = find(Mod, GrNames1, TCs1, GroupDefs, [],
66		 GroupDefs, FindAll),
67    [Conf || Conf <- Found, Conf /= 'NOMATCH'].
68
69%% Locate all groups
70find(Mod, all, all, [{Name,Props,Tests} | Gs], Known, Defs, _)
71  when is_atom(Name), is_list(Props), is_list(Tests) ->
72    cyclic_test(Mod, Name, Known),
73    trim(make_conf(Mod, Name, Props,
74		   find(Mod, all, all, Tests, [Name | Known],
75			Defs, true))) ++
76	find(Mod, all, all, Gs, Known, Defs, true);
77
78%% Locate particular TCs in all groups
79find(Mod, all, TCs, [{Name,Props,Tests} | Gs], Known, Defs, _)
80  when is_atom(Name), is_list(Props), is_list(Tests) ->
81    cyclic_test(Mod, Name, Known),
82    Tests1 = modify_tc_list(Tests, TCs, []),
83    trim(make_conf(Mod, Name, Props,
84		   find(Mod, all, TCs, Tests1, [Name | Known],
85			Defs, true))) ++
86	find(Mod, all, TCs, Gs, Known, Defs, true);
87
88%% Found next group is in search path
89find(Mod, [Name|GrNames]=SPath, TCs, [{Name,Props,Tests} | Gs], Known,
90     Defs, FindAll) when is_atom(Name), is_list(Props), is_list(Tests) ->
91    cyclic_test(Mod, Name, Known),
92    Tests1 = modify_tc_list(Tests, TCs, GrNames),
93    trim(make_conf(Mod, Name, Props,
94		   find(Mod, GrNames, TCs, Tests1, [Name|Known],
95			Defs, FindAll))) ++
96	find(Mod, SPath, TCs, Gs, Known, Defs, FindAll);
97
98%% Group path terminated, stop the search
99find(Mod, [], TCs, Tests, _Known, _Defs, false) ->
100    Cases = lists:flatmap(fun(TC) when is_atom(TC), TCs == all ->
101				  [{Mod,TC}];
102			     ({group,_}) ->
103				  [];
104                             ({testcase,TC,[Prop]}) when is_atom(TC), TC ==all ->
105                                  [{repeat,{Mod,TC},Prop}];
106			     ({_,_}=TC) when TCs == all ->
107				  [TC];
108			     (TC) when is_atom(TC) ->
109                                  Tuple = {Mod,TC},
110                                  case lists:member(Tuple, TCs) of
111                                      true  ->
112                                          [Tuple];
113                                      false ->
114                                          case lists:member(TC, TCs) of
115                                              true  -> [Tuple];
116                                              false -> []
117                                          end
118                                  end;
119                             ({testcase,TC,[Prop]}) when is_atom(TC) ->
120                                  Tuple = {Mod,TC},
121                                  case lists:member(Tuple, TCs) of
122                                      true  ->
123                                          [{repeat,Tuple,Prop}];
124                                      false ->
125                                          case lists:member(TC, TCs) of
126                                              true  -> [{repeat,Tuple,Prop}];
127                                              false -> []
128                                          end
129                                  end;
130                             (_) ->
131                                  []
132			  end, Tests),
133    if Cases == [] -> ['NOMATCH'];
134       true -> Cases
135    end;
136
137%% No more groups
138find(_Mod, [_|_], _TCs, [], _Known, _Defs, _) ->
139    ['NOMATCH'];
140
141%% Found group not next in search path
142find(Mod, GrNames, TCs, [{Name,Props,Tests} | Gs], Known,
143     Defs, FindAll) when is_atom(Name), is_list(Props), is_list(Tests) ->
144    cyclic_test(Mod, Name, Known),
145    Tests1 = modify_tc_list(Tests, TCs, GrNames),
146    trim(make_conf(Mod, Name, Props,
147		   find(Mod, GrNames, TCs, Tests1, [Name|Known],
148			Defs, FindAll))) ++
149	find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll);
150
151%% A nested group defined on top level found
152find(Mod, GrNames, TCs, [{group,Name1} | Gs], Known, Defs, FindAll)
153  when is_atom(Name1) ->
154    find(Mod, GrNames, TCs, [expand(Mod, Name1, Defs) | Gs], Known,
155	 Defs, FindAll);
156
157%% Undocumented remote group feature, use with caution
158find(Mod, GrNames, TCs, [{group, ExtMod, ExtGrp} | Gs], Known,
159     Defs, FindAll) when is_atom(ExtMod), is_atom(ExtGrp) ->
160    ExternalDefs = ExtMod:groups(),
161    ExternalTCs = find(ExtMod, ExtGrp, TCs, [{group, ExtGrp}],
162		       [], ExternalDefs, FindAll),
163    ExternalTCs ++ find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll);
164
165%% Group definition without properties, add an empty property list
166find(Mod, GrNames, TCs, [{Name1,Tests} | Gs], Known, Defs, FindAll)
167  when is_atom(Name1), is_list(Tests) ->
168    find(Mod, GrNames, TCs, [{Name1,[],Tests} | Gs], Known, Defs, FindAll);
169
170%% Save, and keep searching
171find(Mod, GrNames, TCs, [{ExternalTC, Case} = TC | Gs], Known,
172     Defs, FindAll) when is_atom(ExternalTC),
173			 is_atom(Case) ->
174    [TC | find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll)];
175
176%% Save test case
177find(Mod, GrNames, all, [TC | Gs], Known,
178     Defs, FindAll) when is_atom(TC) ->
179    [{Mod,TC} | find(Mod, GrNames, all, Gs, Known, Defs, FindAll)];
180
181%% Save test case
182find(Mod, GrNames, all, [{M,TC} | Gs], Known,
183     Defs, FindAll) when is_atom(M), M /= group, is_atom(TC) ->
184    [{M,TC} | find(Mod, GrNames, all, Gs, Known, Defs, FindAll)];
185
186%% Save test case
187find(Mod, GrNames, all, [{testcase,TC,[Prop]} | Gs], Known,
188     Defs, FindAll) when is_atom(TC) ->
189    [{repeat,{Mod,TC},Prop} | find(Mod, GrNames, all, Gs, Known, Defs, FindAll)];
190
191%% Check if test case should be saved
192find(Mod, GrNames, TCs, [TC | Gs], Known, Defs, FindAll)
193  when is_atom(TC) orelse
194       ((size(TC) == 3) andalso (element(1,TC) == testcase)) orelse
195       ((size(TC) == 2) and (element(1,TC) /= group)) ->
196    Case =
197        case TC of
198            _ when is_atom(TC) ->
199		Tuple = {Mod,TC},
200		case lists:member(Tuple, TCs) of
201		    true  ->
202			Tuple;
203		    false ->
204			case lists:member(TC, TCs) of
205			    true  -> {Mod,TC};
206			    false -> []
207			end
208		end;
209            {testcase,TC0,[Prop]} when is_atom(TC0) ->
210		Tuple = {Mod,TC0},
211		case lists:member(Tuple, TCs) of
212		    true  ->
213			{repeat,Tuple,Prop};
214		    false ->
215			case lists:member(TC0, TCs) of
216			    true  -> {repeat,{Mod,TC0},Prop};
217			    false -> []
218			end
219		end;
220            _ ->
221		case lists:member(TC, TCs) of
222		    true  -> {Mod,TC};
223		    false -> []
224		end
225	end,
226    if Case == [] ->
227	    find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll);
228       true ->
229	    [Case | find(Mod, GrNames, TCs, Gs, Known, Defs, FindAll)]
230    end;
231
232%% Unexpeted term in group list
233find(Mod, _GrNames, _TCs, [BadTerm | _Gs], Known, _Defs, _FindAll) ->
234    Where = if length(Known) == 0 ->
235		    atom_to_list(Mod)++":groups/0";
236	       true ->
237		    "group "++atom_to_list(lists:last(Known))++
238			" in "++atom_to_list(Mod)++":groups/0"
239	    end,
240    Term = io_lib:format("~tp", [BadTerm]),
241    E = "Bad term "++lists:flatten(Term)++" in "++Where,
242    throw({error,list_to_atom(E)});
243
244%% No more groups
245find(_Mod, _GrNames, _TCs, [], _Known, _Defs, _) ->
246    [].
247
248%%%-----------------------------------------------------------------
249
250%% We have to always search bottom up to only remove a branch
251%% if there's 'NOMATCH' in the leaf (otherwise, the branch should
252%% be kept)
253
254trim({conf,Props,Init,Tests,End}) ->
255    try trim(Tests) of
256	[] -> [];
257	Tests1 -> [{conf,Props,Init,Tests1,End}]
258    catch
259	throw:_ -> []
260    end;
261
262trim(Tests) when is_list(Tests) ->
263    %% we need to compare the result of trimming each test on this
264    %% level, and only let a 'NOMATCH' fail the level if no
265    %% successful sub group can be found
266    Tests1 =
267	lists:flatmap(fun(Test) ->
268			      IsConf = case Test of
269					   {conf,_,_,_,_} ->
270					       true;
271					   _ ->
272					       false
273				       end,
274			      try trim_test(Test) of
275				  [] -> [];
276				  Test1 when IsConf -> [{conf,Test1}];
277				  Test1 -> [Test1]
278			      catch
279				  throw:_ -> ['NOMATCH']
280			      end
281		      end, Tests),
282    case lists:keymember(conf, 1, Tests1) of
283	true ->					% at least one successful group
284	    lists:flatmap(fun({conf,Test}) -> [Test];
285			     ('NOMATCH') -> [];	% ignore any 'NOMATCH'
286			     (Test) -> [Test]
287			  end, Tests1);
288	false ->
289	    case lists:member('NOMATCH', Tests1) of
290		true ->
291		    throw('NOMATCH');
292		false ->
293		    Tests1
294	    end
295    end.
296
297trim_test({conf,Props,Init,Tests,End}) ->
298    case trim(Tests) of
299	[] ->
300	    [];
301	Tests1 ->
302	    {conf,Props,Init,Tests1,End}
303    end;
304
305trim_test('NOMATCH') ->
306    throw('NOMATCH');
307
308trim_test(Test) ->
309    Test.
310
311%% GrNames is [] if the terminating group has been found. From
312%% that point, all specified test should be included (as well as
313%% sub groups for deeper search).
314modify_tc_list(GrSpecTs, all, []) ->
315    GrSpecTs;
316
317modify_tc_list(GrSpecTs, TSCs, []) ->
318    modify_tc_list1(GrSpecTs, TSCs);
319
320modify_tc_list(GrSpecTs, _TSCs, _) ->
321    [Test || Test <- GrSpecTs, not is_atom(Test), element(1,Test)=/=testcase].
322
323modify_tc_list1(GrSpecTs, TSCs) ->
324    %% remove all cases in group tc list that should not be executed
325    GrSpecTs1 =
326	lists:flatmap(fun(Test={testcase,TC,_}) ->
327			      case lists:keysearch(TC, 2, TSCs) of
328				  {value,_} ->
329				      [Test];
330				  _ ->
331				      case lists:member(TC, TSCs) of
332					  true  -> [Test];
333					  false -> []
334				      end
335			      end;
336                         (Test) when is_tuple(Test),
337				     (size(Test) > 2) ->
338			      [Test];
339			 (Test={group,_}) ->
340			      [Test];
341			 (Test={_M,TC}) ->
342			      case lists:member(TC, TSCs) of
343				  true  -> [Test];
344				  false -> []
345			      end;
346			 (Test) when is_atom(Test) ->
347			      case lists:keysearch(Test, 2, TSCs) of
348				  {value,_} ->
349				      [Test];
350				  _ ->
351				      case lists:member(Test, TSCs) of
352					  true  -> [Test];
353					  false -> []
354				      end
355			      end;
356			 (Test) -> [Test]
357		      end, GrSpecTs),
358    {TSCs2,GrSpecTs3} =
359	lists:foldr(
360	  fun(TC, {TSCs1,GrSpecTs2}) ->
361		  case lists:member(TC,GrSpecTs1) of
362		      true ->
363			  {[TC|TSCs1],lists:delete(TC,GrSpecTs2)};
364		      false ->
365			  case lists:keysearch(TC, 2, GrSpecTs) of
366			      {value,Test} ->
367				  {[Test|TSCs1],
368				   lists:keydelete(TC, 2, GrSpecTs2)};
369			      false ->
370				  {TSCs1,GrSpecTs2}
371			  end
372		  end
373	  end, {[],GrSpecTs1}, TSCs),
374    TSCs2 ++ GrSpecTs3.
375
376%%%-----------------------------------------------------------------
377
378delete_subs([{conf, _,_,_,_} = Conf | Confs], All) ->
379    All1 = delete_conf(Conf, All),
380    case is_sub(Conf, All1) of
381	true ->
382	    delete_subs(Confs, All1);
383	false ->
384	    delete_subs(Confs, All)
385    end;
386delete_subs([_Else | Confs], All) ->
387    delete_subs(Confs, All);
388delete_subs([], All) ->
389    All.
390
391delete_conf({conf,Props,_,_,_}, Confs) ->
392    Name = ?val(name, Props),
393    [Conf || Conf = {conf,Props0,_,_,_} <- Confs,
394	     Name =/= ?val(name, Props0)].
395
396is_sub({conf,Props,_,_,_}=Conf, [{conf,_,_,Tests,_} | Confs]) ->
397    Name = ?val(name, Props),
398    case lists:any(fun({conf,Props0,_,_,_}) ->
399			   case ?val(name, Props0) of
400			       N when N == Name ->
401				   true;
402			       _ ->
403				   false
404			   end;
405		      (_) ->
406			   false
407		   end, Tests) of
408	true ->
409	    true;
410	false ->
411	    is_sub(Conf, Tests) orelse is_sub(Conf, Confs)
412    end;
413
414is_sub(Conf, [_TC | Tests]) ->
415    is_sub(Conf, Tests);
416
417is_sub(_Conf, []) ->
418    false.
419
420
421cyclic_test(Mod, Name, Names) ->
422    case lists:member(Name, Names) of
423	true ->
424	    E = "Cyclic reference to group "++atom_to_list(Name)++
425		" in "++atom_to_list(Mod)++":groups/0",
426	    throw({error,list_to_atom(E)});
427	false ->
428	    ok
429    end.
430
431expand(Mod, Name, Defs) ->
432    case lists:keysearch(Name, 1, Defs) of
433	{value,Def} ->
434	    Def;
435	false ->
436	    E = "Invalid group "++atom_to_list(Name)++
437		" in "++atom_to_list(Mod)++":groups/0",
438	    throw({error,list_to_atom(E)})
439    end.
440
441make_all_conf(Dir, Mod, Props, TestSpec) ->
442    _ = load_abs(Dir, Mod),
443    make_all_conf(Mod, Props, TestSpec).
444
445make_all_conf(Mod, Props, TestSpec) ->
446    case catch apply(Mod, groups, []) of
447	{'EXIT',_} ->
448	    exit({invalid_group_definition,Mod});
449	GroupDefs when is_list(GroupDefs) ->
450	    case catch find_groups(Mod, all, TestSpec, GroupDefs) of
451		{error,_} = Error ->
452		    %% this makes test_server call error_in_suite as first
453		    %% (and only) test case so we can report Error properly
454		    [{ct_framework,error_in_suite,[[Error]]}];
455		[] ->
456		    exit({invalid_group_spec,Mod});
457		_ConfTests ->
458		    make_conf(Mod, all, Props, TestSpec)
459	    end
460    end.
461
462make_conf(Dir, Mod, Name, Props, TestSpec) ->
463    _ = load_abs(Dir, Mod),
464    make_conf(Mod, Name, Props, TestSpec).
465
466load_abs(Dir, Mod) ->
467    case code:is_loaded(Mod) of
468	false ->
469	    code:load_abs(filename:join(Dir,atom_to_list(Mod)));
470	_ ->
471	    ok
472    end.
473
474make_conf(Mod, Name, Props, TestSpec) ->
475    _ = case code:is_loaded(Mod) of
476	false ->
477	    code:load_file(Mod);
478	_ ->
479	    ok
480    end,
481    {InitConf,EndConf,ExtraProps} =
482	case {erlang:function_exported(Mod,init_per_group,2),
483              erlang:function_exported(Mod,end_per_group,2)} of
484	    {false,false} ->
485		ct_logs:log("TEST INFO", "init_per_group/2 and "
486			    "end_per_group/2 missing for group "
487			    "~tw in ~w, using default.",
488			    [Name,Mod]),
489		{{ct_framework,init_per_group},
490		 {ct_framework,end_per_group},
491		 [{suite,Mod}]};
492	    _ ->
493                %% If any of these exist, the other should too
494                %% (required and documented). If it isn't, it will fail
495                %% with reason 'undef'.
496		{{Mod,init_per_group},{Mod,end_per_group},[]}
497	end,
498    {conf,[{name,Name}|Props++ExtraProps],InitConf,TestSpec,EndConf}.
499
500%%%-----------------------------------------------------------------
501
502expand_groups([H | T], ConfTests, Mod) ->
503    [expand_groups(H, ConfTests, Mod) | expand_groups(T, ConfTests, Mod)];
504expand_groups([], _ConfTests, _Mod) ->
505    [];
506expand_groups({group,Name}, ConfTests, Mod) ->
507    expand_groups({group,Name,default,[]}, ConfTests, Mod);
508expand_groups({group,Name,default}, ConfTests, Mod) ->
509    expand_groups({group,Name,default,[]}, ConfTests, Mod);
510expand_groups({group,Name,ORProps}, ConfTests, Mod) when is_list(ORProps) ->
511    expand_groups({group,Name,ORProps,[]}, ConfTests, Mod);
512expand_groups({group,Name,ORProps,SubORSpec}, ConfTests, Mod) ->
513    FindConf =
514	fun(Conf = {conf,Props,Init,Ts,End}) ->
515		case ?val(name, Props) of
516		    Name when ORProps == default ->
517			[Conf];
518		    Name ->
519			Props1 = case ?val(suite, Props) of
520				     undefined ->
521					 ORProps;
522				     SuiteName ->
523					 [{suite,SuiteName}|ORProps]
524				 end,
525			[{conf,[{name,Name}|Props1],Init,Ts,End}];
526		    _    ->
527			[]
528		end
529	end,
530    case lists:flatmap(FindConf, ConfTests) of
531	[] ->
532	    throw({error,invalid_ref_msg(Name, Mod)});
533	Matching when SubORSpec == [] ->
534	    Matching;
535	Matching ->
536	    override_props(Matching, SubORSpec, Name,Mod)
537    end;
538expand_groups(SeqOrTC, _ConfTests, _Mod) ->
539    SeqOrTC.
540
541%% search deep for the matching conf test and modify it and any
542%% sub tests according to the override specification
543search_and_override([Conf = {conf,Props,Init,Tests,End}], ORSpec, Mod) ->
544    InsProps = fun(GrName, undefined, Ps) ->
545		       [{name,GrName} | Ps];
546		  (GrName, Suite, Ps) ->
547		       [{name,GrName}, {suite,Suite} | Ps]
548	       end,
549    Name = ?val(name, Props),
550    Suite = ?val(suite, Props),
551    case lists:keysearch(Name, 1, ORSpec) of
552	{value,{Name,default}} ->
553	    [Conf];
554	{value,{Name,ORProps}} ->
555	    [{conf,InsProps(Name,Suite,ORProps),Init,Tests,End}];
556	{value,{Name,default,[]}} ->
557	    [Conf];
558	{value,{Name,default,SubORSpec}} ->
559	    override_props([Conf], SubORSpec, Name,Mod);
560	{value,{Name,ORProps,SubORSpec}} ->
561	    override_props([{conf,InsProps(Name,Suite,ORProps),
562			    Init,Tests,End}], SubORSpec, Name,Mod);
563	_ ->
564	    [{conf,Props,Init,search_and_override(Tests,ORSpec,Mod),End}]
565    end.
566
567%% Modify the Tests element according to the override specification
568override_props([{conf,Props,Init,Tests,End} | Confs], SubORSpec, Name,Mod) ->
569    {Subs,SubORSpec1} = override_sub_props(Tests, [], SubORSpec, Mod),
570    [{conf,Props,Init,Subs,End} | override_props(Confs, SubORSpec1, Name,Mod)];
571override_props([], [], _,_) ->
572    [];
573override_props([], SubORSpec, Name,Mod) ->
574    Es = [invalid_ref_msg(Name, element(1,Spec), Mod) || Spec <- SubORSpec],
575    throw({error,Es}).
576
577override_sub_props([], New, ORSpec, _) ->
578    {?rev(New),ORSpec};
579override_sub_props([T = {conf,Props,Init,Tests,End} | Ts],
580		   New, ORSpec, Mod) ->
581    Name = ?val(name, Props),
582    Suite = ?val(suite, Props),
583    case lists:keysearch(Name, 1, ORSpec) of
584	{value,Spec} ->				% group found in spec
585	    Props1 =
586		case element(2, Spec) of
587		    default -> Props;
588		    ORProps when Suite == undefined -> [{name,Name} | ORProps];
589		    ORProps -> [{name,Name}, {suite,Suite} | ORProps]
590		end,
591	    case catch element(3, Spec) of
592		Undef when Undef == [] ; 'EXIT' == element(1, Undef) ->
593		    override_sub_props(Ts, [{conf,Props1,Init,Tests,End} | New],
594				       lists:keydelete(Name, 1, ORSpec), Mod);
595		SubORSpec when is_list(SubORSpec) ->
596		    case override_sub_props(Tests, [], SubORSpec, Mod) of
597			{Subs,[]} ->
598			    override_sub_props(Ts, [{conf,Props1,Init,
599						     Subs,End} | New],
600					       lists:keydelete(Name, 1, ORSpec),
601					       Mod);
602			{_,NonEmptySpec} ->
603			    Es = [invalid_ref_msg(Name, element(1, GrRef),
604						  Mod) || GrRef <- NonEmptySpec],
605			    throw({error,Es})
606		    end;
607		BadGrSpec ->
608		    throw({error,{invalid_form,BadGrSpec}})
609	    end;
610	_ ->					% not a group in spec
611	    override_sub_props(Ts, [T | New], ORSpec, Mod)
612    end;
613override_sub_props([TC | Ts], New, ORSpec, Mod) ->
614    override_sub_props(Ts, [TC | New], ORSpec, Mod).
615
616invalid_ref_msg(Name, Mod) ->
617    E = "Invalid reference to group "++
618	atom_to_list(Name)++" in "++
619	atom_to_list(Mod)++":all/0",
620    list_to_atom(E).
621
622invalid_ref_msg(Name0, Name1, Mod) ->
623    E = "Invalid reference to group "++
624	atom_to_list(Name1)++" from "++atom_to_list(Name0)++
625	" in "++atom_to_list(Mod)++":all/0",
626    list_to_atom(E).
627