1%%-------------------------------------------------------------------- 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2012-2018. All Rights Reserved. 5%% 6%% Licensed under the Apache License, Version 2.0 (the "License"); 7%% you may not use this file except in compliance with the License. 8%% You may obtain a copy of the License at 9%% 10%% http://www.apache.org/licenses/LICENSE-2.0 11%% 12%% Unless required by applicable law or agreed to in writing, software 13%% distributed under the License is distributed on an "AS IS" BASIS, 14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15%% See the License for the specific language governing permissions and 16%% limitations under the License. 17%% 18%% %CopyrightEnd% 19%%-------------------------------------------------------------------- 20 21%%% Common Test Framework functions handling test specifications. 22%%% 23%%% This module creates a junit report of the test run if plugged in 24%%% as a suite_callback. 25 26-module(cth_surefire). 27 28%% Suite Callbacks 29-export([id/1, init/2]). 30 31-export([pre_init_per_suite/3]). 32-export([post_init_per_suite/4]). 33-export([pre_end_per_suite/3]). 34-export([post_end_per_suite/4]). 35 36-export([pre_init_per_group/4]). 37-export([post_init_per_group/5]). 38-export([pre_end_per_group/4]). 39-export([post_end_per_group/5]). 40 41-export([pre_init_per_testcase/4]). 42-export([post_end_per_testcase/5]). 43 44-export([on_tc_fail/4]). 45-export([on_tc_skip/4]). 46 47-export([terminate/1]). 48 49-record(state, { filepath, axis, properties, package, hostname, 50 curr_suite, curr_suite_ts, curr_group = [], 51 curr_log_dir, timer, tc_log, url_base, 52 test_cases = [], 53 test_suites = [] }). 54 55-record(testcase, { log, url, group, classname, name, time, result, timestamp }). 56-record(testsuite, { errors, failures, skipped, hostname, name, tests, 57 time, timestamp, id, package, 58 properties, testcases, log, url }). 59 60-define(default_report,"junit_report.xml"). 61-define(suite_log,"suite.log.html"). 62 63-define(now, os:timestamp()). 64 65%% Number of dirs from log root to testcase log file. 66%% ct_run.<node>.<timestamp>/<test_name>/run.<timestamp>/<tc_log>.html 67-define(log_depth,3). 68 69id(Opts) -> 70 case proplists:get_value(path, Opts) of 71 undefined -> ?default_report; 72 Path -> filename:absname(Path) 73 end. 74 75init(Path, Opts) -> 76 {ok, Host} = inet:gethostname(), 77 #state{ filepath = Path, 78 hostname = proplists:get_value(hostname,Opts,Host), 79 package = proplists:get_value(package,Opts), 80 axis = proplists:get_value(axis,Opts,[]), 81 properties = proplists:get_value(properties,Opts,[]), 82 url_base = proplists:get_value(url_base,Opts), 83 timer = ?now }. 84 85pre_init_per_suite(Suite,SkipOrFail,#state{ test_cases = [] } = State) 86 when is_tuple(SkipOrFail) -> 87 {SkipOrFail, init_tc(State#state{curr_suite = Suite, 88 curr_suite_ts = ?now}, 89 SkipOrFail) }; 90pre_init_per_suite(Suite,Config,#state{ test_cases = [] } = State) -> 91 TcLog = proplists:get_value(tc_logfile,Config), 92 CurrLogDir = filename:dirname(TcLog), 93 Path = 94 case State#state.filepath of 95 ?default_report -> 96 RootDir = get_test_root(TcLog), 97 filename:join(RootDir,?default_report); 98 P -> 99 P 100 end, 101 {Config, init_tc(State#state{ filepath = Path, 102 curr_suite = Suite, 103 curr_suite_ts = ?now, 104 curr_log_dir = CurrLogDir}, 105 Config) }; 106pre_init_per_suite(Suite,Config,State) -> 107 %% Have to close the previous suite 108 pre_init_per_suite(Suite,Config,close_suite(State)). 109 110post_init_per_suite(_Suite,Config, Result, State) -> 111 {Result, end_tc(init_per_suite,Config,Result,State)}. 112 113pre_end_per_suite(_Suite,Config,State) -> 114 {Config, init_tc(State, Config)}. 115 116post_end_per_suite(_Suite,Config,Result,State) -> 117 {Result, end_tc(end_per_suite,Config,Result,State)}. 118 119pre_init_per_group(_Suite,Group,Config,State) -> 120 {Config, init_tc(State#state{ curr_group = [Group|State#state.curr_group]}, 121 Config)}. 122 123post_init_per_group(_Suite,_Group,Config,Result,State) -> 124 {Result, end_tc(init_per_group,Config,Result,State)}. 125 126pre_end_per_group(_Suite,_Group,Config,State) -> 127 {Config, init_tc(State, Config)}. 128 129post_end_per_group(_Suite,_Group,Config,Result,State) -> 130 NewState = end_tc(end_per_group, Config, Result, State), 131 {Result, NewState#state{ curr_group = tl(NewState#state.curr_group)}}. 132 133pre_init_per_testcase(_Suite,_TC,Config,State) -> 134 {Config, init_tc(State, Config)}. 135 136post_end_per_testcase(_Suite,TC,Config,Result,State) -> 137 {Result, end_tc(TC,Config, Result,State)}. 138 139on_tc_fail(_Suite,_TC, _Res, State = #state{test_cases = []}) -> 140 State; 141on_tc_fail(_Suite,_TC, Res, State) -> 142 TCs = State#state.test_cases, 143 TC = hd(TCs), 144 NewTC = TC#testcase{ 145 result = 146 {fail,lists:flatten(io_lib:format("~tp",[Res]))} }, 147 State#state{ test_cases = [NewTC | tl(TCs)]}. 148 149on_tc_skip(Suite,{ConfigFunc,_GrName}, Res, State) -> 150 on_tc_skip(Suite,ConfigFunc, Res, State); 151on_tc_skip(Suite,Tc, Res, State0) -> 152 TcStr = atom_to_list(Tc), 153 State = 154 case State0#state.test_cases of 155 [#testcase{name=TcStr}|TCs] -> 156 State0#state{test_cases=TCs}; 157 _ -> 158 State0 159 end, 160 do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(set_suite(Suite,State),[]))). 161 162do_tc_skip(Res, State) -> 163 TCs = State#state.test_cases, 164 TC = hd(TCs), 165 NewTC = TC#testcase{ 166 result = 167 {skipped,lists:flatten(io_lib:format("~tp",[Res]))} }, 168 State#state{ test_cases = [NewTC | tl(TCs)]}. 169 170init_tc(State, Config) when is_list(Config) == false -> 171 State#state{ timer = ?now, tc_log = "" }; 172init_tc(State, Config) -> 173 State#state{ timer = ?now, 174 tc_log = proplists:get_value(tc_logfile, Config, [])}. 175 176end_tc(Func, Config, Res, State) when is_atom(Func) -> 177 end_tc(atom_to_list(Func), Config, Res, State); 178end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite, 179 curr_group = Groups, 180 curr_log_dir = CurrLogDir, 181 timer = TS, 182 tc_log = Log0, 183 url_base = UrlBase } ) -> 184 Log = 185 case Log0 of 186 "" -> 187 LowerSuiteName = string:lowercase(atom_to_list(Suite)), 188 filename:join(CurrLogDir,LowerSuiteName++"."++Name++".html"); 189 _ -> 190 Log0 191 end, 192 Url = make_url(UrlBase,Log), 193 ClassName = atom_to_list(Suite), 194 PGroup = lists:concat(lists:join(".",lists:reverse(Groups))), 195 TimeTakes = io_lib:format("~f",[timer:now_diff(?now,TS) / 1000000]), 196 State#state{ test_cases = [#testcase{ log = Log, 197 url = Url, 198 timestamp = now_to_string(TS), 199 classname = ClassName, 200 group = PGroup, 201 name = Name, 202 time = TimeTakes, 203 result = passed }| 204 State#state.test_cases], 205 tc_log = ""}. % so old tc_log is not set if next is on_tc_skip 206 207set_suite(Suite,#state{curr_suite=undefined}=State) -> 208 State#state{curr_suite=Suite, curr_suite_ts=?now}; 209set_suite(_,State) -> 210 State. 211 212close_suite(#state{ test_cases = [] } = State) -> 213 State; 214close_suite(#state{ test_cases = TCs, url_base = UrlBase } = State) -> 215 {Total,Fail,Skip} = count_tcs(TCs,0,0,0), 216 TimeTaken = timer:now_diff(?now,State#state.curr_suite_ts) / 1000000, 217 SuiteLog = filename:join(State#state.curr_log_dir,?suite_log), 218 SuiteUrl = make_url(UrlBase,SuiteLog), 219 Suite = #testsuite{ name = atom_to_list(State#state.curr_suite), 220 package = State#state.package, 221 hostname = State#state.hostname, 222 time = io_lib:format("~f",[TimeTaken]), 223 timestamp = now_to_string(State#state.curr_suite_ts), 224 errors = 0, 225 failures = Fail, 226 skipped = Skip, 227 tests = Total, 228 testcases = lists:reverse(TCs), 229 log = SuiteLog, 230 url = SuiteUrl}, 231 State#state{ curr_suite = undefined, 232 test_cases = [], 233 test_suites = [Suite | State#state.test_suites]}. 234 235terminate(State = #state{ test_cases = [] }) -> 236 {ok,D} = file:open(State#state.filepath,[write,{encoding,utf8}]), 237 io:format(D, "<?xml version=\"1.0\" encoding= \"UTF-8\" ?>", []), 238 io:format(D, "~ts", [to_xml(State)]), 239 catch file:sync(D), 240 catch file:close(D); 241terminate(State) -> 242 %% Have to close the last suite 243 terminate(close_suite(State)). 244 245 246 247to_xml(#testcase{ group = Group, classname = CL, log = L, url = U, name = N, time = T, timestamp = TS, result = R}) -> 248 ["<testcase ", 249 [["group=\"",Group,"\" "]||Group /= ""], 250 "name=\"",N,"\" " 251 "time=\"",T,"\" " 252 "timestamp=\"",TS,"\" ", 253 [["url=\"",U,"\" "]||U /= undefined], 254 "log=\"",L,"\">", 255 case R of 256 passed -> 257 []; 258 {skipped,Reason} -> 259 ["<skipped type=\"skip\" message=\"Test ",N," in ",CL, 260 " skipped!\">", sanitize(Reason),"</skipped>"]; 261 {fail,Reason} -> 262 ["<failure message=\"Test ",N," in ",CL," failed!\" type=\"crash\">", 263 sanitize(Reason),"</failure>"] 264 end,"</testcase>"]; 265to_xml(#testsuite{ package = P, hostname = H, errors = E, failures = F, 266 skipped = S, time = Time, timestamp = TS, tests = T, name = N, 267 testcases = Cases, log = Log, url = Url }) -> 268 ["<testsuite ", 269 [["package=\"",P,"\" "]||P /= undefined], 270 "hostname=\"",H,"\" " 271 "name=\"",N,"\" " 272 "time=\"",Time,"\" " 273 "timestamp=\"",TS,"\" " 274 "errors=\"",integer_to_list(E),"\" " 275 "failures=\"",integer_to_list(F),"\" " 276 "skipped=\"",integer_to_list(S),"\" " 277 "tests=\"",integer_to_list(T),"\" ", 278 [["url=\"",Url,"\" "]||Url /= undefined], 279 "log=\"",Log,"\">", 280 [to_xml(Case) || Case <- Cases], 281 "</testsuite>"]; 282to_xml(#state{ test_suites = TestSuites, axis = Axis, properties = Props }) -> 283 ["<testsuites>",properties_to_xml(Axis,Props), 284 [to_xml(TestSuite) || TestSuite <- TestSuites],"</testsuites>"]. 285 286properties_to_xml([],[]) -> 287 []; 288properties_to_xml(Axis,Props) -> 289 ["<properties>", 290 [["<property name=\"",Name,"\" axis=\"yes\" value=\"",Value,"\" />"] || {Name,Value} <- Axis], 291 [["<property name=\"",Name,"\" value=\"",Value,"\" />"] || {Name,Value} <- Props], 292 "</properties>" 293 ]. 294 295sanitize([$>|T]) -> 296 ">" ++ sanitize(T); 297sanitize([$<|T]) -> 298 "<" ++ sanitize(T); 299sanitize([$"|T]) -> 300 """ ++ sanitize(T); 301sanitize([$'|T]) -> 302 "'" ++ sanitize(T); 303sanitize([$&|T]) -> 304 "&" ++ sanitize(T); 305sanitize([H|T]) -> 306 [H|sanitize(T)]; 307sanitize([]) -> 308 []. 309 310now_to_string(Now) -> 311 {{YY,MM,DD},{HH,Mi,SS}} = calendar:now_to_local_time(Now), 312 io_lib:format("~w-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B",[YY,MM,DD,HH,Mi,SS]). 313 314make_url(undefined,_) -> 315 undefined; 316make_url(_,[]) -> 317 undefined; 318make_url(UrlBase0,Log) -> 319 UrlBase = string:trim(UrlBase0,trailing,[$/]), 320 RelativeLog = get_relative_log_url(Log), 321 lists:flatten(lists:join($/,[UrlBase,RelativeLog])). 322 323get_test_root(Log) -> 324 LogParts = filename:split(Log), 325 filename:join(lists:sublist(LogParts,1,length(LogParts)-?log_depth)). 326 327get_relative_log_url(Log) -> 328 LogParts = filename:split(Log), 329 Start = length(LogParts)-?log_depth, 330 Length = ?log_depth+1, 331 lists:flatten(lists:join($/,lists:sublist(LogParts,Start,Length))). 332 333count_tcs([#testcase{name=ConfCase}|TCs],Ok,F,S) 334 when ConfCase=="init_per_suite"; 335 ConfCase=="end_per_suite"; 336 ConfCase=="init_per_group"; 337 ConfCase=="end_per_group" -> 338 count_tcs(TCs,Ok,F,S); 339count_tcs([#testcase{result=passed}|TCs],Ok,F,S) -> 340 count_tcs(TCs,Ok+1,F,S); 341count_tcs([#testcase{result={fail,_}}|TCs],Ok,F,S) -> 342 count_tcs(TCs,Ok,F+1,S); 343count_tcs([#testcase{result={skipped,_}}|TCs],Ok,F,S) -> 344 count_tcs(TCs,Ok,F,S+1); 345count_tcs([#testcase{result={auto_skipped,_}}|TCs],Ok,F,S) -> 346 count_tcs(TCs,Ok,F,S+1); 347count_tcs([],Ok,F,S) -> 348 {Ok+F+S,F,S}. 349