test(sessds): Create a property-based test for the session state
This commit is contained in:
parent
893656f092
commit
e7b03cdc59
|
@ -120,7 +120,7 @@
|
||||||
-define(rank_tab, emqx_ds_session_ranks).
|
-define(rank_tab, emqx_ds_session_ranks).
|
||||||
-define(pmap_tables, [?stream_tab, ?seqno_tab, ?rank_tab, ?subscription_tab]).
|
-define(pmap_tables, [?stream_tab, ?seqno_tab, ?rank_tab, ?subscription_tab]).
|
||||||
|
|
||||||
-ifndef(TEST).
|
-ifndef(CHECK_SEQNO).
|
||||||
-define(set_dirty, dirty => true).
|
-define(set_dirty, dirty => true).
|
||||||
-define(unset_dirty, dirty => false).
|
-define(unset_dirty, dirty => false).
|
||||||
-else.
|
-else.
|
||||||
|
@ -562,7 +562,7 @@ ro_transaction(Fun) ->
|
||||||
|
|
||||||
-compile({inline, check_sequence/1}).
|
-compile({inline, check_sequence/1}).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(CHECK_SEQNO).
|
||||||
do_seqno() ->
|
do_seqno() ->
|
||||||
case erlang:get(?MODULE) of
|
case erlang:get(?MODULE) of
|
||||||
undefined ->
|
undefined ->
|
||||||
|
|
|
@ -0,0 +1,372 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed 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(emqx_persistent_session_ds_state_tests).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include_lib("proper/include/proper.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(tab, ?MODULE).
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% Type declarations
|
||||||
|
%%================================================================================
|
||||||
|
|
||||||
|
-record(s, {subs = #{}, metadata = #{}, streams = #{}, seqno = #{}, committed = false}).
|
||||||
|
|
||||||
|
-type state() :: #{emqx_persistent_session_ds:id() => #s{}}.
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% Properties
|
||||||
|
%%================================================================================
|
||||||
|
|
||||||
|
seqno_proper_test_() ->
|
||||||
|
Props = [prop_consistency()],
|
||||||
|
Opts = [{numtests, 10}, {to_file, user}, {max_size, 100}],
|
||||||
|
{timeout, 300, [?_assert(proper:quickcheck(Prop, Opts)) || Prop <- Props]}.
|
||||||
|
|
||||||
|
prop_consistency() ->
|
||||||
|
?FORALL(
|
||||||
|
Cmds,
|
||||||
|
commands(?MODULE),
|
||||||
|
?TRAPEXIT(
|
||||||
|
begin
|
||||||
|
init(),
|
||||||
|
{_History, State, Result} = run_commands(?MODULE, Cmds),
|
||||||
|
clean(),
|
||||||
|
?WHENFAIL(
|
||||||
|
io:format(
|
||||||
|
user,
|
||||||
|
"Operations: ~p~nState: ~p\nResult: ~p~n",
|
||||||
|
[Cmds, State, Result]
|
||||||
|
),
|
||||||
|
aggregate(command_names(Cmds), Result =:= ok)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% Generators
|
||||||
|
%%================================================================================
|
||||||
|
|
||||||
|
-define(n_sessions, 10).
|
||||||
|
|
||||||
|
session_id() ->
|
||||||
|
oneof([integer_to_binary(I) || I <- lists:seq(1, ?n_sessions)]).
|
||||||
|
|
||||||
|
topic() ->
|
||||||
|
oneof([<<"foo">>, <<"bar">>, <<"foo/#">>, <<"//+/#">>]).
|
||||||
|
|
||||||
|
subid() ->
|
||||||
|
oneof([[]]).
|
||||||
|
|
||||||
|
subscription() ->
|
||||||
|
oneof([#{}]).
|
||||||
|
|
||||||
|
session_id(S) ->
|
||||||
|
oneof(maps:keys(S)).
|
||||||
|
|
||||||
|
batch_size() ->
|
||||||
|
range(1, ?n_sessions).
|
||||||
|
|
||||||
|
put_metadata() ->
|
||||||
|
oneof([
|
||||||
|
?LET(
|
||||||
|
Val,
|
||||||
|
range(0, 100),
|
||||||
|
{last_alive_at, set_last_alive_at, Val}
|
||||||
|
),
|
||||||
|
?LET(
|
||||||
|
Val,
|
||||||
|
range(0, 100),
|
||||||
|
{created_at, set_created_at, Val}
|
||||||
|
)
|
||||||
|
]).
|
||||||
|
|
||||||
|
get_metadata() ->
|
||||||
|
oneof([
|
||||||
|
{last_alive_at, get_last_alive_at},
|
||||||
|
{created_at, get_created_at}
|
||||||
|
]).
|
||||||
|
|
||||||
|
seqno_track() ->
|
||||||
|
range(0, 1).
|
||||||
|
|
||||||
|
seqno() ->
|
||||||
|
range(1, 100).
|
||||||
|
|
||||||
|
stream_id() ->
|
||||||
|
range(1, 1).
|
||||||
|
|
||||||
|
stream() ->
|
||||||
|
oneof([#{}]).
|
||||||
|
|
||||||
|
put_req() ->
|
||||||
|
oneof([
|
||||||
|
?LET(
|
||||||
|
{Id, Stream},
|
||||||
|
{stream_id(), stream()},
|
||||||
|
{#s.streams, put_stream, Id, Stream}
|
||||||
|
),
|
||||||
|
?LET(
|
||||||
|
{Track, Seqno},
|
||||||
|
{seqno_track(), seqno()},
|
||||||
|
{#s.seqno, put_seqno, Track, Seqno}
|
||||||
|
)
|
||||||
|
]).
|
||||||
|
|
||||||
|
get_req() ->
|
||||||
|
oneof([
|
||||||
|
{#s.streams, get_stream, stream_id()},
|
||||||
|
{#s.seqno, get_seqno, seqno_track()}
|
||||||
|
]).
|
||||||
|
|
||||||
|
del_req() ->
|
||||||
|
oneof([
|
||||||
|
{#s.streams, del_stream, stream_id()}
|
||||||
|
]).
|
||||||
|
|
||||||
|
command(S) ->
|
||||||
|
case maps:size(S) > 0 of
|
||||||
|
true ->
|
||||||
|
frequency([
|
||||||
|
%% Global CRUD operations:
|
||||||
|
{1, {call, ?MODULE, create_new, [session_id()]}},
|
||||||
|
{1, {call, ?MODULE, delete, [session_id(S)]}},
|
||||||
|
{2, {call, ?MODULE, reopen, [session_id(S)]}},
|
||||||
|
{2, {call, ?MODULE, commit, [session_id(S)]}},
|
||||||
|
|
||||||
|
%% Subscriptions:
|
||||||
|
{3,
|
||||||
|
{call, ?MODULE, put_subscription, [
|
||||||
|
session_id(S), topic(), subid(), subscription()
|
||||||
|
]}},
|
||||||
|
{3, {call, ?MODULE, del_subscription, [session_id(S), topic(), subid()]}},
|
||||||
|
|
||||||
|
%% Metadata:
|
||||||
|
{3, {call, ?MODULE, put_metadata, [session_id(S), put_metadata()]}},
|
||||||
|
{3, {call, ?MODULE, get_metadata, [session_id(S), get_metadata()]}},
|
||||||
|
|
||||||
|
%% Key-value:
|
||||||
|
{3, {call, ?MODULE, gen_put, [session_id(S), put_req()]}},
|
||||||
|
{3, {call, ?MODULE, gen_get, [session_id(S), get_req()]}},
|
||||||
|
{3, {call, ?MODULE, gen_del, [session_id(S), del_req()]}},
|
||||||
|
|
||||||
|
%% Getters:
|
||||||
|
{4, {call, ?MODULE, get_subscriptions, [session_id(S)]}},
|
||||||
|
{1, {call, ?MODULE, iterate_sessions, [batch_size()]}}
|
||||||
|
]);
|
||||||
|
false ->
|
||||||
|
frequency([
|
||||||
|
{1, {call, ?MODULE, create_new, [session_id()]}},
|
||||||
|
{1, {call, ?MODULE, iterate_sessions, [batch_size()]}}
|
||||||
|
])
|
||||||
|
end.
|
||||||
|
|
||||||
|
precondition(_, _) ->
|
||||||
|
true.
|
||||||
|
|
||||||
|
postcondition(S, {call, ?MODULE, iterate_sessions, [_]}, Result) ->
|
||||||
|
{Sessions, _} = lists:unzip(Result),
|
||||||
|
%% No lingering sessions:
|
||||||
|
?assertMatch([], Sessions -- maps:keys(S)),
|
||||||
|
%% All committed sessions are visited by the iterator:
|
||||||
|
CommittedSessions = lists:sort([K || {K, #s{committed = true}} <- maps:to_list(S)]),
|
||||||
|
?assertMatch([], CommittedSessions -- Sessions),
|
||||||
|
true;
|
||||||
|
postcondition(S, {call, ?MODULE, get_metadata, [SessionId, {MetaKey, _Fun}]}, Result) ->
|
||||||
|
#{SessionId := #s{metadata = Meta}} = S,
|
||||||
|
?assertEqual(
|
||||||
|
maps:get(MetaKey, Meta, undefined),
|
||||||
|
Result,
|
||||||
|
#{session_id => SessionId, meta => MetaKey}
|
||||||
|
),
|
||||||
|
true;
|
||||||
|
postcondition(S, {call, ?MODULE, gen_get, [SessionId, {Idx, Fun, Key}]}, Result) ->
|
||||||
|
#{SessionId := Record} = S,
|
||||||
|
?assertEqual(
|
||||||
|
maps:get(Key, element(Idx, Record), undefined),
|
||||||
|
Result,
|
||||||
|
#{session_id => SessionId, key => Key, 'fun' => Fun}
|
||||||
|
),
|
||||||
|
true;
|
||||||
|
postcondition(S, {call, ?MODULE, get_subscriptions, [SessionId]}, Result) ->
|
||||||
|
#{SessionId := #s{subs = Subs}} = S,
|
||||||
|
?assertEqual(maps:size(Subs), emqx_topic_gbt:size(Result)),
|
||||||
|
maps:foreach(
|
||||||
|
fun({TopicFilter, Id}, Expected) ->
|
||||||
|
?assertEqual(
|
||||||
|
Expected,
|
||||||
|
emqx_topic_gbt:lookup(TopicFilter, Id, Result, default)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
Subs
|
||||||
|
),
|
||||||
|
true;
|
||||||
|
postcondition(_, _, _) ->
|
||||||
|
true.
|
||||||
|
|
||||||
|
next_state(S, _V, {call, ?MODULE, create_new, [SessionId]}) ->
|
||||||
|
S#{SessionId => #s{}};
|
||||||
|
next_state(S, _V, {call, ?MODULE, delete, [SessionId]}) ->
|
||||||
|
maps:remove(SessionId, S);
|
||||||
|
next_state(S, _V, {call, ?MODULE, put_subscription, [SessionId, TopicFilter, SubId, Subscription]}) ->
|
||||||
|
Key = {TopicFilter, SubId},
|
||||||
|
update(
|
||||||
|
SessionId,
|
||||||
|
#s.subs,
|
||||||
|
fun(Subs) -> Subs#{Key => Subscription} end,
|
||||||
|
S
|
||||||
|
);
|
||||||
|
next_state(S, _V, {call, ?MODULE, del_subscription, [SessionId, TopicFilter, SubId]}) ->
|
||||||
|
Key = {TopicFilter, SubId},
|
||||||
|
update(
|
||||||
|
SessionId,
|
||||||
|
#s.subs,
|
||||||
|
fun(Subs) -> maps:remove(Key, Subs) end,
|
||||||
|
S
|
||||||
|
);
|
||||||
|
next_state(S, _V, {call, ?MODULE, put_metadata, [SessionId, {Key, _Fun, Val}]}) ->
|
||||||
|
update(
|
||||||
|
SessionId,
|
||||||
|
#s.metadata,
|
||||||
|
fun(Map) -> Map#{Key => Val} end,
|
||||||
|
S
|
||||||
|
);
|
||||||
|
next_state(S, _V, {call, ?MODULE, gen_put, [SessionId, {Idx, _Fun, Key, Val}]}) ->
|
||||||
|
update(
|
||||||
|
SessionId,
|
||||||
|
Idx,
|
||||||
|
fun(Map) -> Map#{Key => Val} end,
|
||||||
|
S
|
||||||
|
);
|
||||||
|
next_state(S, _V, {call, ?MODULE, gen_del, [SessionId, {Idx, _Fun, Key}]}) ->
|
||||||
|
update(
|
||||||
|
SessionId,
|
||||||
|
Idx,
|
||||||
|
fun(Map) -> maps:remove(Key, Map) end,
|
||||||
|
S
|
||||||
|
);
|
||||||
|
next_state(S, _V, {call, ?MODULE, commit, [SessionId]}) ->
|
||||||
|
update(
|
||||||
|
SessionId,
|
||||||
|
#s.committed,
|
||||||
|
fun(_) -> true end,
|
||||||
|
S
|
||||||
|
);
|
||||||
|
next_state(S, _V, {call, ?MODULE, _, _}) ->
|
||||||
|
S.
|
||||||
|
|
||||||
|
initial_state() ->
|
||||||
|
#{}.
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% Operations
|
||||||
|
%%================================================================================
|
||||||
|
|
||||||
|
create_new(SessionId) ->
|
||||||
|
put_state(SessionId, emqx_persistent_session_ds_state:create_new(SessionId)).
|
||||||
|
|
||||||
|
delete(SessionId) ->
|
||||||
|
emqx_persistent_session_ds_state:delete(SessionId),
|
||||||
|
ets:delete(?tab, SessionId).
|
||||||
|
|
||||||
|
commit(SessionId) ->
|
||||||
|
put_state(SessionId, emqx_persistent_session_ds_state:commit(get_state(SessionId))).
|
||||||
|
|
||||||
|
reopen(SessionId) ->
|
||||||
|
_ = emqx_persistent_session_ds_state:commit(get_state(SessionId)),
|
||||||
|
{ok, S} = emqx_persistent_session_ds_state:open(SessionId),
|
||||||
|
put_state(SessionId, S).
|
||||||
|
|
||||||
|
put_subscription(SessionId, TopicFilter, SubId, Subscription) ->
|
||||||
|
S = emqx_persistent_session_ds_state:put_subscription(
|
||||||
|
TopicFilter, SubId, Subscription, get_state(SessionId)
|
||||||
|
),
|
||||||
|
put_state(SessionId, S).
|
||||||
|
|
||||||
|
del_subscription(SessionId, TopicFilter, SubId) ->
|
||||||
|
S = emqx_persistent_session_ds_state:del_subscription(TopicFilter, SubId, get_state(SessionId)),
|
||||||
|
put_state(SessionId, S).
|
||||||
|
|
||||||
|
get_subscriptions(SessionId) ->
|
||||||
|
emqx_persistent_session_ds_state:get_subscriptions(get_state(SessionId)).
|
||||||
|
|
||||||
|
put_metadata(SessionId, {_MetaKey, Fun, Value}) ->
|
||||||
|
S = apply(emqx_persistent_session_ds_state, Fun, [Value, get_state(SessionId)]),
|
||||||
|
put_state(SessionId, S).
|
||||||
|
|
||||||
|
get_metadata(SessionId, {_MetaKey, Fun}) ->
|
||||||
|
apply(emqx_persistent_session_ds_state, Fun, [get_state(SessionId)]).
|
||||||
|
|
||||||
|
gen_put(SessionId, {_Idx, Fun, Key, Value}) ->
|
||||||
|
S = apply(emqx_persistent_session_ds_state, Fun, [Key, Value, get_state(SessionId)]),
|
||||||
|
put_state(SessionId, S).
|
||||||
|
|
||||||
|
gen_del(SessionId, {_Idx, Fun, Key}) ->
|
||||||
|
S = apply(emqx_persistent_session_ds_state, Fun, [Key, get_state(SessionId)]),
|
||||||
|
put_state(SessionId, S).
|
||||||
|
|
||||||
|
gen_get(SessionId, {_Idx, Fun, Key}) ->
|
||||||
|
apply(emqx_persistent_session_ds_state, Fun, [Key, get_state(SessionId)]).
|
||||||
|
|
||||||
|
iterate_sessions(BatchSize) ->
|
||||||
|
Fun = fun F(It0) ->
|
||||||
|
case emqx_persistent_session_ds_state:session_iterator_next(It0, BatchSize) of
|
||||||
|
{[], _} ->
|
||||||
|
[];
|
||||||
|
{Sessions, It} ->
|
||||||
|
Sessions ++ F(It)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Fun(emqx_persistent_session_ds_state:make_session_iterator()).
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% Misc.
|
||||||
|
%%================================================================================
|
||||||
|
|
||||||
|
update(SessionId, Key, Fun, S) ->
|
||||||
|
maps:update_with(
|
||||||
|
SessionId,
|
||||||
|
fun(SS) ->
|
||||||
|
setelement(Key, SS, Fun(erlang:element(Key, SS)))
|
||||||
|
end,
|
||||||
|
S
|
||||||
|
).
|
||||||
|
|
||||||
|
get_state(SessionId) ->
|
||||||
|
case ets:lookup(?tab, SessionId) of
|
||||||
|
[{_, S}] ->
|
||||||
|
S;
|
||||||
|
[] ->
|
||||||
|
error({not_found, SessionId})
|
||||||
|
end.
|
||||||
|
|
||||||
|
put_state(SessionId, S) ->
|
||||||
|
ets:insert(?tab, {SessionId, S}).
|
||||||
|
|
||||||
|
init() ->
|
||||||
|
_ = ets:new(?tab, [named_table, public, {keypos, 1}]),
|
||||||
|
mria:start(),
|
||||||
|
emqx_persistent_session_ds_state:create_tables().
|
||||||
|
|
||||||
|
clean() ->
|
||||||
|
ets:delete(?tab),
|
||||||
|
mria:stop(),
|
||||||
|
mria_mnesia:delete_schema().
|
Loading…
Reference in New Issue