1-module(cth_retry).
2
3%% Callbacks
4-export([id/1]).
5-export([init/2]).
6
7-export([pre_init_per_suite/3]).
8-export([post_init_per_suite/4]).
9-export([pre_end_per_suite/3]).
10-export([post_end_per_suite/4]).
11
12-export([pre_init_per_group/3]).
13-export([post_init_per_group/4]).
14-export([pre_end_per_group/3]).
15-export([post_end_per_group/4]).
16
17-export([pre_init_per_testcase/3]).
18-export([post_end_per_testcase/4]).
19
20-export([on_tc_fail/3]).
21-export([on_tc_skip/3, on_tc_skip/4]).
22
23-export([terminate/1]).
24
25-record(state, {id, suite, groups, acc=[]}).
26
27%% @doc Return a unique id for this CTH.
28id(_Opts) ->
29    {?MODULE, make_ref()}.
30
31%% @doc Always called before any other callback function. Use this to initiate
32%% any common state.
33init(Id, _Opts) ->
34    {ok, #state{id=Id}}.
35
36%% @doc Called before init_per_suite is called.
37pre_init_per_suite(Suite,Config,State) ->
38    {Config, State#state{suite=Suite, groups=[]}}.
39
40%% @doc Called after init_per_suite.
41post_init_per_suite(_Suite,_Config,Return,State) ->
42    {Return, State}.
43
44%% @doc Called before end_per_suite.
45pre_end_per_suite(_Suite,Config,State) ->
46    {Config, State}.
47
48%% @doc Called after end_per_suite.
49post_end_per_suite(_Suite,_Config,Return,State) ->
50    {Return, State#state{suite=undefined, groups=[]}}.
51
52%% @doc Called before each init_per_group.
53pre_init_per_group(_Group,Config,State) ->
54    {Config, State}.
55
56%% @doc Called after each init_per_group.
57post_init_per_group(Group,_Config,Return, State=#state{groups=Groups}) ->
58    {Return, State#state{groups=[Group|Groups]}}.
59
60%% @doc Called after each end_per_group.
61pre_end_per_group(_Group,Config,State) ->
62    {Config, State}.
63
64%% @doc Called after each end_per_group.
65post_end_per_group(_Group,_Config,Return, State=#state{groups=Groups}) ->
66    {Return, State#state{groups=tl(Groups)}}.
67
68%% @doc Called before each test case.
69pre_init_per_testcase(_TC,Config,State) ->
70    {Config, State}.
71
72%% @doc Called after each test case.
73post_end_per_testcase(_TC,_Config,ok,State) ->
74    {ok, State};
75post_end_per_testcase(TC,_Config,Error,State=#state{suite=Suite, groups=Groups, acc=Acc}) ->
76    Test = case TC of
77        {_Group, Case} -> Case;
78        TC -> TC
79    end,
80    {Error, State#state{acc=[{Suite, Groups, Test}|Acc]}}.
81
82%% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group,
83%% post_end_per_group and post_end_per_testcase if the suite, group or test case failed.
84on_tc_fail(_TC, _Reason, State) ->
85    State.
86
87%% @doc Called when a test case is skipped by either user action
88%% or due to an init function failing. (>= 19.3)
89on_tc_skip(Suite, TC, {tc_auto_skip, _}, State=#state{suite=Suite, groups=Groups, acc=Acc}) ->
90    NewAcc = case TC of
91        init_per_testcase -> Acc;
92        end_per_testcase -> Acc;
93        {init_per_group,_} -> Acc;
94        {end_per_group, _} -> Acc;
95        init_per_suite -> Acc;
96        end_per_suite -> Acc;
97        {Case, _Group} -> [{Suite, Groups, Case}|Acc];
98        TC -> [{Suite, Groups, TC}|Acc]
99    end,
100    State#state{suite=Suite, acc=NewAcc};
101on_tc_skip(Suite, _TC, _Reason, State) ->
102    State#state{suite=Suite}.
103
104%% @doc Called when a test case is skipped by either user action
105%% or due to an init function failing. (Pre-19.3)
106on_tc_skip(TC, {tc_auto_skip, _}, State=#state{suite=Suite, groups=Groups, acc=Acc}) ->
107    NewAcc = case TC of
108        init_per_testcase -> Acc;
109        end_per_testcase -> Acc;
110        {init_per_group,_} -> Acc;
111        {end_per_group, _} -> Acc;
112        init_per_suite -> Acc;
113        end_per_suite -> Acc;
114        {Case, _Group} -> [{Suite, Groups, Case}|Acc];
115        TC -> [{Suite, Groups, TC}|Acc]
116    end,
117    State#state{acc=NewAcc};
118on_tc_skip(_TC, _Reason, State) ->
119    State.
120
121%% @doc Called when the scope of the CTH is done
122terminate(#state{acc=[]}) ->
123    ok;
124terminate(#state{acc=Acc}) ->
125    Spec = to_spec(Acc),
126    {ok, Cwd} = file:get_cwd(),
127    Path = filename:join(lists:droplast(filename:split(Cwd))++["retry.spec"]),
128    io:format(user,
129              "EXPERIMENTAL: Writing retry specification at ~s~n"
130              "              call rebar3 ct with '--retry' to re-run failing cases.~n",
131             [Path]),
132    file:write_file(Path, Spec),
133    ok.
134
135%%% Helpers
136to_spec(List) ->
137    [to_spec_entry(X) || X <- merge(List)].
138
139merge([]) -> [];
140merge([{Suite, Groups, Case}|T]) when is_atom(Case) ->
141    merge([{Suite, Groups, [Case]}|T]);
142merge([{Suite, Groups, Cases}, {Suite, Groups, Case} | T]) ->
143    merge([{Suite, Groups, [Case|Cases]}|T]);
144merge([{Suite, Groups, Cases} | T]) ->
145    [{Suite, Groups, Cases} | merge(T)].
146
147to_spec_entry({Suite, [], Cases}) ->
148    Dir = filename:dirname(proplists:get_value(source, Suite:module_info(compile))),
149    io_lib:format("~p.~n", [{cases, Dir, Suite, Cases}]);
150to_spec_entry({Suite, Groups, Cases}) ->
151    Dir = filename:dirname(proplists:get_value(source, Suite:module_info(compile))),
152    ExpandedGroups = expand_groups(lists:reverse(Groups)),
153    io_lib:format("~p.~n", [{groups, Dir, Suite, ExpandedGroups, {cases,Cases}}]).
154
155expand_groups([Group]) ->
156    {Group, []};
157expand_groups([H|T]) ->
158    {H,[],[expand_groups(T)]}.
159
160