1%% 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2002-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-module(test_server_ctrl). 21%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 22%% %% 23%% The Erlang Test Server %% 24%% %% 25%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 26 27%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 28%% 29%% MODULE DEPENDENCIES: 30%% HARD TO REMOVE: erlang, lists, io_lib, gen_server, file, io, string, 31%% code, ets, rpc, gen_tcp, inet, erl_tar, sets, 32%% test_server, test_server_sup, test_server_node 33%% EASIER TO REMOVE: filename, filelib, lib, re 34%% 35%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 36 37%%% SUPERVISOR INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 38-export([start/0, start/1, start_link/1, stop/0]). 39 40%%% OPERATOR INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 41-export([add_spec/1, add_dir/2, add_dir/3]). 42-export([add_module/1, add_module/2, 43 add_conf/3, 44 add_case/2, add_case/3, add_cases/2, add_cases/3]). 45-export([add_dir_with_skip/3, add_dir_with_skip/4, add_tests_with_skip/3]). 46-export([add_module_with_skip/2, add_module_with_skip/3, 47 add_conf_with_skip/4, 48 add_case_with_skip/3, add_case_with_skip/4, 49 add_cases_with_skip/3, add_cases_with_skip/4]). 50-export([jobs/0, run_test/1, wait_finish/0, idle_notify/1, 51 abort_current_testcase/1, abort/0]). 52-export([start_get_totals/1, stop_get_totals/0]). 53-export([reject_io_reqs/1, get_levels/0, set_levels/3]). 54-export([multiply_timetraps/1, scale_timetraps/1, get_timetrap_parameters/0]). 55-export([create_priv_dir/1]). 56-export([cover/1, cover/2, cover/3, 57 cover_compile/7, cover_analyse/2, cross_cover_analyse/2, 58 trc/1, stop_trace/0]). 59-export([testcase_callback/1]). 60-export([set_random_seed/1]). 61-export([kill_slavenodes/0]). 62 63%%% TEST_SERVER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 64-export([print/2, print/3, print/4, print_timestamp/2]). 65-export([start_node/3, stop_node/1, wait_for_node/1, is_release_available/1]). 66-export([format/1, format/2, format/3, to_string/1]). 67-export([get_target_info/0]). 68-export([get_hosts/0]). 69-export([node_started/1]). 70-export([uri_encode/1,uri_encode/2]). 71 72%%% DEBUGGER INTERFACE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 73-export([i/0, p/1, p/3, pi/2, pi/4, t/0, t/1]). 74 75%%% PRIVATE EXPORTED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 76-export([init/1, terminate/2]). 77-export([handle_call/3, handle_cast/2, handle_info/2]). 78-export([do_test_cases/4]). 79-export([do_spec/2, do_spec_list/2]). 80-export([xhtml/2, escape_chars/1]). 81 82%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 83 84-include("test_server_internal.hrl"). 85-include_lib("kernel/include/file.hrl"). 86-define(suite_ext, "_SUITE"). 87-define(log_ext, ".log.html"). 88-define(src_listing_ext, ".src.html"). 89-define(logdir_ext, ".logs"). 90-define(data_dir_suffix, "_data/"). 91-define(suitelog_name, "suite.log"). 92-define(suitelog_latest_name, "suite.log.latest"). 93-define(coverlog_name, "cover.html"). 94-define(raw_coverlog_name, "cover.log"). 95-define(cross_coverlog_name, "cross_cover.html"). 96-define(raw_cross_coverlog_name, "cross_cover.log"). 97-define(cross_cover_info, "cross_cover.info"). 98-define(cover_total, "total_cover.log"). 99-define(unexpected_io_log, "unexpected_io.log.html"). 100-define(last_file, "last_name"). 101-define(last_link, "last_link"). 102-define(last_test, "last_test"). 103-define(html_ext, ".html"). 104-define(now, os:timestamp()). 105 106-define(void_fun, fun() -> ok end). 107-define(mod_result(X), if X == skip -> skipped; 108 X == auto_skip -> skipped; 109 true -> X end). 110 111-define(auto_skip_color, "#FFA64D"). 112-define(user_skip_color, "#FF8000"). 113-define(sortable_table_name, "SortableTable"). 114 115-record(state,{jobs=[], levels={1,19,10}, reject_io_reqs=false, 116 multiply_timetraps=1, scale_timetraps=true, 117 create_priv_dir=auto_per_run, finish=false, 118 target_info, trc=false, cover=false, wait_for_node=[], 119 testcase_callback=undefined, idle_notify=[], 120 get_totals=false, random_seed=undefined}). 121 122%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 123%% OPERATOR INTERFACE 124 125add_dir(Name, Job=[Dir|_Dirs]) when is_list(Dir) -> 126 add_job(cast_to_list(Name), 127 lists:map(fun(D)-> {dir,cast_to_list(D)} end, Job)); 128add_dir(Name, Dir) -> 129 add_job(cast_to_list(Name), {dir,cast_to_list(Dir)}). 130 131add_dir(Name, Job=[Dir|_Dirs], Pattern) when is_list(Dir) -> 132 add_job(cast_to_list(Name), 133 lists:map(fun(D)-> {dir,cast_to_list(D), 134 cast_to_list(Pattern)} end, Job)); 135add_dir(Name, Dir, Pattern) -> 136 add_job(cast_to_list(Name), {dir,cast_to_list(Dir),cast_to_list(Pattern)}). 137 138add_module(Mod) when is_atom(Mod) -> 139 add_job(atom_to_list(Mod), {Mod,all}). 140 141add_module(Name, Mods) when is_list(Mods) -> 142 add_job(cast_to_list(Name), lists:map(fun(Mod) -> {Mod,all} end, Mods)). 143 144add_conf(Name, Mod, Conf) when is_tuple(Conf) -> 145 add_job(cast_to_list(Name), {Mod,[Conf]}); 146 147add_conf(Name, Mod, Confs) when is_list(Confs) -> 148 add_job(cast_to_list(Name), {Mod,Confs}). 149 150add_case(Mod, Case) when is_atom(Mod), is_atom(Case) -> 151 add_job(atom_to_list(Mod), {Mod,Case}). 152 153add_case(Name, Mod, Case) when is_atom(Mod), is_atom(Case) -> 154 add_job(Name, {Mod,Case}). 155 156add_cases(Mod, Cases) when is_atom(Mod), is_list(Cases) -> 157 add_job(atom_to_list(Mod), {Mod,Cases}). 158 159add_cases(Name, Mod, Cases) when is_atom(Mod), is_list(Cases) -> 160 add_job(Name, {Mod,Cases}). 161 162add_spec(Spec) -> 163 Name = filename:rootname(Spec, ".spec"), 164 case filelib:is_file(Spec) of 165 true -> add_job(Name, {spec,Spec}); 166 false -> {error,nofile} 167 end. 168 169%% This version of the interface is to be used if there are 170%% suites or cases that should be skipped. 171 172add_dir_with_skip(Name, Job=[Dir|_Dirs], Skip) when is_list(Dir) -> 173 add_job(cast_to_list(Name), 174 lists:map(fun(D)-> {dir,cast_to_list(D)} end, Job), 175 Skip); 176add_dir_with_skip(Name, Dir, Skip) -> 177 add_job(cast_to_list(Name), {dir,cast_to_list(Dir)}, Skip). 178 179add_dir_with_skip(Name, Job=[Dir|_Dirs], Pattern, Skip) when is_list(Dir) -> 180 add_job(cast_to_list(Name), 181 lists:map(fun(D)-> {dir,cast_to_list(D), 182 cast_to_list(Pattern)} end, Job), 183 Skip); 184add_dir_with_skip(Name, Dir, Pattern, Skip) -> 185 add_job(cast_to_list(Name), 186 {dir,cast_to_list(Dir),cast_to_list(Pattern)}, Skip). 187 188add_module_with_skip(Mod, Skip) when is_atom(Mod) -> 189 add_job(atom_to_list(Mod), {Mod,all}, Skip). 190 191add_module_with_skip(Name, Mods, Skip) when is_list(Mods) -> 192 add_job(cast_to_list(Name), lists:map(fun(Mod) -> {Mod,all} end, Mods), Skip). 193 194add_conf_with_skip(Name, Mod, Conf, Skip) when is_tuple(Conf) -> 195 add_job(cast_to_list(Name), {Mod,[Conf]}, Skip); 196 197add_conf_with_skip(Name, Mod, Confs, Skip) when is_list(Confs) -> 198 add_job(cast_to_list(Name), {Mod,Confs}, Skip). 199 200add_case_with_skip(Mod, Case, Skip) when is_atom(Mod), is_atom(Case) -> 201 add_job(atom_to_list(Mod), {Mod,Case}, Skip). 202 203add_case_with_skip(Name, Mod, Case, Skip) when is_atom(Mod), is_atom(Case) -> 204 add_job(Name, {Mod,Case}, Skip). 205 206add_cases_with_skip(Mod, Cases, Skip) when is_atom(Mod), is_list(Cases) -> 207 add_job(atom_to_list(Mod), {Mod,Cases}, Skip). 208 209add_cases_with_skip(Name, Mod, Cases, Skip) when is_atom(Mod), is_list(Cases) -> 210 add_job(Name, {Mod,Cases}, Skip). 211 212add_tests_with_skip(LogDir, Tests, Skip) -> 213 add_job(LogDir, 214 lists:map(fun({Dir,all,all}) -> 215 {Dir,{dir,Dir}}; 216 ({Dir,Mods,all}) -> 217 {Dir,lists:map(fun(M) -> {M,all} end, Mods)}; 218 ({Dir,Mod,Cases}) -> 219 {Dir,{Mod,Cases}} 220 end, Tests), 221 Skip). 222 223%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 224%% COMMAND LINE INTERFACE 225 226parse_cmd_line(Cmds) -> 227 parse_cmd_line(Cmds, [], [], local, false, false, undefined). 228 229parse_cmd_line(['SPEC',Spec|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> 230 case file:consult(Spec) of 231 {ok, TermList} -> 232 Name = filename:rootname(Spec), 233 parse_cmd_line(Cmds, TermList++SpecList, [Name|Names], Param, 234 Trc, Cov, TCCB); 235 {error,Reason} -> 236 io:format("Can't open ~tw: ~tp\n",[Spec, file:format_error(Reason)]), 237 parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, TCCB) 238 end; 239parse_cmd_line(['NAME',Name|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> 240 parse_cmd_line(Cmds, SpecList, [{name,atom_to_list(Name)}|Names], 241 Param, Trc, Cov, TCCB); 242parse_cmd_line(['SKIPMOD',Mod|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> 243 parse_cmd_line(Cmds, [{skip,{Mod,"by command line"}}|SpecList], Names, 244 Param, Trc, Cov, TCCB); 245parse_cmd_line(['SKIPCASE',Mod,Case|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> 246 parse_cmd_line(Cmds, [{skip,{Mod,Case,"by command line"}}|SpecList], Names, 247 Param, Trc, Cov, TCCB); 248parse_cmd_line(['DIR',Dir|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> 249 Name = filename:basename(Dir), 250 parse_cmd_line(Cmds, [{topcase,{dir,Name}}|SpecList], [Name|Names], 251 Param, Trc, Cov, TCCB); 252parse_cmd_line(['MODULE',Mod|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> 253 parse_cmd_line(Cmds,[{topcase,{Mod,all}}|SpecList],[atom_to_list(Mod)|Names], 254 Param, Trc, Cov, TCCB); 255parse_cmd_line(['CASE',Mod,Case|Cmds], SpecList, Names, Param, Trc, Cov, TCCB) -> 256 parse_cmd_line(Cmds,[{topcase,{Mod,Case}}|SpecList],[atom_to_list(Mod)|Names], 257 Param, Trc, Cov, TCCB); 258parse_cmd_line(['TRACE',Trc|Cmds], SpecList, Names, Param, _Trc, Cov, TCCB) -> 259 parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, TCCB); 260parse_cmd_line(['COVER',App,CF,Analyse|Cmds], SpecList, Names, Param, Trc, _Cov, TCCB) -> 261 parse_cmd_line(Cmds, SpecList, Names, Param, Trc, {{App,CF}, Analyse}, TCCB); 262parse_cmd_line(['TESTCASE_CALLBACK',Mod,Func|Cmds], SpecList, Names, Param, Trc, Cov, _) -> 263 parse_cmd_line(Cmds, SpecList, Names, Param, Trc, Cov, {Mod,Func}); 264parse_cmd_line([Obj|_Cmds], _SpecList, _Names, _Param, _Trc, _Cov, _TCCB) -> 265 io:format("~w: Bad argument: ~tw\n", [?MODULE,Obj]), 266 io:format(" Use the `ts' module to start tests.\n", []), 267 io:format(" (If you ARE using `ts', there is a bug in `ts'.)\n", []), 268 halt(1); 269parse_cmd_line([], SpecList, Names, Param, Trc, Cov, TCCB) -> 270 NameList = lists:reverse(Names, ["suite"]), 271 Name = case lists:keysearch(name, 1, NameList) of 272 {value,{name,N}} -> N; 273 false -> hd(NameList) 274 end, 275 {lists:reverse(SpecList), Name, Param, Trc, Cov, TCCB}. 276 277%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 278%% cast_to_list(X) -> string() 279%% X = list() | atom() | void() 280%% Returns a string representation of whatever was input 281 282cast_to_list(X) when is_list(X) -> X; 283cast_to_list(X) when is_atom(X) -> atom_to_list(X); 284cast_to_list(X) -> lists:flatten(io_lib:format("~tw", [X])). 285 286%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 287%% START INTERFACE 288 289%% Kept for backwards compatibility 290start(_) -> 291 start(). 292start_link(_) -> 293 start_link(). 294 295 296start() -> 297 case gen_server:start({local,?MODULE}, ?MODULE, [], []) of 298 {error, {already_started, Pid}} -> 299 {ok, Pid}; 300 Other -> 301 Other 302 end. 303 304start_link() -> 305 case gen_server:start_link({local,?MODULE}, ?MODULE, [], []) of 306 {error, {already_started, Pid}} -> 307 {ok, Pid}; 308 Other -> 309 Other 310 end. 311 312run_test(CommandLine) -> 313 process_flag(trap_exit,true), 314 {SpecList,Name,Param,Trc,Cov,TCCB} = parse_cmd_line(CommandLine), 315 {ok,_TSPid} = start_link(Param), 316 case Trc of 317 false -> ok; 318 File -> trc(File) 319 end, 320 case Cov of 321 false -> ok; 322 {{App,CoverFile},Analyse} -> cover(App, maybe_file(CoverFile), Analyse) 323 end, 324 testcase_callback(TCCB), 325 add_job(Name, {command_line,SpecList}), 326 327 wait_finish(). 328 329%% Converted CoverFile to a string unless it is 'none' 330maybe_file(none) -> 331 none; 332maybe_file(CoverFile) -> 333 atom_to_list(CoverFile). 334 335idle_notify(Fun) -> 336 {ok, Pid} = controller_call({idle_notify,Fun}), 337 Pid. 338 339start_get_totals(Fun) -> 340 {ok, Pid} = controller_call({start_get_totals,Fun}), 341 Pid. 342 343stop_get_totals() -> 344 ok = controller_call(stop_get_totals), 345 ok. 346 347wait_finish() -> 348 OldTrap = process_flag(trap_exit, true), 349 {ok, Pid} = finish(true), 350 link(Pid), 351 receive 352 {'EXIT',Pid,_} -> 353 ok 354 end, 355 process_flag(trap_exit, OldTrap), 356 ok. 357 358abort_current_testcase(Reason) -> 359 controller_call({abort_current_testcase,Reason}). 360 361abort() -> 362 OldTrap = process_flag(trap_exit, true), 363 {ok, Pid} = finish(abort), 364 link(Pid), 365 receive 366 {'EXIT',Pid,_} -> 367 ok 368 end, 369 process_flag(trap_exit, OldTrap), 370 ok. 371 372finish(Abort) -> 373 controller_call({finish,Abort}). 374 375stop() -> 376 controller_call(stop). 377 378jobs() -> 379 controller_call(jobs). 380 381get_levels() -> 382 controller_call(get_levels). 383 384set_levels(Show, Major, Minor) -> 385 controller_call({set_levels,Show,Major,Minor}). 386 387reject_io_reqs(Bool) -> 388 controller_call({reject_io_reqs,Bool}). 389 390multiply_timetraps(N) -> 391 controller_call({multiply_timetraps,N}). 392 393scale_timetraps(Bool) -> 394 controller_call({scale_timetraps,Bool}). 395 396get_timetrap_parameters() -> 397 controller_call(get_timetrap_parameters). 398 399create_priv_dir(Value) -> 400 controller_call({create_priv_dir,Value}). 401 402trc(TraceFile) -> 403 controller_call({trace,TraceFile}, 2*?ACCEPT_TIMEOUT). 404 405stop_trace() -> 406 controller_call(stop_trace). 407 408node_started(Node) -> 409 gen_server:cast(?MODULE, {node_started,Node}). 410 411cover(App, Analyse) when is_atom(App) -> 412 cover(App, none, Analyse); 413cover(CoverFile, Analyse) -> 414 cover(none, CoverFile, Analyse). 415cover(App, CoverFile, Analyse) -> 416 {Excl,Incl,Cross} = read_cover_file(CoverFile), 417 CoverInfo = #cover{app=App, 418 file=CoverFile, 419 excl=Excl, 420 incl=Incl, 421 cross=Cross, 422 level=Analyse}, 423 controller_call({cover,CoverInfo}). 424 425cover(CoverInfo) -> 426 controller_call({cover,CoverInfo}). 427 428cover_compile(App,File,Excl,Incl,Cross,Analyse,Stop) -> 429 cover_compile(#cover{app=App, 430 file=File, 431 excl=Excl, 432 incl=Incl, 433 cross=Cross, 434 level=Analyse, 435 stop=Stop}). 436 437testcase_callback(ModFunc) -> 438 controller_call({testcase_callback,ModFunc}). 439 440set_random_seed(Seed) -> 441 controller_call({set_random_seed,Seed}). 442 443kill_slavenodes() -> 444 controller_call(kill_slavenodes). 445 446get_hosts() -> 447 get(test_server_hosts). 448 449%%-------------------------------------------------------------------- 450 451add_job(Name, TopCase) -> 452 add_job(Name, TopCase, []). 453 454add_job(Name, TopCase, Skip) -> 455 SuiteName = 456 case Name of 457 "." -> "current_dir"; 458 ".." -> "parent_dir"; 459 Other -> Other 460 end, 461 Dir = filename:absname(SuiteName), 462 controller_call({add_job,Dir,SuiteName,TopCase,Skip}). 463 464controller_call(Arg) -> 465 case catch gen_server:call(?MODULE, Arg, infinity) of 466 {'EXIT',{{badarg,_},{gen_server,call,_}}} -> 467 exit(test_server_ctrl_not_running); 468 {'EXIT',Reason} -> 469 exit(Reason); 470 Other -> 471 Other 472 end. 473controller_call(Arg, Timeout) -> 474 case catch gen_server:call(?MODULE, Arg, Timeout) of 475 {'EXIT',{{badarg,_},{gen_server,call,_}}} -> 476 exit(test_server_ctrl_not_running); 477 {'EXIT',Reason} -> 478 exit(Reason); 479 Other -> 480 Other 481 end. 482 483 484%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 485 486%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 487%% init([]) 488%% 489%% init() is the init function of the test_server's gen_server. 490%% 491init([]) -> 492 case os:getenv("TEST_SERVER_CALL_TRACE") of 493 false -> 494 ok; 495 "" -> 496 ok; 497 TraceSpec -> 498 test_server_sup:call_trace(TraceSpec) 499 end, 500 process_flag(trap_exit, true), 501 %% copy format_exception setting from init arg to application environment 502 case init:get_argument(test_server_format_exception) of 503 {ok,[[TSFE]]} -> 504 application:set_env(test_server, format_exception, list_to_atom(TSFE)); 505 _ -> 506 ok 507 end, 508 test_server_sup:cleanup_crash_dumps(), 509 test_server_sup:util_start(), 510 State = #state{jobs=[],finish=false}, 511 TI0 = test_server:init_target_info(), 512 TargetHost = test_server_sup:hoststr(), 513 TI = TI0#target_info{host=TargetHost, 514 naming=naming(), 515 master=TargetHost}, 516 _ = ets:new(slave_tab, [named_table,set,public,{keypos,2}]), 517 set_hosts([TI#target_info.host]), 518 {ok,State#state{target_info=TI}}. 519 520naming() -> 521 case lists:member($., test_server_sup:hoststr()) of 522 true -> "-name"; 523 false -> "-sname" 524 end. 525 526 527%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 528%% handle_call(kill_slavenodes, From, State) -> ok 529%% 530%% Kill all slave nodes that remain after a test case 531%% is completed. 532%% 533handle_call(kill_slavenodes, _From, State) -> 534 Nodes = test_server_node:kill_nodes(), 535 {reply, Nodes, State}; 536 537%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 538%% handle_call({set_hosts, HostList}, From, State) -> ok 539%% 540%% Set the global hostlist. 541%% 542handle_call({set_hosts, Hosts}, _From, State) -> 543 set_hosts(Hosts), 544 {reply, ok, State}; 545 546%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 547%% handle_call(get_hosts, From, State) -> [Hosts] 548%% 549%% Returns the lists of hosts that the test server 550%% can use for slave nodes. This is primarily used 551%% for nodename generation. 552%% 553handle_call(get_hosts, _From, State) -> 554 Hosts = get_hosts(), 555 {reply, Hosts, State}; 556 557%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 558%% handle_call({add_job,Dir,Name,TopCase,Skip}, _, State) -> 559%% ok | {error,Reason} 560%% 561%% Dir = string() 562%% Name = string() 563%% TopCase = term() 564%% Skip = [SkipItem] 565%% SkipItem = {Mod,Comment} | {Mod,Case,Comment} | {Mod,Cases,Comment} 566%% Mod = Case = atom() 567%% Comment = string() 568%% Cases = [Case] 569%% 570%% Adds a job to the job queue. The name of the job is Name. A log directory 571%% will be created in Dir/Name.logs. TopCase may be anything that 572%% collect_cases/3 accepts, plus the following: 573%% 574%% {spec,SpecName} executes the named test suite specification file. Commands 575%% in the file should be in the format accepted by do_spec_list/1. 576%% 577%% {command_line,SpecList} executes the list of specification instructions 578%% supplied, which should be in the format accepted by do_spec_list/1. 579 580handle_call({add_job,Dir,Name,TopCase,Skip}, _From, State) -> 581 LogDir = Dir ++ ?logdir_ext, 582 ExtraTools = 583 case State#state.cover of 584 false -> []; 585 CoverInfo -> [{cover,CoverInfo}] 586 end, 587 ExtraTools1 = 588 case State#state.random_seed of 589 undefined -> ExtraTools; 590 Seed -> [{random_seed,Seed}|ExtraTools] 591 end, 592 case lists:keysearch(Name, 1, State#state.jobs) of 593 false -> 594 case TopCase of 595 {spec,SpecName} -> 596 Pid = spawn_tester( 597 ?MODULE, do_spec, 598 [SpecName,{State#state.multiply_timetraps, 599 State#state.scale_timetraps}], 600 LogDir, Name, State#state.levels, 601 State#state.reject_io_reqs, 602 State#state.create_priv_dir, 603 State#state.testcase_callback, ExtraTools1), 604 NewJobs = [{Name,Pid}|State#state.jobs], 605 {reply, ok, State#state{jobs=NewJobs}}; 606 {command_line,SpecList} -> 607 Pid = spawn_tester( 608 ?MODULE, do_spec_list, 609 [SpecList,{State#state.multiply_timetraps, 610 State#state.scale_timetraps}], 611 LogDir, Name, State#state.levels, 612 State#state.reject_io_reqs, 613 State#state.create_priv_dir, 614 State#state.testcase_callback, ExtraTools1), 615 NewJobs = [{Name,Pid}|State#state.jobs], 616 {reply, ok, State#state{jobs=NewJobs}}; 617 TopCase -> 618 case State#state.get_totals of 619 {CliPid,Fun} -> 620 Result = count_test_cases(TopCase, Skip), 621 Fun(CliPid, Result), 622 {reply, ok, State}; 623 _ -> 624 Cfg = make_config([]), 625 Pid = spawn_tester( 626 ?MODULE, do_test_cases, 627 [TopCase,Skip,Cfg, 628 {State#state.multiply_timetraps, 629 State#state.scale_timetraps}], 630 LogDir, Name, State#state.levels, 631 State#state.reject_io_reqs, 632 State#state.create_priv_dir, 633 State#state.testcase_callback, ExtraTools1), 634 NewJobs = [{Name,Pid}|State#state.jobs], 635 {reply, ok, State#state{jobs=NewJobs}} 636 end 637 end; 638 _ -> 639 {reply,{error,name_already_in_use},State} 640 end; 641 642%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 643%% handle_call(jobs, _, State) -> JobList 644%% JobList = [{Name,Pid}, ...] 645%% Name = string() 646%% Pid = pid() 647%% 648%% Return the list of current jobs. 649 650handle_call(jobs, _From, State) -> 651 {reply,State#state.jobs,State}; 652 653 654%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 655%% handle_call({abort_current_testcase,Reason}, _, State) -> Result 656%% Reason = term() 657%% Result = ok | {error,no_testcase_running} 658%% 659%% Attempts to abort the test case that's currently running. 660 661handle_call({abort_current_testcase,Reason}, _From, State) -> 662 case State#state.jobs of 663 [{_,Pid}|_] -> 664 Pid ! {abort_current_testcase,Reason,self()}, 665 receive 666 {Pid,abort_current_testcase,Result} -> 667 {reply, Result, State} 668 after 10000 -> 669 {reply, {error,no_testcase_running}, State} 670 end; 671 _ -> 672 {reply, {error,no_testcase_running}, State} 673 end; 674 675%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 676%% handle_call({finish,Fini}, _, State) -> {ok,Pid} 677%% Fini = true | abort 678%% 679%% Tells the test_server to stop as soon as there are no test suites 680%% running. Immediately if none are running. Abort is handled as soon 681%% as current test finishes. 682 683handle_call({finish,Fini}, _From, State) -> 684 case State#state.jobs of 685 [] -> 686 lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Fini) end, 687 State#state.idle_notify), 688 State2 = State#state{finish=false}, 689 {stop,shutdown,{ok,self()}, State2}; 690 _SomeJobs -> 691 State2 = State#state{finish=Fini}, 692 {reply, {ok,self()}, State2} 693 end; 694 695%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 696%% handle_call({idle_notify,Fun}, From, State) -> {ok,Pid} 697%% 698%% Lets a test client subscribe to receive a notification when the 699%% test server becomes idle (can be used to syncronize jobs). 700%% test_server calls Fun(From) when idle. 701 702handle_call({idle_notify,Fun}, {Cli,_Ref}, State) -> 703 case State#state.jobs of 704 [] -> self() ! report_idle; 705 _ -> ok 706 end, 707 Subscribed = State#state.idle_notify, 708 {reply, {ok,self()}, State#state{idle_notify=[{Cli,Fun}|Subscribed]}}; 709 710%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 711%% handle_call(start_get_totals, From, State) -> {ok,Pid} 712%% 713%% Switch on the mode where the test server will only 714%% report back the number of tests it would execute 715%% given some subsequent jobs. 716 717handle_call({start_get_totals,Fun}, {Cli,_Ref}, State) -> 718 {reply, {ok,self()}, State#state{get_totals={Cli,Fun}}}; 719 720%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 721%% handle_call(stop_get_totals, From, State) -> ok 722%% 723%% Lets a test client subscribe to receive a notification when the 724%% test server becomes idle (can be used to syncronize jobs). 725%% test_server calls Fun(From) when idle. 726 727handle_call(stop_get_totals, {_Cli,_Ref}, State) -> 728 {reply, ok, State#state{get_totals=false}}; 729 730%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 731%% handle_call(get_levels, _, State) -> {Show,Major,Minor} 732%% Show = integer() 733%% Major = integer() 734%% Minor = integer() 735%% 736%% Returns a 3-tuple with the logging thresholds. 737%% All output and information from a test suite is tagged with a detail 738%% level. Lower values are more "important". Text that is output using 739%% io:format or similar is automatically tagged with detail level 50. 740%% 741%% All output with detail level: 742%% less or equal to Show is displayed on the screen (default 1) 743%% less or equal to Major is logged in the major log file (default 19) 744%% greater or equal to Minor is logged in the minor log files (default 10) 745 746handle_call(get_levels, _From, State) -> 747 {reply,State#state.levels,State}; 748 749%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 750%% handle_call({set_levels,Show,Major,Minor}, _, State) -> ok 751%% Show = integer() 752%% Major = integer() 753%% Minor = integer() 754%% 755%% Sets the logging thresholds, see handle_call(get_levels,...) above. 756 757handle_call({set_levels,Show,Major,Minor}, _From, State) -> 758 {reply,ok,State#state{levels={Show,Major,Minor}}}; 759 760%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 761%% handle_call({reject_io_reqs,Bool}, _, State) -> ok 762%% Bool = bool() 763%% 764%% May be used to switch off stdout printouts to the minor log file 765 766handle_call({reject_io_reqs,Bool}, _From, State) -> 767 {reply,ok,State#state{reject_io_reqs=Bool}}; 768 769%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 770%% handle_call({multiply_timetraps,N}, _, State) -> ok 771%% N = integer() | infinity 772%% 773%% Multiplies all timetraps set by test cases with N 774 775handle_call({multiply_timetraps,N}, _From, State) -> 776 {reply,ok,State#state{multiply_timetraps=N}}; 777 778%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 779%% handle_call({scale_timetraps,Bool}, _, State) -> ok 780%% Bool = true | false 781%% 782%% Specifies if test_server should scale the timetrap value 783%% automatically if e.g. cover is running. 784 785handle_call({scale_timetraps,Bool}, _From, State) -> 786 {reply,ok,State#state{scale_timetraps=Bool}}; 787 788%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 789%% handle_call(get_timetrap_parameters, _, State) -> {Multiplier,Scale} 790%% Multiplier = integer() | infinity 791%% Scale = true | false 792%% 793%% Returns the parameter values that affect timetraps. 794 795handle_call(get_timetrap_parameters, _From, State) -> 796 {reply,{State#state.multiply_timetraps,State#state.scale_timetraps},State}; 797 798%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 799%% handle_call({trace,TraceFile}, _, State) -> ok | {error,Reason} 800%% 801%% Starts a separate node (trace control node) which 802%% starts tracing on target and all slave nodes 803%% 804%% TraceFile is a text file with elements of type 805%% {Trace,Mod,TracePattern}. 806%% {Trace,Mod,Func,TracePattern}. 807%% {Trace,Mod,Func,Arity,TracePattern}. 808%% 809%% Trace = tp | tpl; local or global call trace 810%% Mod,Func = atom(), Arity=integer(); defines what to trace 811%% TracePattern = [] | match_spec() 812%% 813%% The 'call' trace flag is set on all processes, and then 814%% the given trace patterns are set. 815 816handle_call({trace,TraceFile}, _From, State=#state{trc=false}) -> 817 TI = State#state.target_info, 818 case test_server_node:start_tracer_node(TraceFile, TI) of 819 {ok,Tracer} -> {reply,ok,State#state{trc=Tracer}}; 820 Error -> {reply,Error,State} 821 end; 822handle_call({trace,_TraceFile}, _From, State) -> 823 {reply,{error,already_tracing},State}; 824 825%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 826%% handle_call(stop_trace, _, State) -> ok | {error,Reason} 827%% 828%% Stops tracing on target and all slave nodes and 829%% terminates trace control node 830 831handle_call(stop_trace, _From, State=#state{trc=false}) -> 832 {reply,{error,not_tracing},State}; 833handle_call(stop_trace, _From, State) -> 834 R = test_server_node:stop_tracer_node(State#state.trc), 835 {reply,R,State#state{trc=false}}; 836 837%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 838%% handle_call({cover,CoverInfo}, _, State) -> ok | {error,Reason} 839%% 840%% Set specification of cover analysis to be used when running tests 841%% (see start_extra_tools/1 and stop_extra_tools/1) 842 843handle_call({cover,CoverInfo}, _From, State) -> 844 {reply,ok,State#state{cover=CoverInfo}}; 845 846%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 847%% handle_call({create_priv_dir,Value}, _, State) -> ok | {error,Reason} 848%% 849%% Set create_priv_dir to either auto_per_run (create common priv dir once 850%% per test run), manual_per_tc (the priv dir name will be unique for each 851%% test case, but the user has to call test_server:make_priv_dir/0 to create 852%% it), or auto_per_tc (unique priv dir created automatically for each test 853%% case). 854 855handle_call({create_priv_dir,Value}, _From, State) -> 856 {reply,ok,State#state{create_priv_dir=Value}}; 857 858%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 859%% handle_call({testcase_callback,{Mod,Func}}, _, State) -> ok | {error,Reason} 860%% 861%% Add a callback function that will be called before and after every 862%% test case (on the test case process): 863%% 864%% Mod:Func(Suite,TestCase,InitOrEnd,Config) 865%% 866%% InitOrEnd = init | 'end'. 867 868handle_call({testcase_callback,ModFunc}, _From, State) -> 869 case ModFunc of 870 {Mod,Func} -> 871 _ = case code:is_loaded(Mod) of 872 {file,_} -> 873 ok; 874 false -> 875 code:load_file(Mod) 876 end, 877 case erlang:function_exported(Mod,Func,4) of 878 true -> 879 ok; 880 false -> 881 io:format(user, 882 "WARNING! Callback function ~w:~tw/4 undefined.~n~n", 883 [Mod,Func]) 884 end; 885 _ -> 886 ok 887 end, 888 {reply,ok,State#state{testcase_callback=ModFunc}}; 889 890%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 891%% handle_call({set_random_seed,Seed}, _, State) -> ok | {error,Reason} 892%% 893%% Let operator set a random seed value to be used e.g. for shuffling 894%% test cases. 895 896handle_call({set_random_seed,Seed}, _From, State) -> 897 {reply,ok,State#state{random_seed=Seed}}; 898 899%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 900%% handle_call(stop, _, State) -> ok 901%% 902%% Stops the test server immediately. 903%% Some cleanup is done by terminate/2 904 905handle_call(stop, _From, State) -> 906 {stop, shutdown, ok, State}; 907 908%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 909%% handle_call(get_target_info, _, State) -> TI 910%% 911%% TI = #target_info{} 912%% 913%% Returns information about target 914 915handle_call(get_target_info, _From, State) -> 916 {reply, State#state.target_info, State}; 917 918%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 919%% handle_call({start_node,Name,Type,Options}, _, State) -> 920%% ok | {error,Reason} 921%% 922%% Starts a new node (slave or peer) 923 924handle_call({start_node, Name, Type, Options}, From, State) -> 925 %% test_server_ctrl does gen_server:reply/2 explicitly 926 test_server_node:start_node(Name, Type, Options, From, 927 State#state.target_info), 928 {noreply,State}; 929 930 931%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 932%% handle_call({wait_for_node,Node}, _, State) -> ok 933%% 934%% Waits for a new node to take contact. Used if 935%% node is started with option {wait,false} 936 937handle_call({wait_for_node, Node}, From, State) -> 938 NewWaitList = 939 case ets:lookup(slave_tab,Node) of 940 [] -> 941 [{Node,From}|State#state.wait_for_node]; 942 _ -> 943 gen_server:reply(From,ok), 944 State#state.wait_for_node 945 end, 946 {noreply,State#state{wait_for_node=NewWaitList}}; 947 948%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 949%% handle_call({stop_node,Name}, _, State) -> ok | {error,Reason} 950%% 951%% Stops a slave or peer node. This is actually only some cleanup 952%% - the node is really stopped by test_server when this returns. 953 954handle_call({stop_node, Name}, _From, State) -> 955 R = test_server_node:stop_node(Name), 956 {reply, R, State}; 957 958%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 959%% handle_call({is_release_available,Name}, _, State) -> ok | {error,Reason} 960%% 961%% Tests if the release is available. 962 963handle_call({is_release_available, Release}, _From, State) -> 964 R = test_server_node:is_release_available(Release), 965 {reply, R, State}. 966 967%%-------------------------------------------------------------------- 968set_hosts(Hosts) -> 969 put(test_server_hosts, Hosts). 970 971%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 972%% handle_cast({node_started,Name}, _, State) 973%% 974%% Called by test_server_node when a slave/peer node is fully started. 975 976handle_cast({node_started,Node}, State) -> 977 case State#state.trc of 978 false -> ok; 979 Trc -> test_server_node:trace_nodes(Trc, [Node]) 980 end, 981 NewWaitList = 982 case lists:keysearch(Node,1,State#state.wait_for_node) of 983 {value,{Node,From}} -> 984 gen_server:reply(From, ok), 985 lists:keydelete(Node, 1, State#state.wait_for_node); 986 false -> 987 State#state.wait_for_node 988 end, 989 {noreply, State#state{wait_for_node=NewWaitList}}. 990 991%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 992%% handle_info({'EXIT',Pid,Reason}, State) 993%% Pid = pid() 994%% Reason = term() 995%% 996%% Handles exit messages from linked processes. Only test suites are 997%% expected to be linked. When a test suite terminates, it is removed 998%% from the job queue. 999 1000handle_info(report_idle, State) -> 1001 Finish = State#state.finish, 1002 lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end, 1003 State#state.idle_notify), 1004 {noreply,State#state{idle_notify=[]}}; 1005 1006 1007handle_info({'EXIT',Pid,Reason}, State) -> 1008 case lists:keysearch(Pid,2,State#state.jobs) of 1009 false -> 1010 %% not our problem 1011 {noreply,State}; 1012 {value,{Name,_}} -> 1013 NewJobs = lists:keydelete(Pid, 2, State#state.jobs), 1014 case Reason of 1015 normal -> 1016 fine; 1017 killed -> 1018 io:format("Suite ~ts was killed\n", [Name]); 1019 _Other -> 1020 io:format("Suite ~ts was killed with reason ~tp\n", 1021 [Name,Reason]) 1022 end, 1023 State2 = State#state{jobs=NewJobs}, 1024 Finish = State2#state.finish, 1025 case NewJobs of 1026 [] -> 1027 lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end, 1028 State2#state.idle_notify), 1029 case Finish of 1030 false -> 1031 {noreply,State2#state{idle_notify=[]}}; 1032 _ -> % true | abort 1033 %% test_server:finish() has been called and 1034 %% there are no jobs in the job queue => 1035 %% stop the test_server_ctrl 1036 {stop,shutdown,State2#state{finish=false}} 1037 end; 1038 _ -> % pending jobs 1039 case Finish of 1040 abort -> % abort test now! 1041 lists:foreach(fun({Cli,Fun}) -> Fun(Cli,Finish) end, 1042 State2#state.idle_notify), 1043 {stop,shutdown,State2#state{finish=false}}; 1044 _ -> % true | false 1045 {noreply, State2} 1046 end 1047 end 1048 end; 1049 1050 1051%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1052%% handle_info({tcp_closed,Sock}, State) 1053%% 1054%% A Socket was closed. This indicates that a node died. 1055%% This can be 1056%% *Slave or peer node started by a test suite 1057%% *Trace controll node 1058 1059handle_info({tcp_closed,Sock}, State=#state{trc=Sock}) -> 1060 %% Tracer node died - can't really do anything 1061 %%! Maybe print something??? 1062 {noreply,State#state{trc=false}}; 1063handle_info({tcp_closed,Sock}, State) -> 1064 test_server_node:nodedown(Sock), 1065 {noreply,State}; 1066handle_info(_, State) -> 1067 %% dummy; accept all, do nothing. 1068 {noreply, State}. 1069 1070%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1071%% terminate(Reason, State) -> ok 1072%% Reason = term() 1073%% 1074%% Cleans up when the test_server is terminating. Kills the running 1075%% test suites (if any) and any possible remainting slave node 1076 1077terminate(_Reason, State) -> 1078 test_server_sup:util_stop(), 1079 case State#state.trc of 1080 false -> ok; 1081 Sock -> test_server_node:stop_tracer_node(Sock) 1082 end, 1083 ok = kill_all_jobs(State#state.jobs), 1084 _ = test_server_node:kill_nodes(), 1085 ok. 1086 1087kill_all_jobs([{_Name,JobPid}|Jobs]) -> 1088 exit(JobPid, kill), 1089 kill_all_jobs(Jobs); 1090kill_all_jobs([]) -> 1091 ok. 1092 1093 1094%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1095%%----------------------- INTERNAL FUNCTIONS -----------------------%% 1096%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1097 1098%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1099%% spawn_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs, 1100%% CreatePrivDir, TestCaseCallback, ExtraTools) -> Pid 1101%% Mod = atom() 1102%% Func = atom() 1103%% Args = [term(),...] 1104%% Dir = string() 1105%% Name = string() 1106%% Levels = {integer(),integer(),integer()} 1107%% RejectIoReqs = bool() 1108%% CreatePrivDir = auto_per_run | manual_per_tc | auto_per_tc 1109%% TestCaseCallback = {CBMod,CBFunc} | undefined 1110%% ExtraTools = [ExtraTool,...] 1111%% ExtraTool = CoverInfo | TraceInfo | RandomSeed 1112%% 1113%% Spawns a test suite execute-process, just an ordinary spawn, except 1114%% that it will set a lot of dictionary information before starting the 1115%% named function. Also, the execution is timed and protected by a catch. 1116%% When the named function is done executing, a summary of the results 1117%% is printed to the log files. 1118 1119spawn_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs, 1120 CreatePrivDir, TCCallback, ExtraTools) -> 1121 spawn_link(fun() -> 1122 init_tester(Mod, Func, Args, Dir, Name, Levels, RejectIoReqs, 1123 CreatePrivDir, TCCallback, ExtraTools) 1124 end). 1125 1126init_tester(Mod, Func, Args, Dir, Name, {_,_,MinLev}=Levels, 1127 RejectIoReqs, CreatePrivDir, TCCallback, ExtraTools) -> 1128 process_flag(trap_exit, true), 1129 _ = test_server_io:start_link(), 1130 put(app, common_test), 1131 put(test_server_name, Name), 1132 put(test_server_dir, Dir), 1133 put(test_server_total_time, 0), 1134 put(test_server_ok, 0), 1135 put(test_server_failed, 0), 1136 put(test_server_skipped, {0,0}), 1137 put(test_server_minor_level, MinLev), 1138 put(test_server_create_priv_dir, CreatePrivDir), 1139 put(test_server_random_seed, proplists:get_value(random_seed, ExtraTools)), 1140 put(test_server_testcase_callback, TCCallback), 1141 case os:getenv("TEST_SERVER_FRAMEWORK") of 1142 FW when FW =:= false; FW =:= "undefined" -> 1143 put(test_server_framework, '$none'); 1144 FW -> 1145 put(test_server_framework_name, list_to_atom(FW)), 1146 case os:getenv("TEST_SERVER_FRAMEWORK_NAME") of 1147 FWName when FWName =:= false; FWName =:= "undefined" -> 1148 put(test_server_framework_name, '$none'); 1149 FWName -> 1150 put(test_server_framework_name, list_to_atom(FWName)) 1151 end 1152 end, 1153 1154 %% before first print, read and set logging options 1155 FWLogDir = 1156 case test_server_sup:framework_call(get_log_dir, [], []) of 1157 {ok,FwDir} -> FwDir; 1158 _ -> filename:dirname(Dir) 1159 end, 1160 put(test_server_framework_logdir, FWLogDir), 1161 LogOpts = test_server_sup:framework_call(get_logopts, [], []), 1162 put(test_server_logopts, LogOpts), 1163 1164 StartedExtraTools = start_extra_tools(ExtraTools), 1165 1166 test_server_io:set_job_name(Name), 1167 test_server_io:set_gl_props([{levels,Levels}, 1168 {auto_nl,not lists:member(no_nl, LogOpts)}, 1169 {reject_io_reqs,RejectIoReqs}]), 1170 group_leader(test_server_io:get_gl(true), self()), 1171 {TimeMy,Result} = ts_tc(Mod, Func, Args), 1172 set_io_buffering(undefined), 1173 test_server_io:set_job_name(undefined), 1174 catch stop_extra_tools(StartedExtraTools), 1175 case Result of 1176 {'EXIT',test_suites_done} -> 1177 ok; 1178 {'EXIT',_Pid,Reason} -> 1179 print(1, "EXIT, reason ~tp", [Reason]); 1180 {'EXIT',Reason} -> 1181 report_severe_error(Reason), 1182 print(1, "EXIT, reason ~tp", [Reason]) 1183 end, 1184 Time = TimeMy/1000000, 1185 SuccessStr = 1186 case get(test_server_failed) of 1187 0 -> "Ok"; 1188 _ -> "FAILED" 1189 end, 1190 {SkippedN,SkipStr} = 1191 case get(test_server_skipped) of 1192 {0,0} -> 1193 {0,""}; 1194 {USkipped,ASkipped} -> 1195 Skipped = USkipped+ASkipped, 1196 {Skipped,io_lib:format(", ~w Skipped", [Skipped])} 1197 end, 1198 OkN = get(test_server_ok), 1199 FailedN = get(test_server_failed), 1200 print(html,"\n</tbody>\n<tfoot>\n" 1201 "<tr><td></td><td><b>TOTAL</b></td><td></td><td></td><td></td>" 1202 "<td>~.3fs</td><td><b>~ts</b></td><td>~w Ok, ~w Failed~ts of ~w</td></tr>\n" 1203 "</tfoot>\n", 1204 [Time,SuccessStr,OkN,FailedN,SkipStr,OkN+FailedN+SkippedN]), 1205 1206 test_server_io:stop([major,html,unexpected_io]), 1207 {UnexpectedIoName,UnexpectedIoFooter} = get(test_server_unexpected_footer), 1208 {ok,UnexpectedIoFd} = open_html_file(UnexpectedIoName, [append]), 1209 io:put_chars(UnexpectedIoFd, "\n</pre>\n"++UnexpectedIoFooter), 1210 ok = file:close(UnexpectedIoFd). 1211 1212report_severe_error(Reason) -> 1213 test_server_sup:framework_call(report, [severe_error,Reason]). 1214 1215ts_tc(M,F,A) -> 1216 Before = erlang:monotonic_time(), 1217 Result = (catch apply(M, F, A)), 1218 After = erlang:monotonic_time(), 1219 Elapsed = erlang:convert_time_unit(After-Before, 1220 native, 1221 micro_seconds), 1222 {Elapsed, Result}. 1223 1224start_extra_tools(ExtraTools) -> 1225 start_extra_tools(ExtraTools, []). 1226start_extra_tools([{cover,CoverInfo} | ExtraTools], Started) -> 1227 case start_cover(CoverInfo) of 1228 {ok,NewCoverInfo} -> 1229 start_extra_tools(ExtraTools,[{cover,NewCoverInfo}|Started]); 1230 {error,_} -> 1231 start_extra_tools(ExtraTools, Started) 1232 end; 1233start_extra_tools([_ | ExtraTools], Started) -> 1234 start_extra_tools(ExtraTools, Started); 1235start_extra_tools([], Started) -> 1236 Started. 1237 1238stop_extra_tools(ExtraTools) -> 1239 TestDir = get(test_server_log_dir_base), 1240 case lists:keymember(cover, 1, ExtraTools) of 1241 false -> 1242 write_default_coverlog(TestDir); 1243 true -> 1244 ok 1245 end, 1246 stop_extra_tools(ExtraTools, TestDir). 1247 1248stop_extra_tools([{cover,CoverInfo}|ExtraTools], TestDir) -> 1249 stop_cover(CoverInfo,TestDir), 1250 stop_extra_tools(ExtraTools, TestDir); 1251%%stop_extra_tools([_ | ExtraTools], TestDir) -> 1252%% stop_extra_tools(ExtraTools, TestDir); 1253stop_extra_tools([], _) -> 1254 ok. 1255 1256 1257%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1258%% do_spec(SpecName, TimetrapSpec) -> {error,Reason} | exit(Result) 1259%% SpecName = string() 1260%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap} 1261%% MultiplyTimetrap = integer() | infinity 1262%% ScaleTimetrap = bool() 1263%% 1264%% Reads the named test suite specification file, and executes it. 1265%% 1266%% This function is meant to be called by a process created by 1267%% spawn_tester/10, which sets up some necessary dictionary values. 1268 1269do_spec(SpecName, TimetrapSpec) when is_list(SpecName) -> 1270 case file:consult(SpecName) of 1271 {ok,TermList} -> 1272 do_spec_list(TermList,TimetrapSpec); 1273 {error,Reason} -> 1274 io:format("Can't open ~ts: ~tp\n", [SpecName,Reason]), 1275 {error,{cant_open_spec,Reason}} 1276 end. 1277 1278%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1279%% do_spec_list(TermList, TimetrapSpec) -> exit(Result) 1280%% TermList = [term()|...] 1281%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap} 1282%% MultiplyTimetrap = integer() | infinity 1283%% ScaleTimetrap = bool() 1284%% 1285%% Executes a list of test suite specification commands. The following 1286%% commands are available, and may occur zero or more times (if several, 1287%% the contents is appended): 1288%% 1289%% {topcase,TopCase} Specifies top level test goals. TopCase has the syntax 1290%% specified by collect_cases/3. 1291%% 1292%% {skip,Skip} Specifies test cases to skip, and lists requirements that 1293%% cannot be granted during the test run. Skip has the syntax specified 1294%% by collect_cases/3. 1295%% 1296%% {nodes,Nodes} Lists node names avaliable to the test suites. Nodes have 1297%% the syntax specified by collect_cases/3. 1298%% 1299%% {require_nodenames, Num} Specifies how many nodenames the test suite will 1300%% need. Theese are automaticly generated and inserted into the Config by the 1301%% test_server. The caller may specify other hosts to run theese nodes by 1302%% using the {hosts, Hosts} option. If there are no hosts specified, all 1303%% nodenames will be generated from the local host. 1304%% 1305%% {hosts, Hosts} Specifies a list of available hosts on which to start 1306%% slave nodes. It is used when the {remote, true} option is given to the 1307%% test_server:start_node/3 function. Also, if {require_nodenames, Num} is 1308%% contained in the TermList, the generated nodenames will be spread over 1309%% all hosts given in this Hosts list. The hostnames are given as atoms or 1310%% strings. 1311%% 1312%% {diskless, true}</c></tag> is kept for backwards compatiblilty and 1313%% should not be used. Use a configuration test case instead. 1314%% 1315%% This function is meant to be called by a process created by 1316%% spawn_tester/10, which sets up some necessary dictionary values. 1317 1318do_spec_list(TermList0, TimetrapSpec) -> 1319 Nodes = [], 1320 TermList = 1321 case lists:keysearch(hosts, 1, TermList0) of 1322 {value, {hosts, Hosts0}} -> 1323 Hosts = lists:map(fun(H) -> cast_to_list(H) end, Hosts0), 1324 controller_call({set_hosts, Hosts}), 1325 lists:keydelete(hosts, 1, TermList0); 1326 _ -> 1327 TermList0 1328 end, 1329 DefaultConfig = make_config([{nodes,Nodes}]), 1330 {TopCases,SkipList,Config} = do_spec_terms(TermList, [], [], DefaultConfig), 1331 do_test_cases(TopCases, SkipList, Config, TimetrapSpec). 1332 1333do_spec_terms([], TopCases, SkipList, Config) -> 1334 {TopCases,SkipList,Config}; 1335do_spec_terms([{topcase,TopCase}|Terms], TopCases, SkipList, Config) -> 1336 do_spec_terms(Terms,[TopCase|TopCases], SkipList, Config); 1337do_spec_terms([{skip,Skip}|Terms], TopCases, SkipList, Config) -> 1338 do_spec_terms(Terms, TopCases, [Skip|SkipList], Config); 1339do_spec_terms([{nodes,Nodes}|Terms], TopCases, SkipList, Config) -> 1340 do_spec_terms(Terms, TopCases, SkipList, 1341 update_config(Config, {nodes,Nodes})); 1342do_spec_terms([{diskless,How}|Terms], TopCases, SkipList, Config) -> 1343 do_spec_terms(Terms, TopCases, SkipList, 1344 update_config(Config, {diskless,How})); 1345do_spec_terms([{config,MoreConfig}|Terms], TopCases, SkipList, Config) -> 1346 do_spec_terms(Terms, TopCases, SkipList, Config++MoreConfig); 1347do_spec_terms([{default_timeout,Tmo}|Terms], TopCases, SkipList, Config) -> 1348 do_spec_terms(Terms, TopCases, SkipList, 1349 update_config(Config, {default_timeout,Tmo})); 1350 1351do_spec_terms([{require_nodenames,NumNames}|Terms], TopCases, SkipList, Config) -> 1352 NodeNames0=generate_nodenames(NumNames), 1353 NodeNames=lists:delete([], NodeNames0), 1354 do_spec_terms(Terms, TopCases, SkipList, 1355 update_config(Config, {nodenames,NodeNames})); 1356do_spec_terms([Other|Terms], TopCases, SkipList, Config) -> 1357 io:format("** WARNING: Spec file contains unknown directive ~tp\n", 1358 [Other]), 1359 do_spec_terms(Terms, TopCases, SkipList, Config). 1360 1361 1362 1363generate_nodenames(Num) -> 1364 Hosts = case controller_call(get_hosts) of 1365 [] -> 1366 TI = controller_call(get_target_info), 1367 [TI#target_info.host]; 1368 List -> 1369 List 1370 end, 1371 generate_nodenames2(Num, Hosts, []). 1372 1373generate_nodenames2(0, _Hosts, Acc) -> 1374 Acc; 1375generate_nodenames2(N, Hosts, Acc) -> 1376 Host=lists:nth((N rem (length(Hosts)))+1, Hosts), 1377 Name=list_to_atom(temp_nodename("nod", []) ++ "@" ++ Host), 1378 generate_nodenames2(N-1, Hosts, [Name|Acc]). 1379 1380temp_nodename([], Acc) -> 1381 lists:flatten(Acc); 1382temp_nodename([Chr|Base], Acc) -> 1383 {A,B,C} = ?now, 1384 New = [Chr | integer_to_list(Chr bxor A bxor B+A bxor C+B)], 1385 temp_nodename(Base, [New|Acc]). 1386 1387%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1388%% count_test_cases(TopCases, SkipCases) -> {Suites,NoOfCases} | error 1389%% TopCases = term() (See collect_cases/3) 1390%% SkipCases = term() (See collect_cases/3) 1391%% Suites = list() 1392%% NoOfCases = integer() | unknown 1393%% 1394%% Counts the test cases that are about to run and returns that number. 1395%% If there's a conf group in TestSpec with a repeat property, the total number 1396%% of cases can not be calculated and NoOfCases = unknown. 1397count_test_cases(TopCases, SkipCases) when is_list(TopCases) -> 1398 case collect_all_cases(TopCases, SkipCases) of 1399 {error,_Why} = Error -> 1400 Error; 1401 TestSpec -> 1402 {get_suites(TestSpec, []), 1403 case remove_conf(TestSpec) of 1404 {repeats,_} -> 1405 unknown; 1406 TestSpec1 -> 1407 length(TestSpec1) 1408 end} 1409 end; 1410 1411count_test_cases(TopCase, SkipCases) -> 1412 count_test_cases([TopCase], SkipCases). 1413 1414 1415remove_conf(Cases) -> 1416 remove_conf(Cases, [], false). 1417 1418remove_conf([{conf, _Ref, Props, _MF}|Cases], NoConf, Repeats) -> 1419 case get_repeat(Props) of 1420 undefined -> 1421 remove_conf(Cases, NoConf, Repeats); 1422 {_RepType,1} -> 1423 remove_conf(Cases, NoConf, Repeats); 1424 _ -> 1425 remove_conf(Cases, NoConf, true) 1426 end; 1427remove_conf([{make,_Ref,_MF}|Cases], NoConf, Repeats) -> 1428 remove_conf(Cases, NoConf, Repeats); 1429remove_conf([{skip_case,{{_M,all},_Cmt},_Mode}|Cases], NoConf, Repeats) -> 1430 remove_conf(Cases, NoConf, Repeats); 1431remove_conf([{skip_case,{Type,_Ref,_MF,_Cmt}}|Cases], 1432 NoConf, Repeats) when Type==conf; 1433 Type==make -> 1434 remove_conf(Cases, NoConf, Repeats); 1435remove_conf([{skip_case,{Type,_Ref,_MF,_Cmt},_Mode}|Cases], 1436 NoConf, Repeats) when Type==conf; 1437 Type==make -> 1438 remove_conf(Cases, NoConf, Repeats); 1439remove_conf([C={Mod,error_in_suite,_}|Cases], NoConf, Repeats) -> 1440 FwMod = get_fw_mod(?MODULE), 1441 if Mod == FwMod -> 1442 remove_conf(Cases, NoConf, Repeats); 1443 true -> 1444 remove_conf(Cases, [C|NoConf], Repeats) 1445 end; 1446remove_conf([C={repeat,_,_}|Cases], NoConf, _Repeats) -> 1447 remove_conf(Cases, [C|NoConf], true); 1448remove_conf([C|Cases], NoConf, Repeats) -> 1449 remove_conf(Cases, [C|NoConf], Repeats); 1450remove_conf([], NoConf, true) -> 1451 {repeats,lists:reverse(NoConf)}; 1452remove_conf([], NoConf, false) -> 1453 lists:reverse(NoConf). 1454 1455get_suites([{skip_case,{{Mod,_F},_Cmt},_Mode}|Tests], Mods) when is_atom(Mod) -> 1456 case add_mod(Mod, Mods) of 1457 true -> get_suites(Tests, [Mod|Mods]); 1458 false -> get_suites(Tests, Mods) 1459 end; 1460get_suites([{Mod,_Case}|Tests], Mods) when is_atom(Mod) -> 1461 case add_mod(Mod, Mods) of 1462 true -> get_suites(Tests, [Mod|Mods]); 1463 false -> get_suites(Tests, Mods) 1464 end; 1465get_suites([{Mod,_Func,_Args}|Tests], Mods) when is_atom(Mod) -> 1466 case add_mod(Mod, Mods) of 1467 true -> get_suites(Tests, [Mod|Mods]); 1468 false -> get_suites(Tests, Mods) 1469 end; 1470get_suites([_|Tests], Mods) -> 1471 get_suites(Tests, Mods); 1472 1473get_suites([], Mods) -> 1474 lists:reverse(Mods). 1475 1476add_mod(Mod, Mods) -> 1477 case lists:reverse(atom_to_list(Mod)) of 1478 "ETIUS_" ++ _ -> % test suite 1479 case lists:member(Mod, Mods) of 1480 true -> false; 1481 false -> true 1482 end; 1483 _ -> 1484 false 1485 end. 1486 1487 1488%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1489%% do_test_cases(TopCases, SkipCases, Config, TimetrapSpec) -> 1490%% exit(Result) 1491%% 1492%% TopCases = term() (See collect_cases/3) 1493%% SkipCases = term() (See collect_cases/3) 1494%% Config = term() (See collect_cases/3) 1495%% TimetrapSpec = MultiplyTimetrap | {MultiplyTimetrap,ScaleTimetrap} 1496%% MultiplyTimetrap = integer() | infinity 1497%% ScaleTimetrap = bool() 1498%% 1499%% Initializes and starts the test run, for "ordinary" test suites. 1500%% Creates log directories and log files, inserts initial timestamps and 1501%% configuration information into the log files. 1502%% 1503%% This function is meant to be called by a process created by 1504%% spawn_tester/10, which sets up some necessary dictionary values. 1505do_test_cases(TopCases, SkipCases, 1506 Config, MultiplyTimetrap) when is_integer(MultiplyTimetrap); 1507 MultiplyTimetrap == infinity -> 1508 do_test_cases(TopCases, SkipCases, Config, {MultiplyTimetrap,true}); 1509 1510do_test_cases(TopCases, SkipCases, 1511 Config, TimetrapData) when is_list(TopCases), 1512 is_tuple(TimetrapData) -> 1513 {ok,TestDir} = start_log_file(), 1514 FwMod = get_fw_mod(?MODULE), 1515 case collect_all_cases(TopCases, SkipCases) of 1516 {error,Why} -> 1517 print(1, "Error starting: ~tp", [Why]), 1518 exit(test_suites_done); 1519 TestSpec0 -> 1520 N = case remove_conf(TestSpec0) of 1521 {repeats,_} -> unknown; 1522 TS -> length(TS) 1523 end, 1524 put(test_server_cases, N), 1525 put(test_server_case_num, 0), 1526 1527 TestSpec = 1528 add_init_and_end_per_suite(TestSpec0, undefined, undefined, FwMod), 1529 1530 TI = get_target_info(), 1531 print(1, "Starting test~ts", 1532 [print_if_known(N, {", ~w test cases",[N]}, 1533 {" (with repeated test cases)",[]})]), 1534 Test = get(test_server_name), 1535 TestName = if is_list(Test) -> 1536 lists:flatten(io_lib:format("~ts", [Test])); 1537 true -> 1538 lists:flatten(io_lib:format("~tp", [Test])) 1539 end, 1540 TestDescr = "Test " ++ TestName ++ " results", 1541 1542 test_server_sup:framework_call(report, [tests_start,{Test,N}]), 1543 1544 {Header,Footer} = 1545 case test_server_sup:framework_call(get_html_wrapper, 1546 [TestDescr,true,TestDir, 1547 {[],[2,3,4,7,8],[1,6]}], "") of 1548 Empty when (Empty == "") ; (element(2,Empty) == "") -> 1549 put(basic_html, true), 1550 {[html_header(TestDescr), 1551 "<h2>Results for test ", TestName, "</h2>\n"], 1552 "\n</body>\n</html>\n"}; 1553 {basic_html,Html0,Html1} -> 1554 put(basic_html, true), 1555 {Html0++["<h1>Results for <i>",TestName,"</i></h1>\n"], 1556 Html1}; 1557 {xhtml,Html0,Html1} -> 1558 put(basic_html, false), 1559 {Html0++["<h1>Results for <i>",TestName,"</i></h1>\n"], 1560 Html1} 1561 end, 1562 1563 print(html, Header), 1564 1565 print(html, xhtml("<p>", "<h4>")), 1566 print_timestamp(html, "Test started at "), 1567 print(html, xhtml("</p>", "</h4>")), 1568 1569 print(html, xhtml("\n<p><b>Host info:</b><br>\n", 1570 "\n<p><b>Host info:</b><br />\n")), 1571 print_who(test_server_sup:hoststr(), test_server_sup:get_username()), 1572 print(html, xhtml("<br>Used Erlang v~ts in <tt>~ts</tt></p>\n", 1573 "<br />Used Erlang v~ts in \"~ts\"</p>\n"), 1574 [erlang:system_info(version), code:root_dir()]), 1575 1576 if FwMod == ?MODULE -> 1577 print(html, xhtml("\n<p><b>Target Info:</b><br>\n", 1578 "\n<p><b>Target Info:</b><br />\n")), 1579 print_who(TI#target_info.host, TI#target_info.username), 1580 print(html,xhtml("<br>Used Erlang v~ts in <tt>~ts</tt></p>\n", 1581 "<br />Used Erlang v~ts in \"~ts\"</p>\n"), 1582 [TI#target_info.version, TI#target_info.root_dir]); 1583 true -> 1584 case test_server_sup:framework_call(target_info, []) of 1585 TargetInfo when is_list(TargetInfo), 1586 length(TargetInfo) > 0 -> 1587 print(html, xhtml("\n<p><b>Target info:</b><br>\n", 1588 "\n<p><b>Target info:</b><br />\n")), 1589 print(html, "~ts</p>\n", [TargetInfo]); 1590 _ -> 1591 ok 1592 end 1593 end, 1594 CoverLog = 1595 case get(test_server_cover_log_dir) of 1596 undefined -> 1597 ?coverlog_name; 1598 AbsLogDir -> 1599 AbsLog = filename:join(AbsLogDir,?coverlog_name), 1600 make_relative(AbsLog, TestDir) 1601 end, 1602 print(html, 1603 "<p><ul>\n" 1604 "<li><a href=\"~ts\">Full textual log</a></li>\n" 1605 "<li><a href=\"~ts\">Coverage log</a></li>\n" 1606 "<li><a href=\"~ts\">Unexpected I/O log</a></li>\n</ul></p>\n", 1607 [?suitelog_name,CoverLog,?unexpected_io_log]), 1608 print(html, 1609 "<p>~ts</p>\n" ++ 1610 xhtml(["<table bgcolor=\"white\" border=\"3\" cellpadding=\"5\">\n", 1611 "<thead>\n"], 1612 ["<table id=\"",?sortable_table_name,"\">\n", 1613 "<thead>\n"]) ++ 1614 "<tr><th>Num</th><th>Module</th><th>Group</th>" ++ 1615 "<th>Case</th><th>Log</th><th>Time</th><th>Result</th>" ++ 1616 "<th>Comment</th></tr>\n</thead>\n<tbody>\n", 1617 [print_if_known(N, {"<i>Executing <b>~w</b> test cases...</i>" 1618 ++ xhtml("\n<br>\n", "\n<br />\n"),[N]}, 1619 {"",[]})]), 1620 1621 print(major, "=cases ~w", [get(test_server_cases)]), 1622 print(major, "=user ~ts", [TI#target_info.username]), 1623 print(major, "=host ~ts", [TI#target_info.host]), 1624 1625 %% If there are no hosts specified,use only the local host 1626 case controller_call(get_hosts) of 1627 [] -> 1628 print(major, "=hosts ~ts", [TI#target_info.host]), 1629 controller_call({set_hosts, [TI#target_info.host]}); 1630 Hosts -> 1631 Str = lists:flatten(lists:map(fun(X) -> [X," "] end, Hosts)), 1632 print(major, "=hosts ~ts", [Str]) 1633 end, 1634 print(major, "=emulator_vsn ~ts", [TI#target_info.version]), 1635 print(major, "=emulator ~ts", [TI#target_info.emulator]), 1636 print(major, "=otp_release ~ts", [TI#target_info.otp_release]), 1637 print(major, "=started ~s", 1638 [lists:flatten(timestamp_get(""))]), 1639 1640 test_server_io:set_footer(Footer), 1641 1642 run_test_cases(TestSpec, Config, TimetrapData) 1643 end; 1644 1645do_test_cases(TopCase, SkipCases, Config, TimetrapSpec) -> 1646 %% when not list(TopCase) 1647 do_test_cases([TopCase], SkipCases, Config, TimetrapSpec). 1648 1649 1650%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1651%% start_log_file() -> {ok,TestDirName} | exit({Error,Reason}) 1652%% Stem = string() 1653%% 1654%% Creates the log directories, the major log file and the html log file. 1655%% The log files are initialized with some header information. 1656%% 1657%% The name of the log directory will be <Name>.logs/run.<Date>/ where 1658%% Name is the test suite name and Date is the current date and time. 1659 1660start_log_file() -> 1661 Dir = get(test_server_dir), 1662 case file:make_dir(Dir) of 1663 ok -> 1664 ok; 1665 {error, eexist} -> 1666 ok; 1667 MkDirError -> 1668 log_file_error(MkDirError, Dir) 1669 end, 1670 TestDir = timestamp_filename_get(filename:join(Dir, "run.")), 1671 TestDir1 = 1672 case file:make_dir(TestDir) of 1673 ok -> 1674 TestDir; 1675 {error,eexist} -> 1676 timer:sleep(1000), 1677 %% we need min 1 second between timestamps unfortunately 1678 TestDirX = timestamp_filename_get(filename:join(Dir, "run.")), 1679 case file:make_dir(TestDirX) of 1680 ok -> 1681 TestDirX; 1682 MkDirError2 -> 1683 log_file_error(MkDirError2, TestDirX) 1684 end; 1685 MkDirError2 -> 1686 log_file_error(MkDirError2, TestDir) 1687 end, 1688 FilenameMode = file:native_name_encoding(), 1689 ok = write_file(filename:join(Dir, ?last_file), 1690 TestDir1 ++ "\n", 1691 FilenameMode), 1692 ok = write_file(?last_file, TestDir1 ++ "\n", FilenameMode), 1693 put(test_server_log_dir_base,TestDir1), 1694 1695 MajorName = filename:join(TestDir1, ?suitelog_name), 1696 HtmlName = MajorName ++ ?html_ext, 1697 UnexpectedName = filename:join(TestDir1, ?unexpected_io_log), 1698 1699 {ok,Major} = open_utf8_file(MajorName), 1700 {ok,Html} = open_html_file(HtmlName), 1701 1702 {UnexpHeader,UnexpFooter} = 1703 case test_server_sup:framework_call(get_html_wrapper, 1704 ["Unexpected I/O log",false, 1705 TestDir, undefined],"") of 1706 UEmpty when (UEmpty == "") ; (element(2,UEmpty) == "") -> 1707 {html_header("Unexpected I/O log"),"\n</body>\n</html>\n"}; 1708 {basic_html,UH,UF} -> 1709 {UH,UF}; 1710 {xhtml,UH,UF} -> 1711 {UH,UF} 1712 end, 1713 1714 {ok,Unexpected} = open_html_file(UnexpectedName), 1715 io:put_chars(Unexpected, [UnexpHeader, 1716 xhtml("<br>\n<h2>Unexpected I/O</h2>", 1717 "<br />\n<h3>Unexpected I/O</h3>"), 1718 "\n<pre>\n"]), 1719 put(test_server_unexpected_footer,{UnexpectedName,UnexpFooter}), 1720 1721 test_server_io:set_fd(major, Major), 1722 test_server_io:set_fd(html, Html), 1723 test_server_io:set_fd(unexpected_io, Unexpected), 1724 1725 %% we must assume the redirection file (to the latest suite index) can 1726 %% be stored on the level above the log directory of the current test 1727 TopDir = filename:dirname(get(test_server_framework_logdir)), 1728 RedirectLink = filename:join(TopDir, ?suitelog_latest_name ++ ?html_ext), 1729 make_html_link(RedirectLink, HtmlName, redirect), 1730 1731 make_html_link(filename:absname(?last_test ++ ?html_ext), 1732 HtmlName, filename:basename(Dir)), 1733 LinkName = filename:join(Dir, ?last_link), 1734 make_html_link(LinkName ++ ?html_ext, HtmlName, 1735 filename:basename(Dir)), 1736 1737 PrivDir = filename:join(TestDir1, ?priv_dir), 1738 ok = file:make_dir(PrivDir), 1739 put(test_server_priv_dir,PrivDir++"/"), 1740 print_timestamp(major, "Suite started at "), 1741 1742 LogInfo = [{topdir,Dir},{rundir,lists:flatten(TestDir1)}], 1743 test_server_sup:framework_call(report, [loginfo,LogInfo]), 1744 {ok,TestDir1}. 1745 1746log_file_error(Error, Dir) -> 1747 exit({cannot_create_log_dir,{Error,lists:flatten(Dir)}}). 1748 1749make_html_link(LinkName, Target, Explanation) -> 1750 %% if possible use a relative reference to Target. 1751 TargetL = filename:split(Target), 1752 PwdL = filename:split(filename:dirname(LinkName)), 1753 Href = case lists:prefix(PwdL, TargetL) of 1754 true -> 1755 uri_encode(filename:join(lists:nthtail(length(PwdL),TargetL))); 1756 false -> 1757 "file:" ++ uri_encode(Target) 1758 end, 1759 H = if Explanation == redirect -> 1760 Meta = ["<meta http-equiv=\"refresh\" " 1761 "content=\"0; url=", Href, "\" />\n"], 1762 [html_header("redirect", Meta), "</html>\n"]; 1763 true -> 1764 [html_header(Explanation), 1765 "<h1>Last test</h1>\n" 1766 "<a href=\"",Href,"\">",Explanation,"</a>\n" 1767 "</body>\n</html>\n"] 1768 end, 1769 ok = write_html_file(LinkName, H). 1770 1771 1772%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1773%% start_minor_log_file(Mod, Func, ParallelTC) -> AbsName 1774%% Mod = atom() 1775%% Func = atom() 1776%% ParallelTC = bool() 1777%% AbsName = string() 1778%% 1779%% Create a minor log file for the test case Mod,Func,Args. The log file 1780%% will be stored in the log directory under the name <Mod>.<Func>.html. 1781%% Some header info will also be inserted into the log file. If the test 1782%% case runs in a parallel group, then to avoid clashing file names if the 1783%% case is executed more than once, the name <Mod>.<Func>.<Timestamp>.html 1784%% is used. 1785 1786start_minor_log_file(Mod, Func, ParallelTC) -> 1787 MFA = {Mod,Func,1}, 1788 LogDir = get(test_server_log_dir_base), 1789 Name = minor_log_file_name(Mod,Func), 1790 AbsName = filename:join(LogDir, Name), 1791 case (ParallelTC orelse (element(1,file:read_file_info(AbsName))==ok)) of 1792 false -> %% normal case, unique name 1793 start_minor_log_file1(Mod, Func, LogDir, AbsName, MFA); 1794 true -> %% special case, duplicate names 1795 Tag = test_server_sup:unique_name(), 1796 Name1 = minor_log_file_name(Mod,Func,[$.|Tag]), 1797 AbsName1 = filename:join(LogDir, Name1), 1798 start_minor_log_file1(Mod, Func, LogDir, AbsName1, MFA) 1799 end. 1800 1801start_minor_log_file1(Mod, Func, LogDir, AbsName, MFA) -> 1802 {ok,Fd} = open_html_file(AbsName), 1803 Lev = get(test_server_minor_level)+1000, %% far down in the minor levels 1804 put(test_server_minor_fd, Fd), 1805 test_server_gl:set_minor_fd(group_leader(), Fd, MFA), 1806 1807 TestDescr = io_lib:format("Test ~w:~tw result", [Mod,Func]), 1808 {Header,Footer} = 1809 case test_server_sup:framework_call(get_html_wrapper, 1810 [TestDescr,false, 1811 filename:dirname(AbsName), 1812 undefined], "") of 1813 Empty when (Empty == "") ; (element(2,Empty) == "") -> 1814 put(basic_html, true), 1815 {html_header(TestDescr), "\n</body>\n</html>\n"}; 1816 {basic_html,Html0,Html1} -> 1817 put(basic_html, true), 1818 {Html0,Html1}; 1819 {xhtml,Html0,Html1} -> 1820 put(basic_html, false), 1821 {Html0,Html1} 1822 end, 1823 put(test_server_minor_footer, Footer), 1824 io:put_chars(Fd, Header), 1825 1826 io:put_chars(Fd, "<a name=\"top\"></a>"), 1827 io:put_chars(Fd, "<pre>\n"), 1828 1829 SrcListing = downcase(atom_to_list(Mod)) ++ ?src_listing_ext, 1830 1831 case get_fw_mod(?MODULE) of 1832 Mod when Func == error_in_suite -> 1833 ok; 1834 _ -> 1835 {Info,Arity} = 1836 if Func == init_per_suite; Func == end_per_suite -> 1837 {"Config function: ", 1}; 1838 Func == init_per_group; Func == end_per_group -> 1839 {"Config function: ", 2}; 1840 true -> 1841 {"Test case: ", 1} 1842 end, 1843 1844 case {filelib:is_file(filename:join(LogDir, SrcListing)), 1845 lists:member(no_src, get(test_server_logopts))} of 1846 {true,false} -> 1847 print(Lev, ["$tc_html", 1848 Info ++ "<a href=\"~ts#~ts\">~w:~tw/~w</a> " 1849 "(click for source code)\n"], 1850 [uri_encode(SrcListing), 1851 uri_encode(atom_to_list(Func)++"-1",utf8), 1852 Mod,Func,Arity]); 1853 _ -> 1854 print(Lev, ["$tc_html",Info ++ "~w:~tw/~w\n"], [Mod,Func,Arity]) 1855 end 1856 end, 1857 1858 AbsName. 1859 1860stop_minor_log_file() -> 1861 test_server_gl:unset_minor_fd(group_leader()), 1862 Fd = get(test_server_minor_fd), 1863 Footer = get(test_server_minor_footer), 1864 io:put_chars(Fd, "</pre>\n" ++ Footer), 1865 ok = file:close(Fd), 1866 put(test_server_minor_fd, undefined). 1867 1868minor_log_file_name(Mod,Func) -> 1869 minor_log_file_name(Mod,Func,""). 1870minor_log_file_name(Mod,Func,Tag) -> 1871 Name = 1872 downcase( 1873 lists:flatten( 1874 io_lib:format("~w.~tw~s~s", [Mod,Func,Tag,?html_ext]))), 1875 Ok = file:native_name_encoding()==utf8 1876 orelse io_lib:printable_latin1_list(Name), 1877 if Ok -> Name; 1878 true -> exit({error,unicode_name_on_latin1_file_system}) 1879 end. 1880 1881downcase(S) -> downcase(S, []). 1882downcase([Uc|Rest], Result) when $A =< Uc, Uc =< $Z -> 1883 downcase(Rest, [Uc-$A+$a|Result]); 1884downcase([C|Rest], Result) -> 1885 downcase(Rest, [C|Result]); 1886downcase([], Result) -> 1887 lists:reverse(Result). 1888 1889%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 1890%% html_convert_modules(TestSpec, Config) -> ok 1891%% Isolate the modules affected by TestSpec and 1892%% make sure they are converted to html. 1893%% 1894%% Errors are silently ignored. 1895 1896html_convert_modules(TestSpec, _Config, FwMod) -> 1897 Mods = html_isolate_modules(TestSpec, FwMod), 1898 html_convert_modules(Mods), 1899 copy_html_files(get(test_server_dir), get(test_server_log_dir_base)). 1900 1901%% Retrieve a list of modules out of the test spec. 1902html_isolate_modules(List, FwMod) -> 1903 html_isolate_modules(List, sets:new(), FwMod). 1904 1905html_isolate_modules([], Set, _) -> sets:to_list(Set); 1906html_isolate_modules([{skip_case,{_Case,_Cmt},_Mode}|Cases], Set, FwMod) -> 1907 html_isolate_modules(Cases, Set, FwMod); 1908html_isolate_modules([{conf,_Ref,Props,{FwMod,_Func}}|Cases], Set, FwMod) -> 1909 Set1 = case proplists:get_value(suite, Props) of 1910 undefined -> Set; 1911 Mod -> sets:add_element(Mod, Set) 1912 end, 1913 html_isolate_modules(Cases, Set1, FwMod); 1914html_isolate_modules([{conf,_Ref,_Props,{Mod,_Func}}|Cases], Set, FwMod) -> 1915 html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod); 1916html_isolate_modules([{skip_case,{conf,_Ref,{FwMod,_Func},_Cmt},Mode}|Cases], 1917 Set, FwMod) -> 1918 Set1 = case proplists:get_value(suite, get_props(Mode)) of 1919 undefined -> Set; 1920 Mod -> sets:add_element(Mod, Set) 1921 end, 1922 html_isolate_modules(Cases, Set1, FwMod); 1923html_isolate_modules([{skip_case,{conf,_Ref,{Mod,_Func},_Cmt},_Props}|Cases], 1924 Set, FwMod) -> 1925 html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod); 1926html_isolate_modules([{Mod,_Case}|Cases], Set, FwMod) -> 1927 html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod); 1928html_isolate_modules([{Mod,_Case,_Args}|Cases], Set, FwMod) -> 1929 html_isolate_modules(Cases, sets:add_element(Mod, Set), FwMod). 1930 1931%% Given a list of modules, convert each module's source code to HTML. 1932html_convert_modules([Mod|Mods]) -> 1933 case code:which(Mod) of 1934 Path when is_list(Path) -> 1935 SrcFile = filename:rootname(Path) ++ ".erl", 1936 FoundSrcFile = 1937 case file:read_file_info(SrcFile) of 1938 {ok,SInfo} -> 1939 {SrcFile,SInfo}; 1940 {error,_} -> 1941 ModInfo = Mod:module_info(compile), 1942 case proplists:get_value(source, ModInfo) of 1943 undefined -> 1944 undefined; 1945 OtherSrcFile -> 1946 case file:read_file_info(OtherSrcFile) of 1947 {ok,SInfo} -> 1948 {OtherSrcFile,SInfo}; 1949 {error,_} -> 1950 undefined 1951 end 1952 end 1953 end, 1954 case FoundSrcFile of 1955 undefined -> 1956 html_convert_modules(Mods); 1957 {SrcFile1,SrcFileInfo} -> 1958 DestDir = get(test_server_dir), 1959 Name = atom_to_list(Mod), 1960 DestFile = filename:join(DestDir, 1961 downcase(Name)++?src_listing_ext), 1962 _ = html_possibly_convert(SrcFile1, SrcFileInfo, DestFile), 1963 html_convert_modules(Mods) 1964 end; 1965 _Other -> 1966 html_convert_modules(Mods) 1967 end; 1968html_convert_modules([]) -> ok. 1969 1970%% Convert source code to HTML if possible and needed. 1971html_possibly_convert(Src, SrcInfo, Dest) -> 1972 case file:read_file_info(Dest) of 1973 {ok,DestInfo} when DestInfo#file_info.mtime >= SrcInfo#file_info.mtime -> 1974 ok; % dest file up to date 1975 _ -> 1976 InclPath = case application:get_env(test_server, include) of 1977 {ok,Incls} -> Incls; 1978 _ -> [] 1979 end, 1980 1981 OutDir = get(test_server_log_dir_base), 1982 case test_server_sup:framework_call(get_html_wrapper, 1983 ["Module "++Src,false, 1984 OutDir,undefined, 1985 encoding(Src)], "") of 1986 Empty when (Empty == "") ; (element(2,Empty) == "") -> 1987 erl2html2:convert(Src, Dest, InclPath); 1988 {_,Header,_} -> 1989 erl2html2:convert(Src, Dest, InclPath, Header) 1990 end 1991 end. 1992 1993%% Copy all HTML files in InDir to OutDir. 1994copy_html_files(InDir, OutDir) -> 1995 Files = filelib:wildcard(filename:join(InDir, "*" ++ ?src_listing_ext)), 1996 lists:foreach(fun (Src) -> copy_html_file(Src, OutDir) end, Files). 1997 1998copy_html_file(Src, DestDir) -> 1999 Dest = filename:join(DestDir, filename:basename(Src)), 2000 case file:read_file(Src) of 2001 {ok,Bin} -> 2002 ok = write_binary_file(Dest, Bin); 2003 {error,_Reason} -> 2004 io:format("File ~ts: read failed\n", [Src]) 2005 end. 2006 2007%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2008%% add_init_and_end_per_suite(TestSpec, Mod, Ref, FwMod) -> NewTestSpec 2009%% 2010%% Expands TestSpec with an initial init_per_suite, and a final 2011%% end_per_suite element, per each discovered suite in the list. 2012 2013add_init_and_end_per_suite([{make,_,_}=Case|Cases], LastMod, LastRef, FwMod) -> 2014 [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; 2015add_init_and_end_per_suite([{skip_case,{{Mod,all},_},_}=Case|Cases], LastMod, 2016 LastRef, FwMod) when Mod =/= LastMod -> 2017 {PreCases, NextMod, NextRef} = 2018 do_add_end_per_suite_and_skip(LastMod, LastRef, Mod, FwMod), 2019 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2020 NextRef, FwMod)]; 2021add_init_and_end_per_suite([{skip_case,{{Mod,_},_Cmt},_Mode}=Case|Cases], 2022 LastMod, LastRef, FwMod) when Mod =/= LastMod -> 2023 {PreCases, NextMod, NextRef} = 2024 do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), 2025 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2026 NextRef, FwMod)]; 2027add_init_and_end_per_suite([{skip_case,{conf,_,{Mod,_},_},_}=Case|Cases], 2028 LastMod, LastRef, FwMod) when Mod =/= LastMod -> 2029 {PreCases, NextMod, NextRef} = 2030 do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), 2031 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2032 NextRef, FwMod)]; 2033add_init_and_end_per_suite([{skip_case,{conf,_,{Mod,_},_}}=Case|Cases], LastMod, 2034 LastRef, FwMod) when Mod =/= LastMod -> 2035 {PreCases, NextMod, NextRef} = 2036 do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), 2037 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2038 NextRef, FwMod)]; 2039add_init_and_end_per_suite([{conf,Ref,Props,{FwMod,Func}}=Case|Cases], LastMod, 2040 LastRef, FwMod) -> 2041 %% if Mod == FwMod, this conf test is (probably) a test case group where 2042 %% the init- and end-functions are missing in the suite, and if so, 2043 %% the suite name should be stored as {suite,Suite} in Props 2044 case proplists:get_value(suite, Props) of 2045 Suite when Suite =/= undefined, Suite =/= LastMod -> 2046 {PreCases, NextMod, NextRef} = 2047 do_add_init_and_end_per_suite(LastMod, LastRef, Suite, FwMod), 2048 Case1 = {conf,Ref,[{suite,NextMod}|proplists:delete(suite,Props)], 2049 {FwMod,Func}}, 2050 PreCases ++ [Case1|add_init_and_end_per_suite(Cases, NextMod, 2051 NextRef, FwMod)]; 2052 _ -> 2053 [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)] 2054 end; 2055add_init_and_end_per_suite([{conf,_,_,{Mod,_}}=Case|Cases], LastMod, 2056 LastRef, FwMod) when Mod =/= LastMod, Mod =/= FwMod -> 2057 {PreCases, NextMod, NextRef} = 2058 do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), 2059 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2060 NextRef, FwMod)]; 2061add_init_and_end_per_suite([SkipCase|Cases], LastMod, LastRef, FwMod) 2062 when element(1,SkipCase) == skip_case; element(1,SkipCase) == auto_skip_case-> 2063 [SkipCase|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; 2064add_init_and_end_per_suite([{conf,_,_,_}=Case|Cases], LastMod, LastRef, FwMod) -> 2065 [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; 2066add_init_and_end_per_suite([{repeat,{Mod,_},_}=Case|Cases], LastMod, LastRef, FwMod) 2067 when Mod =/= LastMod, Mod =/= FwMod -> 2068 {PreCases, NextMod, NextRef} = 2069 do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), 2070 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2071 NextRef, FwMod)]; 2072add_init_and_end_per_suite([{repeat,_,_}=Case|Cases], LastMod, LastRef, FwMod) -> 2073 [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; 2074add_init_and_end_per_suite([{Mod,_}=Case|Cases], LastMod, LastRef, FwMod) 2075 when Mod =/= LastMod, Mod =/= FwMod -> 2076 {PreCases, NextMod, NextRef} = 2077 do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), 2078 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2079 NextRef, FwMod)]; 2080add_init_and_end_per_suite([{Mod,_,_}=Case|Cases], LastMod, LastRef, FwMod) 2081 when Mod =/= LastMod, Mod =/= FwMod -> 2082 {PreCases, NextMod, NextRef} = 2083 do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod), 2084 PreCases ++ [Case|add_init_and_end_per_suite(Cases, NextMod, 2085 NextRef, FwMod)]; 2086add_init_and_end_per_suite([Case|Cases], LastMod, LastRef, FwMod)-> 2087 [Case|add_init_and_end_per_suite(Cases, LastMod, LastRef, FwMod)]; 2088add_init_and_end_per_suite([], _LastMod, undefined, _FwMod) -> 2089 []; 2090add_init_and_end_per_suite([], _LastMod, skipped_suite, _FwMod) -> 2091 []; 2092add_init_and_end_per_suite([], LastMod, LastRef, FwMod) -> 2093 %% we'll add end_per_suite here even if it's not exported 2094 %% (and simply let the call fail if it's missing) 2095 case {erlang:function_exported(LastMod, end_per_suite, 1), 2096 erlang:function_exported(LastMod, init_per_suite, 1)} of 2097 {false,false} -> 2098 %% let's call a "fake" end_per_suite if it exists 2099 case erlang:function_exported(FwMod, end_per_suite, 1) of 2100 true -> 2101 [{conf,LastRef,[{suite,LastMod}],{FwMod,end_per_suite}}]; 2102 false -> 2103 [{conf,LastRef,[],{LastMod,end_per_suite}}] 2104 end; 2105 _ -> 2106 %% If any of these exist, the other should too 2107 %% (required and documented). If it isn't, it will fail 2108 %% with reason 'undef'. 2109 [{conf,LastRef,[],{LastMod,end_per_suite}}] 2110 end. 2111 2112do_add_init_and_end_per_suite(LastMod, LastRef, Mod, FwMod) -> 2113 _ = case code:is_loaded(Mod) of 2114 false -> code:load_file(Mod); 2115 _ -> ok 2116 end, 2117 {Init,NextMod,NextRef} = 2118 case {erlang:function_exported(Mod, init_per_suite, 1), 2119 erlang:function_exported(Mod, end_per_suite, 1)} of 2120 {false,false} -> 2121 %% let's call a "fake" init_per_suite if it exists 2122 case erlang:function_exported(FwMod, init_per_suite, 1) of 2123 true -> 2124 Ref = make_ref(), 2125 {[{conf,Ref,[{suite,Mod}], 2126 {FwMod,init_per_suite}}],Mod,Ref}; 2127 false -> 2128 {[],Mod,undefined} 2129 end; 2130 _ -> 2131 %% If any of these exist, the other should too 2132 %% (required and documented). If it isn't, it will fail 2133 %% with reason 'undef'. 2134 Ref = make_ref(), 2135 {[{conf,Ref,[],{Mod,init_per_suite}}],Mod,Ref} 2136 end, 2137 Cases = 2138 if LastRef==undefined -> 2139 Init; 2140 LastRef==skipped_suite -> 2141 Init; 2142 true -> 2143 %% we'll add end_per_suite here even if it's not exported 2144 %% (and simply let the call fail if it's missing) 2145 case {erlang:function_exported(LastMod, end_per_suite, 1), 2146 erlang:function_exported(LastMod, init_per_suite, 1)} of 2147 {false,false} -> 2148 %% let's call a "fake" end_per_suite if it exists 2149 case erlang:function_exported(FwMod, end_per_suite, 1) of 2150 true -> 2151 [{conf,LastRef,[{suite,LastMod}], 2152 {FwMod,end_per_suite}}|Init]; 2153 false -> 2154 [{conf,LastRef,[],{LastMod,end_per_suite}}|Init] 2155 end; 2156 _ -> 2157 %% If any of these exist, the other should too 2158 %% (required and documented). If it isn't, it will fail 2159 %% with reason 'undef'. 2160 [{conf,LastRef,[],{LastMod,end_per_suite}}|Init] 2161 end 2162 end, 2163 {Cases,NextMod,NextRef}. 2164 2165do_add_end_per_suite_and_skip(LastMod, LastRef, Mod, FwMod) -> 2166 case LastRef of 2167 No when No==undefined ; No==skipped_suite -> 2168 {[],Mod,skipped_suite}; 2169 _Ref -> 2170 case {erlang:function_exported(LastMod, end_per_suite, 1), 2171 erlang:function_exported(LastMod, init_per_suite, 1)} of 2172 {false,false} -> 2173 case erlang:function_exported(FwMod, end_per_suite, 1) of 2174 true -> 2175 %% let's call "fake" end_per_suite if it exists 2176 {[{conf,LastRef,[],{FwMod,end_per_suite}}], 2177 Mod,skipped_suite}; 2178 false -> 2179 {[{conf,LastRef,[],{LastMod,end_per_suite}}], 2180 Mod,skipped_suite} 2181 end; 2182 _ -> 2183 %% If any of these exist, the other should too 2184 %% (required and documented). If it isn't, it will fail 2185 %% with reason 'undef'. 2186 {[{conf,LastRef,[],{LastMod,end_per_suite}}], 2187 Mod,skipped_suite} 2188 end 2189 end. 2190 2191 2192%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2193%% run_test_cases(TestSpec, Config, TimetrapData) -> exit(Result) 2194%% 2195%% Runs the specified tests, then displays/logs the summary. 2196 2197run_test_cases(TestSpec, Config, TimetrapData) -> 2198 test_server:init_valgrind(), 2199 case lists:member(no_src, get(test_server_logopts)) of 2200 true -> 2201 ok; 2202 false -> 2203 FwMod = get_fw_mod(?MODULE), 2204 html_convert_modules(TestSpec, Config, FwMod) 2205 end, 2206 2207 run_test_cases_loop(TestSpec, [Config], TimetrapData, [], []), 2208 2209 {AllSkippedN,UserSkipN,AutoSkipN,SkipStr} = 2210 case get(test_server_skipped) of 2211 {0,0} -> {0,0,0,""}; 2212 {US,AS} -> {US+AS,US,AS,io_lib:format(", ~w skipped", [US+AS])} 2213 end, 2214 OkN = get(test_server_ok), 2215 FailedN = get(test_server_failed), 2216 print(1, "TEST COMPLETE, ~w ok, ~w failed~ts of ~w test cases\n", 2217 [OkN,FailedN,SkipStr,OkN+FailedN+AllSkippedN]), 2218 test_server_sup:framework_call(report, [tests_done, 2219 {OkN,FailedN,{UserSkipN,AutoSkipN}}]), 2220 print(major, "=finished ~s", [lists:flatten(timestamp_get(""))]), 2221 print(major, "=failed ~w", [FailedN]), 2222 print(major, "=successful ~w", [OkN]), 2223 print(major, "=user_skipped ~w", [UserSkipN]), 2224 print(major, "=auto_skipped ~w", [AutoSkipN]), 2225 exit(test_suites_done). 2226 2227 2228%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2229%% run_test_cases_loop(TestCases, Config, TimetrapData, Mode, Status) -> ok 2230%% TestCases = [Test,...] 2231%% Config = [[{Key,Val},...],...] 2232%% TimetrapData = {MultiplyTimetrap,ScaleTimetrap} 2233%% MultiplyTimetrap = integer() | infinity 2234%% ScaleTimetrap = bool() 2235%% Mode = [{Ref,[Prop,..],StartTime}] 2236%% Ref = reference() 2237%% Prop = {name,Name} | sequence | parallel | 2238%% shuffle | {shuffle,Seed} | 2239%% repeat | {repeat,N} | 2240%% repeat_until_all_ok | {repeat_until_all_ok,N} | 2241%% repeat_until_any_ok | {repeat_until_any_ok,N} | 2242%% repeat_until_any_fail | {repeat_until_any_fail,N} | 2243%% repeat_until_all_fail | {repeat_until_all_fail,N} 2244%% Status = [{Ref,{{Ok,Skipped,Failed},CopiedCases}}] 2245%% Ok = Skipped = Failed = [Case,...] 2246%% 2247%% Execute the TestCases under configuration Config. Config is a list 2248%% of lists, where hd(Config) holds the config tuples for the current 2249%% conf case and tl(Config) is the data for the higher level conf cases. 2250%% Config data is "inherited" from top to nested conf cases, but 2251%% never the other way around. if length(Config) == 1, Config contains 2252%% only the initial config data for the suite. 2253%% 2254%% Test may be one of the following: 2255%% 2256%% {conf,Ref,Props,{Mod,Func}} Mod:Func is a configuration modification 2257%% function, call it with the current configuration as argument. It will 2258%% return a new configuration. 2259%% 2260%% {make,Ref,{Mod,Func,Args}} Mod:Func is a make function, and it is called 2261%% with the given arguments. 2262%% 2263%% {Mod,Case} This is a normal test case. Determine the correct 2264%% configuration, and insert {Mod,Case,Config} as head of the list, 2265%% then reiterate. 2266%% 2267%% {Mod,Case,Args} A test case with predefined argument (usually a normal 2268%% test case which just got a fresh configuration (see above)). 2269%% 2270%% {skip_case,{conf,Ref,Case,Comment}} An init conf case gets skipped 2271%% by the user. This will also cause the end conf case to be skipped. 2272%% Note that it is not possible to skip an end conf case directly (it 2273%% can only be skipped indirectly by a skipped init conf case). The 2274%% comment (which gets printed in the log files) describes why the case 2275%% was skipped. 2276%% 2277%% {skip_case,{Case,Comment},Mode} A normal test case skipped by the user. 2278%% The comment (which gets printed in the log files) describes why the 2279%% case was skipped. 2280%% 2281%% {auto_skip_case,{conf,Ref,Case,Comment},Mode} This is the result of 2282%% an end conf case being automatically skipped due to a failing init 2283%% conf case. It could also be a nested conf case that gets skipped 2284%% because of a failed or skipped top level conf. 2285%% 2286%% {auto_skip_case,{Case,Comment},Mode} This is a normal test case which 2287%% gets automatically skipped because of a failing init conf case or 2288%% because of a failing previous test case in a sequence. 2289%% 2290%% ------------------------------------------------------------------- 2291%% Description of IO handling during execution of parallel test cases: 2292%% ------------------------------------------------------------------- 2293%% 2294%% A conf group can have an associated list of properties. If the 2295%% parallel property is specified for a group, it means the test cases 2296%% should be spawned and run in parallel rather than called sequentially 2297%% (which is always the default mode). Test cases that execute in parallel 2298%% also write to their respective minor log files in parallel. Printouts 2299%% to common log files, such as the summary html file and the major log 2300%% file on text format, still have to be processed sequentially. For this 2301%% reason, the Mode argument specifies if a parallel group is currently 2302%% being executed. 2303%% 2304%% The low-level mechanism for buffering IO for the common log files 2305%% is handled by the test_server_io module. Buffering is turned on by 2306%% test_server_io:start_transaction/0 and off by calling 2307%% test_server_io:end_transaction/0. The buffered data for the transaction 2308%% can printed by calling test_server_io:print_buffered/1. 2309%% 2310%% This module is responsible for turning on IO buffering and to later 2311%% test_server_io:print_buffered/1 to print the data. To help with this, 2312%% two variables in the process dictionary are used: 2313%% 'test_server_common_io_handler' and 'test_server_queued_io'. The values 2314%% are set to as following: 2315%% 2316%% Value Meaning 2317%% ----- ------- 2318%% undefined No parallel test cases running 2319%% {tc,Pid} Running test cases in a top-level parallel group 2320%% {Ref,Pid} Running sequential test case inside a parallel group 2321%% 2322%% FIXME: The Pid is no longer used. 2323%% 2324%% If a conf group nested under a parallel group in the test 2325%% specification should be started, the 'test_server_common_io_handler' 2326%% value gets set also on the main process. 2327%% 2328%% During execution of a parallel group (or of a group nested under a 2329%% parallel group), *any* new test case being started gets registered 2330%% in a list saved in the dictionary with 'test_server_queued_io' as key. 2331%% When the top level parallel group is finished (only then can we be 2332%% sure all parallel test cases have finished and "reported in"), the 2333%% list of test cases is traversed in order and test_server_io:print_buffered/1 2334%% can be called for each test case. See handle_test_case_io_and_status/0 2335%% for details. 2336%% 2337%% To be able to handle nested conf groups with different properties, 2338%% the Mode argument specifies a list of {Ref,Properties} tuples. 2339%% The head of the Mode list at any given time identifies the group 2340%% currently being processed. The tail of the list identifies groups 2341%% on higher level. 2342%% 2343%% ------------------------------------------------------------------- 2344%% Notes on parallel execution of test cases 2345%% ------------------------------------------------------------------- 2346%% 2347%% A group nested under a parallel group will start executing in 2348%% parallel with previous (parallel) test cases (no matter what 2349%% properties the nested group has). Test cases are however never 2350%% executed in parallel with the start or end conf case of the same 2351%% group! Because of this, the test_server_ctrl loop waits at 2352%% the end conf of a group for all parallel cases to finish 2353%% before the end conf case actually executes. This has the effect 2354%% that it's only after a nested group has finished that any 2355%% remaining parallel cases in the previous group get spawned (*). 2356%% Example (all parallel cases): 2357%% 2358%% group1_init |----> 2359%% group1_case1 | ---------> 2360%% group1_case2 | ---------------------------------> 2361%% group2_init | ----> 2362%% group2_case1 | ------> 2363%% group2_case2 | ----------> 2364%% group2_end | ---> 2365%% group1_case3 (*)| ----> 2366%% group1_case4 (*)| --> 2367%% group1_end | ---> 2368%% 2369 2370run_test_cases_loop([{SkipTag,CaseData={Type,_Ref,_Case,_Comment}}|Cases], 2371 Config, TimetrapData, Mode, Status) when 2372 ((SkipTag==auto_skip_case) or (SkipTag==skip_case)) and 2373 ((Type==conf) or (Type==make)) -> 2374 run_test_cases_loop([{SkipTag,CaseData,Mode}|Cases], 2375 Config, TimetrapData, Mode, Status); 2376 2377run_test_cases_loop([{SkipTag,{Type,Ref,Case,Comment},SkipMode}|Cases], 2378 Config, TimetrapData, Mode, Status) when 2379 ((SkipTag==auto_skip_case) or (SkipTag==skip_case)) and 2380 ((Type==conf) or (Type==make)) -> 2381 ok = file:set_cwd(filename:dirname(get(test_server_dir))), 2382 CurrIOHandler = get(test_server_common_io_handler), 2383 ParentMode = tl(Mode), 2384 2385 {AutoOrUser,ReportTag} = 2386 if SkipTag == auto_skip_case -> {auto,tc_auto_skip}; 2387 SkipTag == skip_case -> {user,tc_user_skip} 2388 end, 2389 2390 %% check and update the mode for test case execution and io msg handling 2391 case {curr_ref(Mode),check_props(parallel, Mode)} of 2392 {Ref,Ref} -> 2393 case check_props(parallel, ParentMode) of 2394 false -> 2395 %% this is a skipped end conf for a top level parallel 2396 %% group, buffered io can be flushed 2397 _ = handle_test_case_io_and_status(), 2398 set_io_buffering(undefined), 2399 {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, 2400 false, SkipMode), 2401 ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, 2402 test_server_sup:framework_call(report, 2403 [ReportTag,ConfData]), 2404 run_test_cases_loop(Cases, Config, TimetrapData, ParentMode, 2405 delete_status(Ref, Status)); 2406 _ -> 2407 %% this is a skipped end conf for a parallel group nested 2408 %% under a parallel group (io buffering is active) 2409 _ = wait_for_cases(Ref), 2410 {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, 2411 true, SkipMode), 2412 ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, 2413 test_server_sup:framework_call(report, [ReportTag,ConfData]), 2414 case CurrIOHandler of 2415 {Ref,_} -> 2416 %% current_io_handler was set by start conf of this 2417 %% group, so we can unset it now (no more io from main 2418 %% process needs to be buffered) 2419 set_io_buffering(undefined); 2420 _ -> 2421 ok 2422 end, 2423 run_test_cases_loop(Cases, Config, 2424 TimetrapData, ParentMode, 2425 delete_status(Ref, Status)) 2426 end; 2427 {Ref,false} -> 2428 %% this is a skipped end conf for a non-parallel group that's not 2429 %% nested under a parallel group 2430 {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, 2431 false, SkipMode), 2432 ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, 2433 test_server_sup:framework_call(report, [ReportTag,ConfData]), 2434 2435 %% Check if this group is auto skipped because of error in the 2436 %% init conf. If so, check if the parent group is a sequence, 2437 %% and if it is, skip all proceeding tests in that group. 2438 GrName = get_name(Mode), 2439 Cases1 = 2440 case get_tc_results(Status) of 2441 {_,_,Fails} when length(Fails) > 0 -> 2442 case lists:member({group_result,GrName}, Fails) of 2443 true -> 2444 case check_prop(sequence, ParentMode) of 2445 false -> 2446 Cases; 2447 ParentRef -> 2448 Reason = {group_result,GrName,failed}, 2449 skip_cases_upto(ParentRef, Cases, 2450 Reason, tc, ParentMode, 2451 SkipTag) 2452 end; 2453 false -> 2454 Cases 2455 end; 2456 _ -> 2457 Cases 2458 end, 2459 run_test_cases_loop(Cases1, Config, TimetrapData, ParentMode, 2460 delete_status(Ref, Status)); 2461 {Ref,_} -> 2462 %% this is a skipped end conf for a non-parallel group nested under 2463 %% a parallel group (io buffering is active) 2464 {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, 2465 true, SkipMode), 2466 ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, 2467 test_server_sup:framework_call(report, [ReportTag,ConfData]), 2468 case CurrIOHandler of 2469 {Ref,_} -> 2470 %% current_io_handler was set by start conf of this 2471 %% group, so we can unset it now (no more io from main 2472 %% process needs to be buffered) 2473 set_io_buffering(undefined); 2474 _ -> 2475 ok 2476 end, 2477 run_test_cases_loop(Cases, Config, TimetrapData, tl(Mode), 2478 delete_status(Ref, Status)); 2479 {_,false} -> 2480 %% this is a skipped start conf for a group which is not nested 2481 %% under a parallel group 2482 {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, 2483 false, SkipMode), 2484 ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, 2485 test_server_sup:framework_call(report, [ReportTag,ConfData]), 2486 run_test_cases_loop(Cases, Config, TimetrapData, 2487 [conf(Ref,[])|Mode], Status); 2488 {_,Ref0} when is_reference(Ref0) -> 2489 %% this is a skipped start conf for a group nested under a parallel 2490 %% group and if this is the first nested group, io buffering must 2491 %% be activated 2492 if CurrIOHandler == undefined -> 2493 set_io_buffering({Ref,self()}); 2494 true -> 2495 ok 2496 end, 2497 {Mod,Func} = skip_case(AutoOrUser, Ref, 0, Case, Comment, 2498 true, SkipMode), 2499 ConfData = {Mod,{Func,get_name(SkipMode)},Comment}, 2500 test_server_sup:framework_call(report, [ReportTag,ConfData]), 2501 run_test_cases_loop(Cases, Config, TimetrapData, 2502 [conf(Ref,[])|Mode], Status) 2503 end; 2504 2505run_test_cases_loop([{auto_skip_case,{Case,Comment},SkipMode}|Cases], 2506 Config, TimetrapData, Mode, Status) -> 2507 {Mod,Func} = skip_case(auto, undefined, get(test_server_case_num)+1, 2508 Case, Comment, is_io_buffered(), SkipMode), 2509 test_server_sup:framework_call(report, [tc_auto_skip, 2510 {Mod,{Func,get_name(SkipMode)}, 2511 Comment}]), 2512 run_test_cases_loop(Cases, Config, TimetrapData, Mode, 2513 update_status(skipped, Mod, Func, Status)); 2514 2515run_test_cases_loop([{skip_case,{{Mod,all}=Case,Comment},SkipMode}|Cases], 2516 Config, TimetrapData, Mode, Status) -> 2517 _ = skip_case(user, undefined, 0, Case, Comment, false, SkipMode), 2518 test_server_sup:framework_call(report, [tc_user_skip, 2519 {Mod,{all,get_name(SkipMode)}, 2520 Comment}]), 2521 run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status); 2522 2523run_test_cases_loop([{skip_case,{Case,Comment},SkipMode}|Cases], 2524 Config, TimetrapData, Mode, Status) -> 2525 {Mod,Func} = skip_case(user, undefined, get(test_server_case_num)+1, 2526 Case, Comment, is_io_buffered(), SkipMode), 2527 test_server_sup:framework_call(report, [tc_user_skip, 2528 {Mod,{Func,get_name(SkipMode)}, 2529 Comment}]), 2530 run_test_cases_loop(Cases, Config, TimetrapData, Mode, 2531 update_status(skipped, Mod, Func, Status)); 2532 2533%% a start *or* end conf case, wrapping test cases or other conf cases 2534run_test_cases_loop([{conf,Ref,Props,{Mod,Func}}|_Cases]=Cs0, 2535 Config, TimetrapData, Mode0, Status) -> 2536 CurrIOHandler = get(test_server_common_io_handler), 2537 %% check and update the mode for test case execution and io msg handling 2538 {StartConf,Mode,IOHandler,ConfTime,Status1} = 2539 case {curr_ref(Mode0),check_props(parallel, Mode0)} of 2540 {Ref,Ref} -> 2541 case check_props(parallel, tl(Mode0)) of 2542 false -> 2543 %% this is an end conf for a top level parallel group, 2544 %% collect results from the test case processes 2545 %% and calc total time 2546 OkSkipFail = handle_test_case_io_and_status(), 2547 ok = file:set_cwd(filename:dirname(get(test_server_dir))), 2548 After = ?now, 2549 Before = get(test_server_parallel_start_time), 2550 Elapsed = timer:now_diff(After, Before)/1000000, 2551 put(test_server_total_time, Elapsed), 2552 {false,tl(Mode0),undefined,Elapsed, 2553 update_status(Ref, OkSkipFail, Status)}; 2554 _ -> 2555 %% this is an end conf for a parallel group nested under a 2556 %% parallel group (io buffering is active) 2557 OkSkipFail = wait_for_cases(Ref), 2558 queue_test_case_io(Ref, self(), 0, Mod, Func), 2559 Elapsed = timer:now_diff(?now, conf_start(Ref, Mode0))/1000000, 2560 case CurrIOHandler of 2561 {Ref,_} -> 2562 %% current_io_handler was set by start conf of this 2563 %% group, so we can unset it after this case (no 2564 %% more io from main process needs to be buffered) 2565 {false,tl(Mode0),undefined,Elapsed, 2566 update_status(Ref, OkSkipFail, Status)}; 2567 _ -> 2568 {false,tl(Mode0),CurrIOHandler,Elapsed, 2569 update_status(Ref, OkSkipFail, Status)} 2570 end 2571 end; 2572 {Ref,false} -> 2573 %% this is an end conf for a non-parallel group that's not 2574 %% nested under a parallel group, so no need to buffer io 2575 {false,tl(Mode0),undefined, 2576 timer:now_diff(?now, conf_start(Ref, Mode0))/1000000, Status}; 2577 {Ref,_} -> 2578 %% this is an end conf for a non-parallel group nested under 2579 %% a parallel group (io buffering is active) 2580 queue_test_case_io(Ref, self(), 0, Mod, Func), 2581 Elapsed = timer:now_diff(?now, conf_start(Ref, Mode0))/1000000, 2582 case CurrIOHandler of 2583 {Ref,_} -> 2584 %% current_io_handler was set by start conf of this 2585 %% group, so we can unset it after this case (no 2586 %% more io from main process needs to be buffered) 2587 {false,tl(Mode0),undefined,Elapsed,Status}; 2588 _ -> 2589 {false,tl(Mode0),CurrIOHandler,Elapsed,Status} 2590 end; 2591 {_,false} -> 2592 %% this is a start conf for a group which is not nested under a 2593 %% parallel group, check if this case starts a new parallel group 2594 case lists:member(parallel, Props) of 2595 true -> 2596 %% prepare for execution of parallel group 2597 put(test_server_parallel_start_time, ?now), 2598 put(test_server_queued_io, []); 2599 false -> 2600 ok 2601 end, 2602 {true,[conf(Ref,Props)|Mode0],undefined,0,Status}; 2603 {_,_Ref0} -> 2604 %% this is a start conf for a group nested under a parallel group, the 2605 %% parallel_start_time and parallel_test_cases values have already been set 2606 queue_test_case_io(Ref, self(), 0, Mod, Func), 2607 %% if this is the first nested group under a parallel group, io 2608 %% buffering must be activated 2609 IOHandler1 = if CurrIOHandler == undefined -> 2610 IOH = {Ref,self()}, 2611 set_io_buffering(IOH), 2612 IOH; 2613 true -> 2614 CurrIOHandler 2615 end, 2616 {true,[conf(Ref,Props)|Mode0],IOHandler1,0,Status} 2617 end, 2618 2619 %% if this is a start conf we check if cases should be shuffled 2620 {[_Conf|Cases1]=Cs1,Shuffle} = 2621 if StartConf -> 2622 case get_shuffle(Props) of 2623 undefined -> 2624 {Cs0,undefined}; 2625 {_,repeated} -> 2626 %% if group is repeated, a new seed should not be set every 2627 %% turn - last one is saved in dictionary 2628 CurrSeed = get(test_server_curr_random_seed), 2629 {shuffle_cases(Ref, Cs0, CurrSeed),{shuffle,CurrSeed}}; 2630 {_,Seed} -> 2631 UseSeed= 2632 %% Determine which seed to use by: 2633 %% 1. check the TS_RANDOM_SEED env variable 2634 %% 2. check random_seed in process state 2635 %% 3. use value provided with shuffle option 2636 %% 4. use timestamp() values for seed 2637 case os:getenv("TS_RANDOM_SEED") of 2638 Undef when Undef == false ; Undef == "undefined" -> 2639 case get(test_server_random_seed) of 2640 undefined -> Seed; 2641 TSRS -> TSRS 2642 end; 2643 NumStr -> 2644 %% Ex: "123 456 789" or "123,456,789" -> {123,456,789} 2645 list_to_tuple([list_to_integer(NS) || 2646 NS <- string:lexemes(NumStr, [$ ,$:,$,])]) 2647 end, 2648 {shuffle_cases(Ref, Cs0, UseSeed),{shuffle,UseSeed}} 2649 end; 2650 not StartConf -> 2651 {Cs0,undefined} 2652 end, 2653 2654 %% if this is a start conf we check if Props specifies repeat and if so 2655 %% we copy the group and carry the copy until the end conf where we 2656 %% decide to perform the repetition or not 2657 {Repeating,Status2,Cases,ReportRepeatStop} = 2658 if StartConf -> 2659 case get_repeat(Props) of 2660 undefined -> 2661 %% we *must* have a status entry for every conf since we 2662 %% will continously update status with test case results 2663 %% without knowing the Ref (but update hd(Status)) 2664 {false,new_status(Ref, Status1),Cases1,?void_fun}; 2665 {_RepType,N} when N =< 1 -> 2666 {false,new_status(Ref, Status1),Cases1,?void_fun}; 2667 _ -> 2668 {Copied,_} = copy_cases(Ref, make_ref(), Cs1), 2669 {true,new_status(Ref, Copied, Status1),Cases1,?void_fun} 2670 end; 2671 not StartConf -> 2672 RepVal = get_repeat(get_props(Mode0)), 2673 ReportStop = 2674 fun() -> 2675 print(minor, "~n*** Stopping repeat operation ~w", [RepVal]), 2676 print(1, "Stopping repeat operation ~w", [RepVal]) 2677 end, 2678 CopiedCases = get_copied_cases(Status1), 2679 EndStatus = delete_status(Ref, Status1), 2680 %% check in Mode0 if this is a repeat conf 2681 case RepVal of 2682 undefined -> 2683 {false,EndStatus,Cases1,?void_fun}; 2684 {_RepType,N} when N =< 1 -> 2685 {false,EndStatus,Cases1,?void_fun}; 2686 {repeat,_} -> 2687 {true,EndStatus,CopiedCases++Cases1,?void_fun}; 2688 {repeat_until_all_ok,_} -> 2689 {RestCs,Fun} = case get_tc_results(Status1) of 2690 {_,_,[]} -> 2691 {Cases1,ReportStop}; 2692 _ -> 2693 {CopiedCases++Cases1,?void_fun} 2694 end, 2695 {true,EndStatus,RestCs,Fun}; 2696 {repeat_until_any_ok,_} -> 2697 {RestCs,Fun} = case get_tc_results(Status1) of 2698 {Ok,_,_Fails} when length(Ok) > 0 -> 2699 {Cases1,ReportStop}; 2700 _ -> 2701 {CopiedCases++Cases1,?void_fun} 2702 end, 2703 {true,EndStatus,RestCs,Fun}; 2704 {repeat_until_any_fail,_} -> 2705 {RestCs,Fun} = case get_tc_results(Status1) of 2706 {_,_,Fails} when length(Fails) > 0 -> 2707 {Cases1,ReportStop}; 2708 _ -> 2709 {CopiedCases++Cases1,?void_fun} 2710 end, 2711 {true,EndStatus,RestCs,Fun}; 2712 {repeat_until_all_fail,_} -> 2713 {RestCs,Fun} = case get_tc_results(Status1) of 2714 {[],_,_} -> 2715 {Cases1,ReportStop}; 2716 _ -> 2717 {CopiedCases++Cases1,?void_fun} 2718 end, 2719 {true,EndStatus,RestCs,Fun} 2720 end 2721 end, 2722 2723 ReportAbortRepeat = fun(What) when Repeating -> 2724 print(minor, "~n*** Aborting repeat operation " 2725 "(configuration case ~w)", [What]), 2726 print(1, "Aborting repeat operation " 2727 "(configuration case ~w)", [What]); 2728 (_) -> ok 2729 end, 2730 CfgProps = if StartConf -> 2731 if Shuffle == undefined -> 2732 [{tc_group_properties,Props}]; 2733 true -> 2734 [{tc_group_properties, 2735 [Shuffle|delete_shuffle(Props)]}] 2736 end; 2737 not StartConf -> 2738 {TcOk,TcSkip,TcFail} = get_tc_results(Status1), 2739 [{tc_group_properties,get_props(Mode0)}, 2740 {tc_group_result,[{ok,TcOk}, 2741 {skipped,TcSkip}, 2742 {failed,TcFail}]}] 2743 end, 2744 2745 SuiteName = proplists:get_value(suite, Props), 2746 case get(test_server_create_priv_dir) of 2747 auto_per_run -> % use common priv_dir 2748 TSDirs = [{priv_dir,get(test_server_priv_dir)}, 2749 {data_dir,get_data_dir(Mod, SuiteName)}]; 2750 _ -> 2751 TSDirs = [{data_dir,get_data_dir(Mod, SuiteName)}] 2752 end, 2753 2754 ActualCfg = 2755 if not StartConf -> 2756 update_config(hd(Config), TSDirs ++ CfgProps); 2757 true -> 2758 GroupPath = lists:flatmap(fun({_Ref,[],_T}) -> []; 2759 ({_Ref,GrProps,_T}) -> [GrProps] 2760 end, Mode0), 2761 update_config(hd(Config), 2762 TSDirs ++ [{tc_group_path,GroupPath} | CfgProps]) 2763 end, 2764 2765 CurrMode = curr_mode(Ref, Mode0, Mode), 2766 ConfCaseResult = run_test_case(Ref, 0, Mod, Func, [ActualCfg], skip_init, 2767 TimetrapData, CurrMode), 2768 2769 case ConfCaseResult of 2770 {_,NewCfg,_} when Func == init_per_suite, is_list(NewCfg) -> 2771 %% check that init_per_suite returned data on correct format 2772 case lists:filter(fun({_,_}) -> false; 2773 (_) -> true end, NewCfg) of 2774 [] -> 2775 set_io_buffering(IOHandler), 2776 stop_minor_log_file(), 2777 run_test_cases_loop(Cases, [NewCfg|Config], 2778 TimetrapData, Mode, Status2); 2779 Bad -> 2780 print(minor, 2781 "~n*** ~tw returned bad elements in Config: ~tp.~n", 2782 [Func,Bad]), 2783 Reason = {failed,{Mod,init_per_suite,bad_return}}, 2784 Cases2 = skip_cases_upto(Ref, Cases, Reason, conf, CurrMode, 2785 auto_skip_case), 2786 set_io_buffering(IOHandler), 2787 stop_minor_log_file(), 2788 run_test_cases_loop(Cases2, Config, TimetrapData, Mode, 2789 delete_status(Ref, Status2)) 2790 end; 2791 {_,NewCfg,_} when StartConf, is_list(NewCfg) -> 2792 print_conf_time(ConfTime), 2793 set_io_buffering(IOHandler), 2794 stop_minor_log_file(), 2795 run_test_cases_loop(Cases, [NewCfg|Config], TimetrapData, Mode, Status2); 2796 {_,{framework_error,{FwMod,FwFunc},Reason},_} -> 2797 print(minor, "~n*** ~w failed in ~tw. Reason: ~tp~n", 2798 [FwMod,FwFunc,Reason]), 2799 print(1, "~w failed in ~tw. Reason: ~tp~n", [FwMod,FwFunc,Reason]), 2800 exit(framework_error); 2801 {_,Fail,_} when element(1,Fail) == 'EXIT'; 2802 element(1,Fail) == timetrap_timeout; 2803 element(1,Fail) == user_timetrap_error; 2804 element(1,Fail) == failed -> 2805 {Cases2,Config1,Status3} = 2806 if StartConf -> 2807 ReportAbortRepeat(failed), 2808 print(minor, "~n*** ~tw failed.~n" 2809 " Skipping all cases.", [Func]), 2810 Reason = {failed,{Mod,Func,Fail}}, 2811 {skip_cases_upto(Ref, Cases, Reason, conf, CurrMode, 2812 auto_skip_case), 2813 Config, 2814 update_status(failed, group_result, get_name(Mode), 2815 delete_status(Ref, Status2))}; 2816 not StartConf -> 2817 ReportRepeatStop(), 2818 print_conf_time(ConfTime), 2819 {Cases,tl(Config),delete_status(Ref, Status2)} 2820 end, 2821 set_io_buffering(IOHandler), 2822 stop_minor_log_file(), 2823 run_test_cases_loop(Cases2, Config1, TimetrapData, Mode, Status3); 2824 2825 {_,{auto_skip,SkipReason},_} -> 2826 %% this case can only happen if the framework (not the user) 2827 %% decides to skip execution of a conf function 2828 {Cases2,Config1,Status3} = 2829 if StartConf -> 2830 ReportAbortRepeat(auto_skipped), 2831 print(minor, "~n*** ~tw auto skipped.~n" 2832 " Skipping all cases.", [Func]), 2833 {skip_cases_upto(Ref, Cases, SkipReason, conf, CurrMode, 2834 auto_skip_case), 2835 Config, 2836 delete_status(Ref, Status2)}; 2837 not StartConf -> 2838 ReportRepeatStop(), 2839 print_conf_time(ConfTime), 2840 {Cases,tl(Config),delete_status(Ref, Status2)} 2841 end, 2842 set_io_buffering(IOHandler), 2843 stop_minor_log_file(), 2844 run_test_cases_loop(Cases2, Config1, TimetrapData, Mode, Status3); 2845 2846 {_,{Skip,Reason},_} when StartConf and ((Skip==skip) or (Skip==skipped)) -> 2847 ReportAbortRepeat(skipped), 2848 print(minor, "~n*** ~tw skipped.~n" 2849 " Skipping all cases.", [Func]), 2850 set_io_buffering(IOHandler), 2851 stop_minor_log_file(), 2852 run_test_cases_loop(skip_cases_upto(Ref, Cases, Reason, conf, 2853 CurrMode, skip_case), 2854 [hd(Config)|Config], TimetrapData, Mode, 2855 delete_status(Ref, Status2)); 2856 {_,{skip_and_save,Reason,_SavedConfig},_} when StartConf -> 2857 ReportAbortRepeat(skipped), 2858 print(minor, "~n*** ~tw skipped.~n" 2859 " Skipping all cases.", [Func]), 2860 set_io_buffering(IOHandler), 2861 stop_minor_log_file(), 2862 run_test_cases_loop(skip_cases_upto(Ref, Cases, Reason, conf, 2863 CurrMode, skip_case), 2864 [hd(Config)|Config], TimetrapData, Mode, 2865 delete_status(Ref, Status2)); 2866 {_,_Other,_} when Func == init_per_suite -> 2867 print(minor, "~n*** init_per_suite failed to return a Config list.~n", []), 2868 Reason = {failed,{Mod,init_per_suite,bad_return}}, 2869 Cases2 = skip_cases_upto(Ref, Cases, Reason, conf, CurrMode, 2870 auto_skip_case), 2871 set_io_buffering(IOHandler), 2872 stop_minor_log_file(), 2873 run_test_cases_loop(Cases2, Config, TimetrapData, Mode, 2874 delete_status(Ref, Status2)); 2875 {_,_Other,_} when StartConf -> 2876 print_conf_time(ConfTime), 2877 set_io_buffering(IOHandler), 2878 ReportRepeatStop(), 2879 stop_minor_log_file(), 2880 run_test_cases_loop(Cases, [hd(Config)|Config], TimetrapData, 2881 Mode, Status2); 2882 {_,_EndConfRetVal,Opts} -> 2883 %% Check if return_group_result is set (ok, skipped or failed) and 2884 %% if so: 2885 %% 1) *If* the parent group is a sequence, skip all proceeding tests 2886 %% in that group. 2887 %% 2) Return the value to the group "above" so that result may be 2888 %% used for evaluating a 'repeat_until_*' property. 2889 GrName = get_name(Mode0, Func), 2890 {Cases2,Status3} = 2891 case lists:keysearch(return_group_result, 1, Opts) of 2892 {value,{_,failed}} -> 2893 case {curr_ref(Mode),check_prop(sequence, Mode)} of 2894 {ParentRef,ParentRef} -> 2895 Reason = {group_result,GrName,failed}, 2896 {skip_cases_upto(ParentRef, Cases, Reason, tc, 2897 Mode, auto_skip_case), 2898 update_status(failed, group_result, GrName, 2899 delete_status(Ref, Status2))}; 2900 _ -> 2901 {Cases,update_status(failed, group_result, GrName, 2902 delete_status(Ref, Status2))} 2903 end; 2904 {value,{_,GroupResult}} -> 2905 {Cases,update_status(GroupResult, group_result, GrName, 2906 delete_status(Ref, Status2))}; 2907 false -> 2908 {Cases,update_status(ok, group_result, GrName, 2909 delete_status(Ref, Status2))} 2910 end, 2911 print_conf_time(ConfTime), 2912 ReportRepeatStop(), 2913 set_io_buffering(IOHandler), 2914 stop_minor_log_file(), 2915 run_test_cases_loop(Cases2, tl(Config), TimetrapData, 2916 Mode, Status3) 2917 end; 2918 2919run_test_cases_loop([{make,Ref,{Mod,Func,Args}}|Cases0], Config, TimetrapData, 2920 Mode, Status) -> 2921 case run_test_case(Ref, 0, Mod, Func, Args, skip_init, TimetrapData) of 2922 {_,Why={'EXIT',_},_} -> 2923 print(minor, "~n*** ~tw failed.~n" 2924 " Skipping all cases.", [Func]), 2925 Reason = {failed,{Mod,Func,Why}}, 2926 Cases = skip_cases_upto(Ref, Cases0, Reason, conf, Mode, 2927 auto_skip_case), 2928 stop_minor_log_file(), 2929 run_test_cases_loop(Cases, Config, TimetrapData, Mode, Status); 2930 {_,_Whatever,_} -> 2931 stop_minor_log_file(), 2932 run_test_cases_loop(Cases0, Config, TimetrapData, Mode, Status) 2933 end; 2934 2935run_test_cases_loop([{conf,_Ref,_Props,_X}=Conf|_Cases0], 2936 Config, _TimetrapData, _Mode, _Status) -> 2937 erlang:error(badarg, [Conf,Config]); 2938 2939run_test_cases_loop([{repeat,Case,{RepeatType,N}}|Cases0], Config, 2940 TimeTrapData, Mode, Status) -> 2941 Ref = make_ref(), 2942 Parallel = check_prop(parallel, Mode) =/= false, 2943 Sequence = check_prop(sequence, Mode) =/= false, 2944 RepeatStop = RepeatType=:=repeat_until_fail 2945 orelse RepeatType=:=repeat_until_ok, 2946 2947 if Parallel andalso RepeatStop -> 2948 %% Cannot check results of test case during parallal 2949 %% execution, so only RepeatType=:=repeat is allowed in 2950 %% combination with parallel groups. 2951 erlang:error({illegal_combination,{parallel,RepeatType}}); 2952 Sequence andalso RepeatStop -> 2953 %% Sequence is stop on fail + skip rest, so only 2954 %% RepeatType=:=repeat makes sense inside a sequence. 2955 erlang:error({illegal_combination,{sequence,RepeatType}}); 2956 true -> 2957 Mode1 = [{Ref,[{repeat,{RepeatType,1,N}}],?now}|Mode], 2958 run_test_cases_loop([Case | Cases0], Config, TimeTrapData, 2959 Mode1, Status) 2960 end; 2961 2962run_test_cases_loop([{Mod,Case}|Cases], Config, TimetrapData, Mode, Status) -> 2963 ActualCfg = 2964 case get(test_server_create_priv_dir) of 2965 auto_per_run -> 2966 update_config(hd(Config), [{priv_dir,get(test_server_priv_dir)}, 2967 {data_dir,get_data_dir(Mod)}]); 2968 _ -> 2969 update_config(hd(Config), [{data_dir,get_data_dir(Mod)}]) 2970 end, 2971 run_test_cases_loop([{Mod,Case,[ActualCfg]}|Cases], Config, 2972 TimetrapData, Mode, Status); 2973 2974run_test_cases_loop([{Mod,Func,Args}=Case|Cases], Config, TimetrapData, Mode0, Status) -> 2975 {Num,RunInit} = 2976 case FwMod = get_fw_mod(?MODULE) of 2977 Mod when Func == error_in_suite -> 2978 {-1,skip_init}; 2979 _ -> 2980 {put(test_server_case_num, get(test_server_case_num)+1), 2981 run_init} 2982 end, 2983 2984 Mode = 2985 case Mode0 of 2986 [{_,[{repeat,{_,_,_}}],_}|RestMode] -> 2987 RestMode; 2988 _ -> 2989 Mode0 2990 end, 2991 2992 %% check the current execution mode and save info about the case if 2993 %% detected that printouts to common log files is handled later 2994 2995 case check_prop(parallel, Mode) =:= false andalso is_io_buffered() of 2996 true -> 2997 %% sequential test case nested in a parallel group; 2998 %% io is buffered, so we must queue this test case 2999 queue_test_case_io(undefined, self(), Num+1, Mod, Func); 3000 false -> 3001 ok 3002 end, 3003 3004 case run_test_case(undefined, Num+1, Mod, Func, Args, 3005 RunInit, TimetrapData, Mode) of 3006 %% callback to framework module failed, exit immediately 3007 {_,{framework_error,{FwMod,FwFunc},Reason},_} -> 3008 print(minor, "~n*** ~w failed in ~tw. Reason: ~tp~n", 3009 [FwMod,FwFunc,Reason]), 3010 print(1, "~w failed in ~tw. Reason: ~tp~n", [FwMod,FwFunc,Reason]), 3011 stop_minor_log_file(), 3012 exit(framework_error); 3013 %% sequential execution of test case finished 3014 {Time,RetVal,_} -> 3015 RetTag = 3016 if is_tuple(RetVal) -> element(1,RetVal); 3017 true -> undefined 3018 end, 3019 {Result,Failed,Status1} = 3020 case RetTag of 3021 Skip when Skip==skip; Skip==skipped -> 3022 {skipped,false,update_status(skipped, Mod, Func, Status)}; 3023 Fail when Fail=='EXIT'; Fail==failed -> 3024 {failed,true,update_status(failed, Mod, Func, Status)}; 3025 _ when Time==died, RetVal=/=ok -> 3026 {failed,true,update_status(failed, Mod, Func, Status)}; 3027 _ -> 3028 {ok,false,update_status(ok, Mod, Func, Status)} 3029 end, 3030 case check_prop(sequence, Mode) of 3031 false -> 3032 {Cases1,Mode1} = 3033 check_repeat_testcase(Case,Result,Cases,Mode0), 3034 stop_minor_log_file(), 3035 run_test_cases_loop(Cases1, Config, TimetrapData, Mode1, Status1); 3036 Ref -> 3037 %% the case is in a sequence; we must check the result and 3038 %% determine if the following cases should run or be skipped 3039 if not Failed -> % proceed with next case 3040 {Cases1,Mode1} = 3041 check_repeat_testcase(Case,Result,Cases,Mode0), 3042 stop_minor_log_file(), 3043 run_test_cases_loop(Cases1, Config, TimetrapData, Mode1, Status1); 3044 true -> % skip rest of cases in sequence 3045 print(minor, "~n*** ~tw failed.~n" 3046 " Skipping all other cases in sequence.", 3047 [Func]), 3048 {Cases1,Mode1} = 3049 check_repeat_testcase(Case,Result,Cases,Mode0), 3050 Reason = {failed,{Mod,Func}}, 3051 Cases2 = skip_cases_upto(Ref, Cases1, Reason, tc, 3052 Mode, auto_skip_case), 3053 stop_minor_log_file(), 3054 run_test_cases_loop(Cases2, Config, TimetrapData, Mode1, Status1) 3055 end 3056 end; 3057 %% the test case is being executed in parallel with the main process (and 3058 %% other test cases) and Pid is the dedicated process executing the case 3059 Pid -> 3060 %% io from Pid will be buffered by the test_server_io process and 3061 %% handled later, so we have to save info about the case 3062 queue_test_case_io(undefined, Pid, Num+1, Mod, Func), 3063 {Cases1,Mode1} = check_repeat_testcase(Case,ok,Cases,Mode0), 3064 run_test_cases_loop(Cases1, Config, TimetrapData, Mode1, Status) 3065 end; 3066 3067%% TestSpec processing finished 3068run_test_cases_loop([], _Config, _TimetrapData, _, _) -> 3069 ok. 3070 3071%%-------------------------------------------------------------------- 3072%% various help functions 3073 3074new_status(Ref, Status) -> 3075 [{Ref,{{[],[],[]},[]}} | Status]. 3076 3077new_status(Ref, CopiedCases, Status) -> 3078 [{Ref,{{[],[],[]},CopiedCases}} | Status]. 3079 3080delete_status(Ref, Status) -> 3081 lists:keydelete(Ref, 1, Status). 3082 3083update_status(ok, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) -> 3084 [{Ref,{{Ok++[{Mod,Func}],Skip,Fail},Cs}} | Status]; 3085 3086update_status(skipped, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) -> 3087 [{Ref,{{Ok,Skip++[{Mod,Func}],Fail},Cs}} | Status]; 3088 3089update_status(failed, Mod, Func, [{Ref,{{Ok,Skip,Fail},Cs}} | Status]) -> 3090 [{Ref,{{Ok,Skip,Fail++[{Mod,Func}]},Cs}} | Status]; 3091 3092update_status(_, _, _, []) -> 3093 []. 3094 3095update_status(Ref, {Ok,Skip,Fail}, [{Ref,{{Ok0,Skip0,Fail0},Cs}} | Status]) -> 3096 [{Ref,{{Ok0++Ok,Skip0++Skip,Fail0++Fail},Cs}} | Status]. 3097 3098get_copied_cases([{_,{_,Cases}} | _Status]) -> 3099 Cases. 3100 3101get_tc_results([{_,{OkSkipFail,_}} | _Status]) -> 3102 OkSkipFail; 3103get_tc_results([]) -> % in case init_per_suite crashed 3104 {[],[],[]}. 3105 3106conf(Ref, Props) -> 3107 {Ref,Props,?now}. 3108 3109curr_ref([{Ref,_Props,_}|_]) -> 3110 Ref; 3111curr_ref([]) -> 3112 undefined. 3113 3114curr_mode(Ref, Mode0, Mode1) -> 3115 case curr_ref(Mode1) of 3116 Ref -> Mode1; 3117 _ -> Mode0 3118 end. 3119 3120get_props([{_,Props,_} | _]) -> 3121 Props; 3122get_props([]) -> 3123 []. 3124 3125check_prop(_Attrib, []) -> 3126 false; 3127check_prop(Attrib, [{Ref,Props,_}|_]) -> 3128 case lists:member(Attrib, Props) of 3129 true -> Ref; 3130 false -> false 3131 end. 3132 3133check_props(Attrib, Mode) -> 3134 case [R || {R,Ps,_} <- Mode, lists:member(Attrib, Ps)] of 3135 [] -> false; 3136 [Ref|_] -> Ref 3137 end. 3138 3139get_name(Mode, Def) -> 3140 case get_name(Mode) of 3141 undefined -> Def; 3142 Name -> Name 3143 end. 3144 3145get_name([{_Ref,Props,_}|_]) -> 3146 proplists:get_value(name, Props); 3147get_name([]) -> 3148 undefined. 3149 3150conf_start(Ref, Mode) -> 3151 case lists:keysearch(Ref, 1, Mode) of 3152 {value,{_,_,T}} -> T; 3153 false -> 0 3154 end. 3155 3156 3157get_data_dir(Mod) -> 3158 get_data_dir(Mod, undefined). 3159 3160get_data_dir(Mod, Suite) -> 3161 UseMod = if Suite == undefined -> Mod; 3162 true -> Suite 3163 end, 3164 case code:which(UseMod) of 3165 non_existing -> 3166 print(12, "The module ~w is not loaded", [Mod]), 3167 []; 3168 cover_compiled -> 3169 MainCoverNode = cover:get_main_node(), 3170 {file,File} = rpc:call(MainCoverNode,cover,is_compiled,[UseMod]), 3171 do_get_data_dir(UseMod,File); 3172 FullPath -> 3173 do_get_data_dir(UseMod,FullPath) 3174 end. 3175 3176do_get_data_dir(Mod,File) -> 3177 filename:dirname(File) ++ "/" ++ atom_to_list(Mod) ++ ?data_dir_suffix. 3178 3179print_conf_time(0) -> 3180 ok; 3181print_conf_time(ConfTime) -> 3182 print(major, "=group_time ~.3fs", [ConfTime]), 3183 print(minor, "~n=== Total execution time of group: ~.3fs~n", [ConfTime]). 3184 3185print_props([]) -> 3186 ok; 3187print_props(Props) -> 3188 print(major, "=group_props ~tp", [Props]), 3189 print(minor, "Group properties: ~tp~n", [Props]). 3190 3191%% repeat N times: {repeat,N} 3192%% repeat N times or until all successful: {repeat_until_all_ok,N} 3193%% repeat N times or until at least one successful: {repeat_until_any_ok,N} 3194%% repeat N times or until at least one case fails: {repeat_until_any_fail,N} 3195%% repeat N times or until all fails: {repeat_until_all_fail,N} 3196%% N = integer() | forever 3197get_repeat(Props) -> 3198 get_prop([repeat,repeat_until_all_ok,repeat_until_any_ok, 3199 repeat_until_any_fail,repeat_until_all_fail], forever, Props). 3200 3201update_repeat(Props) -> 3202 case get_repeat(Props) of 3203 undefined -> 3204 Props; 3205 {RepType,N} -> 3206 Props1 = 3207 if N == forever -> 3208 [{RepType,N}|lists:keydelete(RepType, 1, Props)]; 3209 N < 3 -> 3210 lists:keydelete(RepType, 1, Props); 3211 N >= 3 -> 3212 [{RepType,N-1}|lists:keydelete(RepType, 1, Props)] 3213 end, 3214 %% if shuffle is used in combination with repeat, a new 3215 %% seed shouldn't be set every new turn 3216 case get_shuffle(Props1) of 3217 undefined -> 3218 Props1; 3219 _ -> 3220 [{shuffle,repeated}|delete_shuffle(Props1)] 3221 end 3222 end. 3223 3224get_shuffle(Props) -> 3225 get_prop([shuffle], ?now, Props). 3226 3227delete_shuffle(Props) -> 3228 delete_prop([shuffle], Props). 3229 3230%% Return {Item,Value} if found, else if Item alone 3231%% is found, return {Item,Default} 3232get_prop([Item|Items], Default, Props) -> 3233 case lists:keysearch(Item, 1, Props) of 3234 {value,R} -> 3235 R; 3236 false -> 3237 case lists:member(Item, Props) of 3238 true -> 3239 {Item,Default}; 3240 false -> 3241 get_prop(Items, Default, Props) 3242 end 3243 end; 3244get_prop([], _Def, _Props) -> 3245 undefined. 3246 3247delete_prop([Item|Items], Props) -> 3248 Props1 = lists:delete(Item, lists:keydelete(Item, 1, Props)), 3249 delete_prop(Items, Props1); 3250delete_prop([], Props) -> 3251 Props. 3252 3253%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3254%% shuffle_cases(Ref, Cases, Seed) -> Cases1 3255%% 3256%% Shuffles the order of Cases. 3257 3258shuffle_cases(Ref, Cases, undefined) -> 3259 shuffle_cases(Ref, Cases, rand:seed_s(exsplus)); 3260 3261shuffle_cases(Ref, [{conf,Ref,_,_}=Start | Cases], Seed0) -> 3262 {N,CasesToShuffle,Rest} = cases_to_shuffle(Ref, Cases), 3263 Seed = case Seed0 of 3264 {X,Y,Z} when is_integer(X+Y+Z) -> 3265 rand:seed(exsplus, Seed0); 3266 _ -> 3267 Seed0 3268 end, 3269 ShuffledCases = random_order(N, rand:uniform_s(N, Seed), CasesToShuffle, []), 3270 [Start|ShuffledCases] ++ Rest. 3271 3272cases_to_shuffle(Ref, Cases) -> 3273 cases_to_shuffle(Ref, Cases, 1, []). 3274 3275cases_to_shuffle(Ref, [{conf,Ref,_,_} | _]=Cs, N, Ix) -> % end 3276 {N-1,Ix,Cs}; 3277cases_to_shuffle(Ref, [{skip_case,{_,Ref,_,_},_} | _]=Cs, N, Ix) -> % end 3278 {N-1,Ix,Cs}; 3279 3280cases_to_shuffle(Ref, [{conf,Ref1,_,_}=C | Cs], N, Ix) -> % nested group 3281 {Cs1,Rest} = get_subcases(Ref1, Cs, []), 3282 cases_to_shuffle(Ref, Rest, N+1, [{N,[C|Cs1]} | Ix]); 3283cases_to_shuffle(Ref, [{skip_case,{_,Ref1,_,_},_}=C | Cs], N, Ix) -> % nested group 3284 {Cs1,Rest} = get_subcases(Ref1, Cs, []), 3285 cases_to_shuffle(Ref, Rest, N+1, [{N,[C|Cs1]} | Ix]); 3286 3287cases_to_shuffle(Ref, [C | Cs], N, Ix) -> 3288 cases_to_shuffle(Ref, Cs, N+1, [{N,[C]} | Ix]). 3289 3290get_subcases(SubRef, [{conf,SubRef,_,_}=C | Cs], SubCs) -> 3291 {lists:reverse([C|SubCs]),Cs}; 3292get_subcases(SubRef, [{skip_case,{_,SubRef,_,_},_}=C | Cs], SubCs) -> 3293 {lists:reverse([C|SubCs]),Cs}; 3294get_subcases(SubRef, [C|Cs], SubCs) -> 3295 get_subcases(SubRef, Cs, [C|SubCs]). 3296 3297random_order(1, {_Pos,Seed}, [{_Ix,CaseOrGroup}], Shuffled) -> 3298 %% save current seed to be used if test cases are repeated 3299 put(test_server_curr_random_seed, Seed), 3300 Shuffled++CaseOrGroup; 3301random_order(N, {Pos,NewSeed}, IxCases, Shuffled) -> 3302 {First,[{_Ix,CaseOrGroup}|Rest]} = lists:split(Pos-1, IxCases), 3303 random_order(N-1, rand:uniform_s(N-1, NewSeed), 3304 First++Rest, Shuffled++CaseOrGroup). 3305 3306 3307%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3308%% skip_case(Type, Ref, CaseNum, Case, Comment, SendSync) -> {Mod,Func} 3309%% 3310%% Prints info about a skipped case in the major and html log files. 3311%% SendSync determines if start and finished messages must be sent so 3312%% that the printouts can be buffered and handled in order with io from 3313%% parallel processes. 3314skip_case(Type, Ref, CaseNum, Case, Comment, SendSync, Mode) -> 3315 MF = {Mod,Func} = case Case of 3316 {M,F,_A} -> {M,F}; 3317 {M,F} -> {M,F} 3318 end, 3319 if SendSync -> 3320 queue_test_case_io(Ref, self(), CaseNum, Mod, Func), 3321 self() ! {started,Ref,self(),CaseNum,Mod,Func}, 3322 test_server_io:start_transaction(), 3323 skip_case1(Type, CaseNum, Mod, Func, Comment, Mode), 3324 test_server_io:end_transaction(), 3325 self() ! {finished,Ref,self(),CaseNum,Mod,Func,skipped,{0,skipped,[]}}; 3326 not SendSync -> 3327 skip_case1(Type, CaseNum, Mod, Func, Comment, Mode) 3328 end, 3329 MF. 3330 3331skip_case1(Type, CaseNum, Mod, Func, Comment, Mode) -> 3332 {{Col0,Col1},_} = get_font_style((CaseNum > 0), Mode), 3333 ResultCol = if Type == auto -> ?auto_skip_color; 3334 Type == user -> ?user_skip_color 3335 end, 3336 print(major, "~n=case ~w:~tw", [Mod,Func]), 3337 GroupName = case get_name(Mode) of 3338 undefined -> 3339 ""; 3340 GrName -> 3341 GrName1 = cast_to_list(GrName), 3342 print(major, "=group_props ~tp", [[{name,GrName1}]]), 3343 GrName1 3344 end, 3345 print(major, "=started ~s", [lists:flatten(timestamp_get(""))]), 3346 Comment1 = reason_to_string(Comment), 3347 if Type == auto -> 3348 print(major, "=result auto_skipped: ~ts", [Comment1]); 3349 Type == user -> 3350 print(major, "=result skipped: ~ts", [Comment1]) 3351 end, 3352 if CaseNum == 0 -> 3353 print(2,"*** Skipping ~tw ***", [{Mod,Func}]); 3354 true -> 3355 print(2,"*** Skipping test case #~w ~tw ***", [CaseNum,{Mod,Func}]) 3356 end, 3357 TR = xhtml("<tr valign=\"top\">", ["<tr class=\"",odd_or_even(),"\">"]), 3358 GroupName = case get_name(Mode) of 3359 undefined -> ""; 3360 Name -> cast_to_list(Name) 3361 end, 3362 print(html, 3363 TR ++ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" 3364 "<td>" ++ Col0 ++ "~w" ++ Col1 ++ "</td>" 3365 "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" 3366 "<td>" ++ Col0 ++ "~tw" ++ Col1 ++ "</td>" 3367 "<td>" ++ Col0 ++ "< >" ++ Col1 ++ "</td>" 3368 "<td>" ++ Col0 ++ "0.000s" ++ Col1 ++ "</td>" 3369 "<td><font color=\"~ts\">SKIPPED</font></td>" 3370 "<td>~ts</td></tr>\n", 3371 [num2str(CaseNum),fw_name(Mod),GroupName,Func,ResultCol,Comment1]), 3372 3373 if CaseNum > 0 -> 3374 {US,AS} = get(test_server_skipped), 3375 case Type of 3376 user -> put(test_server_skipped, {US+1,AS}); 3377 auto -> put(test_server_skipped, {US,AS+1}) 3378 end, 3379 put(test_server_case_num, CaseNum); 3380 true -> % conf 3381 ok 3382 end. 3383 3384 3385%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3386%% skip_cases_upto(Ref, Cases, Reason, Origin, Mode, SkipType) -> Cases1 3387%% 3388%% SkipType = skip_case | auto_skip_case 3389%% Mark all cases tagged with Ref as skipped. 3390 3391skip_cases_upto(Ref, Cases, Reason, Origin, Mode, SkipType) -> 3392 {_,Modified,Rest} = 3393 modify_cases_upto(Ref, {skip,Reason,Origin,Mode,SkipType}, Cases), 3394 Modified++Rest. 3395 3396%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3397%% copy_cases(OrigRef, NewRef, Cases) -> Cases1 3398%% 3399%% Copy the test cases marked with OrigRef and tag the copies with NewRef. 3400%% The start conf case copy will also get its repeat property updated. 3401 3402copy_cases(OrigRef, NewRef, Cases) -> 3403 {Original,Altered,Rest} = modify_cases_upto(OrigRef, {copy,NewRef}, Cases), 3404 {Altered,Original++Altered++Rest}. 3405 3406%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3407%% modify_cases_upto(Ref, ModOp, Cases) -> {Original,Altered,Remaining} 3408%% 3409%% ModOp = {skip,Reason,Origin,Mode} | {copy,NewRef} 3410%% Origin = conf | tc 3411%% 3412%% Modifies Cases according to ModOp and returns the original elements, 3413%% the modified versions of these elements and the remaining (untouched) 3414%% cases. 3415 3416modify_cases_upto(Ref, ModOp, Cases) -> 3417 {Original,Altered,Rest} = modify_cases_upto(Ref, ModOp, Cases, [], []), 3418 {lists:reverse(Original),lists:reverse(Altered),Rest}. 3419 3420%% first case of a copy operation is the start conf 3421modify_cases_upto(Ref, {copy,NewRef}=Op, [{conf,Ref,Props,MF}=C|T], Orig, Alt) -> 3422 modify_cases_upto(Ref, Op, T, [C|Orig], [{conf,NewRef,update_repeat(Props),MF}|Alt]); 3423 3424modify_cases_upto(Ref, ModOp, Cases, Orig, Alt) -> 3425 %% we need to check if there's an end conf case with the 3426 %% same ref in the list, if not, this *is* an end conf case 3427 case lists:any(fun({_,R,_,_}) when R == Ref -> true; 3428 ({_,R,_}) when R == Ref -> true; 3429 ({skip_case,{_,R,_,_},_}) when R == Ref -> true; 3430 ({skip_case,{_,R,_,_}}) when R == Ref -> true; 3431 (_) -> false 3432 end, Cases) of 3433 true -> 3434 modify_cases_upto1(Ref, ModOp, Cases, Orig, Alt); 3435 false -> 3436 {[],[],Cases} 3437 end. 3438 3439%% next case is a conf with same ref, must be end conf = we're done 3440modify_cases_upto1(Ref, {skip,Reason,conf,Mode,skip_case}, 3441 [{conf,Ref,_Props,MF}|T], Orig, Alt) -> 3442 {Orig,[{skip_case,{conf,Ref,MF,Reason},Mode}|Alt],T}; 3443modify_cases_upto1(Ref, {skip,Reason,conf,Mode,auto_skip_case}, 3444 [{conf,Ref,_Props,MF}|T], Orig, Alt) -> 3445 {Orig,[{auto_skip_case,{conf,Ref,MF,Reason},Mode}|Alt],T}; 3446modify_cases_upto1(Ref, {copy,NewRef}, [{conf,Ref,Props,MF}=C|T], Orig, Alt) -> 3447 {[C|Orig],[{conf,NewRef,update_repeat(Props),MF}|Alt],T}; 3448 3449%% we've skipped all remaining cases in a sequence 3450modify_cases_upto1(Ref, {skip,_,tc,_,_}, 3451 [{conf,Ref,_Props,_MF}|_]=Cs, Orig, Alt) -> 3452 {Orig,Alt,Cs}; 3453 3454%% next is a make case 3455modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}, 3456 [{make,Ref,MF}|T], Orig, Alt) -> 3457 {Orig,[{SkipType,{make,Ref,MF,Reason},Mode}|Alt],T}; 3458modify_cases_upto1(Ref, {copy,NewRef}, [{make,Ref,MF}=M|T], Orig, Alt) -> 3459 {[M|Orig],[{make,NewRef,MF}|Alt],T}; 3460 3461%% next case is a user skipped end conf with the same ref = we're done 3462modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}, 3463 [{skip_case,{Type,Ref,MF,_Cmt},_}|T], Orig, Alt) -> 3464 {Orig,[{SkipType,{Type,Ref,MF,Reason},Mode}|Alt],T}; 3465modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}, 3466 [{skip_case,{Type,Ref,MF,_Cmt}}|T], Orig, Alt) -> 3467 {Orig,[{SkipType,{Type,Ref,MF,Reason},Mode}|Alt],T}; 3468modify_cases_upto1(Ref, {copy,NewRef}, 3469 [{skip_case,{Type,Ref,MF,Cmt},Mode}=C|T], Orig, Alt) -> 3470 {[C|Orig],[{skip_case,{Type,NewRef,MF,Cmt},Mode}|Alt],T}; 3471modify_cases_upto1(Ref, {copy,NewRef}, 3472 [{skip_case,{Type,Ref,MF,Cmt}}=C|T], Orig, Alt) -> 3473 {[C|Orig],[{skip_case,{Type,NewRef,MF,Cmt}}|Alt],T}; 3474 3475%% next is a skip_case, could be one test case or 'all' in suite, we must proceed 3476modify_cases_upto1(Ref, ModOp, [{skip_case,{_F,_Cmt},_Mode}=MF|T], Orig, Alt) -> 3477 modify_cases_upto1(Ref, ModOp, T, [MF|Orig], [MF|Alt]); 3478 3479%% next is a normal case (possibly in a sequence), mark as skipped, or copy, and proceed 3480modify_cases_upto1(Ref, {skip,Reason,_,Mode,skip_case}=Op, 3481 [{_M,_F}=MF|T], Orig, Alt) -> 3482 modify_cases_upto1(Ref, Op, T, Orig, [{skip_case,{MF,Reason},Mode}|Alt]); 3483modify_cases_upto1(Ref, {skip,Reason,_,Mode,auto_skip_case}=Op, 3484 [{_M,_F}=MF|T], Orig, Alt) -> 3485 modify_cases_upto1(Ref, Op, T, Orig, [{auto_skip_case,{MF,Reason},Mode}|Alt]); 3486modify_cases_upto1(Ref, CopyOp, [{_M,_F}=MF|T], Orig, Alt) -> 3487 modify_cases_upto1(Ref, CopyOp, T, [MF|Orig], [MF|Alt]); 3488 3489%% next is a conf case, modify the Mode arg to keep track of sub groups 3490modify_cases_upto1(Ref, {skip,Reason,FType,Mode,SkipType}, 3491 [{conf,OtherRef,Props,_MF}|T], Orig, Alt) -> 3492 case hd(Mode) of 3493 {OtherRef,_,_} -> % end conf 3494 modify_cases_upto1(Ref, {skip,Reason,FType,tl(Mode),SkipType}, 3495 T, Orig, Alt); 3496 _ -> % start conf 3497 Mode1 = [conf(OtherRef,Props)|Mode], 3498 modify_cases_upto1(Ref, {skip,Reason,FType,Mode1,SkipType}, 3499 T, Orig, Alt) 3500 end; 3501 3502%% next is a repeated test case 3503modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}=Op, 3504 [{repeat,{_M,_F}=MF,_Repeat}|T], Orig, Alt) -> 3505 modify_cases_upto1(Ref, Op, T, Orig, [{SkipType,{MF,Reason},Mode}|Alt]); 3506 3507%% next is an already skipped case, ignore or copy 3508modify_cases_upto1(Ref, {skip,_,_,_,_}=Op, [{SkipType,_,_}|T], Orig, Alt) 3509 when SkipType=:=skip_case; SkipType=:=auto_skip_case -> 3510 modify_cases_upto1(Ref, Op, T, Orig, Alt); 3511 3512%% next is some other case, mark as skipped or copy 3513modify_cases_upto1(Ref, {skip,Reason,_,Mode,SkipType}=Op, [Other|T], Orig, Alt) -> 3514 modify_cases_upto1(Ref, Op, T, Orig, [{SkipType,{Other,Reason},Mode}|Alt]); 3515modify_cases_upto1(Ref, CopyOp, [C|T], Orig, Alt) -> 3516 modify_cases_upto1(Ref, CopyOp, T, [C|Orig], [C|Alt]). 3517 3518 3519%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3520%% set_io_buffering(IOHandler) -> PrevIOHandler 3521%% 3522%% Save info about current process (always the main process) buffering 3523%% io printout messages from parallel test case processes (*and* possibly 3524%% also the main process). 3525 3526set_io_buffering(IOHandler) -> 3527 put(test_server_common_io_handler, IOHandler). 3528 3529%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3530%% is_io_buffered() -> true|false 3531%% 3532%% Test whether is being buffered. 3533 3534is_io_buffered() -> 3535 get(test_server_common_io_handler) =/= undefined. 3536 3537%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3538%% queue_test_case_io(Pid, Num, Mod, Func) -> ok 3539%% 3540%% Save info about test case that gets its io buffered. This can 3541%% be a parallel test case or it can be a test case (conf or normal) 3542%% that belongs to a group nested under a parallel group. The queue 3543%% is processed after io buffering is disabled. See run_test_cases_loop/4 3544%% and handle_test_case_io_and_status/0 for more info. 3545 3546queue_test_case_io(Ref, Pid, Num, Mod, Func) -> 3547 Entry = {Ref,Pid,Num,Mod,Func}, 3548 %% the order of the test cases is very important! 3549 put(test_server_queued_io, 3550 get(test_server_queued_io)++[Entry]). 3551 3552%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3553%% wait_for_cases(Ref) -> {Ok,Skipped,Failed} 3554%% 3555%% At the end of a nested parallel group, we have to wait for the test 3556%% cases to terminate before we can go on (since test cases never execute 3557%% in parallel with the end conf case of the group). When a top level 3558%% parallel group is finished, buffered io messages must be handled and 3559%% this is taken care of by handle_test_case_io_and_status/0. 3560 3561wait_for_cases(Ref) -> 3562 case get(test_server_queued_io) of 3563 [] -> 3564 {[],[],[]}; 3565 Cases -> 3566 [_Start|TCs] = 3567 lists:dropwhile(fun({R,_,_,_,_}) when R == Ref -> false; 3568 (_) -> true 3569 end, Cases), 3570 wait_and_resend(Ref, TCs, [],[],[]) 3571 end. 3572 3573wait_and_resend(Ref, [{OtherRef,_,0,_,_}|Ps], 3574 Ok,Skip,Fail) when is_reference(OtherRef), 3575 OtherRef /= Ref -> 3576 %% ignore cases that belong to nested group 3577 Ps1 = rm_cases_upto(OtherRef, Ps), 3578 wait_and_resend(Ref, Ps1, Ok,Skip,Fail); 3579 3580wait_and_resend(Ref, [{_,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) -> 3581 receive 3582 {finished,_Ref,CurrPid,CaseNum,Mod,Func,Result,_RetVal} = Msg -> 3583 %% resend message to main process so that it can be used 3584 %% to test_server_io:print_buffered/1 later 3585 self() ! Msg, 3586 MF = {Mod,Func}, 3587 {Ok1,Skip1,Fail1} = 3588 case Result of 3589 ok -> {[MF|Ok],Skip,Fail}; 3590 skipped -> {Ok,[MF|Skip],Fail}; 3591 failed -> {Ok,Skip,[MF|Fail]} 3592 end, 3593 wait_and_resend(Ref, Ps, Ok1,Skip1,Fail1); 3594 {'EXIT',CurrPid,Reason} when Reason /= normal -> 3595 %% unexpected termination of test case process 3596 {value,{_,_,CaseNum,Mod,Func}} = lists:keysearch(CurrPid, 2, Cases), 3597 print(1, "Error! Process for test case #~w (~w:~tw) died! Reason: ~tp", 3598 [CaseNum, Mod, Func, Reason]), 3599 exit({unexpected_termination,{CaseNum,Mod,Func},{CurrPid,Reason}}) 3600 end; 3601 3602wait_and_resend(_, [], Ok,Skip,Fail) -> 3603 {lists:reverse(Ok),lists:reverse(Skip),lists:reverse(Fail)}. 3604 3605rm_cases_upto(Ref, [{Ref,_,0,_,_}|Ps]) -> 3606 Ps; 3607rm_cases_upto(Ref, [_|Ps]) -> 3608 rm_cases_upto(Ref, Ps). 3609 3610%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3611%% handle_test_case_io_and_status() -> [Ok,Skipped,Failed} 3612%% 3613%% Each parallel test case process prints to its own minor log file during 3614%% execution. The common log files (major, html etc) must however be 3615%% written to sequentially. This is handled by calling 3616%% test_server_io:start_transaction/0 to tell the test_server_io process 3617%% to buffer all print requests. 3618%% 3619%% An io session is always started with a 3620%% {started,Ref,Pid,Num,Mod,Func} message (and 3621%% test_server_io:start_transaction/0 will be called) and terminated 3622%% with {finished,Ref,Pid,Num,Mod,Func,Result,RetVal} (and 3623%% test_server_io:end_transaction/0 will be called). The result 3624%% shipped with the finished message from a parallel process is used 3625%% to update status data of the current test run. An 'EXIT' message 3626%% from each parallel test case process (after finishing and 3627%% terminating) is also received and handled here. 3628%% 3629%% During execution of a parallel group, any cases (conf or normal) 3630%% belonging to a nested group will also get its io printouts buffered. 3631%% This is necessary to get the major and html log files written in 3632%% correct sequence. This function handles also the print messages 3633%% generated by nested group cases that have been executed sequentially 3634%% by the main process (note that these cases do not generate 'EXIT' 3635%% messages, only 'start' and 'finished' messages). 3636%% 3637%% See the header comment for run_test_cases_loop/4 for more 3638%% info about IO handling. 3639%% 3640%% Note: It is important that the type of messages handled here 3641%% do not get consumed by test_server:run_test_case_msgloop/5 3642%% during the test case execution (e.g. in the catch clause of 3643%% the receive)! 3644 3645handle_test_case_io_and_status() -> 3646 case get(test_server_queued_io) of 3647 [] -> 3648 {[],[],[]}; 3649 Cases -> 3650 %% Cases = [{Ref,Pid,CaseNum,Mod,Func} | ...] 3651 Result = handle_io_and_exit_loop([], Cases, [],[],[]), 3652 Main = self(), 3653 %% flush normal exit messages 3654 lists:foreach(fun({_,Pid,_,_,_}) when Pid /= Main -> 3655 receive 3656 {'EXIT',Pid,normal} -> ok 3657 after 3658 1000 -> ok 3659 end; 3660 (_) -> 3661 ok 3662 end, Cases), 3663 Result 3664 end. 3665 3666%% Handle cases (without Ref) that belong to the top parallel group (i.e. when Refs = []) 3667handle_io_and_exit_loop([], [{undefined,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) -> 3668 %% retrieve the start message for the current io session (= testcase) 3669 receive 3670 {started,_,CurrPid,CaseNum,Mod,Func} -> 3671 {Ok1,Skip1,Fail1} = 3672 case handle_io_and_exits(self(), CurrPid, CaseNum, Mod, Func, Cases) of 3673 {ok,MF} -> {[MF|Ok],Skip,Fail}; 3674 {skipped,MF} -> {Ok,[MF|Skip],Fail}; 3675 {failed,MF} -> {Ok,Skip,[MF|Fail]} 3676 end, 3677 handle_io_and_exit_loop([], Ps, Ok1,Skip1,Fail1) 3678 after 3679 1000 -> 3680 exit({testcase_failed_to_start,Mod,Func}) 3681 end; 3682 3683%% Handle cases that belong to groups nested under top parallel group 3684handle_io_and_exit_loop(Refs, [{Ref,CurrPid,CaseNum,Mod,Func}|Ps] = Cases, Ok,Skip,Fail) -> 3685 receive 3686 {started,_,CurrPid,CaseNum,Mod,Func} -> 3687 _ = handle_io_and_exits(self(), CurrPid, CaseNum, Mod, Func, Cases), 3688 Refs1 = 3689 case Refs of 3690 [Ref|Rs] -> % must be end conf case for subgroup 3691 Rs; 3692 _ when is_reference(Ref) -> % must be start of new subgroup 3693 [Ref|Refs]; 3694 _ -> % must be normal subgroup testcase 3695 Refs 3696 end, 3697 handle_io_and_exit_loop(Refs1, Ps, Ok,Skip,Fail) 3698 after 3699 1000 -> 3700 exit({testcase_failed_to_start,Mod,Func}) 3701 end; 3702 3703handle_io_and_exit_loop(_, [], Ok,Skip,Fail) -> 3704 {lists:reverse(Ok),lists:reverse(Skip),lists:reverse(Fail)}. 3705 3706handle_io_and_exits(Main, CurrPid, CaseNum, Mod, Func, Cases) -> 3707 receive 3708 {abort_current_testcase=Tag,_Reason,From} -> 3709 %% If a parallel group is executing, there is no unique 3710 %% current test case, so we must generate an error. 3711 From ! {self(),Tag,{error,parallel_group}}, 3712 handle_io_and_exits(Main, CurrPid, CaseNum, Mod, Func, Cases); 3713 %% end of io session from test case executed by main process 3714 {finished,_,Main,CaseNum,Mod,Func,Result,_RetVal} -> 3715 test_server_io:print_buffered(CurrPid), 3716 {Result,{Mod,Func}}; 3717 %% end of io session from test case executed by parallel process 3718 {finished,_,CurrPid,CaseNum,Mod,Func,Result,RetVal} -> 3719 test_server_io:print_buffered(CurrPid), 3720 case Result of 3721 ok -> 3722 put(test_server_ok, get(test_server_ok)+1); 3723 failed -> 3724 put(test_server_failed, get(test_server_failed)+1); 3725 skipped -> 3726 SkipCounters = 3727 update_skip_counters(RetVal, get(test_server_skipped)), 3728 put(test_server_skipped, SkipCounters) 3729 end, 3730 {Result,{Mod,Func}}; 3731 3732 %% unexpected termination of test case process 3733 {'EXIT',TCPid,Reason} when Reason /= normal -> 3734 test_server_io:print_buffered(CurrPid), 3735 {value,{_,_,Num,M,F}} = lists:keysearch(TCPid, 2, Cases), 3736 print(1, "Error! Process for test case #~w (~w:~tw) died! Reason: ~tp", 3737 [Num, M, F, Reason]), 3738 exit({unexpected_termination,{Num,M,F},{TCPid,Reason}}) 3739 end. 3740 3741 3742%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3743%% run_test_case(Ref, Num, Mod, Func, Args, RunInit, 3744%% TimetrapData, Mode) -> RetVal 3745%% 3746%% Creates the minor log file and inserts some test case specific headers 3747%% and footers into the log files. Then the test case is executed and the 3748%% result is printed to the log files (also info about lingering processes 3749%% & slave nodes in the system is presented). 3750%% 3751%% RunInit decides if the per test case init is to be run (true for all 3752%% but conf cases). 3753%% 3754%% Mode specifies if the test case should be executed by a dedicated, 3755%% parallel, process rather than sequentially by the main process. If 3756%% the former, the new process is spawned and the dictionary of the main 3757%% process is copied to the test case process. 3758%% 3759%% RetVal is the result of executing the test case. It contains info 3760%% about the execution time and the return value of the test case function. 3761 3762run_test_case(Ref, Num, Mod, Func, Args, RunInit, TimetrapData) -> 3763 ok = file:set_cwd(filename:dirname(get(test_server_dir))), 3764 run_test_case1(Ref, Num, Mod, Func, Args, RunInit, 3765 TimetrapData, [], self()). 3766 3767run_test_case(Ref, Num, Mod, Func, Args, skip_init, TimetrapData, Mode) -> 3768 %% a conf case is always executed by the main process 3769 run_test_case1(Ref, Num, Mod, Func, Args, skip_init, 3770 TimetrapData, Mode, self()); 3771 3772run_test_case(Ref, Num, Mod, Func, Args, RunInit, TimetrapData, Mode) -> 3773 ok = file:set_cwd(filename:dirname(get(test_server_dir))), 3774 Main = self(), 3775 case check_prop(parallel, Mode) of 3776 false -> 3777 %% this is a sequential test case 3778 run_test_case1(Ref, Num, Mod, Func, Args, RunInit, 3779 TimetrapData, Mode, Main); 3780 _Ref -> 3781 %% this a parallel test case, spawn the new process 3782 Dictionary = get(), 3783 {dictionary,Dictionary} = process_info(self(), dictionary), 3784 spawn_link( 3785 fun() -> 3786 process_flag(trap_exit, true), 3787 ct_util:mark_process(), 3788 _ = [put(Key, Val) || {Key,Val} <- Dictionary], 3789 set_io_buffering({tc,Main}), 3790 run_test_case1(Ref, Num, Mod, Func, Args, RunInit, 3791 TimetrapData, Mode, Main) 3792 end) 3793 end. 3794 3795run_test_case1(Ref, Num, Mod, Func, Args, RunInit, 3796 TimetrapData, Mode, Main) -> 3797 group_leader(test_server_io:get_gl(Main == self()), self()), 3798 3799 %% if io is being buffered, send start io session message 3800 %% (no matter if case runs on parallel or main process) 3801 case is_io_buffered() of 3802 false -> ok; 3803 true -> 3804 test_server_io:start_transaction(), 3805 Main ! {started,Ref,self(),Num,Mod,Func}, 3806 ok 3807 end, 3808 TSDir = get(test_server_dir), 3809 3810 print(major, "=case ~w:~tw", [Mod, Func]), 3811 MinorName = start_minor_log_file(Mod, Func, self() /= Main), 3812 MinorBase = filename:basename(MinorName), 3813 print(major, "=logfile ~ts", [filename:basename(MinorName)]), 3814 3815 UpdatedArgs = 3816 %% maybe create unique private directory for test case or config func 3817 case get(test_server_create_priv_dir) of 3818 auto_per_run -> 3819 update_config(hd(Args), [{tc_logfile,MinorName}]); 3820 PrivDirMode -> 3821 %% create unique private directory for test case 3822 RunDir = filename:dirname(MinorName), 3823 Ext = 3824 if Num == 0 -> 3825 Int = erlang:unique_integer([positive,monotonic]), 3826 lists:flatten(io_lib:format(".cfg.~w", [Int])); 3827 true -> 3828 lists:flatten(io_lib:format(".~w", [Num])) 3829 end, 3830 PrivDir = filename:join(RunDir, ?priv_dir) ++ Ext, 3831 if PrivDirMode == auto_per_tc -> 3832 ok = file:make_dir(PrivDir); 3833 PrivDirMode == manual_per_tc -> 3834 ok 3835 end, 3836 update_config(hd(Args), [{priv_dir,PrivDir++"/"}, 3837 {tc_logfile,MinorName}]) 3838 end, 3839 GrName = get_name(Mode), 3840 test_server_sup:framework_call(report, 3841 [tc_start,{{Mod,{Func,GrName}}, 3842 MinorName}]), 3843 3844 {ok,Cwd} = file:get_cwd(), 3845 Args2Print = if is_list(UpdatedArgs) -> 3846 lists:keydelete(tc_group_result, 1, UpdatedArgs); 3847 true -> 3848 UpdatedArgs 3849 end, 3850 if RunInit == skip_init -> 3851 print_props(get_props(Mode)); 3852 true -> 3853 ok 3854 end, 3855 3856 print(minor, 3857 escape_chars(io_lib:format("Config value:\n\n ~tp\n", [Args2Print])), 3858 []), 3859 print(minor, "Current directory is ~tp\n", [Cwd]), 3860 3861 GrNameStr = case GrName of 3862 undefined -> ""; 3863 Name -> cast_to_list(Name) 3864 end, 3865 print(major, "=started ~s", [lists:flatten(timestamp_get(""))]), 3866 {{Col0,Col1},Style} = get_font_style((RunInit==run_init), Mode), 3867 TR = xhtml("<tr valign=\"top\">", ["<tr class=\"",odd_or_even(),"\">"]), 3868 EncMinorBase = uri_encode(MinorBase), 3869 print(html, TR ++ "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" 3870 "<td>" ++ Col0 ++ "~w" ++ Col1 ++ "</td>" 3871 "<td>" ++ Col0 ++ "~ts" ++ Col1 ++ "</td>" 3872 "<td><a href=\"~ts\">~tw</a></td>" 3873 "<td><a href=\"~ts#top\"><</a> <a href=\"~ts#end\">></a></td>", 3874 [num2str(Num),fw_name(Mod),GrNameStr,EncMinorBase,Func, 3875 EncMinorBase,EncMinorBase]), 3876 3877 do_unless_parallel(Main, fun erlang:yield/0), 3878 3879 %% run the test case 3880 {Result,DetectedFail,ProcsBefore,ProcsAfter} = 3881 run_test_case_apply(Num, Mod, Func, [UpdatedArgs], GrName, 3882 RunInit, TimetrapData), 3883 {Time,RetVal,Loc,Opts,Comment} = 3884 case Result of 3885 Normal={_Time,_RetVal,_Loc,_Opts,_Comment} -> Normal; 3886 {died,DReason,DLoc,DCmt} -> {died,DReason,DLoc,[],DCmt} 3887 end, 3888 3889 print(minor, "<a name=\"end\"></a>", [], internal_raw), 3890 print(minor, "\n", [], internal_raw), 3891 print_timestamp(minor, "Ended at "), 3892 print(major, "=ended ~s", [lists:flatten(timestamp_get(""))]), 3893 3894 do_unless_parallel(Main, fun() -> file:set_cwd(filename:dirname(TSDir)) end), 3895 3896 %% call the appropriate progress function clause to print the results to log 3897 Status = 3898 case {Time,RetVal} of 3899 {died,{timetrap_timeout,TimetrapTimeout}} -> 3900 progress(failed, Num, Mod, Func, GrName, Loc, 3901 timetrap_timeout, TimetrapTimeout, Comment, Style); 3902 {died,Reason={auto_skip,_Why}} -> 3903 %% died in init_per_testcase or in a hook in this context 3904 progress(skip, Num, Mod, Func, GrName, Loc, Reason, 3905 Time, Comment, Style); 3906 {died,{Skip,Reason}} when Skip==skip; Skip==skipped -> 3907 %% died in init_per_testcase 3908 progress(skip, Num, Mod, Func, GrName, Loc, Reason, 3909 Time, Comment, Style); 3910 {died,Reason} when Reason=/=ok -> 3911 %% (If Reason==ok it means that process died in 3912 %% end_per_testcase after successfully completing the 3913 %% test case itself - then we shall not fail, but a 3914 %% warning will be issued in the comment field.) 3915 progress(failed, Num, Mod, Func, GrName, Loc, Reason, 3916 Time, Comment, Style); 3917 {_,{'EXIT',{Skip,Reason}}} when Skip==skip; Skip==skipped; 3918 Skip==auto_skip -> 3919 progress(skip, Num, Mod, Func, GrName, Loc, Reason, 3920 Time, Comment, Style); 3921 {_,{'EXIT',_Pid,{Skip,Reason}}} when Skip==skip; Skip==skipped -> 3922 progress(skip, Num, Mod, Func, GrName, Loc, Reason, 3923 Time, Comment, Style); 3924 {_,{'EXIT',_Pid,Reason}} -> 3925 progress(failed, Num, Mod, Func, GrName, Loc, Reason, 3926 Time, Comment, Style); 3927 {_,{'EXIT',Reason}} -> 3928 progress(failed, Num, Mod, Func, GrName, Loc, Reason, 3929 Time, Comment, Style); 3930 {_,{Fail,Reason}} when Fail =:= fail; Fail =:= failed -> 3931 progress(failed, Num, Mod, Func, GrName, Loc, Reason, 3932 Time, Comment, Style); 3933 {_,Reason={auto_skip,_Why}} -> 3934 progress(skip, Num, Mod, Func, GrName, Loc, Reason, 3935 Time, Comment, Style); 3936 {_,{Skip,Reason}} when Skip==skip; Skip==skipped -> 3937 progress(skip, Num, Mod, Func, GrName, Loc, Reason, 3938 Time, Comment, Style); 3939 {Time,RetVal} -> 3940 case DetectedFail of 3941 [] -> 3942 progress(ok, Num, Mod, Func, GrName, Loc, RetVal, 3943 Time, Comment, Style); 3944 3945 Reason -> 3946 progress(failed, Num, Mod, Func, GrName, Loc, Reason, 3947 Time, Comment, Style) 3948 end 3949 end, 3950 %% if the test case was executed sequentially, this updates the 3951 %% status count on the main process (status of parallel test cases 3952 %% is updated later by the handle_test_case_io_and_status/0 function) 3953 case {RunInit,Status} of 3954 {skip_init,_} -> % conf doesn't count 3955 ok; 3956 {_,ok} -> 3957 put(test_server_ok, get(test_server_ok)+1); 3958 {_,failed} -> 3959 put(test_server_failed, get(test_server_failed)+1); 3960 {_,skip} -> 3961 {US,AS} = get(test_server_skipped), 3962 put(test_server_skipped, {US+1,AS}); 3963 {_,auto_skip} -> 3964 {US,AS} = get(test_server_skipped), 3965 put(test_server_skipped, {US,AS+1}) 3966 end, 3967 %% only if test case execution is sequential do we care about the 3968 %% remaining processes and slave nodes count 3969 case self() of 3970 Main -> 3971 case test_server_sup:framework_call(warn, [processes], true) of 3972 true -> 3973 if ProcsBefore < ProcsAfter -> 3974 print(minor, 3975 "WARNING: ~w more processes in system after test case", 3976 [ProcsAfter-ProcsBefore]); 3977 ProcsBefore > ProcsAfter -> 3978 print(minor, 3979 "WARNING: ~w less processes in system after test case", 3980 [ProcsBefore-ProcsAfter]); 3981 true -> ok 3982 end; 3983 false -> 3984 ok 3985 end, 3986 case test_server_sup:framework_call(warn, [nodes], true) of 3987 true -> 3988 case catch controller_call(kill_slavenodes) of 3989 {'EXIT',_} = Exit -> 3990 print(minor, 3991 "WARNING: There might be slavenodes left in the" 3992 " system. I tried to kill them, but I failed: ~tp\n", 3993 [Exit]); 3994 [] -> ok; 3995 List -> 3996 print(minor, "WARNING: ~w slave nodes in system after test"++ 3997 "case. Tried to killed them.~n"++ 3998 " Names:~tp", 3999 [length(List),List]) 4000 end; 4001 false -> 4002 ok 4003 end; 4004 _ -> 4005 ok 4006 end, 4007 %% if the test case was executed sequentially, this updates the execution 4008 %% time count on the main process (adding execution time of parallel test 4009 %% case groups is done in run_test_cases_loop/4) 4010 if is_number(Time) -> 4011 put(test_server_total_time, get(test_server_total_time)+Time); 4012 true -> 4013 ok 4014 end, 4015 test_server_sup:check_new_crash_dumps(), 4016 4017 %% if io is being buffered, send finished message 4018 %% (no matter if case runs on parallel or main process) 4019 case is_io_buffered() of 4020 false -> 4021 ok; 4022 true -> 4023 test_server_io:end_transaction(), 4024 Main ! {finished,Ref,self(),Num,Mod,Func, 4025 ?mod_result(Status),{Time,RetVal,Opts}}, 4026 ok 4027 end, 4028 {Time,RetVal,Opts}. 4029 4030 4031%%-------------------------------------------------------------------- 4032%% various help functions 4033 4034%% Call Action if we are running on the main process (not parallel). 4035do_unless_parallel(Main, Action) when is_function(Action, 0) -> 4036 case self() of 4037 Main -> Action(); 4038 _ -> ok 4039 end. 4040 4041num2str(0) -> ""; 4042num2str(N) -> integer_to_list(N). 4043 4044%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4045%% progress(Result, CaseNum, Mod, Func, Location, Reason, Time, 4046%% Comment, TimeFormat) -> Result 4047%% 4048%% Prints the result of the test case to log file. 4049%% Note: Strings that are to be written to the minor log must 4050%% be prefixed with "=== " here, or the indentation will be wrong. 4051 4052progress(skip, CaseNum, Mod, Func, GrName, Loc, Reason, Time, 4053 Comment, {St0,St1}) -> 4054 {Reason1,{Color,Ret,ReportTag}} = 4055 if_auto_skip(Reason, 4056 fun() -> {?auto_skip_color,auto_skip,auto_skipped} end, 4057 fun() -> {?user_skip_color,skip,skipped} end), 4058 print(major, "=result ~w: ~tp", [ReportTag,Reason1]), 4059 print(1, "*** SKIPPED ~ts ***", 4060 [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), 4061 test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName}, 4062 {ReportTag,Reason1}}]), 4063 TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; 4064 true -> "~w" 4065 end, [Time]), 4066 ReasonStr = escape_chars(reason_to_string(Reason1)), 4067 ReasonStr1 = lists:flatten([string:trim(S,leading,"\s") || 4068 S <- string:lexemes(ReasonStr,[$\n])]), 4069 ReasonLength = string:length(ReasonStr1), 4070 ReasonStr2 = 4071 if ReasonLength > 80 -> 4072 string:slice(ReasonStr1, 0, 77) ++ "..."; 4073 true -> 4074 ReasonStr1 4075 end, 4076 Comment1 = case Comment of 4077 "" -> ""; 4078 _ -> xhtml("<br>(","<br />(") ++ to_string(Comment) ++ ")" 4079 end, 4080 print(html, 4081 "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" 4082 "<td><font color=\"~ts\">SKIPPED</font></td>" 4083 "<td>~ts~ts</td></tr>\n", 4084 [TimeStr,Color,ReasonStr2,Comment1]), 4085 FormatLoc = test_server_sup:format_loc(Loc), 4086 print(minor, "=== Location: ~ts", [FormatLoc]), 4087 print(minor, "=== Reason: ~ts", [ReasonStr1]), 4088 Ret; 4089 4090progress(failed, CaseNum, Mod, Func, GrName, Loc, timetrap_timeout, T, 4091 Comment0, {St0,St1}) -> 4092 print(major, "=result failed: timeout, ~tp", [Loc]), 4093 print(1, "*** FAILED ~ts ***", 4094 [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), 4095 test_server_sup:framework_call(report, 4096 [tc_done,{Mod,{Func,GrName}, 4097 {failed,timetrap_timeout}}]), 4098 FormatLastLoc = test_server_sup:format_loc(get_last_loc(Loc)), 4099 ErrorReason = io_lib:format("{timetrap_timeout,~ts}", [FormatLastLoc]), 4100 Comment = 4101 case Comment0 of 4102 "" -> "<font color=\"red\">" ++ ErrorReason ++ "</font>"; 4103 _ -> "<font color=\"red\">" ++ ErrorReason ++ 4104 xhtml("</font><br>","</font><br />") ++ to_string(Comment0) 4105 end, 4106 print(html, 4107 "<td>" ++ St0 ++ "~.3fs" ++ St1 ++ "</td>" 4108 "<td><font color=\"red\">FAILED</font></td>" 4109 "<td>~ts</td></tr>\n", 4110 [T/1000,Comment]), 4111 FormatLoc = test_server_sup:format_loc(Loc), 4112 print(minor, "=== Location: ~ts", [FormatLoc]), 4113 print(minor, "=== Reason: timetrap timeout", []), 4114 failed; 4115 4116progress(failed, CaseNum, Mod, Func, GrName, Loc, {testcase_aborted,Reason}, _T, 4117 Comment0, {St0,St1}) -> 4118 print(major, "=result failed: testcase_aborted, ~tp", [Loc]), 4119 print(1, "*** FAILED ~ts ***", 4120 [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), 4121 test_server_sup:framework_call(report, 4122 [tc_done,{Mod,{Func,GrName}, 4123 {failed,testcase_aborted}}]), 4124 FormatLastLoc = test_server_sup:format_loc(get_last_loc(Loc)), 4125 ErrorReason = io_lib:format("{testcase_aborted,~ts}", [FormatLastLoc]), 4126 Comment = 4127 case Comment0 of 4128 "" -> "<font color=\"red\">" ++ ErrorReason ++ "</font>"; 4129 _ -> "<font color=\"red\">" ++ ErrorReason ++ 4130 xhtml("</font><br>","</font><br />") ++ to_string(Comment0) 4131 end, 4132 print(html, 4133 "<td>" ++ St0 ++ "died" ++ St1 ++ "</td>" 4134 "<td><font color=\"red\">FAILED</font></td>" 4135 "<td>~ts</td></tr>\n", 4136 [Comment]), 4137 FormatLoc = test_server_sup:format_loc(Loc), 4138 print(minor, "=== Location: ~ts", [FormatLoc]), 4139 print(minor, 4140 escape_chars(io_lib:format("=== Reason: {testcase_aborted,~tp}", 4141 [Reason])), 4142 []), 4143 failed; 4144 4145progress(failed, CaseNum, Mod, Func, GrName, unknown, Reason, Time, 4146 Comment0, {St0,St1}) -> 4147 print(major, "=result failed: ~tp, ~w", [Reason,unknown_location]), 4148 print(1, "*** FAILED ~ts ***", 4149 [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), 4150 test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName}, 4151 {failed,Reason}}]), 4152 TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; 4153 true -> "~w" 4154 end, [Time]), 4155 ErrorReason = escape_chars(lists:flatten(io_lib:format("~tp", [Reason]))), 4156 ErrorReason1 = lists:flatten([string:trim(S,leading,"\s") || 4157 S <- string:lexemes(ErrorReason,[$\n])]), 4158 ErrorReasonLength = string:length(ErrorReason1), 4159 ErrorReason2 = 4160 if ErrorReasonLength > 63 -> 4161 string:slice(ErrorReason1, 0, 60) ++ "..."; 4162 true -> 4163 ErrorReason1 4164 end, 4165 Comment = 4166 case Comment0 of 4167 "" -> "<font color=\"red\">" ++ ErrorReason2 ++ "</font>"; 4168 _ -> "<font color=\"red\">" ++ ErrorReason2 ++ 4169 xhtml("</font><br>","</font><br />") ++ 4170 to_string(Comment0) 4171 end, 4172 print(html, 4173 "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" 4174 "<td><font color=\"red\">FAILED</font></td>" 4175 "<td>~ts</td></tr>\n", 4176 [TimeStr,Comment]), 4177 print(minor, "=== Location: ~w", [unknown]), 4178 {FStr,FormattedReason} = format_exception(Reason), 4179 print(minor, 4180 escape_chars(io_lib:format("=== Reason: " ++ FStr, [FormattedReason])), 4181 []), 4182 failed; 4183 4184progress(failed, CaseNum, Mod, Func, GrName, Loc, Reason, Time, 4185 Comment0, {St0,St1}) -> 4186 {LocMaj,LocMin} = if Func == error_in_suite -> 4187 case get_fw_mod(undefined) of 4188 Mod -> {unknown_location,unknown}; 4189 _ -> {Loc,Loc} 4190 end; 4191 true -> {Loc,Loc} 4192 end, 4193 print(major, "=result failed: ~tp, ~tp", [Reason,LocMaj]), 4194 print(1, "*** FAILED ~ts ***", 4195 [get_info_str(Mod,Func, CaseNum, get(test_server_cases))]), 4196 test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName}, 4197 {failed,Reason}}]), 4198 TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; 4199 true -> "~w" 4200 end, [Time]), 4201 Comment = 4202 case Comment0 of 4203 "" -> ""; 4204 _ -> xhtml("<br>","<br />") ++ to_string(Comment0) 4205 end, 4206 FormatLastLoc = test_server_sup:format_loc(get_last_loc(LocMaj)), 4207 print(html, 4208 "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" 4209 "<td><font color=\"red\">FAILED</font></td>" 4210 "<td><font color=\"red\">~ts</font>~ts</td></tr>\n", 4211 [TimeStr,FormatLastLoc,Comment]), 4212 FormatLoc = test_server_sup:format_loc(LocMin), 4213 print(minor, "=== Location: ~ts", [FormatLoc]), 4214 {FStr,FormattedReason} = format_exception(Reason), 4215 print(minor, "=== Reason: " ++ 4216 escape_chars(io_lib:format(FStr, [FormattedReason])), []), 4217 failed; 4218 4219progress(ok, _CaseNum, Mod, Func, GrName, _Loc, RetVal, Time, 4220 Comment0, {St0,St1}) -> 4221 print(minor, "successfully completed test case", []), 4222 test_server_sup:framework_call(report, [tc_done,{Mod,{Func,GrName},ok}]), 4223 TimeStr = io_lib:format(if is_float(Time) -> "~.3fs"; 4224 true -> "~w" 4225 end, [Time]), 4226 Comment = 4227 case RetVal of 4228 {comment,RetComment} -> 4229 String = to_string(RetComment), 4230 HtmlCmt = test_server_sup:framework_call(format_comment, 4231 [String], 4232 String), 4233 print(major, "=result ok: ~ts", [String]), 4234 "<td>" ++ HtmlCmt ++ "</td>"; 4235 _ -> 4236 print(major, "=result ok", []), 4237 case Comment0 of 4238 "" -> "<td></td>"; 4239 _ -> "<td>" ++ to_string(Comment0) ++ "</td>" 4240 end 4241 end, 4242 print(major, "=elapsed ~p", [Time]), 4243 print(html, 4244 "<td>" ++ St0 ++ "~ts" ++ St1 ++ "</td>" 4245 "<td><font color=\"green\">Ok</font></td>" 4246 "~ts</tr>\n", 4247 [TimeStr,Comment]), 4248 print(minor, 4249 escape_chars(io_lib:format("=== Returned value: ~tp", [RetVal])), 4250 []), 4251 ok. 4252 4253%%-------------------------------------------------------------------- 4254%% various help functions 4255escape_chars(Term) when not is_list(Term), not is_binary(Term) -> 4256 esc_chars_in_list(io_lib:format("~tp", [Term])); 4257escape_chars(List = [Term | _]) when not is_list(Term), not is_integer(Term) -> 4258 esc_chars_in_list(io_lib:format("~tp", [List])); 4259escape_chars(List) -> 4260 esc_chars_in_list(List). 4261 4262esc_chars_in_list([Bin | Io]) when is_binary(Bin) -> 4263 [Bin | esc_chars_in_list(Io)]; 4264esc_chars_in_list([List | Io]) when is_list(List) -> 4265 [esc_chars_in_list(List) | esc_chars_in_list(Io)]; 4266esc_chars_in_list([$< | Io]) -> 4267 ["<" | esc_chars_in_list(Io)]; 4268esc_chars_in_list([$> | Io]) -> 4269 [">" | esc_chars_in_list(Io)]; 4270esc_chars_in_list([$& | Io]) -> 4271 ["&" | esc_chars_in_list(Io)]; 4272esc_chars_in_list([Char | Io]) when is_integer(Char) -> 4273 [Char | esc_chars_in_list(Io)]; 4274esc_chars_in_list([]) -> 4275 []; 4276esc_chars_in_list(Bin) -> 4277 Bin. 4278 4279get_fw_mod(Mod) -> 4280 case get(test_server_framework) of 4281 undefined -> 4282 case os:getenv("TEST_SERVER_FRAMEWORK") of 4283 FW when FW =:= false; FW =:= "undefined" -> 4284 Mod; 4285 FW -> 4286 list_to_atom(FW) 4287 end; 4288 '$none' -> Mod; 4289 FW -> FW 4290 end. 4291 4292fw_name(?MODULE) -> 4293 test_server; 4294fw_name(Mod) -> 4295 case get(test_server_framework_name) of 4296 undefined -> 4297 case get_fw_mod(undefined) of 4298 undefined -> 4299 Mod; 4300 Mod -> 4301 case os:getenv("TEST_SERVER_FRAMEWORK_NAME") of 4302 FWName when FWName =:= false; FWName =:= "undefined" -> 4303 Mod; 4304 FWName -> 4305 list_to_atom(FWName) 4306 end; 4307 _ -> 4308 Mod 4309 end; 4310 '$none' -> 4311 Mod; 4312 FWName -> 4313 case get_fw_mod(Mod) of 4314 Mod -> FWName; 4315 _ -> Mod 4316 end 4317 end. 4318 4319if_auto_skip(Reason={failed,{_,init_per_testcase,_}}, True, _False) -> 4320 {Reason,True()}; 4321if_auto_skip({skip,Reason={failed,{_,init_per_testcase,_}}}, True, _False) -> 4322 {Reason,True()}; 4323if_auto_skip({auto_skip,Reason}, True, _False) -> 4324 {Reason,True()}; 4325if_auto_skip(Reason, _True, False) -> 4326 {Reason,False()}. 4327 4328update_skip_counters({_T,Pat,_Opts}, {US,AS}) -> 4329 {_,Result} = if_auto_skip(Pat, fun() -> {US,AS+1} end, fun() -> {US+1,AS} end), 4330 Result; 4331update_skip_counters(Pat, {US,AS}) -> 4332 {_,Result} = if_auto_skip(Pat, fun() -> {US,AS+1} end, fun() -> {US+1,AS} end), 4333 Result. 4334 4335get_info_str(Mod,Func, 0, _Cases) -> 4336 io_lib:format("~tw", [{Mod,Func}]); 4337get_info_str(_Mod,_Func, CaseNum, unknown) -> 4338 "test case " ++ integer_to_list(CaseNum); 4339get_info_str(_Mod,_Func, CaseNum, Cases) -> 4340 "test case " ++ integer_to_list(CaseNum) ++ 4341 " of " ++ integer_to_list(Cases). 4342 4343print_if_known(Known, {SK,AK}, {SU,AU}) -> 4344 {S,A} = if Known == unknown -> {SU,AU}; 4345 true -> {SK,AK} 4346 end, 4347 io_lib:format(S, A). 4348 4349to_string(Term) when is_list(Term) -> 4350 case (catch io_lib:format("~ts", [Term])) of 4351 {'EXIT',_} -> lists:flatten(io_lib:format("~tp", [Term])); 4352 String -> lists:flatten(String) 4353 end; 4354to_string(Term) -> 4355 lists:flatten(io_lib:format("~tp", [Term])). 4356 4357get_last_loc(Loc) when is_tuple(Loc) -> 4358 Loc; 4359get_last_loc([Loc|_]) when is_tuple(Loc) -> 4360 [Loc]; 4361get_last_loc(Loc) -> 4362 Loc. 4363 4364reason_to_string({failed,{_,FailFunc,bad_return}}) -> 4365 atom_to_list(FailFunc) ++ " bad return value"; 4366reason_to_string({failed,{_,FailFunc,{timetrap_timeout,_}}}) -> 4367 atom_to_list(FailFunc) ++ " timed out"; 4368reason_to_string(FWInitFail = {failed,{_CB,init_tc,_Reason}}) -> 4369 to_string(FWInitFail); 4370reason_to_string({failed,{_,FailFunc,_}}) -> 4371 atom_to_list(FailFunc) ++ " failed"; 4372reason_to_string(Other) -> 4373 to_string(Other). 4374 4375%get_font_style(Prop) -> 4376% {Col,St0,St1} = get_font_style1(Prop), 4377% {{"<font color="++Col++">","</font>"}, 4378% {"<font color="++Col++">"++St0,St1++"</font>"}}. 4379 4380get_font_style(NormalCase, Mode) -> 4381 Prop = if not NormalCase -> 4382 default; 4383 true -> 4384 case check_prop(parallel, Mode) of 4385 false -> 4386 case check_prop(sequence, Mode) of 4387 false -> 4388 default; 4389 _ -> 4390 sequence 4391 end; 4392 _ -> 4393 parallel 4394 end 4395 end, 4396 {Col,St0,St1} = get_font_style1(Prop), 4397 {{"<font color="++Col++">","</font>"}, 4398 {"<font color="++Col++">"++St0,St1++"</font>"}}. 4399 4400get_font_style1(parallel) -> 4401 {"\"darkslategray\"","<i>","</i>"}; 4402get_font_style1(sequence) -> 4403% {"\"darkolivegreen\"","",""}; 4404 {"\"saddlebrown\"","",""}; 4405get_font_style1(default) -> 4406 {"\"black\"","",""}. 4407%%get_font_style1(skipped) -> 4408%% {"\"lightgray\"","",""}. 4409 4410 4411%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4412%% format_exception({Error,Stack}) -> {CtrlSeq,Term} 4413%% 4414%% The default behaviour is that error information gets formatted 4415%% (like in the erlang shell) before printed to the minor log file. 4416%% The framework application can switch this feature off by setting 4417%% *its* application environment variable 'format_exception' to false. 4418%% It is also possible to switch formatting off by starting the 4419%% test_server node with init argument 'test_server_format_exception' 4420%% set to false. 4421 4422format_exception(Reason={_Error,Stack}) when is_list(Stack) -> 4423 case get_fw_mod(undefined) of 4424 undefined -> 4425 case application:get_env(test_server, format_exception) of 4426 {ok,false} -> 4427 {"~tp",Reason}; 4428 _ -> 4429 do_format_exception(Reason) 4430 end; 4431 FW -> 4432 case application:get_env(FW, format_exception) of 4433 {ok,false} -> 4434 {"~tp",Reason}; 4435 _ -> 4436 do_format_exception(Reason) 4437 end 4438 end; 4439format_exception(Error) -> 4440 format_exception({Error,[]}). 4441 4442do_format_exception(Reason={Error,Stack}) -> 4443 StackFun = fun(_, _, _) -> false end, 4444 PF = fun(Term, I) -> 4445 io_lib:format("~." ++ integer_to_list(I) ++ "tp", [Term]) 4446 end, 4447 case catch erl_error:format_exception(1, error, Error, Stack, StackFun, PF, utf8) of 4448 {'EXIT',_R} -> 4449 {"~tp",Reason}; 4450 Formatted -> 4451 Formatted1 = re:replace(Formatted, "exception error: ", "", [{return,list},unicode]), 4452 {"~ts",lists:flatten(Formatted1)} 4453 end. 4454 4455 4456%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4457%% run_test_case_apply(CaseNum, Mod, Func, Args, Name, RunInit, 4458%% TimetrapData) -> 4459%% {{Time,RetVal,Loc,Opts,Comment},DetectedFail,ProcessesBefore,ProcessesAfter} | 4460%% {{died,Reason,unknown,Comment},DetectedFail,ProcessesBefore,ProcessesAfter} 4461%% Name = atom() 4462%% Time = float() (seconds) 4463%% RetVal = term() 4464%% Loc = term() 4465%% Comment = string() 4466%% Reason = term() 4467%% DetectedFail = [{File,Line}] 4468%% ProcessesBefore = ProcessesAfter = integer() 4469%% 4470 4471run_test_case_apply(CaseNum, Mod, Func, Args, Name, RunInit, 4472 TimetrapData) -> 4473 test_server:run_test_case_apply({CaseNum,Mod,Func,Args,Name,RunInit, 4474 TimetrapData}). 4475 4476%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4477%% print(Detail, Format, Args) -> ok 4478%% Detail = integer() 4479%% Format = string() 4480%% Args = [term()] 4481%% 4482%% Just like io:format, except that depending on the Detail value, the output 4483%% is directed to console, major and/or minor log files. 4484 4485print(Detail, Format) -> 4486 print(Detail, Format, []). 4487 4488print(Detail, Format, Args) -> 4489 print(Detail, Format, Args, internal). 4490 4491print(Detail, ["$tc_html",Format], Args, Printer) -> 4492 Msg = io_lib:format(Format, Args), 4493 print_or_buffer(Detail, ["$tc_html",Msg], Printer); 4494 4495print(Detail, Format, Args, Printer) -> 4496 Msg = io_lib:format(Format, Args), 4497 print_or_buffer(Detail, Msg, Printer). 4498 4499print_or_buffer(Detail, Msg, Printer) -> 4500 test_server_gl:print(group_leader(), Detail, Msg, Printer). 4501 4502%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4503%% print_timestamp(Detail, Leader) -> ok 4504%% 4505%% Prints Leader followed by a time stamp (date and time). Depending on 4506%% the Detail value, the output is directed to console, major and/or minor 4507%% log files. 4508 4509print_timestamp(Detail, Leader) -> 4510 print(Detail, timestamp_get(Leader), []). 4511 4512%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4513%% print_who(Host, User) -> ok 4514%% 4515%% Logs who runs the suite. 4516 4517print_who(Host, User) -> 4518 UserStr = case User of 4519 "" -> ""; 4520 _ -> " by " ++ User 4521 end, 4522 print(html, "Run~ts on ~ts", [UserStr,Host]). 4523 4524%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4525%% format(Format) -> IoLibReturn 4526%% format(Detail, Format) -> IoLibReturn 4527%% format(Format, Args) -> IoLibReturn 4528%% format(Detail, Format, Args) -> IoLibReturn 4529%% 4530%% Detail = integer() 4531%% Format = string() 4532%% Args = [term(),...] 4533%% IoLibReturn = term() 4534%% 4535%% Logs the Format string and Args, similar to io:format/1/2 etc. If 4536%% Detail is not specified, the default detail level (which is 50) is used. 4537%% Which log files the string will be logged in depends on the thresholds 4538%% set with set_levels/3. Typically with default detail level, only the 4539%% minor log file is used. 4540 4541format(Format) -> 4542 format(minor, Format, []). 4543 4544format(major, Format) -> 4545 format(major, Format, []); 4546format(minor, Format) -> 4547 format(minor, Format, []); 4548format(Detail, Format) when is_integer(Detail) -> 4549 format(Detail, Format, []); 4550format(Format, Args) -> 4551 format(minor, Format, Args). 4552 4553format(Detail, Format, Args) -> 4554 Str = 4555 case catch io_lib:format(Format, Args) of 4556 {'EXIT',_} -> 4557 io_lib:format("illegal format; ~tp with args ~tp.\n", 4558 [Format,Args]); 4559 Valid -> Valid 4560 end, 4561 print_or_buffer(Detail, Str, self()). 4562 4563%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4564%% xhtml(BasicHtml, XHtml) -> BasicHtml | XHtml 4565%% 4566xhtml(HTML, XHTML) -> 4567 case get(basic_html) of 4568 true -> HTML; 4569 _ -> XHTML 4570 end. 4571 4572%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4573%% odd_or_even() -> "odd" | "even" 4574%% 4575odd_or_even() -> 4576 case get(odd_or_even) of 4577 even -> 4578 put(odd_or_even, odd), 4579 "even"; 4580 _ -> 4581 put(odd_or_even, even), 4582 "odd" 4583 end. 4584 4585%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4586%% timestamp_filename_get(Leader) -> string() 4587%% Leader = string() 4588%% 4589%% Returns a string consisting of Leader concatenated with the current 4590%% date and time. The resulting string is suitable as a filename. 4591timestamp_filename_get(Leader) -> 4592 timestamp_get_internal(Leader, 4593 "~ts~w-~2.2.0w-~2.2.0w_~2.2.0w.~2.2.0w.~2.2.0w"). 4594 4595%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4596%% timestamp_get(Leader) -> string() 4597%% Leader = string() 4598%% 4599%% Returns a string consisting of Leader concatenated with the current 4600%% date and time. The resulting string is suitable for display. 4601timestamp_get(Leader) -> 4602 timestamp_get_internal(Leader, 4603 "~ts~w-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w"). 4604 4605timestamp_get_internal(Leader, Format) -> 4606 {YY,MM,DD,H,M,S} = time_get(), 4607 io_lib:format(Format, [Leader,YY,MM,DD,H,M,S]). 4608 4609%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4610%% time_get() -> {YY,MM,DD,H,M,S} 4611%% YY = integer() 4612%% MM = integer() 4613%% DD = integer() 4614%% H = integer() 4615%% M = integer() 4616%% S = integer() 4617%% 4618%% Returns the current Year,Month,Day,Hours,Minutes,Seconds. 4619%% The function checks that the date doesn't wrap while calling 4620%% getting the time. 4621time_get() -> 4622 {YY,MM,DD} = date(), 4623 {H,M,S} = time(), 4624 case date() of 4625 {YY,MM,DD} -> 4626 {YY,MM,DD,H,M,S}; 4627 _NewDay -> 4628 %% date changed between call to date() and time(), try again 4629 time_get() 4630 end. 4631 4632 4633%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4634%% make_config(Config) -> NewConfig 4635%% Config = [{Key,Value},...] 4636%% NewConfig = [{Key,Value},...] 4637%% 4638%% Creates a configuration list (currently returns it's input) 4639 4640make_config(Initial) -> 4641 Initial. 4642 4643%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4644%% update_config(Config, Update) -> NewConfig 4645%% Config = [{Key,Value},...] 4646%% Update = [{Key,Value},...] | {Key,Value} 4647%% NewConfig = [{Key,Value},...] 4648%% 4649%% Adds or replaces the key-value pairs in config with those in update. 4650%% Returns the updated list. 4651 4652update_config(Config, {Key,Val}) -> 4653 case lists:keymember(Key, 1, Config) of 4654 true -> 4655 lists:keyreplace(Key, 1, Config, {Key,Val}); 4656 false -> 4657 [{Key,Val}|Config] 4658 end; 4659update_config(Config, [Assoc|Assocs]) -> 4660 NewConfig = update_config(Config, Assoc), 4661 update_config(NewConfig, Assocs); 4662update_config(Config, []) -> 4663 Config. 4664 4665 4666%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4667%% collect_cases(CurMod, TopCase, SkipList) -> 4668%% BasicCaseList | {error,Reason} 4669%% 4670%% CurMod = atom() 4671%% TopCase = term() 4672%% SkipList = [term(),...] 4673%% BasicCaseList = [term(),...] 4674%% 4675%% Parses the given test goal(s) in TopCase, and transforms them to a 4676%% simple list of test cases to call, when executing the test suite. 4677%% 4678%% CurMod is the "current" module, that is, the module the last instruction 4679%% was read from. May be be set to 'none' initially. 4680%% 4681%% SkipList is the list of test cases to skip and requirements to deny. 4682%% 4683%% The BasicCaseList is built out of TopCase, which may be any of the 4684%% following terms: 4685%% 4686%% [] Nothing is added 4687%% List list() The list is decomposed, and each element is 4688%% treated according to this table 4689%% Case atom() CurMod:Case(suite) is called 4690%% {module,Case} CurMod:Case(suite) is called 4691%% {Module,Case} Module:Case(suite) is called 4692%% {module,Module,Case} Module:Case(suite) is called 4693%% {module,Module,Case,Args} Module:Case is called with Args as arguments 4694%% {dir,Dir} All modules *_SUITE in the named directory 4695%% are listed, and each Module:all(suite) is called 4696%% {dir,Dir,Pattern} All modules <Pattern>_SUITE in the named dir 4697%% are listed, and each Module:all(suite) is called 4698%% {conf,InitMF,Cases,FinMF} 4699%% {conf,Props,InitMF,Cases,FinMF} 4700%% InitMF is placed in the BasicCaseList, then 4701%% Cases is treated according to this table, then 4702%% FinMF is placed in the BasicCaseList. InitMF 4703%% and FinMF are configuration manipulation 4704%% functions. See below. 4705%% {make,InitMFA,Cases,FinMFA} 4706%% InitMFA is placed in the BasicCaseList, then 4707%% Cases is treated according to this table, then 4708%% FinMFA is placed in the BasicCaseList. InitMFA 4709%% and FinMFA are make/unmake functions. If InitMFA 4710%% fails, Cases are not run. 4711%% 4712%% When a function is called, above, it means that the function is invoked 4713%% and the return is expected to be: 4714%% 4715%% [] Leaf case 4716%% {req,ReqList} Kept for backwards compatibility - same as [] 4717%% {req,ReqList,Cases} Kept for backwards compatibility - 4718%% Cases parsed recursively with collect_cases/3 4719%% Cases (list) Recursively parsed with collect_cases/3 4720%% 4721%% Leaf cases are added to the BasicCaseList as Module:Case(Config). Each 4722%% case is checked against the SkipList. If present, a skip instruction 4723%% is inserted instead, which only prints the case name and the reason 4724%% why the case was skipped in the log files. 4725%% 4726%% Configuration manipulation functions are called with the current 4727%% configuration list as only argument, and are expected to return a new 4728%% configuration list. Such a pair of function may, for example, start a 4729%% server and stop it after a serie of test cases. 4730%% 4731%% SkipCases is expected to be in the format: 4732%% 4733%% Other Recursively parsed with collect_cases/3 4734%% {Mod,Comment} Skip Mod, with Comment 4735%% {Mod,Funcs,Comment} Skip listed functions in Mod with Comment 4736%% {Mod,Func,Comment} Skip named function in Mod with Comment 4737%% 4738-record(cc, {mod, % current module 4739 skip}). % skip list 4740 4741collect_all_cases(Top, Skip) when is_list(Skip) -> 4742 Result = 4743 case collect_cases(Top, #cc{mod=[],skip=Skip}, []) of 4744 {ok,Cases,_St} -> Cases; 4745 Other -> Other 4746 end, 4747 Result. 4748 4749 4750collect_cases([], St, _) -> {ok,[],St}; 4751collect_cases([Case|Cs0], St0, Mode) -> 4752 case collect_cases(Case, St0, Mode) of 4753 {ok,FlatCases1,St1} -> 4754 case collect_cases(Cs0, St1, Mode) of 4755 {ok,FlatCases2,St} -> 4756 {ok,FlatCases1 ++ FlatCases2,St}; 4757 {error,_Reason} = Error -> Error 4758 end; 4759 {error,_Reason} = Error -> Error 4760 end; 4761 4762 4763collect_cases({module,Case}, St, Mode) when is_atom(Case), is_atom(St#cc.mod) -> 4764 collect_case({St#cc.mod,Case}, St, Mode); 4765collect_cases({module,Mod,Case}, St, Mode) -> 4766 collect_case({Mod,Case}, St, Mode); 4767collect_cases({module,Mod,Case,Args}, St, Mode) -> 4768 collect_case({Mod,Case,Args}, St, Mode); 4769 4770collect_cases({dir,SubDir}, St, Mode) -> 4771 collect_files(SubDir, "*_SUITE", St, Mode); 4772collect_cases({dir,SubDir,Pattern}, St, Mode) -> 4773 collect_files(SubDir, Pattern++"*", St, Mode); 4774 4775collect_cases({conf,InitF,CaseList,FinMF}, St, Mode) when is_atom(InitF) -> 4776 collect_cases({conf,[],{St#cc.mod,InitF},CaseList,FinMF}, St, Mode); 4777collect_cases({conf,InitMF,CaseList,FinF}, St, Mode) when is_atom(FinF) -> 4778 collect_cases({conf,[],InitMF,CaseList,{St#cc.mod,FinF}}, St, Mode); 4779collect_cases({conf,InitMF,CaseList,FinMF}, St0, Mode) -> 4780 collect_cases({conf,[],InitMF,CaseList,FinMF}, St0, Mode); 4781collect_cases({conf,Props,InitF,CaseList,FinMF}, St, Mode) when is_atom(InitF) -> 4782 case init_props(Props) of 4783 {error,_} -> 4784 {ok,[],St}; 4785 Props1 -> 4786 collect_cases({conf,Props1,{St#cc.mod,InitF},CaseList,FinMF}, 4787 St, Mode) 4788 end; 4789collect_cases({conf,Props,InitMF,CaseList,FinF}, St, Mode) when is_atom(FinF) -> 4790 case init_props(Props) of 4791 {error,_} -> 4792 {ok,[],St}; 4793 Props1 -> 4794 collect_cases({conf,Props1,InitMF,CaseList,{St#cc.mod,FinF}}, 4795 St, Mode) 4796 end; 4797collect_cases({conf,Props,InitMF,CaseList,FinMF} = Conf, St, Mode) -> 4798 case init_props(Props) of 4799 {error,_} -> 4800 {ok,[],St}; 4801 Props1 -> 4802 Ref = make_ref(), 4803 Skips = St#cc.skip, 4804 Props2 = [{suite,St#cc.mod} | lists:delete(suite,Props1)], 4805 Mode1 = [{Ref,Props2,undefined} | Mode], 4806 case in_skip_list({St#cc.mod,Conf}, Skips) of 4807 {true,Comment} -> % conf init skipped 4808 {ok,[{skip_case,{conf,Ref,InitMF,Comment},Mode1} | 4809 [] ++ [{conf,Ref,[],FinMF}]],St}; 4810 {true,Name,Comment} when is_atom(Name) -> % all cases skipped 4811 case collect_cases(CaseList, St, Mode1) of 4812 {ok,[],_St} = Empty -> 4813 Empty; 4814 {ok,FlatCases,St1} -> 4815 Cases2Skip = FlatCases ++ [{conf,Ref, 4816 keep_name(Props1), 4817 FinMF}], 4818 Skipped = skip_cases_upto(Ref, Cases2Skip, Comment, 4819 conf, Mode1, skip_case), 4820 {ok,[{skip_case,{conf,Ref,InitMF,Comment},Mode1} | 4821 Skipped],St1}; 4822 {error,_Reason} = Error -> 4823 Error 4824 end; 4825 {true,ToSkip,_} when is_list(ToSkip) -> % some cases skipped 4826 case collect_cases(CaseList, 4827 St#cc{skip=ToSkip++Skips}, Mode1) of 4828 {ok,[],_St} = Empty -> 4829 Empty; 4830 {ok,FlatCases,St1} -> 4831 {ok,[{conf,Ref,Props1,InitMF} | 4832 FlatCases ++ [{conf,Ref, 4833 keep_name(Props1), 4834 FinMF}]],St1#cc{skip=Skips}}; 4835 {error,_Reason} = Error -> 4836 Error 4837 end; 4838 false -> 4839 case collect_cases(CaseList, St, Mode1) of 4840 {ok,[],_St} = Empty -> 4841 Empty; 4842 {ok,FlatCases,St1} -> 4843 {ok,[{conf,Ref,Props1,InitMF} | 4844 FlatCases ++ [{conf,Ref, 4845 keep_name(Props1), 4846 FinMF}]],St1}; 4847 {error,_Reason} = Error -> 4848 Error 4849 end 4850 end 4851 end; 4852 4853collect_cases({make,InitMFA,CaseList,FinMFA}, St0, Mode) -> 4854 case collect_cases(CaseList, St0, Mode) of 4855 {ok,[],_St} = Empty -> Empty; 4856 {ok,FlatCases,St} -> 4857 Ref = make_ref(), 4858 {ok,[{make,Ref,InitMFA}|FlatCases ++ 4859 [{make,Ref,FinMFA}]],St}; 4860 {error,_Reason} = Error -> Error 4861 end; 4862 4863collect_cases({repeat,{Module, Case}, Repeat}, St, Mode) -> 4864 case catch collect_case([Case], St#cc{mod=Module}, [], Mode) of 4865 {ok, [{Module,Case}], _} -> 4866 {ok, [{repeat,{Module, Case}, Repeat}], St}; 4867 Other -> 4868 {error,Other} 4869 end; 4870 4871collect_cases({Module, Cases}, St, Mode) when is_list(Cases) -> 4872 case (catch collect_case(Cases, St#cc{mod=Module}, [], Mode)) of 4873 Result = {ok,_,_} -> 4874 Result; 4875 Other -> 4876 {error,Other} 4877 end; 4878 4879collect_cases({_Mod,_Case}=Spec, St, Mode) -> 4880 collect_case(Spec, St, Mode); 4881 4882collect_cases({_Mod,_Case,_Args}=Spec, St, Mode) -> 4883 collect_case(Spec, St, Mode); 4884collect_cases(Case, St, Mode) when is_atom(Case), is_atom(St#cc.mod) -> 4885 collect_case({St#cc.mod,Case}, St, Mode); 4886collect_cases(Other, St, _Mode) -> 4887 {error,{bad_subtest_spec,St#cc.mod,Other}}. 4888 4889collect_case({Mod,{conf,_,_,_,_}=Conf}, St, Mode) -> 4890 collect_case_invoke(Mod, Conf, [], St, Mode); 4891 4892collect_case(MFA, St, Mode) -> 4893 case in_skip_list(MFA, St#cc.skip) of 4894 {true,Comment} when Comment /= make_failed -> 4895 {ok,[{skip_case,{MFA,Comment},Mode}],St}; 4896 _ -> 4897 case MFA of 4898 {Mod,Case} -> collect_case_invoke(Mod, Case, MFA, St, Mode); 4899 {_Mod,_Case,_Args} -> {ok,[MFA],St} 4900 end 4901 end. 4902 4903collect_case([], St, Acc, _Mode) -> 4904 {ok, Acc, St}; 4905 4906collect_case([Case | Cases], St, Acc, Mode) -> 4907 {ok, FlatCases, NewSt} = collect_case({St#cc.mod, Case}, St, Mode), 4908 collect_case(Cases, NewSt, Acc ++ FlatCases, Mode). 4909 4910collect_case_invoke(Mod, Case, MFA, St, Mode) -> 4911 case get_fw_mod(undefined) of 4912 undefined -> 4913 case catch apply(Mod, Case, [suite]) of 4914 {'EXIT',_} -> 4915 {ok,[MFA],St}; 4916 Suite -> 4917 collect_subcases(Mod, Case, MFA, St, Suite, Mode) 4918 end; 4919 _ -> 4920 Suite = test_server_sup:framework_call(get_suite, 4921 [Mod,Case], 4922 []), 4923 collect_subcases(Mod, Case, MFA, St, Suite, Mode) 4924 end. 4925 4926collect_subcases(Mod, Case, MFA, St, Suite, Mode) -> 4927 case Suite of 4928 [] when Case == all -> {ok,[],St}; 4929 [] when element(1, Case) == conf -> {ok,[],St}; 4930 [] -> {ok,[MFA],St}; 4931%%%! --- START Kept for backwards compatibility --- 4932%%%! Requirements are not used 4933 {req,ReqList} -> 4934 collect_case_deny(Mod, Case, MFA, ReqList, [], St, Mode); 4935 {req,ReqList,SubCases} -> 4936 collect_case_deny(Mod, Case, MFA, ReqList, SubCases, St, Mode); 4937%%%! --- END Kept for backwards compatibility --- 4938 {Skip,Reason} when Skip==skip; Skip==skipped -> 4939 {ok,[{skip_case,{MFA,Reason},Mode}],St}; 4940 {error,Reason} -> 4941 throw(Reason); 4942 SubCases -> 4943 collect_case_subcases(Mod, Case, SubCases, St, Mode) 4944 end. 4945 4946collect_case_subcases(Mod, Case, SubCases, St0, Mode) -> 4947 OldMod = St0#cc.mod, 4948 case collect_cases(SubCases, St0#cc{mod=Mod}, Mode) of 4949 {ok,FlatCases,St} -> 4950 {ok,FlatCases,St#cc{mod=OldMod}}; 4951 {error,Reason} -> 4952 {error,{{Mod,Case},Reason}} 4953 end. 4954 4955collect_files(Dir, Pattern, St, Mode) -> 4956 {ok,Cwd} = file:get_cwd(), 4957 Dir1 = filename:join(Cwd, Dir), 4958 Wc = filename:join([Dir1,Pattern++"{.erl,"++code:objfile_extension()++"}"]), 4959 case catch filelib:wildcard(Wc) of 4960 {'EXIT', Reason} -> 4961 io:format("Could not collect files: ~tp~n", [Reason]), 4962 {error,{collect_fail,Dir,Pattern}}; 4963 Files -> 4964 %% convert to module names and remove duplicates 4965 Mods = lists:foldl(fun(File, Acc) -> 4966 Mod = fullname_to_mod(File), 4967 case lists:member(Mod, Acc) of 4968 true -> Acc; 4969 false -> [Mod | Acc] 4970 end 4971 end, [], Files), 4972 Tests = [{Mod,all} || Mod <- lists:sort(Mods)], 4973 collect_cases(Tests, St, Mode) 4974 end. 4975 4976fullname_to_mod(Path) when is_list(Path) -> 4977 %% If this is called with a binary, then we are probably in +fnu 4978 %% mode and have found a beam file with name encoded as latin1. We 4979 %% will let this crash since it can not work to load such a module 4980 %% anyway. It should be removed or renamed! 4981 list_to_atom(filename:rootname(filename:basename(Path))). 4982 4983collect_case_deny(Mod, Case, MFA, ReqList, SubCases, St, Mode) -> 4984 case {check_deny(ReqList, St#cc.skip),SubCases} of 4985 {{denied,Comment},_SubCases} -> 4986 {ok,[{skip_case,{MFA,Comment},Mode}],St}; 4987 {granted,[]} -> 4988 {ok,[MFA],St}; 4989 {granted,SubCases} -> 4990 collect_case_subcases(Mod, Case, SubCases, St, Mode) 4991 end. 4992 4993check_deny([Req|Reqs], DenyList) -> 4994 case check_deny_req(Req, DenyList) of 4995 {denied,_Comment}=Denied -> Denied; 4996 granted -> check_deny(Reqs, DenyList) 4997 end; 4998check_deny([], _DenyList) -> granted; 4999check_deny(Req, DenyList) -> check_deny([Req], DenyList). 5000 5001check_deny_req({Req,Val}, DenyList) -> 5002 %%io:format("ValCheck ~p=~p in ~p\n", [Req,Val,DenyList]), 5003 case lists:keysearch(Req, 1, DenyList) of 5004 {value,{_Req,DenyVal}} when Val >= DenyVal -> 5005 {denied,io_lib:format("Requirement ~tp=~tp", [Req,Val])}; 5006 _ -> 5007 check_deny_req(Req, DenyList) 5008 end; 5009check_deny_req(Req, DenyList) -> 5010 case lists:member(Req, DenyList) of 5011 true -> {denied,io_lib:format("Requirement ~tp", [Req])}; 5012 false -> granted 5013 end. 5014 5015in_skip_list({Mod,{conf,Props,InitMF,_CaseList,_FinMF}}, SkipList) -> 5016 case in_skip_list(InitMF, SkipList) of 5017 {true,_} = Yes -> 5018 Yes; 5019 _ -> 5020 case proplists:get_value(name, Props) of 5021 undefined -> 5022 false; 5023 Name -> 5024 ToSkip = 5025 lists:flatmap( 5026 fun({M,{conf,SProps,_,SCaseList,_},Cmt}) when 5027 M == Mod -> 5028 case proplists:get_value(name, SProps) of 5029 all -> 5030 [{M,all,Cmt}]; 5031 Name -> 5032 case SCaseList of 5033 all -> 5034 [{M,all,Cmt}]; 5035 _ -> 5036 [{M,F,Cmt} || F <- SCaseList] 5037 end; 5038 _ -> 5039 [] 5040 end; 5041 (_) -> 5042 [] 5043 end, SkipList), 5044 case ToSkip of 5045 [] -> 5046 false; 5047 _ -> 5048 case lists:keysearch(all, 2, ToSkip) of 5049 {value,{_,_,Cmt}} -> {true,Name,Cmt}; 5050 _ -> {true,ToSkip,""} 5051 end 5052 end 5053 end 5054 end; 5055 5056in_skip_list({Mod,Func,_Args}, SkipList) -> 5057 in_skip_list({Mod,Func}, SkipList); 5058in_skip_list({Mod,Func}, [{Mod,Funcs,Comment}|SkipList]) when is_list(Funcs) -> 5059 case lists:member(Func, Funcs) of 5060 true -> 5061 {true,Comment}; 5062 _ -> 5063 in_skip_list({Mod,Func}, SkipList) 5064 end; 5065in_skip_list({Mod,Func}, [{Mod,Func,Comment}|_SkipList]) -> 5066 {true,Comment}; 5067in_skip_list({Mod,_Func}, [{Mod,Comment}|_SkipList]) -> 5068 {true,Comment}; 5069in_skip_list({Mod,Func}, [_|SkipList]) -> 5070 in_skip_list({Mod,Func}, SkipList); 5071in_skip_list(_, []) -> 5072 false. 5073 5074%% remove unnecessary properties 5075init_props(Props) -> 5076 case get_repeat(Props) of 5077 Repeat = {_RepType,N} when N < 2 -> 5078 if N == 0 -> 5079 {error,{invalid_property,Repeat}}; 5080 true -> 5081 lists:delete(Repeat, Props) 5082 end; 5083 _ -> 5084 Props 5085 end. 5086 5087keep_name(Props) -> 5088 lists:filter(fun({name,_}) -> true; 5089 ({suite,_}) -> true; 5090 (_) -> false end, Props). 5091 5092%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5093%% Node handling functions %% 5094%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5095 5096%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5097%% get_target_info() -> #target_info 5098%% 5099%% Returns a record containing system information for target 5100 5101get_target_info() -> 5102 controller_call(get_target_info). 5103 5104%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5105%% start_node(SlaveName, Type, Options) -> 5106%% {ok, Slave} | {error, Reason} 5107%% 5108%% Called by test_server. See test_server:start_node/3 for details 5109 5110start_node(Name, Type, Options) -> 5111 T = 10 * ?ACCEPT_TIMEOUT * test_server:timetrap_scale_factor(), 5112 format(minor, "Attempt to start ~w node ~tp with options ~tp", 5113 [Type, Name, Options]), 5114 case controller_call({start_node,Name,Type,Options}, T) of 5115 {{ok,Nodename}, Host, Cmd, Info, Warning} -> 5116 format(minor, 5117 "Successfully started node ~w on ~tp with command: ~ts", 5118 [Nodename, Host, Cmd]), 5119 format(major, "=node_start ~w", [Nodename]), 5120 case Info of 5121 [] -> ok; 5122 _ -> format(minor, Info) 5123 end, 5124 case Warning of 5125 [] -> ok; 5126 _ -> 5127 format(1, Warning), 5128 format(minor, Warning) 5129 end, 5130 {ok, Nodename}; 5131 {fail,{Ret, Host, Cmd}} -> 5132 format(minor, 5133 "Failed to start node ~tp on ~tp with command: ~ts~n" 5134 "Reason: ~tp", 5135 [Name, Host, Cmd, Ret]), 5136 {fail,Ret}; 5137 {Ret, undefined, undefined} -> 5138 format(minor, "Failed to start node ~tp: ~tp", [Name,Ret]), 5139 Ret; 5140 {Ret, Host, Cmd} -> 5141 format(minor, 5142 "Failed to start node ~tp on ~tp with command: ~ts~n" 5143 "Reason: ~tp", 5144 [Name, Host, Cmd, Ret]), 5145 Ret 5146 end. 5147 5148%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5149%% wait_for_node(Node) -> ok | {error,timeout} 5150%% 5151%% Wait for a slave/peer node which has been started with 5152%% the option {wait,false}. This function returns when 5153%% when the new node has contacted test_server_ctrl again 5154 5155wait_for_node(Slave) -> 5156 T = 10000 * test_server:timetrap_scale_factor(), 5157 case catch controller_call({wait_for_node,Slave},T) of 5158 {'EXIT',{timeout,_}} -> {error,timeout}; 5159 ok -> ok 5160 end. 5161 5162%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5163%% is_release_available(Release) -> true | false 5164%% Release -> string() 5165%% 5166%% Test if a release (such as "r10b") is available to be 5167%% started using start_node/3. 5168 5169is_release_available(Release) -> 5170 controller_call({is_release_available,Release}). 5171 5172%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5173%% stop_node(Name) -> ok | {error,Reason} 5174%% 5175%% Clean up - test_server will stop this node 5176 5177stop_node(Slave) -> 5178 controller_call({stop_node,Slave}). 5179 5180 5181%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5182%% DEBUGGER INTERFACE %% 5183%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5184 5185i() -> 5186 hformat("Pid", "Initial Call", "Current Function", "Reducts", "Msgs"), 5187 Line=lists:duplicate(27, "-"), 5188 hformat(Line, Line, Line, Line, Line), 5189 display_info(processes(), 0, 0). 5190 5191p(A,B,C) -> 5192 pinfo(ts_pid(A,B,C)). 5193p(X) when is_atom(X) -> 5194 pinfo(whereis(X)); 5195p({A,B,C}) -> 5196 pinfo(ts_pid(A,B,C)); 5197p(X) -> 5198 pinfo(X). 5199 5200t() -> 5201 t(wall_clock). 5202t(X) -> 5203 element(1, statistics(X)). 5204 5205pi(Item,X) -> 5206 lists:keysearch(Item,1,p(X)). 5207pi(Item,A,B,C) -> 5208 lists:keysearch(Item,1,p(A,B,C)). 5209 5210%% c:pid/3 5211ts_pid(X,Y,Z) when is_integer(X), is_integer(Y), is_integer(Z) -> 5212 list_to_pid("<" ++ integer_to_list(X) ++ "." ++ 5213 integer_to_list(Y) ++ "." ++ 5214 integer_to_list(Z) ++ ">"). 5215 5216 5217%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5218%% display_info(Pids, Reductions, Messages) -> void 5219%% Pids = [pid(),...] 5220%% Reductions = integer() 5221%% Messaged = integer() 5222%% 5223%% Displays info, similar to c:i() about the processes in the list Pids. 5224%% Also counts the total number of reductions and msgs for the listed 5225%% processes, if called with Reductions = Messages = 0. 5226 5227display_info([Pid|T], R, M) -> 5228 case pinfo(Pid) of 5229 undefined -> 5230 display_info(T, R, M); 5231 Info -> 5232 Call = fetch(initial_call, Info), 5233 Curr = case fetch(current_function, Info) of 5234 {Mod,F,Args} when is_list(Args) -> 5235 {Mod,F,length(Args)}; 5236 Other -> 5237 Other 5238 end, 5239 Reds = fetch(reductions, Info), 5240 LM = fetch(message_queue_len, Info), 5241 pformat(io_lib:format("~w", [Pid]), 5242 io_lib:format("~tw", [Call]), 5243 io_lib:format("~tw", [Curr]), Reds, LM), 5244 display_info(T, R+Reds, M + LM) 5245 end; 5246display_info([], R, M) -> 5247 Line=lists:duplicate(27, "-"), 5248 hformat(Line, Line, Line, Line, Line), 5249 pformat("Total", "", "", R, M). 5250 5251hformat(A1, A2, A3, A4, A5) -> 5252 io:format("~-10s ~-27s ~-27s ~8s ~4s~n", [A1,A2,A3,A4,A5]). 5253 5254pformat(A1, A2, A3, A4, A5) -> 5255 io:format("~-10s ~-27s ~-27s ~8w ~4w~n", [A1,A2,A3,A4,A5]). 5256 5257fetch(Key, Info) -> 5258 case lists:keysearch(Key, 1, Info) of 5259 {value, {_, Val}} -> 5260 Val; 5261 _ -> 5262 0 5263 end. 5264 5265pinfo(P) -> 5266 Node = node(), 5267 case node(P) of 5268 Node -> 5269 process_info(P); 5270 _ -> 5271 rpc:call(node(P),erlang,process_info,[P]) 5272 end. 5273 5274 5275%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5276%% Support functions for COVER %% 5277%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5278%% 5279%% A module is included in the cover analysis if 5280%% - it belongs to the tested application and is not listed in the 5281%% {exclude,List} part of the App.cover file 5282%% - it does not belong to the application, but is listed in the 5283%% {include,List} part of the App.cover file 5284%% - it does not belong to the application, but is listed in the 5285%% {cross,[{Tag,List}]} part of the App.cover file 5286%% 5287%% The modules listed in the 'cross' part of the cover file are 5288%% modules that are heavily used by other tests than the one where 5289%% they are explicitly tested. They should then be listed as 'cross' 5290%% in the cover file for the test where they are used but do not 5291%% belong. 5292%% 5293%% After all tests are completed, the these modules can be analysed 5294%% with coverage data from all tests where they are compiled - see 5295%% cross_cover_analyse/2. The result is stored in a file called 5296%% cross_cover.html in the run.<timestamp> directory of the 5297%% test the modules belong to. 5298%% 5299%% Example: 5300%% If the module m1 belongs to system s1 but is heavily used also in 5301%% the tests for another system s2, then the cover files for the two 5302%% systems could be like this: 5303%% 5304%% s1.cover: 5305%% {include,[m1]}. 5306%% 5307%% s2.cover: 5308%% {include,[....]}. % modules belonging to system s2 5309%% {cross,[{s1,[m1]}]}. 5310%% 5311%% When the tests for both s1 and s2 are completed, run 5312%% cross_cover_analyse(Level,[{s1,S1LogDir},{s2,S2LogDir}]), and 5313%% the accumulated cover data for m1 will be written to 5314%% S1LogDir/[run.<timestamp>/]cross_cover.html 5315%% 5316%% S1LogDir and S2LogDir are either the run.<timestamp> directories 5317%% for the two tests, or the parent directory of these, in which case 5318%% the latest run.<timestamp> directory will be chosen. 5319%% 5320%% Note that the m1 module will also be presented in the normal 5321%% coverage log for s1 (due to the include statement in s1.cover), but 5322%% that only includes the coverage achieved by the s1 test itself. 5323%% 5324%% The Tag in the 'cross' statement in the cover file has no other 5325%% purpose than mapping the list of modules ([m1] in the example 5326%% above) to the correct log directory where it should be included in 5327%% the cross_cover.html file (S1LogDir in the example above). 5328%% I.e. the value of the Tag has no meaning, it could be foo as well 5329%% as s1 above, as long as the same Tag is used in the cover file and 5330%% in the call to cross_cover_analyse/2. 5331 5332 5333%% Cover compilation 5334%% The compilation is executed on the target node 5335start_cover(#cover{}=CoverInfo) -> 5336 cover_compile(CoverInfo); 5337start_cover({log,CoverLogDir}=CoverInfo) -> 5338 %% Cover is controlled by the framework - here's the log 5339 put(test_server_cover_log_dir,CoverLogDir), 5340 {ok,CoverInfo}. 5341 5342cover_compile(CoverInfo) -> 5343 test_server:cover_compile(CoverInfo). 5344 5345%% Read the coverfile for an application and return a list of modules 5346%% that are members of the application but shall not be compiled 5347%% (Exclude), and a list of modules that are not members of the 5348%% application but shall be compiled (Include). 5349read_cover_file(none) -> 5350 {[],[],[]}; 5351read_cover_file(CoverFile) -> 5352 case file:consult(CoverFile) of 5353 {ok,List} -> 5354 case check_cover_file(List, [], [], []) of 5355 {ok,Exclude,Include,Cross} -> {Exclude,Include,Cross}; 5356 error -> 5357 io:fwrite("Faulty format of CoverFile ~tp\n", [CoverFile]), 5358 {[],[],[]} 5359 end; 5360 {error,Reason} -> 5361 io:fwrite("Can't read CoverFile ~ts\nReason: ~tp\n", 5362 [CoverFile,Reason]), 5363 {[],[],[]} 5364 end. 5365 5366check_cover_file([{exclude,all}|Rest], _, Include, Cross) -> 5367 check_cover_file(Rest, all, Include, Cross); 5368check_cover_file([{exclude,Exclude}|Rest], _, Include, Cross) -> 5369 case lists:all(fun(M) -> is_atom(M) end, Exclude) of 5370 true -> 5371 check_cover_file(Rest, Exclude, Include, Cross); 5372 false -> 5373 error 5374 end; 5375check_cover_file([{include,Include}|Rest], Exclude, _, Cross) -> 5376 case lists:all(fun(M) -> is_atom(M) end, Include) of 5377 true -> 5378 check_cover_file(Rest, Exclude, Include, Cross); 5379 false -> 5380 error 5381 end; 5382check_cover_file([{cross,Cross}|Rest], Exclude, Include, _) -> 5383 case check_cross(Cross) of 5384 true -> 5385 check_cover_file(Rest, Exclude, Include, Cross); 5386 false -> 5387 error 5388 end; 5389check_cover_file([], Exclude, Include, Cross) -> 5390 {ok,Exclude,Include,Cross}. 5391 5392check_cross([{Tag,Modules}|Rest]) -> 5393 case lists:all(fun(M) -> is_atom(M) end, [Tag|Modules]) of 5394 true -> 5395 check_cross(Rest); 5396 false -> 5397 false 5398 end; 5399check_cross([]) -> 5400 true. 5401 5402 5403%% Cover analysis, per application 5404%% This analysis is executed on the target node once the test is 5405%% completed for an application. This is not the same as the cross 5406%% cover analysis, which can be executed on any node after the tests 5407%% are finshed. 5408%% 5409%% This per application analysis writes the file cover.html in the 5410%% application's run.<timestamp> directory. 5411stop_cover(#cover{}=CoverInfo, TestDir) -> 5412 cover_analyse(CoverInfo, TestDir), 5413 ok; 5414stop_cover(_CoverInfo, _TestDir) -> 5415 %% Cover is probably controlled by the framework 5416 ok. 5417 5418make_relative(AbsDir, VsDir) -> 5419 DirTokens = filename:split(AbsDir), 5420 VsTokens = filename:split(VsDir), 5421 filename:join(make_relative1(DirTokens, VsTokens)). 5422 5423make_relative1([T | DirTs], [T | VsTs]) -> 5424 make_relative1(DirTs, VsTs); 5425make_relative1(Last = [_File], []) -> 5426 Last; 5427make_relative1(Last = [_File], VsTs) -> 5428 Ups = ["../" || _ <- VsTs], 5429 Ups ++ Last; 5430make_relative1(DirTs, []) -> 5431 DirTs; 5432make_relative1(DirTs, VsTs) -> 5433 Ups = ["../" || _ <- VsTs], 5434 Ups ++ DirTs. 5435 5436 5437cover_analyse(CoverInfo, TestDir) -> 5438 write_default_cross_coverlog(TestDir), 5439 5440 {ok,CoverLog} = open_html_file(filename:join(TestDir, ?coverlog_name)), 5441 write_coverlog_header(CoverLog), 5442 #cover{app=App, 5443 file=CoverFile, 5444 excl=Excluded, 5445 cross=Cross} = CoverInfo, 5446 io:fwrite(CoverLog, "<h1>Coverage for application '~w'</h1>\n", [App]), 5447 io:fwrite(CoverLog, 5448 "<p><a href=\"~ts\">Coverdata collected over all tests</a></p>", 5449 [?cross_coverlog_name]), 5450 5451 io:fwrite(CoverLog, "<p>CoverFile: <code>~tp</code>\n", [CoverFile]), 5452 ok = write_cross_cover_info(TestDir,Cross), 5453 5454 case length(cover:imported_modules()) of 5455 Imps when Imps > 0 -> 5456 io:fwrite(CoverLog, 5457 "<p>Analysis includes data from ~w imported module(s).\n", 5458 [Imps]); 5459 _ -> 5460 ok 5461 end, 5462 5463 io:fwrite(CoverLog, "<p>Excluded module(s): <code>~tp</code>\n", [Excluded]), 5464 5465 Coverage = test_server:cover_analyse(TestDir, CoverInfo), 5466 ok = write_binary_file(filename:join(TestDir,?raw_coverlog_name), 5467 term_to_binary(Coverage)), 5468 5469 case lists:filter(fun({_M,{_,_,_}}) -> false; 5470 (_) -> true 5471 end, Coverage) of 5472 [] -> 5473 ok; 5474 Bad -> 5475 io:fwrite(CoverLog, "<p>Analysis failed for ~w module(s): " 5476 "<code>~w</code>\n", 5477 [length(Bad),[BadM || {BadM,{_,_Why}} <- Bad]]) 5478 end, 5479 5480 TotPercent = write_cover_result_table(CoverLog, Coverage), 5481 ok = write_binary_file(filename:join(TestDir, ?cover_total), 5482 term_to_binary(TotPercent)). 5483 5484%% Cover analysis - accumulated over multiple tests 5485%% This can be executed on any node after all tests are finished. 5486%% Analyse = overview | details 5487%% TagDirs = [{Tag,Dir}] 5488%% Tag = atom(), identifier 5489%% Dir = string(), the log directory for Tag, it can be a 5490%% run.<timestamp> directory or the parent directory of 5491%% such (in which case the latest run.<timestamp> directory 5492%% is used) 5493cross_cover_analyse(Analyse, TagDirs0) -> 5494 TagDirs = get_latest_run_dirs(TagDirs0), 5495 TagMods = get_all_cross_info(TagDirs,[]), 5496 TagDirMods = add_cross_modules(TagMods,TagDirs), 5497 CoverdataFiles = get_coverdata_files(TagDirMods), 5498 lists:foreach(fun(CDF) -> cover:import(CDF) end, CoverdataFiles), 5499 io:fwrite("Cover analysing...\n", []), 5500 DetailsFun = 5501 case Analyse of 5502 details -> 5503 fun(Dir,M) -> 5504 OutFile = filename:join(Dir, 5505 atom_to_list(M) ++ 5506 ".CROSS_COVER.html"), 5507 case cover:analyse_to_file(M, OutFile, [html]) of 5508 {ok,_} -> 5509 {file,OutFile}; 5510 Error -> 5511 Error 5512 end 5513 end; 5514 _ -> 5515 fun(_,_) -> undefined end 5516 end, 5517 Coverage = analyse_tests(TagDirMods, DetailsFun, []), 5518 cover:stop(), 5519 write_cross_cover_logs(Coverage,TagDirMods). 5520 5521write_cross_cover_info(_Dir,[]) -> 5522 ok; 5523write_cross_cover_info(Dir,Cross) -> 5524 write_binary_file(filename:join(Dir,?cross_cover_info), 5525 term_to_binary(Cross)). 5526 5527%% For each test from which there are cross cover analysed 5528%% modules, write a cross cover log (cross_cover.html). 5529write_cross_cover_logs([{Tag,Coverage}|T],TagDirMods) -> 5530 case lists:keyfind(Tag,1,TagDirMods) of 5531 {_,Dir,Mods} when Mods=/=[] -> 5532 ok = write_binary_file(filename:join(Dir,?raw_cross_coverlog_name), 5533 term_to_binary(Coverage)), 5534 CoverLogName = filename:join(Dir,?cross_coverlog_name), 5535 {ok,CoverLog} = open_html_file(CoverLogName), 5536 write_coverlog_header(CoverLog), 5537 io:fwrite(CoverLog, 5538 "<h1>Coverage results for \'~w\' from all tests</h1>\n", 5539 [Tag]), 5540 write_cover_result_table(CoverLog, Coverage), 5541 io:fwrite("Written file ~tp\n", [CoverLogName]); 5542 _ -> 5543 ok 5544 end, 5545 write_cross_cover_logs(T,TagDirMods); 5546write_cross_cover_logs([],_) -> 5547 io:fwrite("done\n", []). 5548 5549%% Get the latest run.<timestamp> directories 5550get_latest_run_dirs([{Tag,Dir}|Rest]) -> 5551 [{Tag,get_latest_run_dir(Dir)} | get_latest_run_dirs(Rest)]; 5552get_latest_run_dirs([]) -> 5553 []. 5554 5555get_latest_run_dir(Dir) -> 5556 case filelib:wildcard(filename:join(Dir,"run.[1-2]*")) of 5557 [] -> 5558 Dir; 5559 [H|T] -> 5560 get_latest_dir(T,H) 5561 end. 5562 5563get_latest_dir([H|T],Latest) when H>Latest -> 5564 get_latest_dir(T,H); 5565get_latest_dir([_|T],Latest) -> 5566 get_latest_dir(T,Latest); 5567get_latest_dir([],Latest) -> 5568 Latest. 5569 5570get_all_cross_info([{_Tag,Dir}|Rest],Acc) -> 5571 case file:read_file(filename:join(Dir,?cross_cover_info)) of 5572 {ok,Bin} -> 5573 TagMods = binary_to_term(Bin), 5574 get_all_cross_info(Rest,TagMods++Acc); 5575 _ -> 5576 get_all_cross_info(Rest,Acc) 5577 end; 5578get_all_cross_info([],Acc) -> 5579 Acc. 5580 5581%% Associate the cross cover modules with their log directories 5582add_cross_modules(TagMods,TagDirs)-> 5583 do_add_cross_modules(TagMods,[{Tag,Dir,[]} || {Tag,Dir} <- TagDirs]). 5584do_add_cross_modules([{Tag,Mods1}|TagMods],TagDirMods)-> 5585 NewTagDirMods = 5586 case lists:keytake(Tag,1,TagDirMods) of 5587 {value,{Tag,Dir,Mods},Rest} -> 5588 [{Tag,Dir,lists:umerge(lists:sort(Mods1),Mods)}|Rest]; 5589 false -> 5590 TagDirMods 5591 end, 5592 do_add_cross_modules(TagMods,NewTagDirMods); 5593do_add_cross_modules([],TagDirMods) -> 5594 %% Just to get the modules in the same order as in the normal cover log 5595 [{Tag,Dir,lists:reverse(Mods)} || {Tag,Dir,Mods} <- TagDirMods]. 5596 5597%% Find all exported coverdata files. 5598get_coverdata_files(TagDirMods) -> 5599 lists:flatmap( 5600 fun({_,LatestDir,_}) -> 5601 filelib:wildcard(filename:join(LatestDir,"all.coverdata")) 5602 end, 5603 TagDirMods). 5604 5605 5606%% For each test, analyse all modules 5607%% Used for cross cover analysis. 5608analyse_tests([{Tag,LastTest,Modules}|T], DetailsFun, Acc) -> 5609 Cov = analyse_modules(LastTest, Modules, DetailsFun, []), 5610 analyse_tests(T, DetailsFun, [{Tag,Cov}|Acc]); 5611analyse_tests([], _DetailsFun, Acc) -> 5612 Acc. 5613 5614%% Analyse each module 5615%% Used for cross cover analysis. 5616analyse_modules(Dir, [M|Modules], DetailsFun, Acc) -> 5617 {ok,{M,{Cov,NotCov}}} = cover:analyse(M, module), 5618 Acc1 = [{M,{Cov,NotCov,DetailsFun(Dir,M)}}|Acc], 5619 analyse_modules(Dir, Modules, DetailsFun, Acc1); 5620analyse_modules(_Dir, [], _DetailsFun, Acc) -> 5621 Acc. 5622 5623 5624%% Support functions for writing the cover logs (both cross and normal) 5625write_coverlog_header(CoverLog) -> 5626 case catch io:put_chars(CoverLog,html_header("Coverage results")) of 5627 {'EXIT',Reason} -> 5628 io:format("\n\nERROR: Could not write normal heading in coverlog.\n" 5629 "CoverLog: ~tw\n" 5630 "Reason: ~tp\n", 5631 [CoverLog,Reason]), 5632 io:format(CoverLog,"<html><body>\n", []); 5633 _ -> 5634 ok 5635 end. 5636 5637 5638format_analyse(M,Cov,NotCov,undefined) -> 5639 io_lib:fwrite("<tr><td>~w</td>" 5640 "<td align=right>~w %</td>" 5641 "<td align=right>~w</td>" 5642 "<td align=right>~w</td></tr>\n", 5643 [M,pc(Cov,NotCov),Cov,NotCov]); 5644format_analyse(M,Cov,NotCov,{file,File}) -> 5645 io_lib:fwrite("<tr><td><a href=\"~ts\">~w</a></td>" 5646 "<td align=right>~w %</td>" 5647 "<td align=right>~w</td>" 5648 "<td align=right>~w</td></tr>\n", 5649 [uri_encode(filename:basename(File)), 5650 M,pc(Cov,NotCov),Cov,NotCov]); 5651format_analyse(M,Cov,NotCov,{lines,Lines}) -> 5652 CoverOutName = atom_to_list(M)++".COVER.html", 5653 {ok,CoverOut} = open_html_file(CoverOutName), 5654 write_not_covered(CoverOut,M,Lines), 5655 ok = file:close(CoverOut), 5656 io_lib:fwrite("<tr><td><a href=\"~ts\">~w</a></td>" 5657 "<td align=right>~w %</td>" 5658 "<td align=right>~w</td>" 5659 "<td align=right>~w</td></tr>\n", 5660 [uri_encode(CoverOutName),M,pc(Cov,NotCov),Cov,NotCov]); 5661format_analyse(M,Cov,NotCov,{error,_}) -> 5662 io_lib:fwrite("<tr><td>~w</td>" 5663 "<td align=right>~w %</td>" 5664 "<td align=right>~w</td>" 5665 "<td align=right>~w</td></tr>\n", 5666 [M,pc(Cov,NotCov),Cov,NotCov]). 5667 5668 5669pc(0,0) -> 5670 0; 5671pc(Cov,NotCov) -> 5672 round(Cov/(Cov+NotCov)*100). 5673 5674 5675write_not_covered(CoverOut,M,Lines) -> 5676 io:put_chars(CoverOut,html_header("Coverage results for "++atom_to_list(M))), 5677 io:fwrite(CoverOut, 5678 "The following lines in module ~w are not covered:\n" 5679 "<table border=3 cellpadding=5>\n" 5680 "<th>Line Number</th>\n", 5681 [M]), 5682 lists:foreach(fun({{_M,Line},{0,1}}) -> 5683 io:fwrite(CoverOut,"<tr><td>~w</td></tr>\n", [Line]); 5684 (_) -> 5685 ok 5686 end, 5687 Lines), 5688 io:put_chars(CoverOut,"</table>\n</body>\n</html>\n"). 5689 5690 5691write_default_coverlog(TestDir) -> 5692 {ok,CoverLog} = open_html_file(filename:join(TestDir,?coverlog_name)), 5693 write_coverlog_header(CoverLog), 5694 io:put_chars(CoverLog,"Cover tool is not used\n</body></html>\n"), 5695 ok = file:close(CoverLog). 5696 5697write_default_cross_coverlog(TestDir) -> 5698 {ok,CrossCoverLog} = 5699 open_html_file(filename:join(TestDir,?cross_coverlog_name)), 5700 write_coverlog_header(CrossCoverLog), 5701 io:put_chars(CrossCoverLog, 5702 ["No cross cover modules exist for this application,", 5703 xhtml("<br>","<br />"), 5704 "or cross cover analysis is not completed.\n" 5705 "</body></html>\n"]), 5706 ok = file:close(CrossCoverLog). 5707 5708write_cover_result_table(CoverLog,Coverage) -> 5709 io:fwrite(CoverLog, 5710 "<p><table border=3 cellpadding=5>\n" 5711 "<tr><th>Module</th><th>Covered (%)</th><th>Covered (Lines)</th>" 5712 "<th>Not covered (Lines)</th>\n", 5713 []), 5714 {TotCov,TotNotCov} = 5715 lists:foldl(fun({M,{Cov,NotCov,Details}},{AccCov,AccNotCov}) -> 5716 Str = format_analyse(M,Cov,NotCov,Details), 5717 io:fwrite(CoverLog,"~ts", [Str]), 5718 {AccCov+Cov,AccNotCov+NotCov}; 5719 ({_M,{error,_Reason}},{AccCov,AccNotCov}) -> 5720 {AccCov,AccNotCov} 5721 end, 5722 {0,0}, 5723 Coverage), 5724 TotPercent = pc(TotCov,TotNotCov), 5725 io:fwrite(CoverLog, 5726 "<tr><th align=left>Total</th><th align=right>~w %</th>" 5727 "<th align=right>~w</th><th align=right>~w</th></tr>\n" 5728 "</table>\n" 5729 "</body>\n" 5730 "</html>\n", 5731 [TotPercent,TotCov,TotNotCov]), 5732 ok = file:close(CoverLog), 5733 TotPercent. 5734 5735 5736%%%----------------------------------------------------------------- 5737%%% Support functions for writing files 5738 5739%% HTML files are always written with utf8 encoding 5740html_header(Title) -> 5741 ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n" 5742 "<!-- autogenerated by '", atom_to_list(?MODULE), "'. -->\n" 5743 "<html>\n" 5744 "<head>\n" 5745 "<title>", Title, "</title>\n" 5746 "<meta http-equiv=\"cache-control\" content=\"no-cache\"></meta>\n" 5747 "<meta http-equiv=\"content-type\" content=\"text/html; " 5748 "charset=utf-8\"></meta>\n" 5749 "</head>\n" 5750 "<body bgcolor=\"white\" text=\"black\" " 5751 "link=\"blue\" vlink=\"purple\" alink=\"red\">\n"]. 5752 5753html_header(Title, Meta) -> 5754 ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n" 5755 "<!-- autogenerated by '", atom_to_list(?MODULE), "'. -->\n" 5756 "<html>\n" 5757 "<head>\n" 5758 "<title>", Title, "</title>\n"] ++ Meta ++ ["</head>\n"]. 5759 5760open_html_file(File) -> 5761 open_utf8_file(File). 5762 5763open_html_file(File,Opts) -> 5764 open_utf8_file(File,Opts). 5765 5766write_html_file(File,Content) -> 5767 write_file(File,Content,utf8). 5768 5769%% The 'major' log file, which is a pure text file is also written 5770%% with utf8 encoding 5771open_utf8_file(File) -> 5772 case file:open(File,AllOpts=[write,{encoding,utf8}]) of 5773 {error,Reason} -> {error,{Reason,{File,AllOpts}}}; 5774 Result -> Result 5775 end. 5776 5777open_utf8_file(File,Opts) -> 5778 case file:open(File,AllOpts=[{encoding,utf8}|Opts]) of 5779 {error,Reason} -> {error,{Reason,{File,AllOpts}}}; 5780 Result -> Result 5781 end. 5782 5783%% Write a file with specified encoding 5784write_file(File,Content,latin1) -> 5785 file:write_file(File,Content); 5786write_file(File,Content,utf8) -> 5787 write_binary_file(File,unicode:characters_to_binary(Content)). 5788 5789%% Write a file with only binary data 5790write_binary_file(File,Content) -> 5791 file:write_file(File,Content). 5792 5793%% Encoding of hyperlinks in HTML files 5794uri_encode(File) -> 5795 Encoding = file:native_name_encoding(), 5796 uri_encode(File,Encoding). 5797 5798uri_encode(File,Encoding) -> 5799 Components = filename:split(File), 5800 filename:join([uri_encode_comp(C,Encoding) || C <- Components]). 5801 5802%% Encode the reference to a "filename of the given encoding" so it 5803%% can be inserted in a utf8 encoded HTML file. 5804%% This does almost the same as http_uri:encode/1, except 5805%% 1. it does not convert @, : and / (in order to preserve nodename and c:/) 5806%% 2. if the file name is in latin1, it also encodes all 5807%% characters >127 - i.e. latin1 but not ASCII. 5808uri_encode_comp([Char|Chars],Encoding) -> 5809 Reserved = sets:is_element(Char, reserved()), 5810 case (Char>127 andalso Encoding==latin1) orelse Reserved of 5811 true -> 5812 [ $% | integer_to_list(Char, 16)] ++ 5813 uri_encode_comp(Chars,Encoding); 5814 false -> 5815 [Char | uri_encode_comp(Chars,Encoding)] 5816 end; 5817uri_encode_comp([],_) -> 5818 []. 5819 5820%% Copied from http_uri.erl, but slightly modified 5821%% (not converting @, : and /) 5822reserved() -> 5823 sets:from_list([$;, $&, $=, $+, $,, $?, 5824 $#, $[, $], $<, $>, $\", ${, $}, $|, 5825 $\\, $', $^, $%, $ ]). 5826 5827encoding(File) -> 5828 case epp:read_encoding(File) of 5829 none -> 5830 epp:default_encoding(); 5831 E -> 5832 E 5833 end. 5834 5835check_repeat_testcase(Case,Result,Cases, 5836 [{Ref,[{repeat,RepeatData0}],StartTime}|Mode0]) -> 5837 case do_update_repeat_data(Result,RepeatData0) of 5838 false -> 5839 {Cases,Mode0}; 5840 RepeatData -> 5841 {[Case|Cases],[{Ref,[{repeat,RepeatData}],StartTime}|Mode0]} 5842 end; 5843check_repeat_testcase(_,_,Cases,Mode) -> 5844 {Cases,Mode}. 5845 5846do_update_repeat_data(_,{RT,N,N}) when is_integer(N) -> 5847 report_repeat_testcase(N,N), 5848 report_stop_repeat_testcase(done,{RT,N}), 5849 false; 5850do_update_repeat_data(ok,{repeat_until_ok=RT,M,N}) -> 5851 report_repeat_testcase(M,N), 5852 report_stop_repeat_testcase(RT,{RT,N}), 5853 false; 5854do_update_repeat_data(failed,{repeat_until_fail=RT,M,N}) -> 5855 report_repeat_testcase(M,N), 5856 report_stop_repeat_testcase(RT,{RT,N}), 5857 false; 5858do_update_repeat_data(_,{RT,M,N}) when is_integer(M) -> 5859 report_repeat_testcase(M,N), 5860 {RT,M+1,N}; 5861do_update_repeat_data(_,{_,M,N}=RepeatData) -> 5862 report_repeat_testcase(M,N), 5863 RepeatData. 5864 5865report_stop_repeat_testcase(Reason,RepVal) -> 5866 print(minor, "~n*** Stopping test case repeat operation: ~w", [Reason]), 5867 print(1, "Stopping test case repeat operation: ~w", [RepVal]). 5868 5869report_repeat_testcase(M,forever) -> 5870 print(minor, "~n=== Repeated test case: ~w of infinity", [M]); 5871report_repeat_testcase(M,N) -> 5872 print(minor, "~n=== Repeated test case: ~w of ~w", [M,N]). 5873