Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ferd committed Oct 16, 2015
0 parents commit 13e75b6
Show file tree
Hide file tree
Showing 7 changed files with 995 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .gitignore
@@ -0,0 +1,19 @@
.rebar3
_*
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
_rel
_deps
_plugins
_tdeps
logs
_build
675 changes: 675 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions README.md
@@ -0,0 +1,63 @@
Rebar3 Proper Plugin
=====

Run PropEr test suites.

By default, will look for all modules starting in `prop_` in the `test/`
directories of a rebar3 project, and running all properties (functions of arity
0 with a `prop_` prefix) in them.

Todo/Gotchas
----

- Code coverage is not supported yet.
- The plugin still relies on the `master` branch of proper.
- No tests yet

Use
---

Add the plugin to your rebar config:

{plugins, [
{ rebar3_proper, ".*", {git, "https://github.com/ferd/rebar3_proper.git", {tag, "0.1.0"}}}
]}.

Then just call your plugin directly in an existing application:

Usage: rebar3 proper [-d <dir>] [-m <module>] [-p <properties>]
[-n <numtests>] [-v <verbose>]
[--long_result <long_result>]
[--start_size <start_size>] [--max_size <max_size>]
[--max_shrinks <max_shrinks>]
[--noshrink <noshrink>]
[--constraint_tries <constraint_tries>]
[--spec_timeout <spec_timeout>]
[--any_to_integer <any_to_integer>]

-d, --dir directory where the property tests are located
(defaults to "test")
-m, --module name of one or more modules to test (comma-separated)
-p, --prop name of properties to test within a specified module
(comma-separated)
-n, --numtests number of tests to run when testing a given property
-v, --verbose each propertie tested shows its output or not
(defaults to true)
--long_result enables long-result mode, displaying
counter-examples on failure rather than just false
--start_size specifies the initial value of the size parameter
--max_size specifies the maximum value of the size parameter
--max_shrinks specifies the maximum number of times a failing test
case should be shrunk before returning
--noshrink instructs PropEr to not attempt to shrink any
failing test cases
--constraint_tries specifies the maximum number of tries before the
generator subsystem gives up on producing an
instance that satisfies a ?SUCHTHAT constraint
--spec_timeout duration, in milliseconds, after which PropEr
considers an input to be failing
--any_to_integer converts instances of the any() type to integers in
order to speed up execution

All of [PropEr's standard configurations](http://proper.softlab.ntua.gr/doc/proper.html#Options)
that can be put in a consult file can be put in `{proper_opts, [Options]}.` in your rebar.config file.
3 changes: 3 additions & 0 deletions rebar.config
@@ -0,0 +1,3 @@
{deps, [
{proper, {git, "https://github.com/manopapad/proper.git", {branch, "master"}}}
]}.
16 changes: 16 additions & 0 deletions src/rebar3_proper.app.src
@@ -0,0 +1,16 @@
{application, 'rebar3_proper',
[{description, "Run PropEr test suites"},
{vsn, "0.1.0"},
{registered, []},
{applications,
[kernel,
stdlib,
proper
]},
{env,[]},
{modules, []},

{contributors, []},
{licenses, []},
{links, []}
]}.
8 changes: 8 additions & 0 deletions src/rebar3_proper.erl
@@ -0,0 +1,8 @@
-module(rebar3_proper).

-export([init/1]).

-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
{ok, State1} = rebar3_proper_prv:init(State),
{ok, State1}.
211 changes: 211 additions & 0 deletions src/rebar3_proper_prv.erl
@@ -0,0 +1,211 @@
-module(rebar3_proper_prv).

-export([init/1, do/1, format_error/1]).

-define(PROVIDER, proper).
-define(DEPS, [compile]).
-define(PRV_ERROR(Reason), {error, {?MODULE, Reason}}).

%% ===================================================================
%% Public API
%% ===================================================================
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
Provider = providers:create([
{name, ?PROVIDER}, % The 'user friendly' name of the task
{module, ?MODULE}, % The module implementation of the task
{profiles, [test]},
{bare, true}, % The task can be run by the user, always true
{deps, ?DEPS}, % The list of dependencies
{example, "rebar3 proper"}, % How to use the plugin
{opts, proper_opts()}, % list of options understood by the plugin
{short_desc, "Run PropEr test suites"},
{desc, "Run PropEr test suites"}
]),
{ok, rebar_state:add_provider(State, Provider)}.


-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
{Opts, ProperOpts} = handle_opts(State),
rebar_api:debug("rebar3 proper options: ~p", [Opts]),
rebar_api:debug("proper-specific options: ~p", [ProperOpts]),
rebar_utils:update_code(rebar_state:code_paths(State, all_deps)),
%% TODO handle coverage
Props = find_properties(State, Opts),
Results = [{Mod, Fun, check(Mod, Fun, ProperOpts)} || {Mod, Fun} <- Props],
rebar_api:debug("Results: ~p", [Results]),
rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)),
Failed = [{M,F,Res} || {M,F,Res} <- Results, Res =/= true],
case Failed of
[] ->
Tot = length(Results),
rebar_api:info("~p/~p properties passed", [Tot, Tot]),
{ok, State};
[_|_] ->
Tot = length(Results),
FailedCount = length(Failed),
Passed = Tot - FailedCount,
rebar_api:error("~p/~p properties passed, ~p failed", [Passed, Tot, FailedCount]),
?PRV_ERROR({failed, Failed})
end.


