Skip to content

Commit

Permalink
added btypes_eqc to test bucket types creation invariants
Browse files Browse the repository at this point in the history
the test does not yet cover crashing and chaning the claimant or waiting for
type properties to propogate to other nodes (for that we will probably need/use riak_test).
However, it does reproduce #442
  • Loading branch information
jrwest committed Nov 26, 2013
1 parent bf0048f commit 0a91dfe
Showing 1 changed file with 333 additions and 0 deletions.
333 changes: 333 additions & 0 deletions test/btypes_eqc.erl
@@ -0,0 +1,333 @@
%% -------------------------------------------------------------------
%%
%%
%% Copyright (c) 2013 Basho Technologies, Inc. All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
-module(btypes_eqc).

-ifdef(EQC).
-include_lib("eqc/include/eqc.hrl").
-include_lib("eqc/include/eqc_statem.hrl").
-include_lib("eunit/include/eunit.hrl").

-compile(export_all).

-type type_name() :: binary().
-type type_active_status() :: boolean().
-type type_prop_name() :: binary().
-type type_prop_val() :: boolean().

-record(state, {
%% a list of properties we have created
types :: [{type_name(), type_active_status(), [{type_prop_name(), type_prop_val()}]}]
}).

run_eqc() ->
run_eqc(100).

run_eqc(N) ->
eqc:quickcheck(eqc:numtests(N, prop_btype_invariant())).

run_check() ->
eqc:check(prop_btype_invariant()).

%% @doc Returns the state in which each test case starts. (Unless a different
%% initial state is supplied explicitly to, e.g. commands/2.)
-spec initial_state() -> eqc_statem:symbolic_state().
initial_state() ->
#state{
types = []
}.

%% ------ Grouped operator: create_type
%% @doc create_type args generator
create_type_args(S) ->
[type_name(S), props()].

