Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More specific assertions for instrumentation events #4312

Merged
merged 7 commits into from
Jul 4, 2024
18 changes: 10 additions & 8 deletions big_tests/tests/accounts_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -189,20 +189,22 @@ unregister(Config) ->
assert_event(auth_unregister_user, escalus_users:get_jid(Config, UserSpec)).

already_registered(Config) ->
escalus_fresh:story(Config, [{alice, 1}], fun(Alice) ->
escalus:send(Alice, escalus_stanza:get_registration_fields()),
Stanza = escalus:wait_for_stanza(Alice),
escalus:assert(is_iq_result, Stanza),
true = has_registered_element(Stanza)
escalus_fresh:story(Config, [{alice, 1}], fun already_registered_story/1).

already_registered_story(Alice) ->
AliceJid = escalus_utils:get_short_jid(Alice),
assert_event(auth_register_user, AliceJid), % one event expected
escalus:send(Alice, escalus_stanza:get_registration_fields()),
Stanza = escalus:wait_for_stanza(Alice),
escalus:assert(is_iq_result, Stanza),
true = has_registered_element(Stanza),
assert_event(auth_register_user, AliceJid). % still one event - nothing new

end).
registration_conflict(Config) ->
[Alice] = escalus_users:get_users([alice]),
{ok, result, _Stanza} = escalus_users:create_user(Config, Alice),
{ok, conflict, _Raw} = escalus_users:create_user(Config, Alice).



admin_notify(Config) ->
[{Name1, UserSpec1}, {Name2, UserSpec2}] = escalus_users:get_users([alice, bob]),
[{_, AdminSpec}] = escalus_users:get_users([admin]),
Expand Down
5 changes: 3 additions & 2 deletions big_tests/tests/amp_big_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ amp_test_helper_code() ->
" end.\n".