-spec format_error(any()) -> iolist().
format_error({failed, Failed}) ->
["Failed test cases:",
[io_lib:format("~n ~p:~p() -> ~p", [M,F,Res]) || {M,F,Res} <- Failed]];
format_error(Reason) ->
io_lib:format("~p", [Reason]).

%% ===================================================================
%% Private
%% ===================================================================
check(Mod, Fun, Opts) ->
rebar_api:info("Testing ~p:~p()", [Mod, Fun]),
proper:quickcheck(Mod:Fun(), Opts).

find_properties(State, Opts) ->
Dir = proplists:get_value(dir, Opts, "test"),
case {proplists:get_value(module, Opts), proplists:get_value(properties, Opts)} of
{undefined, undefined} ->
find_properties(State, Dir, any, any);
{Mods, undefined} ->
find_properties(State, Dir, Mods, any);
{undefined, Props} ->
find_properties(State, Dir, any, Props);
{Mods, Props} ->
find_properties(State, Dir, Mods, Props)
end.

find_properties(State, Dir, Mods, Props) ->
%% Need to compile somewhere in there
Dirs = [{App, TestDir}
|| App <- rebar_state:project_apps(State),
not rebar_app_info:is_checkout(App),
TestDir <- [filename:join(rebar_app_info:dir(App), Dir)],
{ok, Files} <- [file:list_dir(TestDir)],
lists:any(fun(File) -> prop_suite(Mods, File) end, Files)],
compile_dirs(State, Dir, Dirs),
[Prop || {_, TestDir} <- Dirs,
{ok, Files} <- [file:list_dir(TestDir)],
File <- Files,
prop_suite(Mods, File),
Prop <- properties(Props, module(File))].

prop_suite(Mods, File) ->
Mod = filename:basename(File, ".erl"),
filename:extension(File) =:= ".erl"
andalso
((Mods =:= any andalso lists:prefix("prop_", Mod))
orelse
(Mods =/= any andalso lists:member(Mod, Mods))).

module(File) ->
list_to_atom(filename:basename(File, ".erl")).

properties(any, Mod) ->
[{Mod, Prop} || {Prop,0} <- Mod:module_info(exports), prop_prefix(Prop)];
properties(Props, Mod) ->
[{Mod, Prop} || {Prop,0} <- Mod:module_info(exports),
lists:member(atom_to_list(Prop), Props)].

prop_prefix(Atom) ->
lists:prefix("prop_", atom_to_list(Atom)).

compile_dirs(State, TestDir, Dirs) -> % [{App, Dir}]
%% Set up directory -- may need to unlink then re-link
%% copy contents into directory
%% call the compiler
[begin
rebar_api:debug("Compiling ~s for PropEr", [rebar_app_info:name(App)]),
OutDir = filename:join([rebar_app_info:out_dir(App), TestDir]),
setup(State, OutDir),
compile(State, Dir, OutDir)
end || {App, Dir} <- Dirs],
rebar_api:debug("App compiled", []).

