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    "&gt;" ++ sanitize(T);
297sanitize([$<|T]) ->
298    "&lt;" ++ sanitize(T);
299sanitize([$"|T]) ->
300    "&quot;" ++ sanitize(T);
301sanitize([$'|T]) ->
302    "&apos;" ++ sanitize(T);
303sanitize([$&|T]) ->
304    "&amp;" ++ 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