declared_events() ->
[
{mod_privacy_set, #{host_type => host_type()}} % tested by privacy helpers
[ % tested by privacy helpers
{mod_privacy_set, #{host_type => host_type()}},
{mod_privacy_get, #{host_type => host_type()}}
].

end_per_suite(C) ->
Expand Down
7 changes: 4 additions & 3 deletions big_tests/tests/anonymous_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ connection_is_registered_with_login(Config) ->
true = F(),
escalus_connection:kill(Anna),
mongoose_helper:wait_until(F, false),
assert_event(auth_anonymous_register_user, JID)
assert_event(auth_anonymous_unregister_user, JID)
end).

messages_story(Config) ->
Expand All @@ -110,5 +110,6 @@ host_type() ->
domain_helper:anonymous_host_type().

assert_event(EventName, #jid{luser = LUser, lserver = LServer}) ->
instrument_helper:assert(EventName, #{host_type => host_type()},
fun(M) -> M =:= #{count => 1, user => LUser, server => LServer} end).
instrument_helper:assert_one(
EventName, #{host_type => host_type()},
fun(M) -> M =:= #{count => 1, user => LUser, server => LServer} end).
5 changes: 3 additions & 2 deletions big_tests/tests/auth_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ assert_event(EventName, BinJid)
F = fun(M) ->
M =:= #{count => 1, user => LUser, server => LServer}
end,
instrument_helper:assert(EventName, #{host_type => host_type()}, F);
instrument_helper:assert_one(EventName, #{host_type => host_type()}, F);
assert_event(EventName, BinJid)
when EventName =:= auth_authorize ->
#jid{lserver = LServer} = jid:from_binary(BinJid),
F = fun(#{time := Time, count := 1, server := Server}) ->
(Time > 0) and (Server =:= LServer)
end,
%% Note: this could match events from other tests because there is no user name
instrument_helper:assert(EventName, #{host_type => host_type()}, F);
assert_event(EventName, BinJid) ->
#jid{luser = LUser, lserver = LServer} = jid:from_binary(BinJid),
F = fun(#{time := Time, count := 1, user := User, server := Server}) ->
(Time > 0) and (User =:= LUser) and (Server =:= LServer)
end,
instrument_helper:assert(EventName, #{host_type => host_type()}, F).
instrument_helper:assert_one(EventName, #{host_type => host_type()}, F).
4 changes: 2 additions & 2 deletions big_tests/tests/disco_and_caps_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -235,5 +235,5 @@ urls(sales) -> [<<"[email protected]">>].

assert_roster_get_event(Client) ->
ClientJid = jid:from_binary(escalus_client:full_jid(Client)),
instrument_helper:assert(mod_disco_roster_get, #{host_type => host_type()},
fun(#{count := 1, jid := Jid}) -> ClientJid =:= Jid end).
instrument_helper:assert_one(mod_disco_roster_get, #{host_type => host_type()},
fun(#{count := 1, jid := Jid}) -> ClientJid =:= Jid end).
2 changes: 1 addition & 1 deletion big_tests/tests/domain_isolation_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ verify_alice_has_no_pending_messages(Alice, Bob) ->
assert_stanza_dropped(Sender, Recipient, Stanza) ->
SenderJid = jid:from_binary(escalus_utils:get_jid(Sender)),
RecipientJid = jid:from_binary(escalus_utils:get_jid(Recipient)),
instrument_helper:assert(
instrument_helper:assert_one(
router_stanza_dropped, #{host_type => host_type()},
fun(#{count := 1, from_jid := From, to_jid := To, stanza := DroppedStanza}) ->
From =:= SenderJid andalso To =:= RecipientJid andalso DroppedStanza =:= Stanza
Expand Down
4 changes: 2 additions & 2 deletions big_tests/tests/graphql_roster_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ admin_list_contacts_story(Config, Alice, Bob) ->
[#{<<"subscription">> := <<"NONE">>, <<"ask">> := <<"NONE">>, <<"jid">> := BobBin,
<<"name">> := BobName, <<"groups">> := ?DEFAULT_GROUPS}] =
get_ok_value([data, roster, listContacts], Res),
roster_helper:assert_roster_event(Alice, mod_roster_get).
roster_helper:assert_roster_event(escalus_client:short_jid(Alice), mod_roster_get).

admin_list_contacts_wrong_user(Config) ->
% User with a non-existent domain
Expand Down Expand Up @@ -548,7 +548,7 @@ user_list_contacts_story(Config, Alice, Bob) ->
[#{<<"subscription">> := <<"NONE">>, <<"ask">> := <<"NONE">>, <<"jid">> := BobBin,
<<"name">> := Name, <<"groups">> := ?DEFAULT_GROUPS}] =
get_ok_value(?LIST_CONTACTS_PATH, Res),
roster_helper:assert_roster_event(Alice, mod_roster_get).
roster_helper:assert_roster_event(escalus_client:short_jid(Alice), mod_roster_get).

user_get_contact(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}],
Expand Down
16 changes: 7 additions & 9 deletions big_tests/tests/instrument_cets_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,13 @@ end_per_group(_, _Config) ->
init_per_testcase(_, Config) ->
Config.

check_instrumentation(Config) ->
instrument_helper:wait_for_new(cets_info, #{}),
instrument_helper:assert(cets_info, #{}, fun(Res) ->
%% Values are integers
lists:all(fun(Name) -> is_integer(maps:get(Name, Res)) end, instrumentation_metrics_names())
andalso
%% Check that there are no unknown fields
[] =:= maps:keys(maps:without(instrumentation_metrics_names(), Res))
end).
check_instrumentation(_Config) ->
instrument_helper:wait_and_assert_new(cets_info, #{}, fun check_info/1).

%% Check that values are integers and there are no unknown fields
check_info(Res) ->
lists:all(fun(Name) -> is_integer(maps:get(Name, Res)) end, instrumentation_metrics_names())
andalso #{} =:= maps:without(instrumentation_metrics_names(), Res).

instrumentation_metrics_names() ->
[available_nodes,
Expand Down
163 changes: 110 additions & 53 deletions big_tests/tests/instrument_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

-module(instrument_helper).

-export([declared_events/1, declared_events/2,
start/1, start/2, stop/0,
assert/3, assert/4, filter/2,
assert_not_emitted/1, assert_not_emitted/2,
wait_for/2, wait_for_new/2,
lookup/2, take/2]).
%% API for setup and teardown in test suites
-export([declared_events/1, declared_events/2, start/1, start/2, stop/0]).

%% API for assertions in test cases
-export([assert/3, assert_one/3, assert_not_emitted/3, assert_not_emitted/2, assert_not_emitted/1,
wait_and_assert/3, wait_and_assert_new/3, assert/4, timestamp/0]).

-import(distributed_helper, [rpc/4, mim/0]).

Expand All @@ -20,8 +20,14 @@
-type event_name() :: atom().
-type labels() :: #{atom() => term()}.
-type measurements() :: #{atom() => term()}.
-type check_fun() :: fun((measurements()) -> boolean()).
-type event_count() :: non_neg_integer() | positive.
-type opts() :: #{min_timestamp => integer(),
retries := non_neg_integer(),
delay := non_neg_integer(),
expected_count := event_count()}.

%% API
%% API for setup and teardown in test suites

%% @doc Helper to get `DeclaredEvents' needed by `start/1'
declared_events(Modules) ->
Expand Down Expand Up @@ -59,65 +65,116 @@ stop() ->
verify_unlogged((Untested -- Logged) -- Negative),
verify_logged_but_untested((Logged -- Tested) -- Negative).

-spec assert(event_name(), labels(), fun((measurements()) -> boolean())) -> ok.
%% API for assertions in test cases

%% @doc Checks that there is at least one event with `EventName', `Labels'
%% and matching measurements.
-spec assert(event_name(), labels(), check_fun()) -> ok.
assert(EventName, Labels, CheckF) ->
assert(EventName, Labels, lookup(EventName, Labels), CheckF).
assert(EventName, Labels, CheckF, #{}).

%% @doc `CheckF' can return a boolean or fail with `function_clause', which means `false'.
%% This is for convenience - you only have to code one clause.
-spec assert(event_name(), labels(), [measurements()], fun((measurements()) -> boolean())) -> ok.
assert(EventName, Labels, MeasurementsList, CheckF) ->
case filter(CheckF, MeasurementsList) of
[] ->
ct:log("All measurements for event ~p with labels ~p:~n~p",
[EventName, Labels, MeasurementsList]),
ct:fail("No instrumentation events matched");
Filtered ->
ct:log("Matching measurements for event ~p with labels ~p:~n~p",
[EventName, Labels, Filtered]),
event_tested(EventName, Labels)
end.
%% @doc Checks that there is exactly one event with `EventName', `Labels'
%% and matching measurements.
-spec assert_one(event_name(), labels(), check_fun()) -> ok.
assert_one(EventName, Labels, CheckF) ->
assert(EventName, Labels, CheckF, #{expected_count => 1}).

%% @doc Checks that there is no event with `EventName', `Labels' and matching measurements.
-spec assert_not_emitted(event_name(), labels(), check_fun()) -> ok.
assert_not_emitted(EventName, Labels, CheckF) ->
assert(EventName, Labels, CheckF, #{expected_count => 0}).

%% @doc Checks that there is no event with `EventName' and `Labels'.
-spec assert_not_emitted(event_name(), labels()) -> ok.
assert_not_emitted(EventName, Labels) ->
case lookup(EventName, Labels) of
[] ->
ok;
Events ->
ct:fail("Measurements emitted but should not ~p", [Events])
end.
assert_not_emitted(EventName, Labels, fun(_) -> true end).

%% @doc Checks that there is no event for any of the `{EventName, Labels}' tuples.
-spec assert_not_emitted([{event_name(), labels()}]) -> ok.
assert_not_emitted(Events) ->
[assert_not_emitted(Event, Label) || {Event, Label} <- Events].
[assert_not_emitted(Event, Label) || {Event, Label} <- Events],
ok.

%% @doc Waits for a matching event.
-spec wait_and_assert(event_name(), labels(), check_fun()) -> ok.
wait_and_assert(EventName, Labels, CheckF) ->
assert(EventName, Labels, CheckF, #{retries => 50, delay => 100}).

%% @doc Waits for a matching event, ignoring past events.
-spec wait_and_assert_new(event_name(), labels(), check_fun()) -> ok.
wait_and_assert_new(EventName, Labels, CheckF) ->
assert(EventName, Labels, CheckF, #{min_timestamp => timestamp(), retries => 50, delay => 100}).

%% @doc Assert that an expected number of events with `EventName' and `Labels' are present.
%% Events are filtered by applying `CheckF' to the map of measurements.
%% `CheckF' can return a boolean or fail with `function_clause', which means `false'.
%% This is for convenience - you only have to code one clause.
-spec assert(event_name(), labels(), check_fun(), opts()) -> ok.
assert(EventName, Labels, CheckF, Opts) ->
FullOpts = maps:merge(default_opts(), Opts),
assert_loop(EventName, Labels, CheckF, FullOpts).

-spec timestamp() -> integer().
timestamp() ->
rpc(mim(), ?HANDLER_MODULE, timestamp, []).

%% Internal functions

-spec default_opts() -> opts().
default_opts() ->
#{retries => 0, delay => timer:seconds(1), expected_count => positive}.

-spec assert_loop(event_name(), labels(), check_fun(), opts()) -> ok.
assert_loop(EventName, Labels, CheckF, Opts) ->
#{retries := Retries, expected_count := ExpectedCount, delay := Delay} = Opts,
All = case Opts of
#{min_timestamp := Timestamp} ->
select_new(EventName, Labels, Timestamp);
#{} ->
select(EventName, Labels)
end,
Filtered = filter(CheckF, All),
case check(Filtered, ExpectedCount) of
false when Retries > 0 ->
timer:sleep(Delay),
assert_loop(EventName, Labels, CheckF, Opts#{retries := Retries - 1});
CheckResult ->
assert_check_result(EventName, Labels, All, Filtered, CheckResult, ExpectedCount)
end.

-spec filter(fun((measurements()) -> boolean()), [measurements()]) -> [measurements()].
filter(CheckF, MeasurementsList) ->
lists:filter(fun(Measurements) ->
try CheckF(Measurements) catch error:function_clause -> false end
end, MeasurementsList).

%% @doc Remove previous events, and wait for a new one. Use for probes only.
-spec wait_for_new(event_name(), labels()) -> [measurements()].
wait_for_new(EventName, Labels) ->
take(EventName, Labels),
wait_for(EventName, Labels).

%% @doc Lookup an element, or wait for it, if it didn't happen yet.
-spec wait_for(event_name(), labels()) -> [measurements()].
wait_for(EventName, Labels) ->
{ok, MeasurementsList} = mongoose_helper:wait_until(fun() -> lookup(EventName, Labels) end,
fun(L) -> L =/= [] end,
#{name => EventName}),
MeasurementsList.

-spec lookup(event_name(), labels()) -> [measurements()].
lookup(EventName, Labels) ->
[Measurements || {_, Measurements} <- rpc(mim(), ?HANDLER_MODULE, lookup, [EventName, Labels])].

-spec take(event_name(), labels()) -> [measurements()].
take(EventName, Labels) ->
[Measurements || {_, Measurements} <- rpc(mim(), ?HANDLER_MODULE, take, [EventName, Labels])].

%% Internal functions
-spec select(event_name(), labels()) -> [measurements()].
select(EventName, Labels) ->
rpc(mim(), ?HANDLER_MODULE, select, [EventName, Labels]).

-spec select_new(event_name(), labels(), integer()) -> [measurements()].
select_new(EventName, Labels, Timestamp) ->
rpc(mim(), ?HANDLER_MODULE, select_new, [EventName, Labels, Timestamp]).

-spec check([measurements()], event_count()) -> boolean().
check(Filtered, positive) ->
length(Filtered) > 0;
check(Filtered, ExpectedCount) ->
length(Filtered) =:= ExpectedCount.

-spec assert_check_result(event_name(), labels(), All :: [measurements()],
Filtered :: [measurements()], CheckResult :: boolean(),
event_count()) -> ok.
assert_check_result(_EventName, _Labels, _All, [], true, _ExpectedCount) ->
ok; % don't mark non-emitted events as tested
assert_check_result(EventName, Labels, _All, Filtered, true, _ExpectedCount) ->
ct:log("Matching measurements for event ~p with labels ~p:~n~p", [EventName, Labels, Filtered]),
event_tested(EventName, Labels);
assert_check_result(EventName, Labels, All, Filtered, false, ExpectedCount) ->
ct:log("Assertion failed for event ~p with labels ~p.~nMatching measurements:~n~p~n~n"
"Other measurements:~n~p", [EventName, Labels, Filtered, All -- Filtered]),
ct:fail("Incorrect number of instrumentation events - matched: ~p, expected: ~p",
[length(Filtered), ExpectedCount]).

%% Don't fail if some events are unlogged, because we don't have full test coverage (yet)
verify_unlogged([]) -> ok;
Expand Down
9 changes: 6 additions & 3 deletions big_tests/tests/mam_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@
assert_lookup_event/2,
assert_flushed_event_if_async/2,
assert_dropped_iq_event/2,
assert_event_with_jid/2
assert_event_with_jid/2,
assert_no_event_with_jid/2
]).

-import(muc_light_helper,
Expand Down Expand Up @@ -3285,7 +3286,7 @@ prefs_set_request(Config) ->
?assert_equal(ResultIQ1, ResultIQ2),
ok
end,
escalus:story(Config, [{alice, 1}], F).
escalus:fresh_story(Config, [{alice, 1}], F).

query_get_request(Config) ->
F = fun(Alice) ->
Expand Down Expand Up @@ -3375,11 +3376,13 @@ muc_prefs_set_request(ConfigIn) ->
muc_prefs_set_request_not_an_owner(ConfigIn) ->
F = fun(Config, _Alice, Bob) ->
Room = ?config(room, Config),
RoomAddr = room_address(Room),
escalus:send(Bob, stanza_to_room(stanza_prefs_set_request(<<"roster">>,
[<<"[email protected]">>],
[<<"[email protected]">>],
mam_ns_binary()), Room)),
escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>], escalus:wait_for_stanza(Bob))
escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>], escalus:wait_for_stanza(Bob)),
assert_no_event_with_jid(mod_mam_muc_get_prefs, RoomAddr)
end,
RoomOpts = [{persistent, true}],
UserSpecs = [{alice, 1}, {bob, 1}],
Expand Down
Loading