%%% @doc Common Test Framework functions handling test specifications. %%% %%%

This module creates a junit report of the test run if plugged in %%% as a suite_callback.

-module(cth_surefire). %% Suite Callbacks -export([id/1, init/2]). -export([pre_init_per_suite/3]). -export([post_init_per_suite/4]). -export([pre_end_per_suite/3]). -export([post_end_per_suite/4]). -export([pre_init_per_group/3]). -export([post_init_per_group/4]). -export([pre_end_per_group/3]). -export([post_end_per_group/4]). -export([pre_init_per_testcase/3]). -export([post_end_per_testcase/4]). -export([on_tc_fail/3]). -export([on_tc_skip/3]). -export([terminate/1]). -record(state, { filepath, axis, properties, package, hostname, curr_suite, curr_suite_ts, curr_group = [], curr_tc, curr_log_dir, timer, tc_log, test_cases = [], test_suites = [] }). -record(testcase, { log, group, classname, name, time, failure, timestamp }). -record(testsuite, { errors, failures, hostname, name, tests, time, timestamp, id, package, properties, testcases }). id(Opts) -> filename:absname(proplists:get_value(path, Opts, "junit_report.xml")). init(Path, Opts) -> {ok, Host} = inet:gethostname(), #state{ filepath = Path, hostname = proplists:get_value(hostname,Opts,Host), package = proplists:get_value(package,Opts), axis = proplists:get_value(axis,Opts,[]), properties = proplists:get_value(properties,Opts,[]), timer = now() }. pre_init_per_suite(Suite,Config,#state{ test_cases = [] } = State) -> {Config, init_tc(State#state{ curr_suite = Suite, curr_suite_ts = now() }, Config) }; pre_init_per_suite(Suite,Config,State) -> %% Have to close the previous suite pre_init_per_suite(Suite,Config,close_suite(State)). post_init_per_suite(_Suite,Config, Result, State) -> {Result, end_tc(init_per_suite,Config,Result,State)}. pre_end_per_suite(_Suite,Config,State) -> {Config, init_tc(State, Config)}. post_end_per_suite(_Suite,Config,Result,State) -> {Result, end_tc(end_per_suite,Config,Result,State)}. pre_init_per_group(Group,Config,State) -> {Config, init_tc(State#state{ curr_group = [Group|State#state.curr_group]}, Config)}. post_init_per_group(_Group,Config,Result,State) -> {Result, end_tc(init_per_group,Config,Result,State)}. pre_end_per_group(_Group,Config,State) -> {Config, init_tc(State, Config)}. post_end_per_group(_Group,Config,Result,State) -> NewState = end_tc(end_per_group, Config, Result, State), {Result, NewState#state{ curr_group = tl(NewState#state.curr_group)}}. pre_init_per_testcase(_TC,Config,State) -> {Config, init_tc(State, Config)}. post_end_per_testcase(TC,Config,Result,State) -> {Result, end_tc(TC,Config, Result,State)}. on_tc_fail(_TC, _Res, State = #state{test_cases = []}) -> State; on_tc_fail(_TC, Res, State) -> TCs = State#state.test_cases, TC = hd(TCs), NewTC = TC#testcase{ failure = {fail,lists:flatten(io_lib:format("~p",[Res]))} }, State#state{ test_cases = [NewTC | tl(TCs)]}. on_tc_skip(Tc,{Type,_Reason} = Res, State) when Type == tc_auto_skip -> do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(State,[]))); on_tc_skip(_Tc, _Res, State = #state{test_cases = []}) -> State; on_tc_skip(_Tc, Res, State) -> do_tc_skip(Res, State). do_tc_skip(Res, State) -> TCs = State#state.test_cases, TC = hd(TCs), NewTC = TC#testcase{ failure = {skipped,lists:flatten(io_lib:format("~p",[Res]))} }, State#state{ test_cases = [NewTC | tl(TCs)]}. init_tc(State, Config) when is_list(Config) == false -> State#state{ timer = now(), tc_log = "" }; init_tc(State, Config) -> State#state{ timer = now(), tc_log = proplists:get_value(tc_logfile, Config, [])}. end_tc(Func, Config, Res, State) when is_atom(Func) -> end_tc(atom_to_list(Func), Config, Res, State); end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite, curr_group = Groups, timer = TS, tc_log = Log } ) -> ClassName = atom_to_list(Suite), PGroup = string:join([ atom_to_list(Group)|| Group <- lists:reverse(Groups)],"."), TimeTakes = io_lib:format("~f",[timer:now_diff(now(),TS) / 1000000]), State#state{ test_cases = [#testcase{ log = Log, timestamp = now_to_string(TS), classname = ClassName, group = PGroup, name = Name, time = TimeTakes, failure = passed }| State#state.test_cases]}. close_suite(#state{ test_cases = [] } = State) -> State; close_suite(#state{ test_cases = TCs } = State) -> Total = length(TCs), Succ = length(lists:filter(fun(#testcase{ failure = F }) -> F == passed end,TCs)), Fail = Total - Succ, TimeTaken = timer:now_diff(now(),State#state.curr_suite_ts) / 1000000, Suite = #testsuite{ name = atom_to_list(State#state.curr_suite), package = State#state.package, time = io_lib:format("~f",[TimeTaken]), timestamp = now_to_string(State#state.curr_suite_ts), errors = Fail, tests = Total, testcases = lists:reverse(TCs) }, State#state{ test_cases = [], test_suites = [Suite | State#state.test_suites]}. terminate(State = #state{ test_cases = [] }) -> {ok,D} = file:open(State#state.filepath,[write,{encoding,utf8}]), io:format(D, "", []), io:format(D, to_xml(State), []), catch file:sync(D), catch file:close(D); terminate(State) -> %% Have to close the last suite terminate(close_suite(State)). to_xml(#testcase{ group = Group, classname = CL, log = L, name = N, time = T, timestamp = TS, failure = F}) -> ["", case F of passed -> []; {skipped,Reason} -> ["", sanitize(Reason),""]; {fail,Reason} -> ["", sanitize(Reason),""] end,""]; to_xml(#testsuite{ package = P, hostname = H, errors = E, time = Time, timestamp = TS, tests = T, name = N, testcases = Cases }) -> ["", [to_xml(Case) || Case <- Cases], ""]; to_xml(#state{ test_suites = TestSuites, axis = Axis, properties = Props }) -> ["",properties_to_xml(Axis,Props), [to_xml(TestSuite) || TestSuite <- TestSuites],""]. properties_to_xml(Axis,Props) -> ["", [[""] || {Name,Value} <- Axis], [[""] || {Name,Value} <- Props], "" ]. sanitize([$>|T]) -> ">" ++ sanitize(T); sanitize([$<|T]) -> "<" ++ sanitize(T); sanitize([$"|T]) -> """ ++ sanitize(T); sanitize([$'|T]) -> "'" ++ sanitize(T); sanitize([$&|T]) -> "&" ++ sanitize(T); sanitize([H|T]) -> [H|sanitize(T)]; sanitize([]) -> []. now_to_string(Now) -> {{YY,MM,DD},{HH,Mi,SS}} = calendar:now_to_local_time(Now), io_lib:format("~p-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B",[YY,MM,DD,HH,Mi,SS]).