1%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
2%% ex: ts=4 sw=4 et
3%% -------------------------------------------------------------------
4%%
5%% rebar: Erlang Build Tools
6%%
7%% Copyright (c) 2011-2016 Tuncer Ayaz
8%%
9%% Permission is hereby granted, free of charge, to any person obtaining a copy
10%% of this software and associated documentation files (the "Software"), to deal
11%% in the Software without restriction, including without limitation the rights
12%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13%% copies of the Software, and to permit persons to whom the Software is
14%% furnished to do so, subject to the following conditions:
15%%
16%% The above copyright notice and this permission notice shall be included in
17%% all copies or substantial portions of the Software.
18%%
19%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25%% THE SOFTWARE.
26%% -------------------------------------------------------------------
27-module(rebar_qc).
28
29-export([qc/2,
30         triq/2,
31         eqc/2,
32         clean/2]).
33
34%% for internal use only
35-export([info/2]).
36
37-include("rebar.hrl").
38
39-define(QC_DIR, ".qc").
40
41%% ===================================================================
42%% Public API
43%% ===================================================================
44
45qc(Config, _AppFile) ->
46    run_qc(Config, qc_opts(Config)).
47
48triq(Config, _AppFile) ->
49    ok = load_qc_mod(triq),
50    run_qc(Config, qc_opts(Config), triq).
51
52eqc(Config, _AppFile) ->
53    ok = load_qc_mod(eqc),
54    run_qc(Config, qc_opts(Config), eqc).
55
56clean(_Config, _File) ->
57    rebar_file_utils:rm_rf(?QC_DIR).
58
59%% ===================================================================
60%% Internal functions
61%% ===================================================================
62
63info(help, qc) ->
64    ?CONSOLE(
65       "Test QuickCheck properties.~n"
66       "~n"
67       "Valid rebar.config options:~n"
68       "  {qc_opts, [{qc_mod, module()}, Options]}~n"
69       "  ~p~n"
70       "  ~p~n"
71       "  ~p~n"
72       "  ~p~n"
73       "  ~p~n"
74       "Valid command line options:~n"
75       "  compile_only=true (Compile but do not test properties)",
76       [
77        {qc_compile_opts, []},
78        {qc_first_files, []},
79        {cover_enabled, false},
80        {cover_print_enabled, false},
81        {cover_export_enabled, false}
82       ]);
83info(help, clean) ->
84    Description = ?FMT("Delete QuickCheck test dir (~s)", [?QC_DIR]),
85    ?CONSOLE("~s.~n", [Description]).
86
87-define(TRIQ_MOD, triq).
88-define(EQC_MOD, eqc).
89
90qc_opts(Config) ->
91    rebar_config:get(Config, qc_opts, []).
92
93run_qc(Config, QCOpts) ->
94    run_qc(Config, QCOpts, select_qc_mod(QCOpts)).
95
96run_qc(Config, RawQCOpts, QC) ->
97    ?DEBUG("Selected QC module: ~p~n", [QC]),
98    QCOpts = lists:filter(fun({qc_mod, _}) -> false;
99                             (_) -> true
100                          end, RawQCOpts),
101    run(Config, QC, QCOpts).
102
103select_qc_mod(QCOpts) ->
104    case proplists:get_value(qc_mod, QCOpts) of
105        undefined ->
106            detect_qc_mod();
107        QC ->
108            case code:ensure_loaded(QC) of
109                {module, QC} ->
110                    QC;
111                {error, nofile} ->
112                    ?ABORT("Configured QC library '~p' not available~n", [QC])
113            end
114    end.
115
116detect_qc_mod() ->
117    case code:ensure_loaded(?TRIQ_MOD) of
118        {module, ?TRIQ_MOD} ->
119            ?TRIQ_MOD;
120        {error, nofile} ->
121            case code:ensure_loaded(?EQC_MOD) of
122                {module, ?EQC_MOD} ->
123                    ?EQC_MOD;
124                {error, nofile} ->
125                    ?ABORT("No QC library available~n", [])
126            end
127    end.
128
129load_qc_mod(Mod) ->
130    case code:ensure_loaded(Mod) of
131        {module, Mod} ->
132            ok;
133        {error, nofile} ->
134            ?ABORT("Failed to load QC lib '~p'~n", [Mod])
135    end.
136
137ensure_dirs() ->
138    ok = filelib:ensure_dir(filename:join(qc_dir(), "dummy")),
139    ok = filelib:ensure_dir(filename:join(rebar_utils:ebin_dir(), "dummy")).
140
141setup_codepath() ->
142    CodePath = code:get_path(),
143    true = code:add_patha(qc_dir()),
144    true = code:add_pathz(rebar_utils:ebin_dir()),
145    CodePath.
146
147qc_dir() ->
148    filename:join(rebar_utils:get_cwd(), ?QC_DIR).
149
150run(Config, QC, QCOpts) ->
151    ?DEBUG("qc_opts: ~p~n", [QCOpts]),
152
153    ok = ensure_dirs(),
154    CodePath = setup_codepath(),
155
156    CompileOnly = rebar_config:get_global(Config, compile_only, false),
157    %% Compile erlang code to ?QC_DIR, using a tweaked config
158    %% with appropriate defines, and include all the test modules
159    %% as well.
160    {ok, SrcErls} = rebar_erlc_compiler:test_compile(Config, "qc", ?QC_DIR),
161
162    case CompileOnly of
163        "true" ->
164            true = rebar_utils:cleanup_code_path(CodePath),
165            ?CONSOLE("Compiled modules for qc~n", []);
166        false ->
167            run1(QC, QCOpts, Config, CodePath, SrcErls)
168    end.
169
170run1(QC, QCOpts, Config, CodePath, SrcErls) ->
171
172    AllBeamFiles = rebar_utils:beams(?QC_DIR),
173    AllModules = [rebar_utils:beam_to_mod(?QC_DIR, N)
174                  || N <- AllBeamFiles],
175    PropMods = find_prop_mods(),
176    FilteredModules = AllModules -- PropMods,
177
178    SrcModules = [rebar_utils:erl_to_mod(M) || M <- SrcErls],
179
180    {ok, CoverLog} = rebar_cover_utils:init(Config, AllBeamFiles, qc_dir()),
181
182    TestModule = fun(M) -> qc_module(QC, QCOpts, M) end,
183    QCResult = lists:flatmap(TestModule, PropMods),
184
185    rebar_cover_utils:perform_cover(Config, FilteredModules, SrcModules,
186                                    qc_dir()),
187    rebar_cover_utils:close(CoverLog),
188    ok = rebar_cover_utils:exit(),
189
190    true = rebar_utils:cleanup_code_path(CodePath),
191
192    case QCResult of
193        [] ->
194            ok;
195        Errors ->
196            ?ABORT("One or more QC properties didn't hold true:~n~p~n",
197                   [Errors])
198    end.
199
200qc_module(QC=triq, _QCOpts, M) ->
201    case QC:module(M) of
202        true ->
203            [];
204        Failed ->
205            [Failed]
206    end;
207qc_module(QC=eqc, [], M) -> QC:module(M);
208qc_module(QC=eqc, QCOpts, M) -> QC:module(QCOpts, M).
209
210find_prop_mods() ->
211    Beams = rebar_utils:find_files_by_ext(?QC_DIR, ".beam"),
212    [M || M <- [rebar_utils:erl_to_mod(Beam) || Beam <- Beams], has_prop(M)].
213
214has_prop(Mod) ->
215    lists:any(fun({F,_A}) -> lists:prefix("prop_", atom_to_list(F)) end,
216              Mod:module_info(exports)).
217