%% @doc create_type next state function
create_type_next(S=#state{types=Types}, _Value, [TypeName, CreateProps]) ->
case type_active(TypeName, S) orelse invalid_props(TypeName, CreateProps, S) of
true -> S;
false ->
S#state{ types = lists:keystore(TypeName, 1, Types, {TypeName, false, CreateProps}) }
end.

%% @doc create_type command
create_type(TypeName, Props) ->
riak_core_bucket_type:create(TypeName, Props).

%% @doc create_type postcondition
create_type_post(S, [TypeName, Props], Res) ->
Active = type_active(TypeName, S),
InvalidProps = invalid_props(TypeName, Props, S),
case {Active, InvalidProps} of
{true, _} -> eq({error, already_active}, Res);
{_, true} ->
case Res of
{error, _} -> true;
_ -> {bad_error_res, Res}
end;
{false, false} -> eq(ok, Res)
end.

%% ------ Grouped operator: activate_type
%% @doc activate_type args generator
activate_type_args(#state{types=[]}) ->
%% test negative case when no types exist
[new_type_name()];
activate_type_args(S) ->
%% TODO: generate fault for type that dne
[existing_type_name(S)].

%% @doc activate_type next state function
activate_type_next(S=#state{types=Types}, _Value, [TypeName]) ->
case type(TypeName, S) of
undefined -> S;
{TypeName, _, Props} ->
S#state{ types = lists:keyreplace(TypeName, 1, Types, {TypeName, true, Props}) }
end.

%% @doc activate_type command
activate_type(TypeName) ->
riak_core_bucket_type:activate(TypeName).

%% @doc activate_type postcondition
activate_type_post(S, [TypeName], Res) ->
Expected = case type(TypeName, S) of
undefined -> {error, undefined};
%% always ok if type exists b/c we spend no time in created state in this test, currently
{TypeName, _, _} -> ok
end,
eq(Expected, Res).

%% ------ Grouped operator: update_type
%% @doc update_type args generator
update_type_args(S) ->
[type_name(S), props()].

%% @doc update_type next state function
update_type_next(S=#state{types=Types}, _Value, [TypeName, UpdateProps]) ->
case type_active(TypeName, S) of
false -> S;
true ->
case invalid_props(TypeName, UpdateProps, S) of
true -> S;
false ->
{TypeName, true, SetProps} = type(TypeName, S),
NewProps = lists:ukeysort(1, UpdateProps ++ SetProps),
S#state{ types = lists:keystore(TypeName, 1, Types, {TypeName, true, NewProps}) }
end
end.

%% @doc update_type command
update_type(TypeName, Props) ->
riak_core_bucket_type:update(TypeName, Props).

%% @doc update_type postcondition
update_type_post(S, [TypeName, Props], Res) ->
Active = type_active(TypeName, S),
InvalidProps = invalid_props(TypeName, Props, S),
case {Active, InvalidProps} of
{false, _} -> eq({error, not_active}, Res);
{_, true} ->
case Res of
{error, _} -> true;
_ -> {bad_error_res, Res}
end;
{true, false} -> eq(ok, Res)
end.

%% ------ Grouped operator: type_status
%% @doc type_status args generator
type_status_args(#state{types=[]}) ->
%% if no types, test negative case
[new_type_name()];
type_status_args(S) ->
%% TODO: add fault that sometimes generates missing type name for negative testing
[existing_type_name(S)].

%% @doc type_status command
type_status(TypeName) ->
riak_core_bucket_type:status(TypeName).

%% @doc type_status postcondition
type_status_post(S, [TypeName], Res) ->
eq(expected_status(TypeName, S), Res).

%% ------ Grouped operator: get_type
%% @doc get_type args generator
get_type_args(#state{types=[]}) ->
%% generate negative case when there are no types to get
[new_type_name()];
get_type_args(S) ->
%% TODO: use faults to generate negative case
[existing_type_name(S)].

%% @doc get_type command
get_type(TypeName) ->
riak_core_bucket_type:get(TypeName).

%% @doc get_type postcondition
get_type_post(S, [TypeName], Res) ->
case type_active(TypeName, S) of
false-> eq(undefined, Res);
true ->
ExpectedDefaults = riak_core_bucket_type:defaults(),
{TypeName, true, ExpectedProps} = type(TypeName, S),
%% TODO: just adding the list here is kind of wrong, use riak_core_bucket_props:merge? or similar?
ExpectedAll = lists:ukeysort(1, [{active, true}, {claimant, node()}] ++ ExpectedProps ++ ExpectedDefaults),
%% properties expected (from state) but not found
Missing = [Prop || Prop <- ExpectedAll, not lists:member(Prop, Res)],
%% properties found but not expected (from state)
Extra = [Prop || Prop <- Res, not lists:member(Prop, ExpectedAll)],
good_props(Missing, Extra)
end.

%% ------ test helpers

expected_status(TypeName, S) ->
case type(TypeName, S) of
undefined -> undefined;
%% TODO: incorporate other nodes so we can include created status?
{TypeName, false, _} -> ready;
{TypeName, true, _} -> active
end.

type_active(TypeName, S) ->
case type(TypeName, S) of
undefined -> false;
{TypeName, Active, _} -> Active
end.

type(TypeName, #state{types=Types}) ->
case lists:keyfind(TypeName, 1, Types) of
false -> undefined;
TypeDetails -> TypeDetails
end.

good_props([], []) ->
true;
good_props([], Extra) ->
{extra_props, Extra};
good_props(Missing, []) ->
{missing_props, Missing};
good_props(Missing, Extra) ->
{bad_props, {missing, Missing}, {extra, Extra}}.

invalid_props(TypeName, Props, S) ->
%% properties are invalid if they include modifications to claimant or active property
HasClaimant = false =/= lists:keyfind(claimant, 1, Props),
HasActive = case lists:keyfind(active, 1, Props) of
false -> false;
{active, Active} ->
%% if included, cannot modify state of active
not (Active =:= type_active(TypeName, S))
end,
HasClaimant orelse HasActive.

%% Generators

type_name(#state{types=Types}) ->
ExistingTypes = [Name || {Name, _, _} <- Types],
ExistingGen = [ oneof(ExistingTypes) || length(ExistingTypes) > 0 ],
oneof([new_type_name() | ExistingGen]).


existing_type_name(#state{types=Types}) ->
ExistingTypes = [Name || {Name, _, _} <- Types],
%% assumes Types is non-empty
oneof(ExistingTypes).


new_type_name() ->
binary(10).

props() ->
fault_rate(1, 10, ?LET(Props, list(prop()), fault([immutable_core_prop() | Props], Props))).

prop() ->
{prop_name(), prop_value()}.

prop_name() ->
binary(10).

prop_value() ->
bool().

immutable_core_prop() ->
%% use a boolean for both values because thats what we use for other props,
%% the value doesn't really matter, except for active 'false' is a valid value
%% which we generate sometimes just to make sure
oneof([{active, bool()}, {claimant, true}]).

%% @doc weight/2 - Distribution of calls
weight(_S, create_type) -> 3;
weight(_S, activate_type) -> 3;
weight(_S, type_status) -> 1;
weight(_S, get_type) -> 1;
weight(_S, _Cmd) -> 1.

%% @doc the property
prop_btype_invariant() ->
?FORALL(Cmds, commands(?MODULE),
aggregate(command_names(Cmds),
?TRAPEXIT(
begin
os:cmd("rm -r ./btypes_eqc_meta"),
application:set_env(riak_core, claimant_tick, 4294967295),
application:set_env(riak_core, broadcast_lazy_timer, 4294967295),
application:set_env(riak_core, broadcast_exchange_timer, 4294967295),
application:set_env(riak_core, metadata_hashtree_timer, 4294967295),
stop_pid(whereis(riak_core_ring_events)),
stop_pid(whereis(riak_core_ring_manager)),
{ok, RingEvents} = riak_core_ring_events:start_link(),
{ok, _RingMgr} = riak_core_ring_manager:start_link(test),
{ok, Claimant} = riak_core_claimant:start_link(),
{ok, MetaMgr} = riak_core_metadata_manager:start_link([{data_dir, "./btypes_eqc_meta"}]),
{ok, Hashtree} = riak_core_metadata_hashtree:start_link("./btypes_eqc_meta/trees"),
{ok, Broadcast} = riak_core_broadcast:start_link(),
{H, S, Res} = run_commands(?MODULE,Cmds),
stop_pid(Broadcast),
stop_pid(Hashtree),
stop_pid(MetaMgr),
stop_pid(Claimant),
riak_core_ring_manager:stop(),
stop_pid(RingEvents),
os:cmd("rm -r ./btypes_eqc_meta"),
pretty_commands(?MODULE, Cmds, {H, S, Res},
Res == ok)
end))).

stop_pid(Other) when not is_pid(Other) ->
ok;
stop_pid(Pid) ->
unlink(Pid),
exit(Pid, shutdown),
ok = wait_for_pid(Pid).

wait_for_pid(Pid) ->
Mref = erlang:monitor(process, Pid),
receive
{'DOWN', Mref, process, _, _} ->
ok
after
5000 ->
{error, didnotexit}
end.

-endif.

0 comments on commit 0a91dfe

Please sign in to comment.