-
Notifications
You must be signed in to change notification settings - Fork 429
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4264 from esl/doctor/utils
Doctor/utils
- Loading branch information
Showing
6 changed files
with
242 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
-module(tr_util_SUITE). | ||
-compile([export_all, nowarn_export_all]). | ||
|
||
-import(distributed_helper, [rpc/4, mim/0]). | ||
|
||
-include_lib("eunit/include/eunit.hrl"). | ||
|
||
all() -> | ||
[c2s_hooks, c2s_elements]. | ||
|
||
suite() -> | ||
escalus:suite(). | ||
|
||
init_per_suite(Config) -> | ||
rpc(mim(), tr, start, []), | ||
escalus:init_per_suite(Config). | ||
|
||
end_per_suite(Config) -> | ||
escalus_fresh:clean(), | ||
escalus:end_per_suite(Config), | ||
rpc(mim(), tr, stop, []). | ||
|
||
init_per_testcase(CaseName, Config) -> | ||
escalus:init_per_testcase(CaseName, Config). | ||
|
||
end_per_testcase(_CaseName, _Config) -> | ||
rpc(mim(), tr, stop_tracing, []), | ||
rpc(mim(), tr, clean, []). | ||
|
||
%% Test Cases | ||
|
||
c2s_hooks(Config) -> | ||
rpc(mim(), tr, trace, [[mongoose_c2s_hooks, gen_hook]]), | ||
[] = rpc(mim(), tr_util, c2s_hooks, []), % nothing collected yet | ||
escalus:fresh_story(Config, [{alice, 1}], fun c2s_hooks_story/1). | ||
|
||
c2s_hooks_story(Alice) -> | ||
C2SHooks = rpc(mim(), tr_util, c2s_hooks, []), | ||
AliceJid = escalus_utils:get_jid(Alice), | ||
|
||
%% Get c2s hooks, and check the first few | ||
?assertMatch([{AliceJid, user_send_packet, #{mongoose_acc := true}}, | ||
{AliceJid, user_send_iq, #{mongoose_acc := true}}, | ||
{AliceJid, user_open_session, #{mongoose_acc := true}} | _], C2SHooks), | ||
|
||
%% Get generic hook statistics, and check one hook | ||
HookStat = rpc(mim(), tr, call_stat, [fun tr_util:tr_to_hook_name_and_tag/1]), | ||
HT = domain_helper:host_type(), | ||
?assertMatch(#{{user_open_session, HT} := {1, _, _}}, HookStat). | ||
|
||
c2s_elements(Config) -> | ||
rpc(mim(), tr, trace, [[mongoose_c2s_hooks]]), | ||
[] = rpc(mim(), tr_util, c2s_elements, []), % nothing collected yet | ||
escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun c2s_elements_story/2). | ||
|
||
c2s_elements_story(Alice, Bob) -> | ||
escalus_client:send(Alice, escalus_stanza:chat_to(Bob, <<"Hello">>)), | ||
escalus:wait_for_stanza(Bob), | ||
AliceBareJid = escalus_utils:get_short_jid(Alice), | ||
BobBareJid = escalus_utils:get_short_jid(Bob), | ||
AliceJid = escalus_utils:get_jid(Alice), | ||
BobJid = escalus_utils:get_jid(Bob), | ||
|
||
%% Get elements exchanged between bare JIDs | ||
[Sent, Recv] = rpc(mim(), tr_util, c2s_elements_between_jids, [[AliceBareJid, BobBareJid]]), | ||
?assertMatch(#{name := <<"message">>, type := <<"chat">>, | ||
jid := AliceJid, from_jid := AliceJid, to_jid := BobJid}, Sent), | ||
?assertMatch(#{name := <<"message">>, type := <<"chat">>, | ||
jid := BobJid, from_jid := AliceJid, to_jid := BobJid}, Recv), | ||
|
||
%% Get elements exchanged between full JIDs | ||
?assertEqual([Sent, Recv], | ||
rpc(mim(), tr_util, c2s_elements_between_jids, [[AliceJid, BobJid]])), | ||
|
||
%% Get all elements | ||
AllElements = rpc(mim(), tr_util, c2s_elements, []), | ||
?assert(lists:member(Sent, AllElements)), | ||
?assert(lists:member(Recv, AllElements)). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,6 +92,7 @@ | |
}. | ||
|
||
-export_type([t/0, | ||
stanza_metadata/0, | ||
new_acc_params/0]). | ||
|
||
-type new_acc_params() :: #{ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
%% @doc This module contains debug utilities intended for use with Erlang Doctor. | ||
%% Beware that this tool has the potential of seriously impacting | ||
%% your system, leading to various issues including system crash or data loss. | ||
%% Therefore, it is intended for use in development or QA environments, | ||
%% and using it in a production environment is risky. | ||
%% | ||
%% Example: let's capture and list all stanzas exchanged between Alice and Bob. | ||
%% | ||
%% ``` | ||
%% tr:start(). | ||
%% tr:trace([mongoose_c2s_hooks]). | ||
%% | ||
%% %% Exchange stanzas between users | ||
%% | ||
%% tr:stop_tracing(). | ||
%% tr_util:c2s_elements_between_jids([<<"alice@localhost">>, <<"bob@localhost">>]). | ||
%% | ||
%% %% You will get a list of `c2s_element_info' maps with the exchanged stanzas. | ||
%% ''' | ||
%% | ||
%% @reference See <a href="https://hexdocs.pm/erlang_doctor/readme.html">Hex Docs</a> | ||
%% for more information about Erlang Doctor. | ||
|
||
-module(tr_util). | ||
|
||
%% Debugging API for mongoose_c2s events and XMPP traffic | ||
-export([c2s_elements_between_jids/1, | ||
c2s_hooks/0, | ||
c2s_elements/0]). | ||
|
||
%% Selectors for use with `tr:call_stat' etc. | ||
-export([tr_to_element_info/1, | ||
tr_to_hook_name_and_tag/1]). | ||
|
||
%% Predicates for use with `tr:filter' etc. | ||
-export([filter_c2s_hook/1]). | ||
|
||
-include_lib("erlang_doctor/include/tr.hrl"). | ||
-include_lib("exml/include/exml.hrl"). | ||
|
||
-ignore_xref(?MODULE). | ||
|
||
-type c2s_element_info() :: #{name := binary(), | ||
contents := binary(), | ||
ref := reference(), | ||
hooks := [atom()], | ||
jid := jid:literal_jid(), | ||
from_jid := jid:literal_jid(), | ||
to_jid := jid:literal_jid(), | ||
id := binary(), | ||
type := binary()}. | ||
|
||
%% Complete utilities | ||
|
||
%% @doc Get a list of XML elements (usually stanzas) exchanged between the listed JIDs. | ||
%% The `to' and `from' attributes must match different JIDs from the list. | ||
%% Matching starts from the beginning of the list. A bare JID matches any resource. | ||
%% | ||
%% Requires traces from modules: `[mongoose_c2s_hooks]'. | ||
-spec c2s_elements_between_jids([jid:literal_jid()]) -> [c2s_element_info()]. | ||
c2s_elements_between_jids(TargetBinJids) -> | ||
Targets = lists:map(fun jid:from_binary_noprep/1, TargetBinJids), | ||
lists:filter(fun(#{from_jid := From, to_jid := To}) -> | ||
case match_target_jids(From, Targets) of | ||
[] -> false; | ||
[H|_] -> match_target_jids(To, Targets -- [H]) =/= [] | ||
end | ||
end, c2s_elements()). | ||
|
||
%% @doc Get a list of all C2S hooks in the execution order, annotated by user JIDs, | ||
%% for which they were executed. | ||
%% | ||
%% Requires traces from modules: `[mongoose_c2s_hooks]'. | ||
-spec c2s_hooks() -> [{jid:literal_jid(), atom(), mongoose_acc:t()}]. | ||
c2s_hooks() -> | ||
[{jid:to_binary(mongoose_c2s:get_jid(Data)), Hook, Acc} || | ||
#tr{mfa = {_, Hook, _}, data = [_HT, Acc, #{c2s_data := Data}]} <- | ||
tr:filter(fun filter_c2s_hook/1) | ||
]. | ||
|
||
%% @doc Get information about XML elements, for which C2S hooks were executed. | ||
%% | ||
%% Requires traces from modules: `[mongoose_c2s_hooks]'. | ||
-spec c2s_elements() -> [c2s_element_info()]. | ||
c2s_elements() -> | ||
join_hooks(c2s_element_hooks()). | ||
|
||
%% Selectors for use with `tr:call_stat' etc. | ||
|
||
-spec tr_to_hook_name_and_tag(tr:tr()) -> {gen_hook:hook_name(), gen_hook:hook_tag()}. | ||
tr_to_hook_name_and_tag(#tr{mfa = {gen_hook, run_fold, _}, data = [HookName, Tag | _]}) -> | ||
{HookName, Tag}. | ||
|
||
-spec tr_to_element_info(tr:tr()) -> c2s_element_info(). | ||
tr_to_element_info(#tr{mfa = {mongoose_c2s_hooks, Hook, _}, | ||
data = [_HT, #{stanza := ElementAcc}, #{c2s_data := Data}]}) -> | ||
element_info(Data, Hook, ElementAcc). | ||
|
||
%% Predicates for use with `tr:filter' etc. | ||
|
||
-spec filter_c2s_hook(tr:tr()) -> boolean(). | ||
filter_c2s_hook(#tr{mfa = {mongoose_c2s_hooks, _, _}}) -> true. | ||
|
||
-spec filter_c2s_hook_with_element(tr:tr()) -> boolean(). | ||
filter_c2s_hook_with_element(#tr{mfa = {mongoose_c2s_hooks, _, _}, | ||
data = [_HT, #{stanza := #{}}, _Data]}) -> | ||
true. | ||
|
||
%% Internal helpers | ||
|
||
-spec c2s_element_hooks() -> [c2s_element_info()]. | ||
c2s_element_hooks() -> | ||
lists:map(fun tr_to_element_info/1, tr:filter(fun filter_c2s_hook_with_element/1)). | ||
|
||
-spec match_target_jids(jid:literal_jid(), [jid:jid()]) -> [jid:jid()]. | ||
match_target_jids(ActualBJid, Targets) -> | ||
Actual = jid:from_binary_noprep(ActualBJid), | ||
lists:filter(fun(Target) -> match_jid(Target, Actual) end, Targets). | ||
|
||
-spec match_jid(jid:jid(), jid:jid()) -> boolean(). | ||
match_jid(Target, Actual) -> | ||
case jid:lresource(Target) of | ||
<<>> -> jid:are_bare_equal(Target, Actual); | ||
_ -> jid:are_equal(Target, Actual) | ||
end. | ||
|
||
-spec join_hooks([c2s_element_info()]) -> [c2s_element_info()]. | ||
join_hooks([First | Rest]) -> | ||
lists:reverse(lists:foldl(fun join_hooks_step/2, [First], Rest)); | ||
join_hooks([]) -> | ||
[]. | ||
|
||
-spec join_hooks_step(c2s_element_info(), [c2s_element_info()]) -> [c2s_element_info()]. | ||
join_hooks_step(Cur, [Prev | Acc]) -> | ||
case {maps:take(hooks, Cur), maps:take(hooks, Prev)} of | ||
{{CurHooks, D}, {PrevHooks, D}} -> | ||
[D#{hooks => PrevHooks ++ CurHooks} | Acc]; | ||
_ -> | ||
[Cur, Prev | Acc] | ||
end. | ||
|
||
-spec element_info(mongoose_c2s:data(), gen_hook:hook_name(), mongoose_acc:stanza_metadata()) -> | ||
c2s_element_info(). | ||
element_info(Data, Hook, #{element := Element, ref := Ref, from_jid := From, to_jid := To}) -> | ||
Info = #{name => Element#xmlel.name, | ||
contents => exml:to_binary(Element#xmlel.children), | ||
ref => Ref, | ||
hooks => [Hook], | ||
jid => jid:to_binary(mongoose_c2s:get_jid(Data)), | ||
from_jid => jid:to_binary(From), | ||
to_jid => jid:to_binary(To)}, | ||
maps:merge(Info, element_attr_info(Element#xmlel.attrs)). | ||
|
||
-spec element_attr_info([exml:attr()]) -> #{atom() => binary()}. | ||
element_attr_info(Attrs) -> | ||
AllowedAttrs = [<<"id">>, <<"type">>], | ||
maps:from_list([{binary_to_existing_atom(Key), Value} || {Key, Value} <- Attrs, | ||
lists:member(Key, AllowedAttrs)]). |