setup(_State, OutDir) ->
filelib:ensure_dir(filename:join([OutDir, "dummy.beam"])).

compile(State, Src, Out) ->
rebar_api:debug("Compiling files in ~s to ~s", [Src, Out]),
NewOpts = lists:foldl(fun({K, V}, Dict) -> rebar_opts:set(Dict, K, V) end,
rebar_state:opts(State),
[{src_dirs, ["."]}]),
rebar_erlc_compiler:compile(NewOpts, Src, ec_cnv:to_list(Out)).

proper_opts() ->
[{dir, $d, "dir", string,
"directory where the property tests are located (defaults to \"test\")"},
{module, $m, "module", string,
"name of one or more modules to test (comma-separated)"},
{properties, $p, "prop", string,
"name of properties to test within a specified module (comma-separated)"},
{numtests, $n, "numtests", integer,
"number of tests to run when testing a given property"},
{verbose, $v, "verbose", boolean,
"each propertie tested shows its output or not (defaults to true)"},
%% no short format for these buddies
{long_result, undefined, "long_result", boolean,
"enables long-result mode, displaying counter-examples on failure "
"rather than just false"},
{start_size, undefined, "start_size", integer,
"specifies the initial value of the size parameter"},
{max_size, undefined, "max_size", integer,
"specifies the maximum value of the size parameter"},
{max_shrinks, undefined, "max_shrinks", integer,
"specifies the maximum number of times a failing test case should be "
"shrunk before returning"},
{noshrink, undefined, "noshrink", boolean,
"instructs PropEr to not attempt to shrink any failing test cases"},
{constraint_tries, undefined, "constraint_tries", integer,
"specifies the maximum number of tries before the generator subsystem "
"gives up on producing an instance that satisfies a ?SUCHTHAT "
"constraint"},
{spec_timeout, undefined, "spec_timeout", integer,
"duration, in milliseconds, after which PropEr considers an input "
"to be failing"},
{any_to_integer, undefined, "any_to_integer", boolean,
"converts instances of the any() type to integers in order to speed "
"up execution"}
].

handle_opts(State) ->
{CliOpts, _} = rebar_state:command_parsed_args(State),
ConfigOpts = rebar_state:get(State, proper_opts, []),
{rebar3_opts(merge_opts(ConfigOpts, CliOpts)),
proper_opts(merge_opts(ConfigOpts, proper_opts(CliOpts)))}.

rebar3_opts([]) ->
[];
rebar3_opts([{dir, Dir} | T]) ->
[{dir, Dir} | rebar3_opts(T)];
rebar3_opts([{module, Mods} | T]) ->
[{module, parse_csv(Mods)} | rebar3_opts(T)];
rebar3_opts([{properties, Props} | T]) ->
[{properties, parse_csv(Props)} | rebar3_opts(T)];
rebar3_opts([_ | T]) ->
rebar3_opts(T).

proper_opts([]) -> [];
proper_opts([{verbose, true} | T]) -> [verbose | proper_opts(T)];
proper_opts([{verbose, false} | T]) -> [quiet | proper_opts(T)];
proper_opts([{long_result, true} | T]) -> [long_result | proper_opts(T)];
proper_opts([{long_result, false} | T]) -> proper_opts(T);
proper_opts([{noshrink, true} | T]) -> [noshrink | proper_opts(T)];
proper_opts([{noshrink, false} | T]) -> proper_opts(T);
proper_opts([{any_to_integer, true} | T]) -> [any_to_integer | proper_opts(T)];
proper_opts([{any_to_integer, false} | T]) -> proper_opts(T);
%% those are rebar3-only options
proper_opts([{dir,_} | T]) -> proper_opts(T);
proper_opts([{module,_} | T]) -> proper_opts(T);
proper_opts([{properties,_} | T]) -> proper_opts(T);
%% fall-through
proper_opts([H|T]) -> [H | proper_opts(T)].

merge_opts(Old, New) ->
rebar_utils:tup_umerge(New, Old).

parse_csv(IoData) ->
re:split(IoData, ", *", [{return, list}]).

0 comments on commit 13e75b6

Please sign in to comment.