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