blob: cd65ab2ae9efa9277a8eb09651a9df767cbf3ade [file] [log] [blame]
-module(ets_lru_behavior_tests).
-behaviour(proper_statem).
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").
-export([
initial_state/0,
command/1,
precondition/2,
postcondition/3,
next_state/3,
random_key/1
]).
-export([
kvs_insert/4,
kvs_lookup/2,
kvs_remove/2,
kvs_hit/2
]).
-record(st, {
ets_lru,
kvs_lru
}).
proper_test_() ->
PropErOpts = [
{to_file, user},
{max_size, 20},
{numtests, 1000}
],
{timeout, 3600, ?_assertEqual([], proper:module(?MODULE, PropErOpts))}.
prop_lru() ->
Fmt = "History: ~p~nState: ~p~nRes: ~p~nCmds:~n~p~n",
?FORALL(Cmds, commands(?MODULE),
begin
{H, S, R} = run_commands(?MODULE, Cmds),
cleanup(S),
?WHENFAIL(
io:format(standard_error, Fmt, [H, S, R, Cmds]),
R =:= ok
)
end
).
initial_state() ->
#st{}.
command(#st{ets_lru=undefined}=S) ->
MaxObjs = 1 + random:uniform(5),
Opts = [{max_objects, MaxObjs}],
{call, ets_lru, create, [proper_ets_lru_tab, Opts]};
command(S) ->
Key = {call, ?MODULE, random_key, [S]},
frequency([
% Common operations
{50, {call, ets_lru, insert, [S#st.ets_lru, key(), val()]}},
{5, {call, ets_lru, lookup, [S#st.ets_lru, Key]}},
% Make removes less common so we hit limits
{3, {call, ets_lru, remove, [S#st.ets_lru, Key]}},
{1, {call, ets_lru, insert, [S#st.ets_lru, Key, val()]}},
{1, {call, ets_lru, lookup, [S#st.ets_lru, key()]}},
{1, {call, ets_lru, remove, [S#st.ets_lru, key()]}},
{1, {call, ets_lru, hit, [S#st.ets_lru, Key]}},
{1, {call, ets_lru, clear, [S#st.ets_lru]}}
]).
precondition(_, _) ->
true.
postcondition(_S, {call, _, create, [_, _]}, {ok, _}) ->
true;
postcondition(S, {call, _, insert, [_, _Key, _]}, ok) ->
check_constraints(S);
postcondition(S, {call, _, lookup, [_, Key]}, Val) ->
case lists:keysearch(Key, 1, S#st.kvs_lru) of
{value, {Key, V}} when {ok, V} == Val ->
check_constraints(S);
false when Val == not_found ->
check_constraints(S);
E ->
io:format(standard_error, "Bad lookup: ~p ~p~n",
[E, Val]),
false
end;
postcondition(S, {call, _, remove, [_, _Key]}, ok) ->
check_constraints(S);
postcondition(S, {call, _, hit, [_, _Key]}, ok) ->
check_constraints(S);
postcondition(S, {call, _, clear, [_]}, ok) ->
check_constraints(S);
postcondition(_S, _C, _V) ->
io:format(standard_error, "BAD CALL: ~p ~p~n", [_C, _V]),
false.
check_constraints(S) ->
ELRU = S#st.ets_lru,
Count = ets:info(element(2, ELRU), size),
MaxCount = element(5, ELRU),
case Count > MaxCount of
true ->
io:format(standard_error, "Max count exceeded: ~p ~p~n",
[Count, MaxCount]);
false ->
ok
end,
Count =< MaxCount.
next_state(S, V, {call, _, create, [_, _]}) ->
S#st{
ets_lru={call, erlang, element, [2, V]},
kvs_lru=[]
};
next_state(#st{kvs_lru=KVs}=S, _V, {call, _, insert, [_, Key, Val]}) ->
S#st{
kvs_lru={call, ?MODULE, kvs_insert, [KVs, Key, Val, S#st.ets_lru]}
};
next_state(#st{kvs_lru=KVs}=S, _V, {call, _, lookup, [_, Key]}) ->
S#st{
kvs_lru={call, ?MODULE, kvs_lookup, [KVs, Key]}
};
next_state(#st{kvs_lru=KVs}=S, _V, {call, _, remove, [_, Key]}) ->
S#st{
kvs_lru={call, ?MODULE, kvs_remove, [KVs, Key]}
};
next_state(#st{kvs_lru=KVs}=S, _V, {call, _, hit, [_, Key]}) ->
S#st{
kvs_lru={call, ?MODULE, kvs_hit, [KVs, Key]}
};
next_state(S, _V, {call, _, clear, [_]}) ->
S#st{
kvs_lru=[]
}.
cleanup(#st{ets_lru=undefined}) ->
ok;
cleanup(S) ->
ets_lru:destroy(S#st.ets_lru).
random_key(#st{kvs_lru=KVs}) ->
Keys = [foo] ++ [K || {K, _V, _T} <- KVs],
NumKeys = erlang:length(Keys),
KeyPos = random:uniform(NumKeys),
lists:nth(KeyPos, Keys).
% Simple inefficient LRU implementation
kvs_insert(KVs, K, V, ELRU) ->
Max = element(5, ELRU),
NewKVs = [{K, V} | lists:keydelete(K, 1, KVs)],
lists:sublist(NewKVs, Max).
kvs_lookup(KVs, K) ->
case lists:keysearch(K, 1, KVs) of
{value, {K, V}} ->
TmpKVs = lists:keydelete(K, 1, KVs),
[{K, V} | TmpKVs];
false ->
KVs
end.
kvs_remove(KVs, K) ->
lists:keydelete(K, 1, KVs).
kvs_hit(S, K) ->
kvs_lookup(S, K).
key() -> any().
val() -> any().