From 60e510c6f2e3fc28053a5eceaa47c8cc790c689c Mon Sep 17 00:00:00 2001 From: flmath Date: Tue, 15 Apr 2025 23:36:54 +0200 Subject: [PATCH 01/35] Add test_statem module for state machine testing functionality --- test/recon_trace_SUITE.erl | 590 +++++++++++++++++++++++++++++++++++++ test/test_statem.erl | 92 ++++++ 2 files changed, 682 insertions(+) create mode 100644 test/recon_trace_SUITE.erl create mode 100644 test/test_statem.erl diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl new file mode 100644 index 0000000..04b87c1 --- /dev/null +++ b/test/recon_trace_SUITE.erl @@ -0,0 +1,590 @@ +-module(recon_trace_SUITE). + + +-include_lib("common_test/include/ct.hrl"). +-include_lib("kernel/include/logger.hrl"). % For logger checks if needed +-include_lib("stdlib/include/ms_transform.hrl"). +-compile({parse_transform, ms_transform}). + +-export([ + all/0, groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2 +]). + +-export([ + spawn_test_server/0, + spawn_test_server_loop/1, + get_trace_output/1, + stop_test_server/1, + assert_trace_match/2, + assert_trace_no_match/2 +]). + +%% Test cases +-export([ + dummy_basic_test/1, % Keep original for reference if needed, or remove + trace_all_test_statem_calls/1, + trace_heavy_state_2_calls/1, + trace_heavy_state_2_rate_limited/1, + trace_heavy_state_2_even_arg/1, + trace_iolist_to_binary_with_binary/1, + trace_test_statem_calls_specific_pid/1, + trace_test_statem_calls_arity/1, + trace_test_statem_states_new_procs/1, + trace_handle_call_new_and_gproc/1, + trace_get_state_return_fun/1, + trace_get_state_return_matchspec/1, + trace_get_state_return_shorthand/1, + dummy_advanced_test/1 % Keep original for reference if needed, or remove +]). + +%%-------------------------------------------------------------------- +%% Suite Configuration +%%-------------------------------------------------------------------- + +%% @doc Returns list of test cases and/or groups to be executed. +all() -> + [{group, basic_ops} + %, {group, advanced_tracing} + ]. + +%% @doc Defines the test groups. +groups() -> + [ + {basic_ops, [sequence], [ + dummy_basic_test, + trace_all_test_statem_calls + %trace_heavy_state_2_calls, + %trace_heavy_state_2_rate_limited, + %trace_heavy_state_2_even_arg, + %trace_iolist_to_binary_with_binary, + %trace_test_statem_calls_specific_pid, + %trace_test_statem_calls_arity, + %trace_get_state_return_fun, + %trace_get_state_return_matchspec, + %trace_get_state_return_shorthand + ]}, + {advanced_tracing, [sequence], [ + %% dummy_advanced_test, % Can remove this later + %trace_test_statem_states_new_procs, + %trace_handle_call_new_and_gproc % Requires gproc setup + ]} + ]. + +%%-------------------------------------------------------------------- +%% Init and Teardown Functions +%%-------------------------------------------------------------------- + +init_per_suite(Config) -> + %% Setup before any tests run + {ok, Pid} = test_statem:start(), + ct:log("Starting test_statem process with PID: ~p", [Pid]), + S = test_statem:get_state(), + ct:log("Init per suite state: ~p", [S]), + Config. + + +end_per_suite(_Config) -> + %% Cleanup after all tests run + test_statem:stop(). + + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + %% Cleanup after each group runs + recon_trace:clear(), % Ensure traces are cleared between groups + Config. + + +%%-------------------------------------------------------------------- +%% Helper Functions +%%-------------------------------------------------------------------- +return_trace() -> + dbg:fun2ms(fun(_) -> return_trace() end). + +spawn_test_server() -> + %% Spawn a test server process to capture trace output + Pid = spawn(?MODULE, spawn_test_server_loop, [[]]), + register(ts_serv, Pid), + ct:log("Spawned test server with PID: ~p", [Pid]), + Pid. + +spawn_test_server_loop(Data) -> + ct:log("Test Server State: ~s", [io_lib:format("~p", [Data])]), % Debug logging + receive + {get_value, From} -> % Get the last value + Rest = case Data of + [] -> + From ! {error, no_data}, + []; + [Value | Tail] -> + From ! {ok, Value}, + Tail + end, + spawn_test_server_loop(Rest); + {get_all_values, From} -> % Get all captured values + From ! {ok, lists:reverse(Data)}, + spawn_test_server_loop([]); % Reset data after getting all + Msg -> + ct:log("Test Server Received: ~s", [io_lib:format("~p", [Msg])]), % Debug logging + spawn_test_server_loop([Msg | Data]) + end. + + +get_trace_output_all(TS) -> + TS ! {get_all_values, self()}, + receive + {ok, Values} -> + lists:flatten([ io_lib:format("~s", [X]) || {io_request,_,_,{put_chars,unicode,io_lib,format,X}} <- Values]); + Other -> + ct:fail({failed_to_get_trace_output, Other}) + after 2500 -> + ct:log("Timeout waiting for trace output from ~p", [TS]), + "" % Return empty string on timeout + end. + +get_trace_output(TS) -> + TS ! {get_value, self()}, + receive + {ok, Value} -> + {io_request,_,_,{put_chars,unicode,io_lib,format,X}} = Value, + lists:flatten(io_lib:format("~s", [X])); + Other -> + ct:fail({failed_to_get_trace_output, Other}) + after 2500 -> + ct:log("Timeout waiting for trace output from ~p", [TS]), + "" % Return empty string on timeout + end. + +stop_test_server(TS) -> + unregister(ts_serv), + exit(TS, normal). + +assert_trace_match(RegexString, TraceOutput) -> + ct:log("Asserting match for '~s' in output:~n~s", [RegexString, TraceOutput]), + case re:run(TraceOutput, RegexString, [{capture, none}]) of + match -> ok; + nomatch -> ct:fail({regex_did_not_match, RegexString}) + end. + +assert_trace_no_match(RegexString, TraceOutput) -> + ct:log("Asserting no match for '~s' in output:~n~s", [RegexString, TraceOutput]), + case re:run(TraceOutput, RegexString, [{capture, none}]) of + match -> ct:fail({regex_unexpectedly_matched, RegexString}); + nomatch -> ok + end. + +%%-------------------------------------------------------------------- +%% Test Cases +%%-------------------------------------------------------------------- + +dummy_basic_test(_Config) -> + TS = spawn_test_server(), + recon_trace:calls({test_statem, light_state, '_'}, 10, [{io_server, TS},{scope,local}]), + + test_statem:switch_state(), + S = test_statem:get_state(), + ct:log("State: ~p", [S]), + timer:sleep(100), % Allow time for trace message processing + + TraceOutput = get_trace_output(TS), + + assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), + stop_test_server(TS), + recon_trace:clear(), + ok. + +%% Test cases based on https://ferd.github.io/recon/recon_trace.html#calls/3 +%% Documentation: All calls from the queue module, with 10 calls printed at most: recon_trace:calls({queue, '_', '_'}, 10) +%% Test: All calls from the test_statem module, with 10 calls printed at most: recon_trace:calls({test_statem, '_', '_'}, 10) +trace_all_test_statem_calls(Config) -> + TS = spawn_test_server(), + + recon_trace:calls({test_statem, '_', '_'}, 100, [{io_server, ts_serv},{scope,local}]), + + _ = test_statem:get_state(), + timer:sleep(100), + ok = test_statem:switch_state(), + timer:sleep(100), + _ = test_statem:get_state(), + timer:sleep(100), + ok = test_statem:switch_state(), % Back to light + timer:sleep(100), + TraceOutput = get_trace_output_all(TS), + ct:log("Stsssssssssssssssssate: ~p", [TraceOutput]), + stop_test_server(TS), + recon_trace:clear(), + + assert_trace_match("test_statem:get_state\\(\\)", TraceOutput), % Initial get_state + assert_trace_match("test_statem:switch_state\\(cast, switch_state, #{iterator=>0", TraceOutput), % First switch + assert_trace_match("test_statem:get_state\\(\\)", TraceOutput), % Second get_state + assert_trace_match("test_statem:switch_state\\(cast, switch_state, #{iterator=>1", TraceOutput), % Second switch + assert_trace_match("test_statem:get_state\\(\\)", TraceOutput), % Third get_state + ok. + +%% Documentation: All calls to lists:seq(A,B), with 100 calls printed at most: recon_trace:calls({lists, seq, 2}, 100) +%% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace:calls({lists, seq, 2}, {100, 1000}) +%% Test: All calls to test_statem:heavy_state(A,B), with 10 calls printed at most: recon_trace:calls({test_statem, heavy_state, 2}, 10) +trace_heavy_state_2_calls(Config) -> + TS = spawn_test_server(), + + % Ensure we are in heavy state first + case test_statem:get_state() of + light -> test_statem:switch_state(); + heavy -> ok + end, + heavy = test_statem:get_state(), % Verify state + + recon_trace:calls({test_statem, heavy_state, 2}, 10, [{io_server, TS},{scope,local}]), + + ok = test_statem:switch_state(), % This call should trigger heavy_state(cast, switch_state, ...) + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput), + % Ensure light_state wasn't traced + assert_trace_no_match("test_statem:light_state", TraceOutput), + ok. + +%% Documentation: All calls to lists:seq(A,B,2) (all sequences increasing by two) with 100 calls at most: recon_trace:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) +%% Test: All calls to test_statem:heavy_state(A,B), with 10 calls per second at most: recon_trace:calls({test_statem, heavy_state, 2}, {10, 1000}) +trace_heavy_state_2_rate_limited(Config) -> + TS = spawn_test_server(), + + % Ensure we are in heavy state first + case test_statem:get_state() of + light -> test_statem:switch_state(); + heavy -> ok + end, + heavy = test_statem:get_state(), % Verify state + + recon_trace:calls({test_statem, heavy_state, 2}, {10, 1000}, [{io_server, TS},{scope,local}]), + + % Call it multiple times quickly - only some should be traced + [ test_statem:switch_state() || _ <- lists:seq(1, 20) ], + + timer:sleep(200), % Allow more time for potential rate limiting delays + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check that at least one trace occurred + assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput), + % Asserting the exact number is difficult due to timing, but it should be <= 10 + % We can count occurrences if needed, but matching one is a basic check. + ok. + +%% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: recon_trace:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) +trace_heavy_state_2_even_arg(Config) -> + TS = spawn_test_server(), + + % Ensure we are in heavy state first and iterator is 0 (even) + case test_statem:get_state() of + light -> test_statem:switch_state(); % Now iterator=0, state=heavy + heavy -> % Need to ensure iterator is even. Switch twice if odd. + State = test_statem:get_state(), + case maps:get(iterator, State) rem 2 of + 0 -> ok; + 1 -> test_statem:switch_state(), % -> light, iter=N+1 + test_statem:switch_state() % -> heavy, iter=N+2 (even) + end + end, + State0 = test_statem:get_state(), + 0 = maps:get(iterator, State0) rem 2, % Verify iterator is even + + MatchFun = fun([_Type, _Msg, State = #{iterator := Iter}]) when is_integer(Iter), Iter rem 2 == 0 -> + ct:log("MatchFun matched even iterator: ~p", [Iter]), + true; + ([_Type, _Msg, State = #{iterator := Iter}]) -> + ct:log("MatchFun rejected odd iterator: ~p", [Iter]), + false + end, + recon_trace:calls({test_statem, heavy_state, MatchFun}, 10, [{io_server, TS},{scope,local}]), + + % Call 1: Iterator is even (e.g., 0), should trace + ok = test_statem:switch_state(), % State -> light, Iterator -> 1 + light = test_statem:get_state(), + + % Call 2: Iterator is odd (e.g., 1), should NOT trace heavy_state (light_state is called) + ok = test_statem:switch_state(), % State -> heavy, Iterator -> 2 + heavy = test_statem:get_state(), + + % Call 3: Iterator is even (e.g., 2), should trace + ok = test_statem:switch_state(), % State -> light, Iterator -> 3 + light = test_statem:get_state(), + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + ct:log("Trace output for even arg test:~n~s", [TraceOutput]), + + % Check that the call with even iterator (0 initially) was traced + assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>" ++ integer_to_list(maps:get(iterator, State0)), TraceOutput), + % Check that the call with even iterator (2) was traced + assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>" ++ integer_to_list(maps:get(iterator, State0)+2), TraceOutput), + % There should be exactly two matches for heavy_state + {match, M} = re:run(TraceOutput, "test_statem:heavy_state", [global, {capture, none}]), + 2 = length(M), + ok. + +%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%% Test: All calls to iolist_to_binary/1 made with a binary argument: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +trace_iolist_to_binary_with_binary(Config) -> + TS = spawn_test_server(), + recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> true end}, 10, [{io_server, TS},{scope,local}]), + + _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace + _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace + _ = erlang:iolist_to_binary([<<"mix">>, "ed"]), % Should NOT trace + _ = erlang:iolist_to_binary(<<"another binary">>), % Should trace + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), + assert_trace_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), + ok. + +%% Documentation: Calls to the queue module only in a given process Pid, at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) +%% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most: recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, }]) +trace_test_statem_calls_specific_pid(Config) -> + TS = spawn_test_server(), + Pid = whereis(test_statem), % Get the PID of the test_statem process + recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}, {io_server, TS},{scope,local}]), + + % Call from the target process (implicitly via gen_statem) - should trace + ok = test_statem:switch_state(), + _ = test_statem:get_state(), + + % Call from another process - should NOT trace + Self = self(), + OtherPid = spawn(fun() -> + _ = test_statem:get_state(), % Call from other process + Self ! other_call_done + end), + receive other_call_done -> ok after 1000 -> ct:fail(timeout_waiting_for_other_call) end, + is_process_alive(OtherPid) andalso exit(OtherPid, kill), % Cleanup spawned proc + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check calls originating from are traced (e.g., handle_event) + assert_trace_match(pid_to_list(Self) ++ ".*test_statem:light_state\\(cast, switch_state", TraceOutput), + assert_trace_match(pid_to_list(Self) ++ ".*test_statem:get_state\\(call, ", TraceOutput), + + % Check calls from the other process are NOT traced + assert_trace_no_match(pid_to_list(OtherPid) ++ ".*test_statem:get_state", TraceOutput), + ok. + +%% Documentation: Print the traces with the function arity instead of literal arguments: recon_trace:calls(TSpec, Max, [{args, arity}]) +%% Test: Print traces for test_statem calls with arity instead of arguments: recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}]) +trace_test_statem_calls_arity(Config) -> + TS = spawn_test_server(), + + recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, TS},{scope,local}]), + + _ = test_statem:get_state(), + ok = test_statem:switch_state(), + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check for arity format, e.g., module:function/arity + assert_trace_match("test_statem:get_state/3", TraceOutput), % gen_statem callback arity + assert_trace_match("test_statem:light_state/3", TraceOutput), % gen_statem callback arity + % Ensure literal args are not present (tricky regex, check absence of typical args) + assert_trace_no_match("switch_state", TraceOutput), + assert_trace_no_match("iterator", TraceOutput), + ok. + +%% Documentation: Matching the filter/2 functions of both dict and lists modules, across new processes only: recon_trace:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) +%% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only: recon_trace:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) +trace_test_statem_states_new_procs(Config) -> + TS = spawn_test_server(), + + recon_trace:calls([{test_statem, light_state, 3}, {test_statem, heavy_state, 3}], 10, [{pid, new}, {io_server, TS},{scope,local}]), % Note: gen_statem callbacks are arity 3 + + % Call from the *current* test process - should NOT trace + ok = test_statem:switch_state(), % light -> heavy + heavy = test_statem:get_state(), + ok = test_statem:switch_state(), % heavy -> light + light = test_statem:get_state(), + + % Call from a *new* process - should trace + Self = self(), + NewPid = spawn(fun() -> + ct:log("New process ~p calling switch_state", [self()]), + ok = test_statem:switch_state(), % light -> heavy (heavy_state/3 should trace) + ct:log("New process ~p calling switch_state again", [self()]), + ok = test_statem:switch_state(), % heavy -> light (light_state/3 should trace) + Self ! new_calls_done + end), + receive new_calls_done -> ok after 1000 -> ct:fail(timeout_waiting_for_new_call) end, + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check calls from the new process ARE traced + assert_trace_match(pid_to_list(NewPid) ++ ".*test_statem:heavy_state\\(cast, switch_state", TraceOutput), + assert_trace_match(pid_to_list(NewPid) ++ ".*test_statem:light_state\\(cast, switch_state", TraceOutput), + + % Check calls from the test process (self()) or the statem process ARE NOT traced + assert_trace_no_match(pid_to_list(self()) ++ ".*test_statem:", TraceOutput), + assert_trace_no_match(pid_to_list(self()) ++ ".*test_statem:", TraceOutput), % The calls happen *in* , but triggered by NewPid + + is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc + ok. + +%% Documentation: Tracing the handle_call/3 functions of a given module for all new processes, and those of an existing one registered with gproc: recon_trace:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) +%% Test: Tracing test_statem:handle_call/3 for new processes and one via gproc (requires gproc setup): recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, gproc, Name}, new]}]) +trace_handle_call_new_and_gproc(Config) -> + case ?config(gproc_name, Config) of + undefined -> + ct:skip("gproc not available or test_statem not registered"); + GprocName -> + TS = spawn_test_server(), + + % Note: test_statem uses handle_event/4 for cast, handle_call/3 for call + % We trace handle_call/3 here. + recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, gproc, GprocName}, new]}, {io_server, TS},{scope,local}]), + + % Call via gproc - should trace + ct:log("Calling get_state via gproc ~p", [GprocName]), + _GprocState = gproc:call(GprocName, {call, get_state}), % Triggers handle_call/3 + + % Call from a new process - should trace + Self = self(), + NewPid = spawn(fun() -> + ct:log("New process ~p calling get_state", [self()]), + _NewState = test_statem:get_state(), % Triggers handle_call/3 + Self ! new_call_done + end), + receive new_call_done -> ok after 1000 -> ct:fail(timeout_waiting_for_new_gproc_call) end, + + % Call directly from test process - should NOT trace + ct:log("Calling get_state directly from test process ~p", [self()]), + _DirectState = test_statem:get_state(), + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check call via gproc IS traced (originating from ) + assert_trace_match(pid_to_list(Self) ++ ".*test_statem:handle_call\\({call,get_state},", TraceOutput), + % Check call from new process IS traced (originating from ) + % We might see two traces if both gproc and new pid calls happened close together + {match, M} = re:run(TraceOutput, pid_to_list(Self) ++ ".*test_statem:handle_call\\({call,get_state},", [global, {capture, none}]), + true = length(M) >= 1, % Should be at least 1, likely 2 + + % Check call from test process is NOT traced (no new trace line added) + % This is harder to assert definitively without counting lines before/after. + % We rely on the {pid, [..., new]} filter working correctly. + + is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc + ok + end. + +%% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) +%% Test: Show the result of test_statem:get_state/0 calls: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) +trace_get_state_return_fun(Config) -> + TS = spawn_test_server(), + + % Ensure state is known (e.g., light) + case test_statem:get_state() of + heavy -> test_statem:switch_state(); + light -> ok + end, + light = test_statem:get_state(), + + % Trace the API function test_statem:get_state/1 + recon_trace:calls({test_statem, get_state, fun([_Pid]) -> return_trace() end}, 10, [{io_server, TS},{scope,local}]), + + Res = test_statem:get_state(), % Call the function + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check for the call and its return value + ExpectedReturn = atom_to_list(Res), % e.g., "light" + assert_trace_match("test_statem:get_state\\(\\) => " ++ ExpectedReturn, TraceOutput), + ok. + +%% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), +%% Test: Show the result of test_statem:get_state/0 calls (using match spec): recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) +trace_get_state_return_matchspec(Config) -> + TS = spawn_test_server(), + + % Ensure state is known (e.g., heavy) + case test_statem:get_state() of + light -> test_statem:switch_state(); + heavy -> ok + end, + heavy = test_statem:get_state(), + + % Trace the API function test_statem:get_state/1 using match spec + recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10, [{io_server, TS},{scope,local}]), + + Res = test_statem:get_state(), % Call the function + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check for the call and its return value + ExpectedReturn = atom_to_list(Res), % e.g., "heavy" + assert_trace_match("test_statem:get_state\\(\\) => " ++ ExpectedReturn, TraceOutput), + ok. + +%% Documentation: A short-hand version for this pattern of 'match anything, trace everything' for a function is recon_trace:calls({Mod, Fun, return_trace}). +%% Test: Show the result of test_statem:get_state/0 calls (shorthand): recon_trace:calls({test_statem, get_state, return_trace}, 10). +trace_get_state_return_shorthand(Config) -> + TS = spawn_test_server(), + + % Ensure state is known (e.g., light) + case test_statem:get_state() of + heavy -> test_statem:switch_state(); + light -> ok + end, + light = test_statem:get_state(), + + % Trace the API function test_statem:get_state/1 using shorthand + recon_trace:calls({test_statem, get_state, return_trace}, 10, [{io_server, TS},{scope,local}]), + + Res = test_statem:get_state(), % Call the function + + timer:sleep(100), + TraceOutput = get_trace_output(TS), + stop_test_server(TS), + recon_trace:clear(), + + % Check for the call and its return value + ExpectedReturn = atom_to_list(Res), % e.g., "light" + assert_trace_match("test_statem:get_state\\(\\) => " ++ ExpectedReturn, TraceOutput), + ok. + + +dummy_advanced_test(_Config) -> + ct:log("This is a placeholder for an advanced recon_trace test"), + ok. diff --git a/test/test_statem.erl b/test/test_statem.erl new file mode 100644 index 0000000..5ced0c9 --- /dev/null +++ b/test/test_statem.erl @@ -0,0 +1,92 @@ +%%%------------------------------------------------------------------- +%%% @copyright (C) 2019, Mathias Green +%%% @doc +%%% Basic statem for testing purposes +%%% @end +%%% Created : 08 Jun 2019 by Mathias Green (flmath) +%%%------------------------------------------------------------------- +-module(test_statem). + +-behaviour(gen_statem). + + +-include_lib("eunit/include/eunit.hrl"). + + +-define(HeavyStateWindowLength, 1000). + +%% API +-export([start/0, stop/0]). +-export([get_state/0, switch_state/0]). +%% gen_statem callbacks +-export([init/1, callback_mode/0, light_state/3, heavy_state/3, + terminate/3, code_change/4]). +-define(NAME, ?MODULE). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%%-------------------------------------------------------------------- +start() -> + gen_statem:start({local, ?NAME}, ?MODULE, [], []). + +switch_state()-> + gen_statem:cast(?NAME, switch_state). + +get_state()-> + gen_statem:call(?NAME, get_value). + +stop() -> + gen_statem:stop(?NAME). + +%%%=================================================================== +%%% gen_statem callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +init([]) -> + ct:pal("Starting ~p~n", [?MODULE]), + {ok, light_state, #{iterator=>0}}. + +callback_mode() -> + state_functions. + +%%-------------------------------------------------------------------- +light_state(cast, switch_state, State) -> + #{iterator:=Number}=State, + traced_function(enter_heavy_state, Number), + {next_state, heavy_state, State#{iterator:=Number+1}, ?HeavyStateWindowLength}; +light_state({call, From}, get_value, State) -> + #{iterator:=Number} = State, + {next_state, light_state, State, [{reply, From, {ok, light_state, Number}}]}. + +heavy_state(cast, switch_state, State) -> + #{iterator:=Number} = State, + traced_function(enter_light_state, Number), + {next_state, light_state, State#{iterator:=Number+1}}; +heavy_state(timeout, _Event, State) -> + #{iterator:=Number} = State, + traced_function(keep_heavy_state, Number), + {next_state, heavy_state, State#{iterator:=Number+1}, ?HeavyStateWindowLength}; +heavy_state({call, From}, get_value, State) -> + #{iterator:=Number} = State, + {next_state, heavy_state, State, + [{reply, From, {ok, heavy_state, Number}}, + {timeout, ?HeavyStateWindowLength, back_to_heavy_state}]}. + + +%%-------------------------------------------------------------------- +terminate(Reason, _StateName, _State) -> + ct:pal("Terminate ~p ~p ~n", [?MODULE, Reason]), + ok. + +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +traced_function(_StateName, _Number)-> + ct:pal("log called from state ~p number ~p~n", [_StateName, _Number]). \ No newline at end of file From c94fef0b748c8ae700dbc78e0bc1facf7e2b4c8b Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 19 Apr 2025 01:26:31 +0200 Subject: [PATCH 02/35] Add a fake registration module for testing and update recon_trace_SUITE - Introduced a new module `fake_reg` that simulates the behavior of the Erlang registry for testing purposes. - Implemented gen_server callbacks and necessary API functions in `fake_reg`. - Updated `recon_trace_SUITE` to utilize the new `fake_reg` for testing. - Refactored existing tests to improve clarity and maintainability, including logging trace outputs to files. --- test/fake_reg.erl | 85 +++++ test/recon_trace_SUITE.erl | 679 ++++++++++++++++++------------------- test/test_statem.erl | 2 +- 3 files changed, 409 insertions(+), 357 deletions(-) create mode 100644 test/fake_reg.erl diff --git a/test/fake_reg.erl b/test/fake_reg.erl new file mode 100644 index 0000000..1152ea5 --- /dev/null +++ b/test/fake_reg.erl @@ -0,0 +1,85 @@ +%% Minimal fake registration module for testing +%% purposes. This module simulates the behavior of the +%% registration in Erlang, allowing us to +%% test the functionality of our code without +%% relying on the actual Erlang registry. +-module(fake_reg). +-behaviour(gen_server). + +%% API for the gen_server +-export([start/0, stop/0]). + +%% Callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%% gen_registry callbacks +-export([register_name/2, unregister_name/1, whereis_name/1, send/2]). + +-define(SERVER, ?MODULE). + +%%%=================================================================== +%%% Public API +%%%=================================================================== + +start() -> + gen_server:start({local, ?SERVER}, ?MODULE, #{}, []). + +stop() -> + gen_server:call(?SERVER, stop). + +%%%=================================================================== +%%% gen_registry required callbacks +%%%=================================================================== + +register_name(Name, Pid) when is_pid(Pid) -> + gen_server:call(?SERVER, {register, Name, Pid}). + +unregister_name(Name) -> + gen_server:call(?SERVER, {unregister, Name}). + +whereis_name(Name) -> + gen_server:call(?SERVER, {whereis, Name}). + +send(Name, Msg) -> + case whereis_name(Name) of + undefined -> exit({badarg, {Name, Msg}}); + Pid when is_pid(Pid) -> Pid ! Msg, Pid + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init(StateMap) -> + {ok, StateMap}. + +handle_call({register, Name, Pid}, _From, Registry) -> + case maps:is_key(Name, Registry) of + true -> {reply, {error, already_registered}, Registry}; + false -> {reply, yes, Registry#{Name => Pid}} + end; + +handle_call({unregister, Name}, _From, Registry) -> + {reply, ok, maps:remove(Name, Registry)}; + +handle_call({whereis, Name}, _From, Registry) -> + {reply, maps:get(Name, Registry, undefined), Registry}; + +handle_call(stop, _From, Registry) -> + {stop, normal, ok, Registry}; + +handle_call(_, _From, State) -> + {reply, error, State}. + +handle_cast(_, State) -> + {noreply, State}. + +handle_info(_, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl index 04b87c1..8a895d6 100644 --- a/test/recon_trace_SUITE.erl +++ b/test/recon_trace_SUITE.erl @@ -1,8 +1,6 @@ -module(recon_trace_SUITE). - -include_lib("common_test/include/ct.hrl"). --include_lib("kernel/include/logger.hrl"). % For logger checks if needed -include_lib("stdlib/include/ms_transform.hrl"). -compile({parse_transform, ms_transform}). @@ -13,10 +11,7 @@ ]). -export([ - spawn_test_server/0, - spawn_test_server_loop/1, - get_trace_output/1, - stop_test_server/1, + count_trace_match/3, assert_trace_match/2, assert_trace_no_match/2 ]). @@ -32,11 +27,10 @@ trace_test_statem_calls_specific_pid/1, trace_test_statem_calls_arity/1, trace_test_statem_states_new_procs/1, - trace_handle_call_new_and_gproc/1, + trace_handle_call_new_and_custom_registry/1, trace_get_state_return_fun/1, trace_get_state_return_matchspec/1, - trace_get_state_return_shorthand/1, - dummy_advanced_test/1 % Keep original for reference if needed, or remove + trace_get_state_return_shorthand/1 ]). %%-------------------------------------------------------------------- @@ -45,30 +39,25 @@ %% @doc Returns list of test cases and/or groups to be executed. all() -> - [{group, basic_ops} - %, {group, advanced_tracing} - ]. + [{group, basic_ops}]. %% @doc Defines the test groups. groups() -> [ {basic_ops, [sequence], [ dummy_basic_test, - trace_all_test_statem_calls - %trace_heavy_state_2_calls, - %trace_heavy_state_2_rate_limited, - %trace_heavy_state_2_even_arg, - %trace_iolist_to_binary_with_binary, - %trace_test_statem_calls_specific_pid, - %trace_test_statem_calls_arity, - %trace_get_state_return_fun, - %trace_get_state_return_matchspec, - %trace_get_state_return_shorthand - ]}, - {advanced_tracing, [sequence], [ - %% dummy_advanced_test, % Can remove this later - %trace_test_statem_states_new_procs, - %trace_handle_call_new_and_gproc % Requires gproc setup + trace_all_test_statem_calls, + trace_heavy_state_2_calls, + trace_heavy_state_2_rate_limited, + trace_heavy_state_2_even_arg, + trace_iolist_to_binary_with_binary, + trace_test_statem_calls_specific_pid, + trace_test_statem_states_new_procs, + trace_handle_call_new_and_custom_registry, + trace_test_statem_calls_arity, + trace_get_state_return_fun, + trace_get_state_return_matchspec, + trace_get_state_return_shorthand ]} ]. @@ -77,7 +66,6 @@ groups() -> %%-------------------------------------------------------------------- init_per_suite(Config) -> - %% Setup before any tests run {ok, Pid} = test_statem:start(), ct:log("Starting test_statem process with PID: ~p", [Pid]), S = test_statem:get_state(), @@ -85,7 +73,7 @@ init_per_suite(Config) -> Config. -end_per_suite(_Config) -> +end_per_suite(__Config) -> %% Cleanup after all tests run test_statem:stop(). @@ -97,81 +85,39 @@ end_per_group(_GroupName, Config) -> %% Cleanup after each group runs recon_trace:clear(), % Ensure traces are cleared between groups Config. +init_per_testcase(_TestName, Config) -> + %% Cleanup before each test runs + recon_trace:clear(), % Ensure traces are cleared between tests + Config. +end_per_testcase(_TestName, Config) -> + %% Cleanup after each test runs + recon_trace:clear(), % Ensure traces are cleared between tests + Config. %%-------------------------------------------------------------------- %% Helper Functions %%-------------------------------------------------------------------- -return_trace() -> - dbg:fun2ms(fun(_) -> return_trace() end). - -spawn_test_server() -> - %% Spawn a test server process to capture trace output - Pid = spawn(?MODULE, spawn_test_server_loop, [[]]), - register(ts_serv, Pid), - ct:log("Spawned test server with PID: ~p", [Pid]), - Pid. - -spawn_test_server_loop(Data) -> - ct:log("Test Server State: ~s", [io_lib:format("~p", [Data])]), % Debug logging - receive - {get_value, From} -> % Get the last value - Rest = case Data of - [] -> - From ! {error, no_data}, - []; - [Value | Tail] -> - From ! {ok, Value}, - Tail - end, - spawn_test_server_loop(Rest); - {get_all_values, From} -> % Get all captured values - From ! {ok, lists:reverse(Data)}, - spawn_test_server_loop([]); % Reset data after getting all - Msg -> - ct:log("Test Server Received: ~s", [io_lib:format("~p", [Msg])]), % Debug logging - spawn_test_server_loop([Msg | Data]) - end. - - -get_trace_output_all(TS) -> - TS ! {get_all_values, self()}, - receive - {ok, Values} -> - lists:flatten([ io_lib:format("~s", [X]) || {io_request,_,_,{put_chars,unicode,io_lib,format,X}} <- Values]); - Other -> - ct:fail({failed_to_get_trace_output, Other}) - after 2500 -> - ct:log("Timeout waiting for trace output from ~p", [TS]), - "" % Return empty string on timeout - end. - -get_trace_output(TS) -> - TS ! {get_value, self()}, - receive - {ok, Value} -> - {io_request,_,_,{put_chars,unicode,io_lib,format,X}} = Value, - lists:flatten(io_lib:format("~s", [X])); - Other -> - ct:fail({failed_to_get_trace_output, Other}) - after 2500 -> - ct:log("Timeout waiting for trace output from ~p", [TS]), - "" % Return empty string on timeout - end. - -stop_test_server(TS) -> - unregister(ts_serv), - exit(TS, normal). assert_trace_match(RegexString, TraceOutput) -> - ct:log("Asserting match for '~s' in output:~n~s", [RegexString, TraceOutput]), + ct:log("Asserting match for ~p in output:~n~p", [RegexString, TraceOutput]), case re:run(TraceOutput, RegexString, [{capture, none}]) of match -> ok; nomatch -> ct:fail({regex_did_not_match, RegexString}) end. +count_trace_match(RegexString, TraceOutput, ExpCnt) -> + ct:log("Counting if ~p matches for ~p in output:~n~p", [ExpCnt, RegexString, TraceOutput]), + + case re:run(TraceOutput, RegexString, [global]) of + {match, List} when length(List) == ExpCnt -> ok; + {match, List} -> ct:fail({wrong_match_count, RegexString, length(List), ExpCnt}); + nomatch -> ct:fail({regex_did_not_match, RegexString}); + _ -> ct:fail({unexpected, RegexString, ExpCnt}) + + end. assert_trace_no_match(RegexString, TraceOutput) -> - ct:log("Asserting no match for '~s' in output:~n~s", [RegexString, TraceOutput]), + ct:log("Asserting match for ~p in output:~n~p", [RegexString, TraceOutput]), case re:run(TraceOutput, RegexString, [{capture, none}]) of match -> ct:fail({regex_unexpectedly_matched, RegexString}); nomatch -> ok @@ -182,164 +128,155 @@ assert_trace_no_match(RegexString, TraceOutput) -> %%-------------------------------------------------------------------- dummy_basic_test(_Config) -> - TS = spawn_test_server(), - recon_trace:calls({test_statem, light_state, '_'}, 10, [{io_server, TS},{scope,local}]), - - test_statem:switch_state(), - S = test_statem:get_state(), - ct:log("State: ~p", [S]), - timer:sleep(100), % Allow time for trace message processing - - TraceOutput = get_trace_output(TS), + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try + recon_trace:calls({test_statem, light_state, '_'}, 10, [{io_server, FH},{scope,local}]), + test_statem:switch_state(), + S = test_statem:get_state(), + ct:log("State: ~p", [S]), + timer:sleep(100), % Allow time for trace message processing + + {ok, TraceOutput} = file:read_file(LogFileName), + assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput) + after + file:close(FH) + end, - assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), - stop_test_server(TS), - recon_trace:clear(), ok. %% Test cases based on https://ferd.github.io/recon/recon_trace.html#calls/3 %% Documentation: All calls from the queue module, with 10 calls printed at most: recon_trace:calls({queue, '_', '_'}, 10) %% Test: All calls from the test_statem module, with 10 calls printed at most: recon_trace:calls({test_statem, '_', '_'}, 10) -trace_all_test_statem_calls(Config) -> - TS = spawn_test_server(), - - recon_trace:calls({test_statem, '_', '_'}, 100, [{io_server, ts_serv},{scope,local}]), +trace_all_test_statem_calls(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try + recon_trace:calls({test_statem, '_', '_'}, 100, [{io_server, FH},{scope,local}]), - _ = test_statem:get_state(), timer:sleep(100), ok = test_statem:switch_state(), - timer:sleep(100), _ = test_statem:get_state(), timer:sleep(100), ok = test_statem:switch_state(), % Back to light timer:sleep(100), - TraceOutput = get_trace_output_all(TS), - ct:log("Stsssssssssssssssssate: ~p", [TraceOutput]), - stop_test_server(TS), - recon_trace:clear(), + lists:foreach(fun(_)->test_statem:get_state(), timer:sleep(50) end, lists:seq(1, 7)), % Call get_state multiple times + + {ok, TraceOutput} = file:read_file(LogFileName), + + count_trace_match("test_statem:get_state\\(\\)", TraceOutput,8), % Initial get_state + count_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), % First switch + count_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput,1) % Second switch + - assert_trace_match("test_statem:get_state\\(\\)", TraceOutput), % Initial get_state - assert_trace_match("test_statem:switch_state\\(cast, switch_state, #{iterator=>0", TraceOutput), % First switch - assert_trace_match("test_statem:get_state\\(\\)", TraceOutput), % Second get_state - assert_trace_match("test_statem:switch_state\\(cast, switch_state, #{iterator=>1", TraceOutput), % Second switch - assert_trace_match("test_statem:get_state\\(\\)", TraceOutput), % Third get_state + after + file:close(FH) + end, + ok. -%% Documentation: All calls to lists:seq(A,B), with 100 calls printed at most: recon_trace:calls({lists, seq, 2}, 100) %% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace:calls({lists, seq, 2}, {100, 1000}) -%% Test: All calls to test_statem:heavy_state(A,B), with 10 calls printed at most: recon_trace:calls({test_statem, heavy_state, 2}, 10) -trace_heavy_state_2_calls(Config) -> - TS = spawn_test_server(), +%% Test: All calls to test_statem:heavy_state(A,B), with 3 calls printed at most: recon_trace:calls({test_statem, heavy_state, 2}, 10) +trace_heavy_state_2_calls(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try % Ensure we are in heavy state first case test_statem:get_state() of - light -> test_statem:switch_state(); - heavy -> ok + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok end, - heavy = test_statem:get_state(), % Verify state - - recon_trace:calls({test_statem, heavy_state, 2}, 10, [{io_server, TS},{scope,local}]), - - ok = test_statem:switch_state(), % This call should trigger heavy_state(cast, switch_state, ...) + + recon_trace:calls({test_statem, heavy_state, 3}, 3, [ {io_server, FH},{scope,local}]), - timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), + timer:sleep(2000), recon_trace:clear(), - assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput), - % Ensure light_state wasn't traced - assert_trace_no_match("test_statem:light_state", TraceOutput), + {ok, TraceOutput} = file:read_file(LogFileName), + + + count_trace_match("test_statem:heavy_state", TraceOutput, 3), + %% Ensure light_state wasn't traced + assert_trace_no_match("test_statem:light_state", TraceOutput) + + after + file:close(FH) + end, + ok. %% Documentation: All calls to lists:seq(A,B,2) (all sequences increasing by two) with 100 calls at most: recon_trace:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) -%% Test: All calls to test_statem:heavy_state(A,B), with 10 calls per second at most: recon_trace:calls({test_statem, heavy_state, 2}, {10, 1000}) -trace_heavy_state_2_rate_limited(Config) -> - TS = spawn_test_server(), +%% Test: All calls to test_statem:heavy_state, with 1 calls per second at most: recon_trace:calls({test_statem, heavy_state, 3}, {1, 1000}) +trace_heavy_state_2_rate_limited(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try - % Ensure we are in heavy state first + case test_statem:get_state() of - light -> test_statem:switch_state(); - heavy -> ok + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok end, - heavy = test_statem:get_state(), % Verify state - - recon_trace:calls({test_statem, heavy_state, 2}, {10, 1000}, [{io_server, TS},{scope,local}]), - - % Call it multiple times quickly - only some should be traced - [ test_statem:switch_state() || _ <- lists:seq(1, 20) ], - - timer:sleep(200), % Allow more time for potential rate limiting delays - TraceOutput = get_trace_output(TS), - stop_test_server(TS), + + recon_trace:calls({test_statem, heavy_state, 3}, {1, 1000}, [ {io_server, FH},{scope,local}]), + + timer:sleep(2200), % Allow more time for potential rate limiting delays recon_trace:clear(), + {ok, TraceOutput} = file:read_file(LogFileName), + + % Check that at least one trace occurred - assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput), + count_trace_match("test_statem:heavy_state", TraceOutput, 2) % Asserting the exact number is difficult due to timing, but it should be <= 10 % We can count occurrences if needed, but matching one is a basic check. + after + + file:close(FH) + end, + ok. %% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: recon_trace:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) -trace_heavy_state_2_even_arg(Config) -> - TS = spawn_test_server(), +trace_heavy_state_2_even_arg(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try % Ensure we are in heavy state first and iterator is 0 (even) case test_statem:get_state() of - light -> test_statem:switch_state(); % Now iterator=0, state=heavy - heavy -> % Need to ensure iterator is even. Switch twice if odd. - State = test_statem:get_state(), - case maps:get(iterator, State) rem 2 of - 0 -> ok; - 1 -> test_statem:switch_state(), % -> light, iter=N+1 - test_statem:switch_state() % -> heavy, iter=N+2 (even) - end + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, - State0 = test_statem:get_state(), - 0 = maps:get(iterator, State0) rem 2, % Verify iterator is even - - MatchFun = fun([_Type, _Msg, State = #{iterator := Iter}]) when is_integer(Iter), Iter rem 2 == 0 -> - ct:log("MatchFun matched even iterator: ~p", [Iter]), - true; - ([_Type, _Msg, State = #{iterator := Iter}]) -> - ct:log("MatchFun rejected odd iterator: ~p", [Iter]), - false - end, - recon_trace:calls({test_statem, heavy_state, MatchFun}, 10, [{io_server, TS},{scope,local}]), - - % Call 1: Iterator is even (e.g., 0), should trace - ok = test_statem:switch_state(), % State -> light, Iterator -> 1 - light = test_statem:get_state(), - - % Call 2: Iterator is odd (e.g., 1), should NOT trace heavy_state (light_state is called) - ok = test_statem:switch_state(), % State -> heavy, Iterator -> 2 - heavy = test_statem:get_state(), - - % Call 3: Iterator is even (e.g., 2), should trace - ok = test_statem:switch_state(), % State -> light, Iterator -> 3 - light = test_statem:get_state(), + MatchSpec = dbg:fun2ms(fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace() end), - timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), + recon_trace:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH},{scope,local}]), + + + timer:sleep(1900), recon_trace:clear(), - - ct:log("Trace output for even arg test:~n~s", [TraceOutput]), - - % Check that the call with even iterator (0 initially) was traced - assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>" ++ integer_to_list(maps:get(iterator, State0)), TraceOutput), - % Check that the call with even iterator (2) was traced - assert_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>" ++ integer_to_list(maps:get(iterator, State0)+2), TraceOutput), - % There should be exactly two matches for heavy_state - {match, M} = re:run(TraceOutput, "test_statem:heavy_state", [global, {capture, none}]), - 2 = length(M), + {ok, TraceOutput} = file:read_file(LogFileName), + + count_trace_match("test_statem:heavy_state\\(timeout", TraceOutput, 3) + after + + file:close(FH) + end, + + ok. %% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) %% Test: All calls to iolist_to_binary/1 made with a binary argument: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -trace_iolist_to_binary_with_binary(Config) -> - TS = spawn_test_server(), - recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> true end}, 10, [{io_server, TS},{scope,local}]), +trace_iolist_to_binary_with_binary(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try + + MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), + recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace @@ -347,244 +284,274 @@ trace_iolist_to_binary_with_binary(Config) -> _ = erlang:iolist_to_binary(<<"another binary">>), % Should trace timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), + {ok, TraceOutput} = file:read_file(LogFileName), + recon_trace:clear(), assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), assert_trace_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), - assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput) + after + + file:close(FH) + end, + ok. %% Documentation: Calls to the queue module only in a given process Pid, at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) %% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most: recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, }]) -trace_test_statem_calls_specific_pid(Config) -> - TS = spawn_test_server(), - Pid = whereis(test_statem), % Get the PID of the test_statem process - recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}, {io_server, TS},{scope,local}]), - - % Call from the target process (implicitly via gen_statem) - should trace - ok = test_statem:switch_state(), - _ = test_statem:get_state(), - +trace_test_statem_calls_specific_pid(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try + + %%default statem state is heavy_state + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + %% new statem in light state + {ok, Pid} = gen_statem:start(test_statem, [], []), + + recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}, {io_server, FH},{scope,local}]), + + gen_statem:call(Pid, get_value), + gen_statem:call(Pid, get_value), + % Call from another process - should NOT trace - Self = self(), - OtherPid = spawn(fun() -> - _ = test_statem:get_state(), % Call from other process - Self ! other_call_done - end), - receive other_call_done -> ok after 1000 -> ct:fail(timeout_waiting_for_other_call) end, - is_process_alive(OtherPid) andalso exit(OtherPid, kill), % Cleanup spawned proc - + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), recon_trace:clear(), + {ok, TraceOutput} = file:read_file(LogFileName), + + % Check calls originating from are traced (e.g., handle_event) - assert_trace_match(pid_to_list(Self) ++ ".*test_statem:light_state\\(cast, switch_state", TraceOutput), - assert_trace_match(pid_to_list(Self) ++ ".*test_statem:get_state\\(call, ", TraceOutput), - + count_trace_match(".*test_statem:light_state", TraceOutput,2), + % Check calls from the other process are NOT traced - assert_trace_no_match(pid_to_list(OtherPid) ++ ".*test_statem:get_state", TraceOutput), + assert_trace_no_match(".*test_statem:heavy_state", TraceOutput), + + is_process_alive(Pid) andalso exit(Pid, kill) % Cleanup spawned proc + + after + + file:close(FH) + end, + ok. %% Documentation: Print the traces with the function arity instead of literal arguments: recon_trace:calls(TSpec, Max, [{args, arity}]) %% Test: Print traces for test_statem calls with arity instead of arguments: recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}]) -trace_test_statem_calls_arity(Config) -> - TS = spawn_test_server(), +trace_test_statem_calls_arity(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try - recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, TS},{scope,local}]), + recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, FH},{scope,local}]), - _ = test_statem:get_state(), + test_statem:get_state(), + ok = test_statem:switch_state(), ok = test_statem:switch_state(), - timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), recon_trace:clear(), + {ok, TraceOutput} = file:read_file(LogFileName), + + % Check for arity format, e.g., module:function/arity - assert_trace_match("test_statem:get_state/3", TraceOutput), % gen_statem callback arity + assert_trace_match("test_statem:get_state/0", TraceOutput), % gen_statem callback arity assert_trace_match("test_statem:light_state/3", TraceOutput), % gen_statem callback arity % Ensure literal args are not present (tricky regex, check absence of typical args) - assert_trace_no_match("switch_state", TraceOutput), - assert_trace_no_match("iterator", TraceOutput), + assert_trace_no_match("switch_state\\(", TraceOutput), + assert_trace_no_match("iterator", TraceOutput) + after + + file:close(FH) + end, + ok. %% Documentation: Matching the filter/2 functions of both dict and lists modules, across new processes only: recon_trace:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) %% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only: recon_trace:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) -trace_test_statem_states_new_procs(Config) -> - TS = spawn_test_server(), - - recon_trace:calls([{test_statem, light_state, 3}, {test_statem, heavy_state, 3}], 10, [{pid, new}, {io_server, TS},{scope,local}]), % Note: gen_statem callbacks are arity 3 - - % Call from the *current* test process - should NOT trace - ok = test_statem:switch_state(), % light -> heavy - heavy = test_statem:get_state(), - ok = test_statem:switch_state(), % heavy -> light - light = test_statem:get_state(), - - % Call from a *new* process - should trace - Self = self(), - NewPid = spawn(fun() -> - ct:log("New process ~p calling switch_state", [self()]), - ok = test_statem:switch_state(), % light -> heavy (heavy_state/3 should trace) - ct:log("New process ~p calling switch_state again", [self()]), - ok = test_statem:switch_state(), % heavy -> light (light_state/3 should trace) - Self ! new_calls_done - end), - receive new_calls_done -> ok after 1000 -> ct:fail(timeout_waiting_for_new_call) end, - - timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), +trace_test_statem_states_new_procs(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + + recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, [{pid, new}, {io_server, FH},{scope,local}]), + + {ok, heavy_state,_} = test_statem:get_state(), +%% Call from a *new* process - should trace + {ok, NewPid} = gen_statem:start(test_statem, [], []), + + gen_statem:call(NewPid, get_value), + gen_statem:call(NewPid, get_value), + + % Call from old process - should NOT trace + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), + + timer:sleep(1000), recon_trace:clear(), - % Check calls from the new process ARE traced - assert_trace_match(pid_to_list(NewPid) ++ ".*test_statem:heavy_state\\(cast, switch_state", TraceOutput), - assert_trace_match(pid_to_list(NewPid) ++ ".*test_statem:light_state\\(cast, switch_state", TraceOutput), + {ok, TraceOutput} = file:read_file(LogFileName), + - % Check calls from the test process (self()) or the statem process ARE NOT traced - assert_trace_no_match(pid_to_list(self()) ++ ".*test_statem:", TraceOutput), - assert_trace_no_match(pid_to_list(self()) ++ ".*test_statem:", TraceOutput), % The calls happen *in* , but triggered by NewPid + % Check calls from the new process ARE traced + count_trace_match("test_statem:light_state", TraceOutput, 2), + assert_trace_no_match("test_statem:heavy_state", TraceOutput), - is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc + is_process_alive(NewPid) andalso exit(NewPid, kill) % Cleanup spawned proc + after + file:close(FH) + end, + ok. %% Documentation: Tracing the handle_call/3 functions of a given module for all new processes, and those of an existing one registered with gproc: recon_trace:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) %% Test: Tracing test_statem:handle_call/3 for new processes and one via gproc (requires gproc setup): recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, gproc, Name}, new]}]) -trace_handle_call_new_and_gproc(Config) -> - case ?config(gproc_name, Config) of - undefined -> - ct:skip("gproc not available or test_statem not registered"); - GprocName -> - TS = spawn_test_server(), - - % Note: test_statem uses handle_event/4 for cast, handle_call/3 for call - % We trace handle_call/3 here. - recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, gproc, GprocName}, new]}, {io_server, TS},{scope,local}]), - - % Call via gproc - should trace - ct:log("Calling get_state via gproc ~p", [GprocName]), - _GprocState = gproc:call(GprocName, {call, get_state}), % Triggers handle_call/3 - - % Call from a new process - should trace - Self = self(), - NewPid = spawn(fun() -> - ct:log("New process ~p calling get_state", [self()]), - _NewState = test_statem:get_state(), % Triggers handle_call/3 - Self ! new_call_done - end), - receive new_call_done -> ok after 1000 -> ct:fail(timeout_waiting_for_new_gproc_call) end, - - % Call directly from test process - should NOT trace - ct:log("Calling get_state directly from test process ~p", [self()]), - _DirectState = test_statem:get_state(), - - timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), - recon_trace:clear(), - - % Check call via gproc IS traced (originating from ) - assert_trace_match(pid_to_list(Self) ++ ".*test_statem:handle_call\\({call,get_state},", TraceOutput), - % Check call from new process IS traced (originating from ) - % We might see two traces if both gproc and new pid calls happened close together - {match, M} = re:run(TraceOutput, pid_to_list(Self) ++ ".*test_statem:handle_call\\({call,get_state},", [global, {capture, none}]), - true = length(M) >= 1, % Should be at least 1, likely 2 - - % Check call from test process is NOT traced (no new trace line added) - % This is harder to assert definitively without counting lines before/after. - % We rely on the {pid, [..., new]} filter working correctly. - - is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc - ok +trace_handle_call_new_and_custom_registry(__Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + fake_reg:start(), + {ok, NewPid} = gen_statem:start({via, fake_reg, ts_test}, test_statem, [], []), + + recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + [{pid, [{via, fake_reg, ts_test}, new]}, {io_server, FH},{scope,local}]), + + gen_statem:call({via, fake_reg, ts_test}, get_value), + gen_statem:call(NewPid, get_value), + + % Call from old process - should NOT trace + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), + + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(LogFileName), + + + % Check calls from the new process ARE traced + count_trace_match("test_statem:light_state", TraceOutput, 2), + assert_trace_no_match("test_statem:heavy_state", TraceOutput), + + gen_statem:stop({via, fake_reg, ts_test}) + + after + fake_reg:stop(), + file:close(FH) end. %% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) %% Test: Show the result of test_statem:get_state/0 calls: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) -trace_get_state_return_fun(Config) -> - TS = spawn_test_server(), +trace_get_state_return_fun(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try % Ensure state is known (e.g., light) case test_statem:get_state() of - heavy -> test_statem:switch_state(); - light -> ok + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() end, - light = test_statem:get_state(), - + + MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), % Trace the API function test_statem:get_state/1 - recon_trace:calls({test_statem, get_state, fun([_Pid]) -> return_trace() end}, 10, [{io_server, TS},{scope,local}]), + recon_trace:calls({test_statem, get_state, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), - Res = test_statem:get_state(), % Call the function + {ok,light_state, N} = test_statem:get_state(), timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), recon_trace:clear(), + {ok, TraceOutput} = file:read_file(LogFileName), + + % Check for the call and its return value - ExpectedReturn = atom_to_list(Res), % e.g., "light" - assert_trace_match("test_statem:get_state\\(\\) => " ++ ExpectedReturn, TraceOutput), + assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput) + after + file:close(FH) + end, + ok. %% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), %% Test: Show the result of test_statem:get_state/0 calls (using match spec): recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) -trace_get_state_return_matchspec(Config) -> - TS = spawn_test_server(), +trace_get_state_return_matchspec(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try - % Ensure state is known (e.g., heavy) case test_statem:get_state() of - light -> test_statem:switch_state(); - heavy -> ok + {ok,heavy_state,_} -> ok; + {ok,light_state,_} -> test_statem:switch_state() end, - heavy = test_statem:get_state(), % Trace the API function test_statem:get_state/1 using match spec - recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10, [{io_server, TS},{scope,local}]), - - Res = test_statem:get_state(), % Call the function + recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10, [ {io_server, FH},{scope,local}]), + {ok,heavy_state, N} = test_statem:get_state(), timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), recon_trace:clear(), + {ok, TraceOutput} = file:read_file(LogFileName), + + % Check for the call and its return value - ExpectedReturn = atom_to_list(Res), % e.g., "heavy" - assert_trace_match("test_statem:get_state\\(\\) => " ++ ExpectedReturn, TraceOutput), + + assert_trace_match("test_statem:get_state/0 --> {ok,heavy_state,"++integer_to_list(N)++"}", TraceOutput) + after + file:close(FH) + end, + ok. %% Documentation: A short-hand version for this pattern of 'match anything, trace everything' for a function is recon_trace:calls({Mod, Fun, return_trace}). %% Test: Show the result of test_statem:get_state/0 calls (shorthand): recon_trace:calls({test_statem, get_state, return_trace}, 10). -trace_get_state_return_shorthand(Config) -> - TS = spawn_test_server(), +trace_get_state_return_shorthand(_Config) -> + LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", + {ok, FH} = file:open(LogFileName, [write]), + try - % Ensure state is known (e.g., light) case test_statem:get_state() of - heavy -> test_statem:switch_state(); - light -> ok + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() end, - light = test_statem:get_state(), + % Trace the API function test_statem:get_state/1 using shorthand - recon_trace:calls({test_statem, get_state, return_trace}, 10, [{io_server, TS},{scope,local}]), - - Res = test_statem:get_state(), % Call the function + recon_trace:calls({test_statem, get_state, return_trace}, 10, [ {io_server, FH},{scope,local}]), + {ok,light_state, N} = test_statem:get_state(), + timer:sleep(100), - TraceOutput = get_trace_output(TS), - stop_test_server(TS), recon_trace:clear(), + {ok, TraceOutput} = file:read_file(LogFileName), + + % Check for the call and its return value - ExpectedReturn = atom_to_list(Res), % e.g., "light" - assert_trace_match("test_statem:get_state\\(\\) => " ++ ExpectedReturn, TraceOutput), + assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput) + after + file:close(FH) + end, + ok. -dummy_advanced_test(_Config) -> - ct:log("This is a placeholder for an advanced recon_trace test"), - ok. diff --git a/test/test_statem.erl b/test/test_statem.erl index 5ced0c9..9a9a912 100644 --- a/test/test_statem.erl +++ b/test/test_statem.erl @@ -13,7 +13,7 @@ -include_lib("eunit/include/eunit.hrl"). --define(HeavyStateWindowLength, 1000). +-define(HeavyStateWindowLength, 300). %% API -export([start/0, stop/0]). From cb5720462e96a8c0e2d4b2d392cd5b8494f496d0 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 19 Apr 2025 02:54:47 +0200 Subject: [PATCH 03/35] Refactor recon_trace_SUITE to improve test case structure and logging functionality --- test/recon_trace_SUITE.erl | 476 ++++++++++++++----------------------- 1 file changed, 177 insertions(+), 299 deletions(-) diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl index 8a895d6..b3aabb7 100644 --- a/test/recon_trace_SUITE.erl +++ b/test/recon_trace_SUITE.erl @@ -5,33 +5,34 @@ -compile({parse_transform, ms_transform}). -export([ - all/0, groups/0, - init_per_suite/1, end_per_suite/1, - init_per_group/2, end_per_group/2 -]). + all/0, groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2 + ]). -export([ - count_trace_match/3, - assert_trace_match/2, - assert_trace_no_match/2 -]). + count_trace_match/3, + assert_trace_match/2, + assert_trace_no_match/2 + ]). %% Test cases -export([ - dummy_basic_test/1, % Keep original for reference if needed, or remove - trace_all_test_statem_calls/1, - trace_heavy_state_2_calls/1, - trace_heavy_state_2_rate_limited/1, - trace_heavy_state_2_even_arg/1, - trace_iolist_to_binary_with_binary/1, - trace_test_statem_calls_specific_pid/1, - trace_test_statem_calls_arity/1, - trace_test_statem_states_new_procs/1, - trace_handle_call_new_and_custom_registry/1, - trace_get_state_return_fun/1, - trace_get_state_return_matchspec/1, - trace_get_state_return_shorthand/1 -]). + dummy_basic_test/1, % Keep original for reference if needed, or remove + trace_all_test_statem_calls/1, + trace_heavy_state_2_calls/1, + trace_heavy_state_2_rate_limited/1, + trace_heavy_state_2_even_arg/1, + trace_iolist_to_binary_with_binary/1, + trace_test_statem_calls_specific_pid/1, + trace_test_statem_calls_arity/1, + trace_test_statem_states_new_procs/1, + trace_handle_call_new_and_custom_registry/1, + trace_get_state_return_fun/1, + trace_get_state_return_matchspec/1, + trace_get_state_return_shorthand/1 + ]). %%-------------------------------------------------------------------- %% Suite Configuration @@ -44,21 +45,21 @@ all() -> %% @doc Defines the test groups. groups() -> [ - {basic_ops, [sequence], [ - dummy_basic_test, - trace_all_test_statem_calls, - trace_heavy_state_2_calls, - trace_heavy_state_2_rate_limited, - trace_heavy_state_2_even_arg, - trace_iolist_to_binary_with_binary, - trace_test_statem_calls_specific_pid, - trace_test_statem_states_new_procs, - trace_handle_call_new_and_custom_registry, - trace_test_statem_calls_arity, - trace_get_state_return_fun, - trace_get_state_return_matchspec, - trace_get_state_return_shorthand - ]} + {basic_ops, [sequence], [ + dummy_basic_test, + trace_all_test_statem_calls, + trace_heavy_state_2_calls, + trace_heavy_state_2_rate_limited, + trace_heavy_state_2_even_arg, + trace_iolist_to_binary_with_binary, + trace_test_statem_calls_specific_pid, + trace_test_statem_states_new_procs, + trace_handle_call_new_and_custom_registry, + trace_test_statem_calls_arity, + trace_get_state_return_fun, + trace_get_state_return_matchspec, + trace_get_state_return_shorthand + ]} ]. %%-------------------------------------------------------------------- @@ -68,12 +69,13 @@ groups() -> init_per_suite(Config) -> {ok, Pid} = test_statem:start(), ct:log("Starting test_statem process with PID: ~p", [Pid]), - S = test_statem:get_state(), - ct:log("Init per suite state: ~p", [S]), + State = test_statem:get_state(), + ct:log("Init per suite state: ~p", [State]), + ct:log("Init per suite config: ~p", [Config]), Config. -end_per_suite(__Config) -> +end_per_suite(_Config) -> %% Cleanup after all tests run test_statem:stop(). @@ -82,18 +84,18 @@ init_per_group(_GroupName, Config) -> Config. end_per_group(_GroupName, Config) -> - %% Cleanup after each group runs - recon_trace:clear(), % Ensure traces are cleared between groups - Config. -init_per_testcase(_TestName, Config) -> - %% Cleanup before each test runs - recon_trace:clear(), % Ensure traces are cleared between tests Config. +init_per_testcase(TestName, Config) -> + LogFileName = "test_statem_"++atom_to_list(TestName)++".log", + {ok, FH} = file:open(LogFileName, [write]), + [{file, {FH, LogFileName}} | Config]. + end_per_testcase(_TestName, Config) -> - %% Cleanup after each test runs + {FH, _FileName} = proplists:get_value(file, Config), + file:close(FH), recon_trace:clear(), % Ensure traces are cleared between tests - Config. + proplists:delete(file, Config). %%-------------------------------------------------------------------- %% Helper Functions @@ -107,13 +109,13 @@ assert_trace_match(RegexString, TraceOutput) -> end. count_trace_match(RegexString, TraceOutput, ExpCnt) -> ct:log("Counting if ~p matches for ~p in output:~n~p", [ExpCnt, RegexString, TraceOutput]), - + case re:run(TraceOutput, RegexString, [global]) of {match, List} when length(List) == ExpCnt -> ok; {match, List} -> ct:fail({wrong_match_count, RegexString, length(List), ExpCnt}); nomatch -> ct:fail({regex_did_not_match, RegexString}); _ -> ct:fail({unexpected, RegexString, ExpCnt}) - + end. assert_trace_no_match(RegexString, TraceOutput) -> @@ -127,154 +129,104 @@ assert_trace_no_match(RegexString, TraceOutput) -> %% Test Cases %%-------------------------------------------------------------------- -dummy_basic_test(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - recon_trace:calls({test_statem, light_state, '_'}, 10, [{io_server, FH},{scope,local}]), - test_statem:switch_state(), - S = test_statem:get_state(), - ct:log("State: ~p", [S]), - timer:sleep(100), % Allow time for trace message processing - - {ok, TraceOutput} = file:read_file(LogFileName), - assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput) - after - file:close(FH) - end, - +dummy_basic_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + recon_trace:calls({test_statem, light_state, '_'}, 10, [{io_server, FH},{scope,local}]), + test_statem:switch_state(), + S = test_statem:get_state(), + ct:log("State: ~p", [S]), + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), ok. %% Test cases based on https://ferd.github.io/recon/recon_trace.html#calls/3 %% Documentation: All calls from the queue module, with 10 calls printed at most: recon_trace:calls({queue, '_', '_'}, 10) %% Test: All calls from the test_statem module, with 10 calls printed at most: recon_trace:calls({test_statem, '_', '_'}, 10) -trace_all_test_statem_calls(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try +trace_all_test_statem_calls(Config) -> + {FH, FileName} = proplists:get_value(file, Config), recon_trace:calls({test_statem, '_', '_'}, 100, [{io_server, FH},{scope,local}]), timer:sleep(100), ok = test_statem:switch_state(), _ = test_statem:get_state(), timer:sleep(100), - ok = test_statem:switch_state(), % Back to light + ok = test_statem:switch_state(), timer:sleep(100), lists:foreach(fun(_)->test_statem:get_state(), timer:sleep(50) end, lists:seq(1, 7)), % Call get_state multiple times - - {ok, TraceOutput} = file:read_file(LogFileName), - - count_trace_match("test_statem:get_state\\(\\)", TraceOutput,8), % Initial get_state - count_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), % First switch - count_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput,1) % Second switch + {ok, TraceOutput} = file:read_file(FileName), - after - file:close(FH) - end, - + count_trace_match("test_statem:get_state\\(\\)", TraceOutput,8), + count_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), + count_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), ok. %% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace:calls({lists, seq, 2}, {100, 1000}) %% Test: All calls to test_statem:heavy_state(A,B), with 3 calls printed at most: recon_trace:calls({test_statem, heavy_state, 2}, 10) -trace_heavy_state_2_calls(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - - % Ensure we are in heavy state first +trace_heavy_state_2_calls(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + %% Ensure we are in heavy state first case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); {ok,heavy_state,_} -> ok end, - - recon_trace:calls({test_statem, heavy_state, 3}, 3, [ {io_server, FH},{scope,local}]), + recon_trace:calls({test_statem, heavy_state, 3}, 3, [ {io_server, FH},{scope,local}]), timer:sleep(2000), recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - - + {ok, TraceOutput} = file:read_file(FileName), + count_trace_match("test_statem:heavy_state", TraceOutput, 3), %% Ensure light_state wasn't traced - assert_trace_no_match("test_statem:light_state", TraceOutput) - - after - file:close(FH) - end, - + assert_trace_no_match("test_statem:light_state", TraceOutput), ok. %% Documentation: All calls to lists:seq(A,B,2) (all sequences increasing by two) with 100 calls at most: recon_trace:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) %% Test: All calls to test_statem:heavy_state, with 1 calls per second at most: recon_trace:calls({test_statem, heavy_state, 3}, {1, 1000}) -trace_heavy_state_2_rate_limited(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - +trace_heavy_state_2_rate_limited(Config) -> + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); {ok,heavy_state,_} -> ok end, - + recon_trace:calls({test_statem, heavy_state, 3}, {1, 1000}, [ {io_server, FH},{scope,local}]), - + timer:sleep(2200), % Allow more time for potential rate limiting delays recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - - - % Check that at least one trace occurred - count_trace_match("test_statem:heavy_state", TraceOutput, 2) - % Asserting the exact number is difficult due to timing, but it should be <= 10 - % We can count occurrences if needed, but matching one is a basic check. - after - - file:close(FH) - end, - + {ok, TraceOutput} = file:read_file(FileName), + + count_trace_match("test_statem:heavy_state", TraceOutput, 2), ok. %% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: recon_trace:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) -trace_heavy_state_2_even_arg(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - - % Ensure we are in heavy state first and iterator is 0 (even) +trace_heavy_state_2_even_arg(Config) -> + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); {ok,heavy_state,_} -> ok - + end, MatchSpec = dbg:fun2ms(fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace() end), recon_trace:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH},{scope,local}]), - - timer:sleep(1900), recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - - count_trace_match("test_statem:heavy_state\\(timeout", TraceOutput, 3) - after - - file:close(FH) - end, - - + {ok, TraceOutput} = file:read_file(FileName), + + count_trace_match("test_statem:heavy_state\\(timeout", TraceOutput, 3), ok. %% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) %% Test: All calls to iolist_to_binary/1 made with a binary argument: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -trace_iolist_to_binary_with_binary(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - +trace_iolist_to_binary_with_binary(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), @@ -284,28 +236,21 @@ trace_iolist_to_binary_with_binary(_Config) -> _ = erlang:iolist_to_binary(<<"another binary">>), % Should trace timer:sleep(100), - {ok, TraceOutput} = file:read_file(LogFileName), - + {ok, TraceOutput} = file:read_file(FileName), + recon_trace:clear(), assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), assert_trace_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), - assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput) - after - - file:close(FH) - end, - + assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), ok. %% Documentation: Calls to the queue module only in a given process Pid, at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) %% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most: recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, }]) -trace_test_statem_calls_specific_pid(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - +trace_test_statem_calls_specific_pid(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + %%default statem state is heavy_state case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); @@ -313,44 +258,31 @@ trace_test_statem_calls_specific_pid(_Config) -> end, %% new statem in light state {ok, Pid} = gen_statem:start(test_statem, [], []), - + recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}, {io_server, FH},{scope,local}]), gen_statem:call(Pid, get_value), gen_statem:call(Pid, get_value), - - % Call from another process - should NOT trace + %% Call from another process - should NOT trace test_statem:get_state(), test_statem:get_state(), test_statem:get_state(), timer:sleep(100), recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - - - % Check calls originating from are traced (e.g., handle_event) + {ok, TraceOutput} = file:read_file(FileName), + %% Check calls originating from are traced (e.g., handle_event) count_trace_match(".*test_statem:light_state", TraceOutput,2), - - % Check calls from the other process are NOT traced + %% Check calls from the other process are NOT traced assert_trace_no_match(".*test_statem:heavy_state", TraceOutput), - - is_process_alive(Pid) andalso exit(Pid, kill) % Cleanup spawned proc - - after - - file:close(FH) - end, - + is_process_alive(Pid) andalso exit(Pid, kill), % Cleanup spawned proc ok. %% Documentation: Print the traces with the function arity instead of literal arguments: recon_trace:calls(TSpec, Max, [{args, arity}]) %% Test: Print traces for test_statem calls with arity instead of arguments: recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}]) -trace_test_statem_calls_arity(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - +trace_test_statem_calls_arity(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, FH},{scope,local}]), test_statem:get_state(), @@ -358,121 +290,94 @@ trace_test_statem_calls_arity(_Config) -> ok = test_statem:switch_state(), timer:sleep(100), recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - - - - % Check for arity format, e.g., module:function/arity + {ok, TraceOutput} = file:read_file(FileName), + %% Check for arity format, e.g., module:function/arity assert_trace_match("test_statem:get_state/0", TraceOutput), % gen_statem callback arity assert_trace_match("test_statem:light_state/3", TraceOutput), % gen_statem callback arity - % Ensure literal args are not present (tricky regex, check absence of typical args) + %% Ensure literal args are not present (tricky regex, check absence of typical args) assert_trace_no_match("switch_state\\(", TraceOutput), - assert_trace_no_match("iterator", TraceOutput) - after - - file:close(FH) - end, - + assert_trace_no_match("iterator", TraceOutput), ok. %% Documentation: Matching the filter/2 functions of both dict and lists modules, across new processes only: recon_trace:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) %% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only: recon_trace:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) -trace_test_statem_states_new_procs(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try +trace_test_statem_states_new_procs(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of - {ok,light_state,_} -> test_statem:switch_state(); - {ok,heavy_state,_} -> ok + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok end, - + recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, [{pid, new}, {io_server, FH},{scope,local}]), - + {ok, heavy_state,_} = test_statem:get_state(), -%% Call from a *new* process - should trace + %% Call from a *new* process - should trace {ok, NewPid} = gen_statem:start(test_statem, [], []), - + gen_statem:call(NewPid, get_value), gen_statem:call(NewPid, get_value), - % Call from old process - should NOT trace + %% Call from old process - should NOT trace test_statem:get_state(), test_statem:get_state(), test_statem:get_state(), - timer:sleep(1000), recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - - - % Check calls from the new process ARE traced + {ok, TraceOutput} = file:read_file(FileName), + %%Check calls from the new process ARE traced count_trace_match("test_statem:light_state", TraceOutput, 2), - assert_trace_no_match("test_statem:heavy_state", TraceOutput), - - is_process_alive(NewPid) andalso exit(NewPid, kill) % Cleanup spawned proc - after - file:close(FH) - end, - + assert_trace_no_match("test_statem:heavy_state", TraceOutput), + is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc ok. %% Documentation: Tracing the handle_call/3 functions of a given module for all new processes, and those of an existing one registered with gproc: recon_trace:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) %% Test: Tracing test_statem:handle_call/3 for new processes and one via gproc (requires gproc setup): recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, gproc, Name}, new]}]) -trace_handle_call_new_and_custom_registry(__Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - case test_statem:get_state() of - {ok,light_state,_} -> test_statem:switch_state(); - {ok,heavy_state,_} -> ok - end, - fake_reg:start(), - {ok, NewPid} = gen_statem:start({via, fake_reg, ts_test}, test_statem, [], []), - - recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, - [{pid, [{via, fake_reg, ts_test}, new]}, {io_server, FH},{scope,local}]), - - gen_statem:call({via, fake_reg, ts_test}, get_value), - gen_statem:call(NewPid, get_value), - - % Call from old process - should NOT trace - test_statem:get_state(), - test_statem:get_state(), - test_statem:get_state(), - - timer:sleep(100), - recon_trace:clear(), - - {ok, TraceOutput} = file:read_file(LogFileName), - - - % Check calls from the new process ARE traced - count_trace_match("test_statem:light_state", TraceOutput, 2), - assert_trace_no_match("test_statem:heavy_state", TraceOutput), - - gen_statem:stop({via, fake_reg, ts_test}) - - after - fake_reg:stop(), - file:close(FH) +trace_handle_call_new_and_custom_registry(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + try + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + fake_reg:start(), + {ok, NewPid} = gen_statem:start({via, fake_reg, ts_test}, test_statem, [], []), + + recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + [{pid, [{via, fake_reg, ts_test}, new]}, {io_server, FH},{scope,local}]), + + gen_statem:call({via, fake_reg, ts_test}, get_value), + gen_statem:call(NewPid, get_value), + + %% Call from old process - should NOT trace + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + %% Check calls from the new process ARE traced + count_trace_match("test_statem:light_state", TraceOutput, 2), + assert_trace_no_match("test_statem:heavy_state", TraceOutput) + after + gen_statem:stop({via, fake_reg, ts_test}), + fake_reg:stop(), + file:close(FH) end. %% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) %% Test: Show the result of test_statem:get_state/0 calls: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) -trace_get_state_return_fun(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - - % Ensure state is known (e.g., light) +trace_get_state_return_fun(Config) -> + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> ok; {ok,heavy_state,_} -> test_statem:switch_state() end, - + MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), - % Trace the API function test_statem:get_state/1 + recon_trace:calls({test_statem, get_state, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), {ok,light_state, N} = test_statem:get_state(), @@ -480,78 +385,51 @@ trace_get_state_return_fun(_Config) -> timer:sleep(100), recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - - - % Check for the call and its return value - assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput) - after - file:close(FH) - end, - + {ok, TraceOutput} = file:read_file(FileName), + %% Check for the call and its return value + assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), ok. %% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), %% Test: Show the result of test_statem:get_state/0 calls (using match spec): recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) -trace_get_state_return_matchspec(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - +trace_get_state_return_matchspec(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of {ok,heavy_state,_} -> ok; {ok,light_state,_} -> test_statem:switch_state() end, - - % Trace the API function test_statem:get_state/1 using match spec + %% Trace the API function test_statem:get_state/1 using match spec recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10, [ {io_server, FH},{scope,local}]), {ok,heavy_state, N} = test_statem:get_state(), timer:sleep(100), recon_trace:clear(), + {ok, TraceOutput} = file:read_file(FileName), - {ok, TraceOutput} = file:read_file(LogFileName), - - - % Check for the call and its return value - - assert_trace_match("test_statem:get_state/0 --> {ok,heavy_state,"++integer_to_list(N)++"}", TraceOutput) - after - file:close(FH) - end, - + %% Check for the call and its return value + assert_trace_match("test_statem:get_state/0 --> {ok,heavy_state,"++integer_to_list(N)++"}", TraceOutput), ok. %% Documentation: A short-hand version for this pattern of 'match anything, trace everything' for a function is recon_trace:calls({Mod, Fun, return_trace}). %% Test: Show the result of test_statem:get_state/0 calls (shorthand): recon_trace:calls({test_statem, get_state, return_trace}, 10). -trace_get_state_return_shorthand(_Config) -> - LogFileName = "test_statem_"++atom_to_list(?FUNCTION_NAME)++".log", - {ok, FH} = file:open(LogFileName, [write]), - try - +trace_get_state_return_shorthand(Config) -> + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> ok; {ok,heavy_state,_} -> test_statem:switch_state() end, - - - % Trace the API function test_statem:get_state/1 using shorthand + %% Trace the API function test_statem:get_state/1 using shorthand recon_trace:calls({test_statem, get_state, return_trace}, 10, [ {io_server, FH},{scope,local}]), {ok,light_state, N} = test_statem:get_state(), - + timer:sleep(100), recon_trace:clear(), - {ok, TraceOutput} = file:read_file(LogFileName), - + {ok, TraceOutput} = file:read_file(FileName), - % Check for the call and its return value - assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput) - after - file:close(FH) - end, - + %% Check for the call and its return value + assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), ok. - From eaeb820906572e71a8661418084bc202850cf704 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 19 Apr 2025 14:31:07 +0200 Subject: [PATCH 04/35] cleaning up and describing recon_trace_SUITE --- test/recon_trace_SUITE.erl | 255 ++++++++++++++++++++++++------------- 1 file changed, 168 insertions(+), 87 deletions(-) diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl index b3aabb7..8cb9fca 100644 --- a/test/recon_trace_SUITE.erl +++ b/test/recon_trace_SUITE.erl @@ -1,3 +1,11 @@ +%%%------------------------------------------------------------------- +%%% @author 2023, Mathias Green +%%% @doc +%%% Common Test Suite for recon_trace functionality. +%%% Tests various scenarios based on the recon_trace documentation. +%%% @end +%%%------------------------------------------------------------------- + -module(recon_trace_SUITE). -include_lib("common_test/include/ct.hrl"). @@ -19,19 +27,19 @@ %% Test cases -export([ - dummy_basic_test/1, % Keep original for reference if needed, or remove - trace_all_test_statem_calls/1, - trace_heavy_state_2_calls/1, - trace_heavy_state_2_rate_limited/1, - trace_heavy_state_2_even_arg/1, - trace_iolist_to_binary_with_binary/1, - trace_test_statem_calls_specific_pid/1, - trace_test_statem_calls_arity/1, - trace_test_statem_states_new_procs/1, - trace_handle_call_new_and_custom_registry/1, - trace_get_state_return_fun/1, - trace_get_state_return_matchspec/1, - trace_get_state_return_shorthand/1 + dummy_basic_test/1, + trace_full_module_test/1, + trace_one_function_test/1, + trace_rate_limit_test/1, + trace_even_arg_test/1, + trace_iolist_to_binary_with_binary_test/1, + trace_specific_pid_test/1, + trace_arity_test/1, + trace_spec_list_new_procs_only_test/1, + trace_handle_call_new_and_custom_registry_test/1, + trace_return_shellfun_test/1, + trace_return_matchspec_test/1, + trace_return_shorthand_test/1 ]). %%-------------------------------------------------------------------- @@ -40,26 +48,29 @@ %% @doc Returns list of test cases and/or groups to be executed. all() -> - [{group, basic_ops}]. + [{group, basic_test}, {group, doc_based_test}]. %% @doc Defines the test groups. groups() -> [ - {basic_ops, [sequence], [ - dummy_basic_test, - trace_all_test_statem_calls, - trace_heavy_state_2_calls, - trace_heavy_state_2_rate_limited, - trace_heavy_state_2_even_arg, - trace_iolist_to_binary_with_binary, - trace_test_statem_calls_specific_pid, - trace_test_statem_states_new_procs, - trace_handle_call_new_and_custom_registry, - trace_test_statem_calls_arity, - trace_get_state_return_fun, - trace_get_state_return_matchspec, - trace_get_state_return_shorthand - ]} + {basic_test, [sequence], [ + dummy_basic_test + ]}, + {doc_based_test, [sequence], [ + trace_full_module_test, + trace_one_function_test, + trace_rate_limit_test, + trace_even_arg_test, + trace_iolist_to_binary_with_binary_test, + trace_specific_pid_test, + trace_arity_test, + trace_spec_list_new_procs_only_test, + trace_handle_call_new_and_custom_registry_test, + trace_return_shellfun_test, + trace_return_matchspec_test, + trace_return_shorthand_test + ] + } ]. %%-------------------------------------------------------------------- @@ -140,10 +151,21 @@ dummy_basic_test(Config) -> assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), ok. +%%====================================================================== +%% --------------------------------------------------------------------------- %% Test cases based on https://ferd.github.io/recon/recon_trace.html#calls/3 -%% Documentation: All calls from the queue module, with 10 calls printed at most: recon_trace:calls({queue, '_', '_'}, 10) -%% Test: All calls from the test_statem module, with 10 calls printed at most: recon_trace:calls({test_statem, '_', '_'}, 10) -trace_all_test_statem_calls(Config) -> +%% --------------------------------------------------------------------------- +%%====================================================================== + +%%====================================================================== +%% Documentation: All calls from the queue module, with 10 calls printed at most: +%% recon_trace:calls({queue, '_', '_'}, 10) +%%--- +%% Test: All calls from the test_statem module, with 10 calls printed at most. +%%--- +%% Function: recon_trace:calls({test_statem, '_', '_'}, 10) +%%====================================================================== +trace_full_module_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), recon_trace:calls({test_statem, '_', '_'}, 100, [{io_server, FH},{scope,local}]), @@ -161,32 +183,39 @@ trace_all_test_statem_calls(Config) -> count_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), count_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), ok. - -%% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace:calls({lists, seq, 2}, {100, 1000}) -%% Test: All calls to test_statem:heavy_state(A,B), with 3 calls printed at most: recon_trace:calls({test_statem, heavy_state, 2}, 10) -trace_heavy_state_2_calls(Config) -> +%%====================================================================== +%% Documentation: All calls to lists:seq(A,B), with 100 calls printed at most: recon_trace:calls({lists, seq, 2}, 100) +%%--- +%% Test: All calls from the test_statem:get_state module, with 10 calls printed at most. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10) +%%====================================================================== +trace_one_function_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), + recon_trace:calls({test_statem, get_state, 0}, 100, [{io_server, FH},{scope,local}]), - %% Ensure we are in heavy state first - case test_statem:get_state() of - {ok,light_state,_} -> test_statem:switch_state(); - {ok,heavy_state,_} -> ok - end, - - recon_trace:calls({test_statem, heavy_state, 3}, 3, [ {io_server, FH},{scope,local}]), - timer:sleep(2000), - recon_trace:clear(), + timer:sleep(100), + ok = test_statem:switch_state(), + _ = test_statem:get_state(), + timer:sleep(100), + ok = test_statem:switch_state(), + timer:sleep(100), + lists:foreach(fun(_)->test_statem:get_state(), timer:sleep(50) end, lists:seq(1, 7)), % Call get_state multiple times {ok, TraceOutput} = file:read_file(FileName), - count_trace_match("test_statem:heavy_state", TraceOutput, 3), - %% Ensure light_state wasn't traced - assert_trace_no_match("test_statem:light_state", TraceOutput), + count_trace_match("test_statem:get_state\\(\\)", TraceOutput,8), ok. -%% Documentation: All calls to lists:seq(A,B,2) (all sequences increasing by two) with 100 calls at most: recon_trace:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) -%% Test: All calls to test_statem:heavy_state, with 1 calls per second at most: recon_trace:calls({test_statem, heavy_state, 3}, {1, 1000}) -trace_heavy_state_2_rate_limited(Config) -> +%%====================================================================== +%% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace:calls({lists, seq, 2}, {100, 1000}) +%%--- +%% Test: All calls to test_statem:heavy_state(A,B), with 1 call per second printed at most: +%%--- +%% Function: recon_trace:calls({test_statem, heavy_state, 2}, 10) +%%====================================================================== + +trace_rate_limit_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of @@ -204,8 +233,16 @@ trace_heavy_state_2_rate_limited(Config) -> count_trace_match("test_statem:heavy_state", TraceOutput, 2), ok. -%% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: recon_trace:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) -trace_heavy_state_2_even_arg(Config) -> +%%====================================================================== +%% Documentation: All calls to lists:seq(A,B,2) (all sequences increasing by two) with 100 calls at most: +%% recon_trace:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) +%%--- +%% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: +%%--- +%% Function: recon_trace:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) +%%====================================================================== + +trace_even_arg_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); @@ -221,10 +258,18 @@ trace_heavy_state_2_even_arg(Config) -> count_trace_match("test_statem:heavy_state\\(timeout", TraceOutput, 3), ok. - -%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -%% Test: All calls to iolist_to_binary/1 made with a binary argument: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -trace_iolist_to_binary_with_binary(Config) -> +%%====================================================================== +%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): +%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% Test: All calls to iolist_to_binary/1 made with a binary argument. +%%--- +%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell +%%====================================================================== + +trace_iolist_to_binary_with_binary_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), @@ -246,9 +291,15 @@ trace_iolist_to_binary_with_binary(Config) -> assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), ok. -%% Documentation: Calls to the queue module only in a given process Pid, at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) -%% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most: recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, }]) -trace_test_statem_calls_specific_pid(Config) -> +%%====================================================================== +%% Documentation: Calls to the queue module only in a given process Pid, +%% at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) +%%--- +%% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most. +%%--- +%% Function: recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}]) +%%====================================================================== +trace_specific_pid_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), %%default statem state is heavy_state @@ -278,9 +329,15 @@ trace_test_statem_calls_specific_pid(Config) -> is_process_alive(Pid) andalso exit(Pid, kill), % Cleanup spawned proc ok. -%% Documentation: Print the traces with the function arity instead of literal arguments: recon_trace:calls(TSpec, Max, [{args, arity}]) -%% Test: Print traces for test_statem calls with arity instead of arguments: recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}]) -trace_test_statem_calls_arity(Config) -> +%%====================================================================== +%% Documentation: Print the traces with the function arity instead of literal arguments: +%% recon_trace:calls(TSpec, Max, [{args, arity}]) +%%--- +%% Test: Print traces for test_statem calls with arity instead of arguments. +%%--- +%% Function: recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}]) +%%====================================================================== +trace_arity_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, FH},{scope,local}]), @@ -294,14 +351,20 @@ trace_test_statem_calls_arity(Config) -> %% Check for arity format, e.g., module:function/arity assert_trace_match("test_statem:get_state/0", TraceOutput), % gen_statem callback arity assert_trace_match("test_statem:light_state/3", TraceOutput), % gen_statem callback arity - %% Ensure literal args are not present (tricky regex, check absence of typical args) + %% Ensure literal args are not present assert_trace_no_match("switch_state\\(", TraceOutput), assert_trace_no_match("iterator", TraceOutput), ok. -%% Documentation: Matching the filter/2 functions of both dict and lists modules, across new processes only: recon_trace:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) -%% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only: recon_trace:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) -trace_test_statem_states_new_procs(Config) -> +%%====================================================================== +%% Documentation: Matching the filter/2 functions of both dict and lists modules, across new processes only: +%% recon_trace:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) +%%--- +%% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only. +%%--- +%% Function: recon_trace:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) +%%====================================================================== +trace_spec_list_new_procs_only_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of @@ -326,15 +389,20 @@ trace_test_statem_states_new_procs(Config) -> recon_trace:clear(), {ok, TraceOutput} = file:read_file(FileName), - %%Check calls from the new process ARE traced + %% Check calls from the new process ARE traced count_trace_match("test_statem:light_state", TraceOutput, 2), assert_trace_no_match("test_statem:heavy_state", TraceOutput), is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc ok. - -%% Documentation: Tracing the handle_call/3 functions of a given module for all new processes, and those of an existing one registered with gproc: recon_trace:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) -%% Test: Tracing test_statem:handle_call/3 for new processes and one via gproc (requires gproc setup): recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, gproc, Name}, new]}]) -trace_handle_call_new_and_custom_registry(Config) -> +%%====================================================================== +%% Documentation: Tracing the handle_call/3 functions of a given module for all new processes, and those of an existing one registered with gproc: +%% recon_trace:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) +%%--- +%% Test: Tracing test_statem for new processes and one via custom process register. +%%--- +%% Function: recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, fake_reg, ts_test}, new]}]) +%%====================================================================== +trace_handle_call_new_and_custom_registry_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), try case test_statem:get_state() of @@ -363,13 +431,18 @@ trace_handle_call_new_and_custom_registry(Config) -> assert_trace_no_match("test_statem:heavy_state", TraceOutput) after gen_statem:stop({via, fake_reg, ts_test}), - fake_reg:stop(), - file:close(FH) + fake_reg:stop() end. - +%%====================================================================== %% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) -%% Test: Show the result of test_statem:get_state/0 calls: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) -trace_get_state_return_fun(Config) -> +%%--- +%% Test: Show the result of test_statem:get_state/0 calls. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) +%%--- +%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell +%%====================================================================== +trace_return_shellfun_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> ok; @@ -389,10 +462,15 @@ trace_get_state_return_fun(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), ok. - -%% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), -%% Test: Show the result of test_statem:get_state/0 calls (using match spec): recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) -trace_get_state_return_matchspec(Config) -> +%%====================================================================== +%% Documentation: Show the result of a given function call: +%% recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), +%%--- +%% Test: Show the result of test_statem:get_state/0 calls (using match spec). +%%--- +%% Function: recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) +%%====================================================================== +trace_return_matchspec_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of @@ -410,10 +488,15 @@ trace_get_state_return_matchspec(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state/0 --> {ok,heavy_state,"++integer_to_list(N)++"}", TraceOutput), ok. - -%% Documentation: A short-hand version for this pattern of 'match anything, trace everything' for a function is recon_trace:calls({Mod, Fun, return_trace}). -%% Test: Show the result of test_statem:get_state/0 calls (shorthand): recon_trace:calls({test_statem, get_state, return_trace}, 10). -trace_get_state_return_shorthand(Config) -> +%%====================================================================== +%% Documentation: A short-hand version for this pattern of 'match anything, trace everything' +%% for a function is recon_trace:calls({Mod, Fun, return_trace}). +%%--- +%% Test: Show the result of test_statem:get_state/0 calls (shorthand). +%%--- +%% Function: recon_trace:calls({test_statem, get_state, return_trace}, 10). +%%====================================================================== +trace_return_shorthand_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> ok; @@ -421,9 +504,7 @@ trace_get_state_return_shorthand(Config) -> end, %% Trace the API function test_statem:get_state/1 using shorthand recon_trace:calls({test_statem, get_state, return_trace}, 10, [ {io_server, FH},{scope,local}]), - {ok,light_state, N} = test_statem:get_state(), - timer:sleep(100), recon_trace:clear(), From 32d3781fba6605b94ccdf5709f007d598879df8e Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Mon, 28 Apr 2025 10:14:45 +0200 Subject: [PATCH 05/35] frame for recon trace use dbg development --- src/recon_trace_use_dbg.erl | 496 +++++++++++++++++++++++++++ test/recon_trace_use_dbg_SUITE.erl | 516 +++++++++++++++++++++++++++++ 2 files changed, 1012 insertions(+) create mode 100644 src/recon_trace_use_dbg.erl create mode 100644 test/recon_trace_use_dbg_SUITE.erl diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl new file mode 100644 index 0000000..2e08ef2 --- /dev/null +++ b/src/recon_trace_use_dbg.erl @@ -0,0 +1,496 @@ +%%% @author Fred Hebert +%%% [http://ferd.ca/] +%%% @doc +%%% `recon_trace' is a module that handles tracing in a safe manner for single +%%% Erlang nodes, currently for function calls only. Functionality includes: +%%% @end +-module(recon_trace_use_dbg). + +%% API +-export([clear/0, calls/2, calls/3]). + +-export([format/1]). + +-record(trace, {pid, type, mfa}). + + +%% Internal exports +-export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]). + +-type matchspec() :: [{[term()] | '_', [term()], [term()]}]. +-type shellfun() :: fun((_) -> term()). +-type formatterfun() :: fun((_) -> iodata()). +-type millisecs() :: non_neg_integer(). +-type pidspec() :: all | existing | new | recon:pid_term(). +-type max_traces() :: non_neg_integer(). +-type max_rate() :: {max_traces(), millisecs()}. + + %% trace options +-type options() :: [ {pid, pidspec() | [pidspec(),...]} % default: all + | {timestamp, formatter | trace} % default: formatter + | {args, args | arity} % default: args + | {io_server, pid() | atom()} % default: group_leader() + | {formatter, formatterfun()} % default: internal formatter + | return_to | {return_to, boolean()} % default: false + %% match pattern options + | {scope, global | local} % default: global + ]. + +-type mod() :: '_' | module(). +-type fn() :: '_' | atom(). +-type args() :: '_' | 0..255 | return_trace | matchspec() | shellfun(). +-type tspec() :: {mod(), fn(), args()}. +-type max() :: max_traces() | max_rate(). +-type num_matches() :: non_neg_integer(). + +-export_type([mod/0, fn/0, args/0, tspec/0, num_matches/0, options/0, + max_traces/0, max_rate/0]). + +%%%%%%%%%%%%%% +%%% PUBLIC %%% +%%%%%%%%%%%%%% + +%% @doc Stops all tracing at once. +-spec clear() -> ok. +clear() -> + dbg:p(all,clear), + dbg:ctp('_'), + dbg:stop(), + erlang:trace(all, false, [all]), + erlang:trace_pattern({'_','_','_'}, false, [local,meta,call_count,call_time]), + erlang:trace_pattern({'_','_','_'}, false, []), % unsets global + maybe_kill(recon_trace_tracer), + maybe_kill(recon_trace_formatter), + maybe_kill(recon_trace_use_dbg), + ok. + +%% @equiv calls({Mod, Fun, Args}, Max, []) +-spec calls(tspec() | [tspec(),...], max()) -> num_matches(). +calls({Mod, Fun, Args}, Max) -> + calls([{Mod,Fun,Args}], Max, []); +calls(TSpecs = [_|_], Max) -> + calls(TSpecs, Max, []). + + +%% @doc Allows to set trace patterns and pid specifications to trace +%% function calls. +%% +%% @end +-spec calls(tspec() | [tspec(),...], max(), options()) -> num_matches(). + + +calls({Mod, Fun, Args}, Max, Opts) -> + calls([{Mod,Fun,Args}], Max, Opts); +calls(TSpecs, Max, Opts) -> + case proplists:get_bool(use_dbg, Opts) of + true -> calls_dbg(TSpecs, Max, Opts); + _ -> calls_tracer(TSpecs, Max, Opts) + end. + +calls_tracer(TSpecs = [_|_], {Max, Time}, Opts) -> + Pid = setup(rate_tracer, [Max, Time], + validate_formatter(Opts), validate_io_server(Opts)), + trace_calls(TSpecs, Pid, Opts); +calls_tracer(TSpecs = [_|_], Max, Opts) -> + Pid = setup(count_tracer, [Max], + validate_formatter(Opts), validate_io_server(Opts)), + trace_calls(TSpecs, Pid, Opts). + +calls_dbg(TSpecs = [_|_], {Max, Time}, Opts) -> + Pid = setup(rate_tracer, [Max, Time], + validate_formatter(Opts), validate_io_server(Opts)), + trace_calls(TSpecs, Pid, Opts); +calls_dbg(TSpecs = [_|_], Max, Opts) -> + Pid = setup(count_tracer, [Max], + validate_formatter(Opts), validate_io_server(Opts)), + trace_calls(TSpecs, Pid, Opts). + +%%%%%%%%%%%%%%%%%%%%%%% +%%% PRIVATE EXPORTS %%% +%%%%%%%%%%%%%%%%%%%%%%% +%% @private Stops when N trace messages have been received +count_tracer(0) -> + exit(normal); +count_tracer(N) -> + receive + Msg -> + recon_trace_formatter ! Msg, + count_tracer(N-1) + end. + +%% @private Stops whenever the trace message rates goes higher than +%% `Max' messages in `Time' milliseconds. Note that if the rate +%% proposed is higher than what the IO system of the formatter +%% can handle, this can still put a node at risk. +%% +%% It is recommended to try stricter rates to begin with. +rate_tracer(Max, Time) -> rate_tracer(Max, Time, 0, os:timestamp()). + +rate_tracer(Max, Time, Count, Start) -> + receive + Msg -> + recon_trace_formatter ! Msg, + Now = os:timestamp(), + Delay = timer:now_diff(Now, Start) div 1000, + if Delay > Time -> rate_tracer(Max, Time, 0, Now) + ; Max > Count -> rate_tracer(Max, Time, Count+1, Start) + ; Max =:= Count -> exit(normal) + end + end. + +%% @private Formats traces to be output +formatter(Tracer, Parent, Ref, FormatterFun, IOServer) -> + process_flag(trap_exit, true), + link(Tracer), + Parent ! {Ref, linked}, + formatter(Tracer, IOServer, FormatterFun). + +formatter(Tracer, IOServer, FormatterFun) -> + receive + {'EXIT', Tracer, normal} -> + io:format("Recon tracer rate limit tripped.~n"), + exit(normal); + {'EXIT', Tracer, Reason} -> + exit(Reason); + TraceMsg -> + case FormatterFun(TraceMsg) of + "" -> ok; + Formatted -> io:format(IOServer, Formatted, []) + end, + formatter(Tracer, IOServer, FormatterFun) + end. + + +%%%%%%%%%%%%%%%%%%%%%%% +%%% SETUP FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%% + +%% starts the tracer and formatter processes, and +%% cleans them up before each call. +setup(TracerFun, TracerArgs, FormatterFun, IOServer) -> + clear(), + Ref = make_ref(), + Tracer = spawn_link(?MODULE, TracerFun, TracerArgs), + register(recon_trace_tracer, Tracer), + Format = spawn(?MODULE, formatter, [Tracer, self(), Ref, FormatterFun, IOServer]), + register(recon_trace_formatter, Format), + receive + {Ref, linked} -> Tracer + after 5000 -> + error(setup_failed) + end. + +%% Sets the traces in action +trace_calls(TSpecs, Pid, Opts) -> + {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), + Matches = [begin + {Arity, Spec} = validate_tspec(Mod, Fun, Args), + erlang:trace_pattern({Mod, Fun, Arity}, Spec, MatchOpts) + end || {Mod, Fun, Args} <- TSpecs], + [erlang:trace(PidSpec, true, [call, {tracer, Pid} | TraceOpts]) + || PidSpec <- PidSpecs], + lists:sum(Matches). + + +%%%%%%%%%%%%%%%%%% +%%% VALIDATION %%% +%%%%%%%%%%%%%%%%%% + +validate_opts(Opts) -> + PidSpecs = validate_pid_specs(proplists:get_value(pid, Opts, all)), + Scope = proplists:get_value(scope, Opts, global), + TraceOpts = case proplists:get_value(timestamp, Opts, formatter) of + formatter -> []; + trace -> [timestamp] + end ++ + case proplists:get_value(args, Opts, args) of + args -> []; + arity -> [arity] + end ++ + case proplists:get_value(return_to, Opts, undefined) of + true when Scope =:= local -> + [return_to]; + true when Scope =:= global -> + io:format("Option return_to only works with option {scope, local}~n"), + %% Set it anyway + [return_to]; + _ -> + [] + end, + MatchOpts = [Scope], + {PidSpecs, TraceOpts, MatchOpts}. + +%% Support the regular specs, but also allow `recon:pid_term()' and lists +%% of further pid specs. +-spec validate_pid_specs(pidspec() | [pidspec(),...]) -> + [all | new | existing | pid(), ...]. +validate_pid_specs(all) -> [all]; +validate_pid_specs(existing) -> [existing]; +validate_pid_specs(new) -> [new]; +validate_pid_specs([Spec]) -> validate_pid_specs(Spec); +validate_pid_specs(PidTerm = [Spec|Rest]) -> + %% can be "" or [pidspec()] + try + [recon_lib:term_to_pid(PidTerm)] + catch + error:function_clause -> + validate_pid_specs(Spec) ++ validate_pid_specs(Rest) + end; +validate_pid_specs(PidTerm) -> + %% has to be `recon:pid_term()'. + [recon_lib:term_to_pid(PidTerm)]. + +validate_tspec(Mod, Fun, Args) when is_function(Args) -> + validate_tspec(Mod, Fun, fun_to_ms(Args)); +%% helper to save typing for common actions +validate_tspec(Mod, Fun, return_trace) -> + validate_tspec(Mod, Fun, [{'_', [], [{return_trace}]}]); +validate_tspec(Mod, Fun, Args) -> + BannedMods = ['_', ?MODULE, io, lists], + %% The banned mod check can be bypassed by using + %% match specs if you really feel like being dumb. + case {lists:member(Mod, BannedMods), Args} of + {true, '_'} -> error({dangerous_combo, {Mod,Fun,Args}}); + {true, []} -> error({dangerous_combo, {Mod,Fun,Args}}); + _ -> ok + end, + case Args of + '_' -> {'_', true}; + _ when is_list(Args) -> {'_', Args}; + _ when Args >= 0, Args =< 255 -> {Args, true} + end. + +validate_formatter(Opts) -> + case proplists:get_value(formatter, Opts) of + F when is_function(F, 1) -> F; + _ -> fun format/1 + end. + +validate_io_server(Opts) -> + proplists:get_value(io_server, Opts, group_leader()). + +%%%%%%%%%%%%%%%%%%%%%%%% +%%% TRACE FORMATTING %%% +%%%%%%%%%%%%%%%%%%%%%%%% +%% Thanks Geoff Cant for the foundations for this. +format(TraceMsg) -> + {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), + {FormatStr, FormatArgs} = case {Type, TraceInfo} of + %% {trace, Pid, 'receive', Msg} + {'receive', [Msg]} -> + {"< ~p", [Msg]}; + %% {trace, Pid, send, Msg, To} + {send, [Msg, To]} -> + {" > ~p: ~p", [To, Msg]}; + %% {trace, Pid, send_to_non_existing_process, Msg, To} + {send_to_non_existing_process, [Msg, To]} -> + {" > (non_existent) ~p: ~p", [To, Msg]}; + %% {trace, Pid, call, {M, F, Args}} + {call, [{M,F,Args}]} -> + {"~p:~p~s", [M,F,format_args(Args)]}; + %% {trace, Pid, call, {M, F, Args}, Msg} + {call, [{M,F,Args}, Msg]} -> + {"~p:~p~s ~s", [M,F,format_args(Args), format_trace_output(Msg)]}; + %% {trace, Pid, return_to, {M, F, Arity}} + {return_to, [{M,F,Arity}]} -> + {" '--> ~p:~p/~p", [M,F,Arity]}; + %% {trace, Pid, return_from, {M, F, Arity}, ReturnValue} + {return_from, [{M,F,Arity}, Return]} -> + {"~p:~p/~p --> ~s", [M,F,Arity, format_trace_output(Return)]}; + %% {trace, Pid, exception_from, {M, F, Arity}, {Class, Value}} + {exception_from, [{M,F,Arity}, {Class,Val}]} -> + {"~p:~p/~p ~p ~p", [M,F,Arity, Class, Val]}; + %% {trace, Pid, spawn, Spawned, {M, F, Args}} + {spawn, [Spawned, {M,F,Args}]} -> + {"spawned ~p as ~p:~p~s", [Spawned, M, F, format_args(Args)]}; + %% {trace, Pid, exit, Reason} + {exit, [Reason]} -> + {"EXIT ~p", [Reason]}; + %% {trace, Pid, link, Pid2} + {link, [Linked]} -> + {"link(~p)", [Linked]}; + %% {trace, Pid, unlink, Pid2} + {unlink, [Linked]} -> + {"unlink(~p)", [Linked]}; + %% {trace, Pid, getting_linked, Pid2} + {getting_linked, [Linker]} -> + {"getting linked by ~p", [Linker]}; + %% {trace, Pid, getting_unlinked, Pid2} + {getting_unlinked, [Unlinker]} -> + {"getting unlinked by ~p", [Unlinker]}; + %% {trace, Pid, register, RegName} + {register, [Name]} -> + {"registered as ~p", [Name]}; + %% {trace, Pid, unregister, RegName} + {unregister, [Name]} -> + {"no longer registered as ~p", [Name]}; + %% {trace, Pid, in, {M, F, Arity} | 0} + {in, [{M,F,Arity}]} -> + {"scheduled in for ~p:~p/~p", [M,F,Arity]}; + {in, [0]} -> + {"scheduled in", []}; + %% {trace, Pid, out, {M, F, Arity} | 0} + {out, [{M,F,Arity}]} -> + {"scheduled out from ~p:~p/~p", [M, F, Arity]}; + {out, [0]} -> + {"scheduled out", []}; + %% {trace, Pid, gc_start, Info} + {gc_start, [Info]} -> + HeapSize = proplists:get_value(heap_size, Info), + OldHeapSize = proplists:get_value(old_heap_size, Info), + MbufSize = proplists:get_value(mbuf_size, Info), + {"gc beginning -- heap ~p bytes", + [HeapSize + OldHeapSize + MbufSize]}; + %% {trace, Pid, gc_end, Info} + {gc_end, [Info]} -> + HeapSize = proplists:get_value(heap_size, Info), + OldHeapSize = proplists:get_value(old_heap_size, Info), + MbufSize = proplists:get_value(mbuf_size, Info), + {"gc finished -- heap ~p bytes", + [HeapSize + OldHeapSize + MbufSize]}; + _ -> + {"unknown trace type ~p -- ~p", [Type, TraceInfo]} + end, + io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", + [Hour, Min, Sec, Pid] ++ FormatArgs). + +extract_info(TraceMsg) -> + case tuple_to_list(TraceMsg) of + [trace_ts, Pid, Type | Info] -> + {TraceInfo, [Timestamp]} = lists:split(length(Info)-1, Info), + {Type, Pid, to_hms(Timestamp), TraceInfo}; + [trace, Pid, Type | TraceInfo] -> + {Type, Pid, to_hms(os:timestamp()), TraceInfo} + end. + +to_hms(Stamp = {_, _, Micro}) -> + {_,{H, M, Secs}} = calendar:now_to_local_time(Stamp), + Seconds = Secs rem 60 + (Micro / 1000000), + {H,M,Seconds}; +to_hms(_) -> + {0,0,0}. + +format_args(Arity) when is_integer(Arity) -> + [$/, integer_to_list(Arity)]; +format_args(Args) when is_list(Args) -> + [$(, join(", ", [format_trace_output(Arg) || Arg <- Args]), $)]. + + +%% @doc formats call arguments and return values - most types are just printed out, except for +%% tuples recognised as records, which mimic the source code syntax +%% @end +format_trace_output(Args) -> + format_trace_output(recon_rec:is_active(), recon_map:is_active(), Args). + +format_trace_output(Recs, Args) -> + format_trace_output(Recs, recon_map:is_active(), Args). + +format_trace_output(true, _, Args) when is_tuple(Args) -> + recon_rec:format_tuple(Args); +format_trace_output(false, true, Args) when is_tuple(Args) -> + format_tuple(false, true, Args); +format_trace_output(Recs, Maps, Args) when is_list(Args), Recs orelse Maps -> + case io_lib:printable_list(Args) of + true -> + io_lib:format("~p", [Args]); + false -> + format_maybe_improper_list(Recs, Maps, Args) + end; +format_trace_output(Recs, true, Args) when is_map(Args) -> + {Label, Map} = case recon_map:process_map(Args) of + {L, M} -> {atom_to_list(L), M}; + M -> {"", M} + end, + ItemList = maps:to_list(Map), + [Label, + "#{", + join(", ", [format_kv(Recs, true, Key, Val) || {Key, Val} <- ItemList]), + "}"]; +format_trace_output(Recs, false, Args) when is_map(Args) -> + ItemList = maps:to_list(Args), + ["#{", + join(", ", [format_kv(Recs, false, Key, Val) || {Key, Val} <- ItemList]), + "}"]; +format_trace_output(_, _, Args) -> + io_lib:format("~p", [Args]). + +format_kv(Recs, Maps, Key, Val) -> + [format_trace_output(Recs, Maps, Key), "=>", format_trace_output(Recs, Maps, Val)]. + + +format_tuple(Recs, Maps, Tup) -> + [${ | format_tuple_(Recs, Maps, tuple_to_list(Tup))]. + +format_tuple_(_Recs, _Maps, []) -> + "}"; +format_tuple_(Recs, Maps, [H|T]) -> + [format_trace_output(Recs, Maps, H), $,, + format_tuple_(Recs, Maps, T)]. + + +format_maybe_improper_list(Recs, Maps, List) -> + [$[ | format_maybe_improper_list_(Recs, Maps, List)]. + +format_maybe_improper_list_(_, _, []) -> + "]"; +format_maybe_improper_list_(Recs, Maps, [H|[]]) -> + [format_trace_output(Recs, Maps, H), $]]; +format_maybe_improper_list_(Recs, Maps, [H|T]) when is_list(T) -> + [format_trace_output(Recs, Maps, H), $,, + format_maybe_improper_list_(Recs, Maps, T)]; +format_maybe_improper_list_(Recs, Maps, [H|T]) when not is_list(T) -> + %% Handling improper lists + [format_trace_output(Recs, Maps, H), $|, + format_trace_output(Recs, Maps, T), $]]. + + +%%%%%%%%%%%%%%% +%%% HELPERS %%% +%%%%%%%%%%%%%%% + +maybe_kill(Name) -> + case whereis(Name) of + undefined -> + ok; + Pid -> + unlink(Pid), + exit(Pid, kill), + wait_for_death(Pid, Name) + end. + +wait_for_death(Pid, Name) -> + case is_process_alive(Pid) orelse whereis(Name) =:= Pid of + true -> + timer:sleep(10), + wait_for_death(Pid, Name); + false -> + ok + end. + +%% Borrowed from dbg +fun_to_ms(ShellFun) when is_function(ShellFun) -> + case erl_eval:fun_data(ShellFun) of + {fun_data,ImportList,Clauses} -> + case ms_transform:transform_from_shell( + dbg,Clauses,ImportList) of + {error,[{_,[{_,_,Code}|_]}|_],_} -> + io:format("Error: ~s~n", + [ms_transform:format_error(Code)]), + {error,transform_error}; + Else -> + Else + end; + false -> + exit(shell_funs_only) + end. + + +-ifdef(OTP_RELEASE). +-spec join(term(), [term()]) -> [term()]. +join(Sep, List) -> + lists:join(Sep, List). +-else. +-spec join(string(), [string()]) -> string(). +join(Sep, List) -> + string:join(List, Sep). +-endif. diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl new file mode 100644 index 0000000..085940d --- /dev/null +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -0,0 +1,516 @@ +%%%------------------------------------------------------------------- +%%% @author 2023, Mathias Green +%%% @doc +%%% Common Test Suite for recon_trace functionality. +%%% Tests various scenarios based on the recon_trace documentation. +%%% @end +%%%------------------------------------------------------------------- + +-module(recon_trace_use_dbg_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-compile({parse_transform, ms_transform}). + +-export([ + all/0, groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2 + ]). + +-export([ + count_trace_match/3, + assert_trace_match/2, + assert_trace_no_match/2 + ]). + +%% Test cases +-export([ + dummy_basic_test/1, + trace_full_module_test/1, + trace_one_function_test/1, + trace_rate_limit_test/1, + trace_even_arg_test/1, + trace_iolist_to_binary_with_binary_test/1, + trace_specific_pid_test/1, + trace_arity_test/1, + trace_spec_list_new_procs_only_test/1, + trace_handle_call_new_and_custom_registry_test/1, + trace_return_shellfun_test/1, + trace_return_matchspec_test/1, + trace_return_shorthand_test/1 + ]). + +%%-------------------------------------------------------------------- +%% Suite Configuration +%%-------------------------------------------------------------------- + +%% @doc Returns list of test cases and/or groups to be executed. +all() -> + [{group, basic_test}, {group, doc_based_test}]. + +%% @doc Defines the test groups. +groups() -> + [ + {basic_test, [sequence], [ + dummy_basic_test + ]}, + {doc_based_test, [sequence], [ + trace_full_module_test, + trace_one_function_test, + trace_rate_limit_test, + trace_even_arg_test, + trace_iolist_to_binary_with_binary_test, + trace_specific_pid_test, + trace_arity_test, + trace_spec_list_new_procs_only_test, + trace_handle_call_new_and_custom_registry_test, + trace_return_shellfun_test, + trace_return_matchspec_test, + trace_return_shorthand_test + ] + } + ]. + +%%-------------------------------------------------------------------- +%% Init and Teardown Functions +%%-------------------------------------------------------------------- + +init_per_suite(Config) -> + {ok, Pid} = test_statem:start(), + ct:log("Starting test_statem process with PID: ~p", [Pid]), + State = test_statem:get_state(), + ct:log("Init per suite state: ~p", [State]), + ct:log("Init per suite config: ~p", [Config]), + Config. + + +end_per_suite(_Config) -> + %% Cleanup after all tests run + test_statem:stop(). + + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + +init_per_testcase(TestName, Config) -> + LogFileName = "test_statem_"++atom_to_list(TestName)++".log", + {ok, FH} = file:open(LogFileName, [write]), + [{file, {FH, LogFileName}} | Config]. + +end_per_testcase(_TestName, Config) -> + {FH, _FileName} = proplists:get_value(file, Config), + file:close(FH), + recon_trace:clear(), % Ensure traces are cleared between tests + proplists:delete(file, Config). + +%%-------------------------------------------------------------------- +%% Helper Functions +%%-------------------------------------------------------------------- + +assert_trace_match(RegexString, TraceOutput) -> + ct:log("Asserting match for ~p in output:~n~p", [RegexString, TraceOutput]), + case re:run(TraceOutput, RegexString, [{capture, none}]) of + match -> ok; + nomatch -> ct:fail({regex_did_not_match, RegexString}) + end. +count_trace_match(RegexString, TraceOutput, ExpCnt) -> + ct:log("Counting if ~p matches for ~p in output:~n~p", [ExpCnt, RegexString, TraceOutput]), + + case re:run(TraceOutput, RegexString, [global]) of + {match, List} when length(List) == ExpCnt -> ok; + {match, List} -> ct:fail({wrong_match_count, RegexString, length(List), ExpCnt}); + nomatch -> ct:fail({regex_did_not_match, RegexString}); + _ -> ct:fail({unexpected, RegexString, ExpCnt}) + + end. + +assert_trace_no_match(RegexString, TraceOutput) -> + ct:log("Asserting match for ~p in output:~n~p", [RegexString, TraceOutput]), + case re:run(TraceOutput, RegexString, [{capture, none}]) of + match -> ct:fail({regex_unexpectedly_matched, RegexString}); + nomatch -> ok + end. + +%%-------------------------------------------------------------------- +%% Test Cases +%%-------------------------------------------------------------------- + +dummy_basic_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + recon_trace_use_dbg:calls({test_statem, light_state, '_'}, 10, [{io_server, FH},{scope,local}]), + test_statem:switch_state(), + S = test_statem:get_state(), + ct:log("State: ~p", [S]), + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), + ok. + +%%====================================================================== +%% --------------------------------------------------------------------------- +%% Test cases based on https://ferd.github.io/recon/recon_trace.html#calls/3 +%% --------------------------------------------------------------------------- +%%====================================================================== + +%%====================================================================== +%% Documentation: All calls from the queue module, with 10 calls printed at most: +%% recon_trace_use_dbg:calls({queue, '_', '_'}, 10) +%%--- +%% Test: All calls from the test_statem module, with 10 calls printed at most. +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10) +%%====================================================================== +trace_full_module_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + recon_trace_use_dbg:calls({test_statem, '_', '_'}, 100, [{io_server, FH},{scope,local}]), + + timer:sleep(100), + ok = test_statem:switch_state(), + _ = test_statem:get_state(), + timer:sleep(100), + ok = test_statem:switch_state(), + timer:sleep(100), + lists:foreach(fun(_)->test_statem:get_state(), timer:sleep(50) end, lists:seq(1, 7)), % Call get_state multiple times + + {ok, TraceOutput} = file:read_file(FileName), + + count_trace_match("test_statem:get_state\\(\\)", TraceOutput,8), + count_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), + count_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), + ok. +%%====================================================================== +%% Documentation: All calls to lists:seq(A,B), with 100 calls printed at most: recon_trace_use_dbg:calls({lists, seq, 2}, 100) +%%--- +%% Test: All calls from the test_statem:get_state module, with 10 calls printed at most. +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, get_state, '_'}, 10) +%%====================================================================== +trace_one_function_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + recon_trace_use_dbg:calls({test_statem, get_state, 0}, 100, [{io_server, FH},{scope,local}]), + + timer:sleep(100), + ok = test_statem:switch_state(), + _ = test_statem:get_state(), + timer:sleep(100), + ok = test_statem:switch_state(), + timer:sleep(100), + lists:foreach(fun(_)->test_statem:get_state(), timer:sleep(50) end, lists:seq(1, 7)), % Call get_state multiple times + + {ok, TraceOutput} = file:read_file(FileName), + + count_trace_match("test_statem:get_state\\(\\)", TraceOutput,8), + ok. + +%%====================================================================== +%% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace_use_dbg:calls({lists, seq, 2}, {100, 1000}) +%%--- +%% Test: All calls to test_statem:heavy_state(A,B), with 1 call per second printed at most: +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, heavy_state, 2}, 10) +%%====================================================================== + +trace_rate_limit_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + + recon_trace_use_dbg:calls({test_statem, heavy_state, 3}, {1, 1000}, [ {io_server, FH},{scope,local}]), + + timer:sleep(2200), % Allow more time for potential rate limiting delays + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + + count_trace_match("test_statem:heavy_state", TraceOutput, 2), + ok. + +%%====================================================================== +%% Documentation: All calls to lists:seq(A,B,2) (all sequences increasing by two) with 100 calls at most: +%% recon_trace_use_dbg:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) +%%--- +%% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) +%%====================================================================== + +trace_even_arg_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + + end, + MatchSpec = dbg:fun2ms(fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace() end), + + recon_trace_use_dbg:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH},{scope,local}]), + timer:sleep(1900), + recon_trace:clear(), + {ok, TraceOutput} = file:read_file(FileName), + + count_trace_match("test_statem:heavy_state\\(timeout", TraceOutput, 3), + ok. +%%====================================================================== +%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): +%% recon_trace_use_dbg:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% Test: All calls to iolist_to_binary/1 made with a binary argument. +%%--- +%% Function: recon_trace_use_dbg:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell +%%====================================================================== + +trace_iolist_to_binary_with_binary_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), + recon_trace_use_dbg:calls({erlang, iolist_to_binary, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), + + _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace + _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace + _ = erlang:iolist_to_binary([<<"mix">>, "ed"]), % Should NOT trace + _ = erlang:iolist_to_binary(<<"another binary">>), % Should trace + + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + + recon_trace:clear(), + + assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), + assert_trace_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), + ok. + +%%====================================================================== +%% Documentation: Calls to the queue module only in a given process Pid, +%% at a rate of 50 per second at most: recon_trace_use_dbg:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) +%%--- +%% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most. +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}]) +%%====================================================================== +trace_specific_pid_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + %%default statem state is heavy_state + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + %% new statem in light state + {ok, Pid} = gen_statem:start(test_statem, [], []), + + recon_trace_use_dbg:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}, {io_server, FH},{scope,local}]), + + gen_statem:call(Pid, get_value), + gen_statem:call(Pid, get_value), + %% Call from another process - should NOT trace + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + %% Check calls originating from are traced (e.g., handle_event) + count_trace_match(".*test_statem:light_state", TraceOutput,2), + %% Check calls from the other process are NOT traced + assert_trace_no_match(".*test_statem:heavy_state", TraceOutput), + is_process_alive(Pid) andalso exit(Pid, kill), % Cleanup spawned proc + ok. + +%%====================================================================== +%% Documentation: Print the traces with the function arity instead of literal arguments: +%% recon_trace_use_dbg:calls(TSpec, Max, [{args, arity}]) +%%--- +%% Test: Print traces for test_statem calls with arity instead of arguments. +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10, [{args, arity}]) +%%====================================================================== +trace_arity_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, FH},{scope,local}]), + + test_statem:get_state(), + ok = test_statem:switch_state(), + ok = test_statem:switch_state(), + timer:sleep(100), + recon_trace:clear(), + {ok, TraceOutput} = file:read_file(FileName), + %% Check for arity format, e.g., module:function/arity + assert_trace_match("test_statem:get_state/0", TraceOutput), % gen_statem callback arity + assert_trace_match("test_statem:light_state/3", TraceOutput), % gen_statem callback arity + %% Ensure literal args are not present + assert_trace_no_match("switch_state\\(", TraceOutput), + assert_trace_no_match("iterator", TraceOutput), + ok. + +%%====================================================================== +%% Documentation: Matching the filter/2 functions of both dict and lists modules, across new processes only: +%% recon_trace_use_dbg:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) +%%--- +%% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only. +%%--- +%% Function: recon_trace_use_dbg:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) +%%====================================================================== +trace_spec_list_new_procs_only_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + + recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, [{pid, new}, {io_server, FH},{scope,local}]), + + {ok, heavy_state,_} = test_statem:get_state(), + %% Call from a *new* process - should trace + {ok, NewPid} = gen_statem:start(test_statem, [], []), + + gen_statem:call(NewPid, get_value), + gen_statem:call(NewPid, get_value), + + %% Call from old process - should NOT trace + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), + timer:sleep(1000), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + %% Check calls from the new process ARE traced + count_trace_match("test_statem:light_state", TraceOutput, 2), + assert_trace_no_match("test_statem:heavy_state", TraceOutput), + is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc + ok. +%%====================================================================== +%% Documentation: Tracing the handle_call/3 functions of a given module for all new processes, and those of an existing one registered with gproc: +%% recon_trace_use_dbg:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) +%%--- +%% Test: Tracing test_statem for new processes and one via custom process register. +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, fake_reg, ts_test}, new]}]) +%%====================================================================== +trace_handle_call_new_and_custom_registry_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + try + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + fake_reg:start(), + {ok, NewPid} = gen_statem:start({via, fake_reg, ts_test}, test_statem, [], []), + + recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + [{pid, [{via, fake_reg, ts_test}, new]}, {io_server, FH},{scope,local}]), + + gen_statem:call({via, fake_reg, ts_test}, get_value), + gen_statem:call(NewPid, get_value), + + %% Call from old process - should NOT trace + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + %% Check calls from the new process ARE traced + count_trace_match("test_statem:light_state", TraceOutput, 2), + assert_trace_no_match("test_statem:heavy_state", TraceOutput) + after + gen_statem:stop({via, fake_reg, ts_test}), + fake_reg:stop() + end. +%%====================================================================== +%% Documentation: Show the result of a given function call: recon_trace_use_dbg:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) +%%--- +%% Test: Show the result of test_statem:get_state/0 calls. +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) +%%--- +%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell +%%====================================================================== +trace_return_shellfun_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, + + MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), + + recon_trace_use_dbg:calls({test_statem, get_state, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), + + {ok,light_state, N} = test_statem:get_state(), + + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + %% Check for the call and its return value + assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), + ok. +%%====================================================================== +%% Documentation: Show the result of a given function call: +%% recon_trace_use_dbg:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), +%%--- +%% Test: Show the result of test_statem:get_state/0 calls (using match spec). +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) +%%====================================================================== +trace_return_matchspec_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + case test_statem:get_state() of + {ok,heavy_state,_} -> ok; + {ok,light_state,_} -> test_statem:switch_state() + end, + %% Trace the API function test_statem:get_state/1 using match spec + recon_trace_use_dbg:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10, [ {io_server, FH},{scope,local}]), + + {ok,heavy_state, N} = test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + {ok, TraceOutput} = file:read_file(FileName), + + %% Check for the call and its return value + assert_trace_match("test_statem:get_state/0 --> {ok,heavy_state,"++integer_to_list(N)++"}", TraceOutput), + ok. +%%====================================================================== +%% Documentation: A short-hand version for this pattern of 'match anything, trace everything' +%% for a function is recon_trace_use_dbg:calls({Mod, Fun, return_trace}). +%%--- +%% Test: Show the result of test_statem:get_state/0 calls (shorthand). +%%--- +%% Function: recon_trace_use_dbg:calls({test_statem, get_state, return_trace}, 10). +%%====================================================================== +trace_return_shorthand_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, + %% Trace the API function test_statem:get_state/1 using shorthand + recon_trace_use_dbg:calls({test_statem, get_state, return_trace}, 10, [ {io_server, FH},{scope,local}]), + {ok,light_state, N} = test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + + %% Check for the call and its return value + assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), + ok. + From 4f5f68f1eb71a97e4bae372e596298af7acac48d Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Thu, 1 May 2025 23:28:22 +0200 Subject: [PATCH 06/35] dirty version of dbg use feature --- src/recon_trace_use_dbg.erl | 119 +++++++++++++++++++---------- test/recon_trace_use_dbg_SUITE.erl | 3 +- 2 files changed, 82 insertions(+), 40 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 2e08ef2..65584cf 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -15,7 +15,7 @@ %% Internal exports --export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]). +-export([count_tracer/3, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]). -type matchspec() :: [{[term()] | '_', [term()], [term()]}]. -type shellfun() :: fun((_) -> term()). @@ -61,7 +61,7 @@ clear() -> erlang:trace_pattern({'_','_','_'}, false, []), % unsets global maybe_kill(recon_trace_tracer), maybe_kill(recon_trace_formatter), - maybe_kill(recon_trace_use_dbg), + maybe_kill(recon_trace_dbg_printer), ok. %% @equiv calls({Mod, Fun, Args}, Max, []) @@ -84,39 +84,87 @@ calls({Mod, Fun, Args}, Max, Opts) -> calls(TSpecs, Max, Opts) -> case proplists:get_bool(use_dbg, Opts) of true -> calls_dbg(TSpecs, Max, Opts); - _ -> calls_tracer(TSpecs, Max, Opts) + _ -> recon_trace:calls(TSpecs, Max, Opts) end. -calls_tracer(TSpecs = [_|_], {Max, Time}, Opts) -> - Pid = setup(rate_tracer, [Max, Time], - validate_formatter(Opts), validate_io_server(Opts)), - trace_calls(TSpecs, Pid, Opts); -calls_tracer(TSpecs = [_|_], Max, Opts) -> - Pid = setup(count_tracer, [Max], - validate_formatter(Opts), validate_io_server(Opts)), - trace_calls(TSpecs, Pid, Opts). - -calls_dbg(TSpecs = [_|_], {Max, Time}, Opts) -> - Pid = setup(rate_tracer, [Max, Time], - validate_formatter(Opts), validate_io_server(Opts)), - trace_calls(TSpecs, Pid, Opts); -calls_dbg(TSpecs = [_|_], Max, Opts) -> - Pid = setup(count_tracer, [Max], - validate_formatter(Opts), validate_io_server(Opts)), - trace_calls(TSpecs, Pid, Opts). +% calls_dbg(TSpecs = [_|_], {Max, Time}, Opts) -> +% Pid = setup(rate_tracer, [Max, Time], +% validate_formatter(Opts), validate_io_server(Opts)), +% trace_calls(TSpecs, Pid, Opts); +printer() -> + receive + {print, Msg} -> + io:format("Trace message: ~p~n", [Msg]), + printer(); + {'EXIT', Tracer, normal} -> + io:format("Recon tracer rate limit tripped.~n"), + exit(normal); + {'EXIT', Tracer, Reason} -> + exit(Reason); + TraceMsg -> + io:format("Trace message: ~p~n", [TraceMsg]), + printer() + end. + + + +calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> + % Formatter= validate_formatter(Opts), + IoServer = validate_io_server(Opts), + + Printer = spawn(fun printer/0), + + PatternsFun = generate_pattern_filter(count_tracer, TSpecs, Max, Printer), + + dbg:tracer(process,{PatternsFun, 0}), + dbg:p(all,[c]), + dbg:tpl({M, F, '_'},[{'_', [], []}]), + dbg:tp({M, F, '_'},[{'_', [], []}]). + + %%%%%%%%%%%%%%%%%%%%%%% %%% PRIVATE EXPORTS %%% %%%%%%%%%%%%%%%%%%%%%%% %% @private Stops when N trace messages have been received -count_tracer(0) -> - exit(normal); -count_tracer(N) -> - receive - Msg -> - recon_trace_formatter ! Msg, - count_tracer(N-1) - end. +count_tracer(Max, {M, F, PatternFun}, IoProc) -> + + + IoProc ! {print,"77eeeas2222222222222222222"}, + + fun + (_X, N) when N > Max -> + + IoProc ! {print,{_X, N}} , + IoProc ! {print,"top3234eeeas2222222222222222222"}, + dbg:stop(); + (#trace{type=call, mfa={M,F,A}}, N) when N =< Max -> + + IoProc ! {print,"23234eeeas2222222222222222222"}, + try PatternFun(A) of + {true, PrintOut} -> + + io:format("asiujfdghas ~p~n", [PrintOut]), + IoProc ! {print, PrintOut}, + N+1; + {false, PrintOut} -> + IoProc ! {print,"2eeeas2222222222222222222"}, + N+1; + false -> + IoProc ! {print,"3eeeas2222222222222222222"}, + N; + XX -> + IoProc ! {print,"4eeeas2222222222222222222"}, + IoProc ! {print,XX} + catch + error:badarg -> + IoProc ! {print,"55eeeas2222222222222222222"}, + io:format("2222222222222222222~p~n", [IoProc]) ; + error:_ -> + IoProc ! {print,"66eeeas2222222222222222222"}, + io:format("2222222222222222222~p~n", [IoProc]) + end +end. %% @private Stops whenever the trace message rates goes higher than %% `Max' messages in `Time' milliseconds. Note that if the rate @@ -167,18 +215,11 @@ formatter(Tracer, IOServer, FormatterFun) -> %% starts the tracer and formatter processes, and %% cleans them up before each call. -setup(TracerFun, TracerArgs, FormatterFun, IOServer) -> +generate_pattern_filter(count_tracer, + [{M,F,PatternFun}] = TSpecs, Max, IoServer) -> clear(), Ref = make_ref(), - Tracer = spawn_link(?MODULE, TracerFun, TracerArgs), - register(recon_trace_tracer, Tracer), - Format = spawn(?MODULE, formatter, [Tracer, self(), Ref, FormatterFun, IOServer]), - register(recon_trace_formatter, Format), - receive - {Ref, linked} -> Tracer - after 5000 -> - error(setup_failed) - end. + count_tracer(Max, {M,F,PatternFun},IoServer). %% Sets the traces in action trace_calls(TSpecs, Pid, Opts) -> @@ -191,7 +232,7 @@ trace_calls(TSpecs, Pid, Opts) -> || PidSpec <- PidSpecs], lists:sum(Matches). - + %%%%%%%%%%%%%%%%%% %%% VALIDATION %%% %%%%%%%%%%%%%%%%%% diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 085940d..b782327 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -142,7 +142,8 @@ assert_trace_no_match(RegexString, TraceOutput) -> dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, light_state, '_'}, 10, [{io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, light_state, fun(A) -> {true, A} end}, 10, + [{io_server, FH},{use_dbg, true},{scope,local}]), test_statem:switch_state(), S = test_statem:get_state(), ct:log("State: ~p", [S]), From 217bece772bc44e25608c9b9501dec3e5879c802 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 4 May 2025 22:04:48 +0200 Subject: [PATCH 07/35] make dummy_basic_test in recon_trace_use_dbg_SUITE.erl work wih use_dbg. --- src/recon_trace_use_dbg.erl | 156 +++++++++++++---------------- test/recon_trace_use_dbg_SUITE.erl | 2 +- 2 files changed, 72 insertions(+), 86 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 65584cf..9ed6df6 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -11,11 +11,11 @@ -export([format/1]). --record(trace, {pid, type, mfa}). - +-record(trace, {pid, type, info}). +-record(trace_ts, {pid, type, info, timestamp}). %% Internal exports --export([count_tracer/3, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]). +-export([count_tracer/4, formatter/5, format_trace_output/1, format_trace_output/2]). -type matchspec() :: [{[term()] | '_', [term()], [term()]}]. -type shellfun() :: fun((_) -> term()). @@ -91,30 +91,16 @@ calls(TSpecs, Max, Opts) -> % Pid = setup(rate_tracer, [Max, Time], % validate_formatter(Opts), validate_io_server(Opts)), % trace_calls(TSpecs, Pid, Opts); -printer() -> - receive - {print, Msg} -> - io:format("Trace message: ~p~n", [Msg]), - printer(); - {'EXIT', Tracer, normal} -> - io:format("Recon tracer rate limit tripped.~n"), - exit(normal); - {'EXIT', Tracer, Reason} -> - exit(Reason); - TraceMsg -> - io:format("Trace message: ~p~n", [TraceMsg]), - printer() - end. - - + + calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> - % Formatter= validate_formatter(Opts), + Formatter= validate_formatter(Opts), IoServer = validate_io_server(Opts), - Printer = spawn(fun printer/0), - - PatternsFun = generate_pattern_filter(count_tracer, TSpecs, Max, Printer), + PatternsFun = + generate_pattern_filter(count_tracer, TSpecs, + Max, IoServer, Formatter), dbg:tracer(process,{PatternsFun, 0}), dbg:p(all,[c]), @@ -126,66 +112,73 @@ calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> %%%%%%%%%%%%%%%%%%%%%%% %%% PRIVATE EXPORTS %%% %%%%%%%%%%%%%%%%%%%%%%% -%% @private Stops when N trace messages have been received -count_tracer(Max, {M, F, PatternFun}, IoProc) -> - - - IoProc ! {print,"77eeeas2222222222222222222"}, - - fun - (_X, N) when N > Max -> - - IoProc ! {print,{_X, N}} , - IoProc ! {print,"top3234eeeas2222222222222222222"}, - dbg:stop(); - (#trace{type=call, mfa={M,F,A}}, N) when N =< Max -> - - IoProc ! {print,"23234eeeas2222222222222222222"}, - try PatternFun(A) of - {true, PrintOut} -> +%% starts the tracer and formatter processes, and +%% cleans them up before each call. +generate_pattern_filter(count_tracer, + [{M,F,PatternFun}] = TSpecs, Max, IoServer, Formater) -> + clear(), + Ref = make_ref(), + count_tracer(Max, {M,F,PatternFun},IoServer, Formater). - io:format("asiujfdghas ~p~n", [PrintOut]), - IoProc ! {print, PrintOut}, - N+1; - {false, PrintOut} -> - IoProc ! {print,"2eeeas2222222222222222222"}, +%% @private Stops when N trace messages have been received +count_tracer(Max, {M, F, PatternFun}, IoServer, Formatter) -> +fun + (_Trace, N) when N > Max -> + io:format("Recon tracer rate limit tripped.~n"), + dbg:stop(); + (Trace, N) when (N =< Max) and is_tuple(Trace) -> + %% Type = element(1, Trace), + Print = filter_call(Trace, M, F, PatternFun), + case Print of + reject -> N; + print -> + Output = Formatter(Trace), + io:format(IoServer, Output, []), N+1; - false -> - IoProc ! {print,"3eeeas2222222222222222222"}, - N; - XX -> - IoProc ! {print,"4eeeas2222222222222222222"}, - IoProc ! {print,XX} - catch - error:badarg -> - IoProc ! {print,"55eeeas2222222222222222222"}, - io:format("2222222222222222222~p~n", [IoProc]) ; - error:_ -> - IoProc ! {print,"66eeeas2222222222222222222"}, - io:format("2222222222222222222~p~n", [IoProc]) - end + _ -> N + end; + + (Trace, N) when N =< Max -> + io:format("aaaaaaaaaaaaaaaaaaa ~p", [Trace]), N+1 end. -%% @private Stops whenever the trace message rates goes higher than -%% `Max' messages in `Time' milliseconds. Note that if the rate -%% proposed is higher than what the IO system of the formatter -%% can handle, this can still put a node at risk. -%% -%% It is recommended to try stricter rates to begin with. -rate_tracer(Max, Time) -> rate_tracer(Max, Time, 0, os:timestamp()). +test_match(M, F, TraceM, TraceF, Args, PatternFun) -> + Match = + case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of + {true, true, '_'} -> true; + {true, true, _} -> check; + _ -> false + end, + + case Match of + true -> print; + false -> reject; + check -> + try PatternFun(Args) of + _ -> print + catch + error:badarg -> + reject; + error:_ -> + reject + end + end. + +% @doc +%% @private Filters the trace messages +%% and calls the pattern function +%% @end -rate_tracer(Max, Time, Count, Start) -> - receive - Msg -> - recon_trace_formatter ! Msg, - Now = os:timestamp(), - Delay = timer:now_diff(Now, Start) div 1000, - if Delay > Time -> rate_tracer(Max, Time, 0, Now) - ; Max > Count -> rate_tracer(Max, Time, Count+1, Start) - ; Max =:= Count -> exit(normal) - end +filter_call(TraceMsg, M, F, PatternFun) -> + case extract_info(TraceMsg) of + {call, _, _, [{TraceM,TraceF, Args}]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + _ -> print end. + %% @private Formats traces to be output formatter(Tracer, Parent, Ref, FormatterFun, IOServer) -> process_flag(trap_exit, true), @@ -212,15 +205,6 @@ formatter(Tracer, IOServer, FormatterFun) -> %%%%%%%%%%%%%%%%%%%%%%% %%% SETUP FUNCTIONS %%% %%%%%%%%%%%%%%%%%%%%%%% - -%% starts the tracer and formatter processes, and -%% cleans them up before each call. -generate_pattern_filter(count_tracer, - [{M,F,PatternFun}] = TSpecs, Max, IoServer) -> - clear(), - Ref = make_ref(), - count_tracer(Max, {M,F,PatternFun},IoServer). - %% Sets the traces in action trace_calls(TSpecs, Pid, Opts) -> {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), @@ -395,6 +379,8 @@ format(TraceMsg) -> io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", [Hour, Min, Sec, Pid] ++ FormatArgs). + + extract_info(TraceMsg) -> case tuple_to_list(TraceMsg) of [trace_ts, Pid, Type | Info] -> diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index b782327..daa4ee3 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -142,7 +142,7 @@ assert_trace_no_match(RegexString, TraceOutput) -> dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, light_state, fun(A) -> {true, A} end}, 10, + recon_trace_use_dbg:calls({test_statem, light_state, fun(A) -> A end}, 10, [{io_server, FH},{use_dbg, true},{scope,local}]), test_statem:switch_state(), S = test_statem:get_state(), From af207d1334885de307575be3c832d04c4bce4fd2 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 10 May 2025 12:08:25 +0200 Subject: [PATCH 08/35] tempplorary before code removal --- recon.mermaid | 49 ++++++++++++++++ src/recon_trace_use_dbg.erl | 94 ++++++++++++++++++++++++++---- test/recon_trace_use_dbg_SUITE.erl | 8 ++- 3 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 recon.mermaid diff --git a/recon.mermaid b/recon.mermaid new file mode 100644 index 0000000..7f23279 --- /dev/null +++ b/recon.mermaid @@ -0,0 +1,49 @@ +graph TD + subgraph recon_trace Internal Function Flow + + API_calls3["calls/3 (API)"] --> Internal_setup_trace(setup_trace) + API_calls2["calls/2 (API)"] --> API_calls3 + + API_clear["clear/0 (API)"] --> Internal_stop_trace(stop_trace) + + API_format["format/1 (API)"] --> Internal_do_format(do_format) + API_format_output1["format_trace_output/1 (API)"] --> Internal_do_format_output(do_format_output) + API_format_output2["format_trace_output/2 (API)"] --> Internal_do_format_output + + %% Internal Logic Functions + Internal_setup_trace --> Internal_validate_args(validate_args) + Internal_setup_trace --> Internal_start_tracer(start_tracer_process) + Internal_setup_trace --> Internal_start_formatter(start_formatter_process) + Internal_setup_trace --> Erlang_trace_pattern["erlang:trace_pattern/3"] + Internal_setup_trace --> Erlang_trace["erlang:trace/3"] + + Internal_start_tracer --> TracerLoop(tracer_loop) + Internal_start_formatter --> FormatterLoop(formatter_loop) + + TracerLoop -- receives trace --> Internal_check_limits(check_limits) + TracerLoop -- forwards trace --> FormatterLoop + TracerLoop -- calls --> RL["recon_lib (utils)"] + + + FormatterLoop -- receives trace --> Internal_do_format + Internal_do_format --> Internal_do_format_output + Internal_do_format_output -- uses --> RR["recon_rec:format/1"] + + Internal_stop_trace --> Internal_stop_process(stop_process: Tracer) + Internal_stop_trace --> Internal_stop_process(stop_process: Formatter) + Internal_stop_trace --> Erlang_trace["erlang:trace/3"] + + end + + %% Styling + classDef api fill:#BD93F9,stroke:#333,stroke-width:2px,color:#fff; + classDef internal fill:#f8f8f2,stroke:#50FA7B,stroke-width:1px,color:#282a36; + classDef external fill:#FF79C6,stroke:#333,stroke-width:1px; + classDef lib fill:#F1FA8C,stroke:#333,stroke-width:1px; + classDef rec fill:#FFB86C,stroke:#333,stroke-width:1px; + + class API_calls3,API_calls2,API_clear,API_format,API_format_output1,API_format_output2 api; + class Internal_setup_trace,Internal_validate_args,Internal_start_tracer,Internal_start_formatter,TracerLoop,FormatterLoop,Internal_check_limits,Internal_do_format,Internal_do_format_output,Internal_stop_trace,Internal_stop_process internal; + class Erlang_trace_pattern,Erlang_trace external; + class RL lib; + class RR rec; diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 9ed6df6..997eabe 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -5,7 +5,7 @@ %%% Erlang nodes, currently for function calls only. Functionality includes: %%% @end -module(recon_trace_use_dbg). - +-include_lib("stdlib/include/ms_transform.hrl"). %% API -export([clear/0, calls/2, calls/3]). @@ -78,7 +78,6 @@ calls(TSpecs = [_|_], Max) -> %% @end -spec calls(tspec() | [tspec(),...], max(), options()) -> num_matches(). - calls({Mod, Fun, Args}, Max, Opts) -> calls([{Mod,Fun,Args}], Max, Opts); calls(TSpecs, Max, Opts) -> @@ -95,6 +94,16 @@ calls(TSpecs, Max, Opts) -> calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> + case trace_function_type(A) of + trace_fun -> + io:format( + "Warning: TSpecs contain erlang trace template function"++ + "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), + recon_trace:calls(TSpecs, Max, proplists:delete_value(use_dbg, Opts); + standard_fun -> + + + Formatter= validate_formatter(Opts), IoServer = validate_io_server(Opts), @@ -105,7 +114,8 @@ calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> dbg:tracer(process,{PatternsFun, 0}), dbg:p(all,[c]), dbg:tpl({M, F, '_'},[{'_', [], []}]), - dbg:tp({M, F, '_'},[{'_', [], []}]). + dbg:tp({M, F, '_'},[{'_', [], []}]) +end. @@ -141,16 +151,73 @@ fun (Trace, N) when N =< Max -> io:format("aaaaaaaaaaaaaaaaaaa ~p", [Trace]), N+1 end. - +trace_function_type(PatternFun) -> + try dbg:fun2ms(PatternFun) of + Patterns -> trace_function_type(Patterns, not_decided) + catch + _:_ -> standard_fun; + end. + +%% all function clauses are '_' +trace_function_type([], trace_fun) -> trace_fun; +trace_function_type([], standard_fun) -> standard_fun; + +trace_function_type([ClauseType | Rest], Type) -> + ClauseType = clause_type(Rest) -> + case Type of + not_decided -> + trace_function_type(Rest, ClauseType); + _ -> + if ClauseType =/= Type -> exit(mixed_clauses_types); + true -> trace_function_type(Rest, Type) + end + end. + +clause_type([_head,_guard, []]) -> standard_fun; +clause_type([_head,_guard, Return]) -> + if {return_trace} == lists:last(Return) -> true; + _ -> false + end. + test_match(M, F, TraceM, TraceF, Args, PatternFun) -> - Match = - case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of - {true, true, '_'} -> true; - {true, true, _} -> check; - _ -> false - end, +case trace_function_type(PatternFun) of + standard_fun -> test_match(standard_fun, M, F, TraceM, TraceF, Args, PatternFun); + trace_fun -> + Patterns = dbg:ms2fun(PatternFun), + test_match(trace_fun, M, F, TraceM, TraceF, Args, Patterns) +end. + + +test_match(standard_fun, M, F, TraceM, TraceF, Args, PatternFun) -> + Match = + case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of + {true, true, '_'} -> true; + {true, true, _} -> check; + _ -> false + end, + + case Match of + true -> print; + false -> reject; + check -> + try PatternFun(Args) of + _ -> print + catch + error:badarg -> + reject; + error:_ -> + reject + end + end; +test_match(trace_fun, M, F, TraceM, TraceF, Args, Patterns) -> + Match = erlang:match_spec_test(Args, Patterns, trace), + case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of + {true, true, '_'} -> true; + {true, true, _} -> check; + _ -> false + end, - case Match of + case Match of true -> print; false -> reject; check -> @@ -162,7 +229,10 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> error:_ -> reject end - end. + end. + + + % @doc %% @private Filters the trace messages diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index daa4ee3..5f4d2f6 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -142,8 +142,12 @@ assert_trace_no_match(RegexString, TraceOutput) -> dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, light_state, fun(A) -> A end}, 10, - [{io_server, FH},{use_dbg, true},{scope,local}]), + + MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), + recon_trace_use_dbg:calls({test_statem, light_state, MatchSpec}, 10, + [{io_server, FH},{use_dbg, true}, {scope,local}]), + + test_statem:switch_state(), S = test_statem:get_state(), ct:log("State: ~p", [S]), From a6a966eb3a28133ffa2e1410745606f20cce450a Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 11 May 2025 13:47:17 +0200 Subject: [PATCH 09/35] both basic pattern and fun works --- src/recon_trace_use_dbg.erl | 136 ++++++++++++----------------- test/recon_trace_use_dbg_SUITE.erl | 50 +++++++---- 2 files changed, 89 insertions(+), 97 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 997eabe..12ad1ff 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -5,14 +5,12 @@ %%% Erlang nodes, currently for function calls only. Functionality includes: %%% @end -module(recon_trace_use_dbg). +-compile(export_all). -include_lib("stdlib/include/ms_transform.hrl"). %% API -export([clear/0, calls/2, calls/3]). -export([format/1]). - --record(trace, {pid, type, info}). --record(trace_ts, {pid, type, info, timestamp}). %% Internal exports -export([count_tracer/4, formatter/5, format_trace_output/1, format_trace_output/2]). @@ -91,33 +89,37 @@ calls(TSpecs, Max, Opts) -> % validate_formatter(Opts), validate_io_server(Opts)), % trace_calls(TSpecs, Pid, Opts); - calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> case trace_function_type(A) of - trace_fun -> - io:format( - "Warning: TSpecs contain erlang trace template function"++ - "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), - recon_trace:calls(TSpecs, Max, proplists:delete_value(use_dbg, Opts); - standard_fun -> - - - - Formatter= validate_formatter(Opts), - IoServer = validate_io_server(Opts), - - PatternsFun = - generate_pattern_filter(count_tracer, TSpecs, - Max, IoServer, Formatter), - - dbg:tracer(process,{PatternsFun, 0}), - dbg:p(all,[c]), - dbg:tpl({M, F, '_'},[{'_', [], []}]), - dbg:tp({M, F, '_'},[{'_', [], []}]) -end. + trace_fun -> + io:format("Warning: TSpecs contain erlang trace template function"++ + "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), + recon_trace:calls(TSpecs, Max, proplists:delete(use_dbg, Opts)); + standard_fun -> + FunTSpecs = tspecs_normalization(TSpecs), + Formatter = validate_formatter(Opts), + IoServer = validate_io_server(Opts), + + PatternsFun = + generate_pattern_filter(count_tracer, FunTSpecs, + Max, IoServer, Formatter), + + dbg:tracer(process,{PatternsFun, 0}), + dbg:p(all,[c]), + dbg:tpl({M, F, '_'},[{'_', [], []}]), + dbg:tp({M, F, '_'},[{'_', [], []}]) + end. + + tspecs_normalization(TSpecs) -> + %% Normalizes the TSpecs to be a list of tuples + %% {Mod, Fun, Args} where Args is a function. + [case Args of + '_' -> {Mod, Fun, fun pass_all/1}; + _ -> TSpec + end || {Mod, Fun, Args} = TSpec <- TSpecs]. - +pass_all(V) -> V. %%%%%%%%%%%%%%%%%%%%%%% %%% PRIVATE EXPORTS %%% @@ -125,9 +127,9 @@ end. %% starts the tracer and formatter processes, and %% cleans them up before each call. generate_pattern_filter(count_tracer, - [{M,F,PatternFun}] = TSpecs, Max, IoServer, Formater) -> + [{M,F,PatternFun}] = TSpecs, Max, IoServer, Formater) -> clear(), - Ref = make_ref(), + %%Ref = make_ref(), count_tracer(Max, {M,F,PatternFun},IoServer, Formater). %% @private Stops when N trace messages have been received @@ -143,52 +145,49 @@ fun reject -> N; print -> Output = Formatter(Trace), + io:format(Output, []), + io:format("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", []), io:format(IoServer, Output, []), N+1; _ -> N - end; - - (Trace, N) when N =< Max -> - io:format("aaaaaaaaaaaaaaaaaaa ~p", [Trace]), N+1 + end + %%(Trace, N) when N =< Max -> + %% io:format("Unexpexted trace~p", [Trace]), N end. -trace_function_type(PatternFun) -> - try dbg:fun2ms(PatternFun) of - Patterns -> trace_function_type(Patterns, not_decided) + +trace_function_type(Patterns) when is_list(Patterns) -> + trace_function_type(Patterns, trace_fun); +trace_function_type(PatternFun) when is_function(PatternFun, 1) -> + try fun_to_ms(PatternFun) of + Patterns -> trace_function_type(Patterns, not_decided) catch - _:_ -> standard_fun; - end. + _:_ -> standard_fun + end. %% all function clauses are '_' trace_function_type([], trace_fun) -> trace_fun; trace_function_type([], standard_fun) -> standard_fun; -trace_function_type([ClauseType | Rest], Type) -> - ClauseType = clause_type(Rest) -> +trace_function_type([Clause | Rest], Type) -> + ClauseType = clause_type(Clause), case Type of - not_decided -> + not_decided -> trace_function_type(Rest, ClauseType); _ -> if ClauseType =/= Type -> exit(mixed_clauses_types); - true -> trace_function_type(Rest, Type) + true -> trace_function_type(Rest, Type) end end. - -clause_type([_head,_guard, []]) -> standard_fun; -clause_type([_head,_guard, Return]) -> - if {return_trace} == lists:last(Return) -> true; - _ -> false +%% actually, it should not be possible since the thirdlist has at least return value +clause_type({_head,_guard, []}) -> standard_fun; +clause_type({_head,_guard, Return}) -> + case lists:last(Return) of + %% can return_trace, current_stacktrace, exception_trace + {_return_trace} -> trace_fun; + _ -> standard_fun end. - -test_match(M, F, TraceM, TraceF, Args, PatternFun) -> -case trace_function_type(PatternFun) of - standard_fun -> test_match(standard_fun, M, F, TraceM, TraceF, Args, PatternFun); - trace_fun -> - Patterns = dbg:ms2fun(PatternFun), - test_match(trace_fun, M, F, TraceM, TraceF, Args, Patterns) -end. - -test_match(standard_fun, M, F, TraceM, TraceF, Args, PatternFun) -> +test_match(M, F, TraceM, TraceF, Args, PatternFun) -> Match = case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of {true, true, '_'} -> true; @@ -196,27 +195,6 @@ test_match(standard_fun, M, F, TraceM, TraceF, Args, PatternFun) -> _ -> false end, - case Match of - true -> print; - false -> reject; - check -> - try PatternFun(Args) of - _ -> print - catch - error:badarg -> - reject; - error:_ -> - reject - end - end; -test_match(trace_fun, M, F, TraceM, TraceF, Args, Patterns) -> - Match = erlang:match_spec_test(Args, Patterns, trace), - case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of - {true, true, '_'} -> true; - {true, true, _} -> check; - _ -> false - end, - case Match of true -> print; false -> reject; @@ -231,9 +209,6 @@ test_match(trace_fun, M, F, TraceM, TraceF, Args, Patterns) -> end end. - - - % @doc %% @private Filters the trace messages %% and calls the pattern function @@ -581,7 +556,6 @@ fun_to_ms(ShellFun) when is_function(ShellFun) -> exit(shell_funs_only) end. - -ifdef(OTP_RELEASE). -spec join(term(), [term()]) -> [term()]. join(Sep, List) -> diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 5f4d2f6..0a870e4 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -27,20 +27,21 @@ %% Test cases -export([ - dummy_basic_test/1, - trace_full_module_test/1, - trace_one_function_test/1, - trace_rate_limit_test/1, - trace_even_arg_test/1, - trace_iolist_to_binary_with_binary_test/1, - trace_specific_pid_test/1, - trace_arity_test/1, - trace_spec_list_new_procs_only_test/1, - trace_handle_call_new_and_custom_registry_test/1, - trace_return_shellfun_test/1, - trace_return_matchspec_test/1, - trace_return_shorthand_test/1 - ]). + dummy_basic_test/1, + dummy_basic_trace_test/1, + trace_full_module_test/1, + trace_one_function_test/1, + trace_rate_limit_test/1, + trace_even_arg_test/1, + trace_iolist_to_binary_with_binary_test/1, + trace_specific_pid_test/1, + trace_arity_test/1, + trace_spec_list_new_procs_only_test/1, + trace_handle_call_new_and_custom_registry_test/1, + trace_return_shellfun_test/1, + trace_return_matchspec_test/1, + trace_return_shorthand_test/1 + ]). %%-------------------------------------------------------------------- %% Suite Configuration @@ -144,10 +145,9 @@ dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), - recon_trace_use_dbg:calls({test_statem, light_state, MatchSpec}, 10, + recon_trace_use_dbg:calls({test_statem, light_state, fun(V) -> V end}, 10, [{io_server, FH},{use_dbg, true}, {scope,local}]), - test_statem:switch_state(), S = test_statem:get_state(), ct:log("State: ~p", [S]), @@ -156,6 +156,24 @@ dummy_basic_test(Config) -> assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), ok. +dummy_basic_trace_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), + recon_trace_use_dbg:calls({test_statem, light_state, MatchSpec}, 10, + [{io_server, FH},{use_dbg, true}, {scope,local}]), + + + test_statem:switch_state(), + S = test_statem:get_state(), + ct:log("State: ~p", [S]), + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), + ok. + + + %%====================================================================== %% --------------------------------------------------------------------------- %% Test cases based on https://ferd.github.io/recon/recon_trace.html#calls/3 From 0e6fe503b49444b92aef1044c4a0f3f37737fd46 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 11 May 2025 16:08:35 +0200 Subject: [PATCH 10/35] use matching lists instead functions for use_dbg --- src/recon_trace_use_dbg.erl | 41 +++++++++++++++--------------- test/recon_trace_use_dbg_SUITE.erl | 6 ++--- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 12ad1ff..ccdc67e 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -145,8 +145,6 @@ fun reject -> N; print -> Output = Formatter(Trace), - io:format(Output, []), - io:format("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", []), io:format(IoServer, Output, []), N+1; _ -> N @@ -187,6 +185,22 @@ clause_type({_head,_guard, Return}) -> _ -> standard_fun end. + + +% @doc +%% @private Filters the trace messages +%% and calls the pattern function +%% @end + +filter_call(TraceMsg, M, F, PatternFun) -> + case extract_info(TraceMsg) of + {call, _, _, [{TraceM,TraceF, Args}]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + _ -> print + end. + test_match(M, F, TraceM, TraceF, Args, PatternFun) -> Match = case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of @@ -198,32 +212,17 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> case Match of true -> print; false -> reject; - check -> - try PatternFun(Args) of + check -> + try erlang:apply(PatternFun, [Args]) of _ -> print catch - error:badarg -> + error:function_clause -> reject; - error:_ -> + error:E -> reject end end. -% @doc -%% @private Filters the trace messages -%% and calls the pattern function -%% @end - -filter_call(TraceMsg, M, F, PatternFun) -> - case extract_info(TraceMsg) of - {call, _, _, [{TraceM,TraceF, Args}]} -> - test_match(M, F, TraceM, TraceF, Args, PatternFun); - {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> - test_match(M, F, TraceM, TraceF, Args, PatternFun); - _ -> print - end. - - %% @private Formats traces to be output formatter(Tracer, Parent, Ref, FormatterFun, IOServer) -> process_flag(trap_exit, true), diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 0a870e4..a572d9d 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -143,9 +143,8 @@ assert_trace_no_match(RegexString, TraceOutput) -> dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - - MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), - recon_trace_use_dbg:calls({test_statem, light_state, fun(V) -> V end}, 10, + + recon_trace_use_dbg:calls({test_statem, light_state, fun([cast, switch_state, _]) -> anything end}, 10, [{io_server, FH},{use_dbg, true}, {scope,local}]), test_statem:switch_state(), @@ -163,7 +162,6 @@ dummy_basic_trace_test(Config) -> recon_trace_use_dbg:calls({test_statem, light_state, MatchSpec}, 10, [{io_server, FH},{use_dbg, true}, {scope,local}]), - test_statem:switch_state(), S = test_statem:get_state(), ct:log("State: ~p", [S]), From 0edf4df05e088ccae0e3621bcc678026ba72c22a Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Mon, 12 May 2025 10:42:47 +0200 Subject: [PATCH 11/35] use_dbg support for arity match --- src/recon_trace_use_dbg.erl | 63 +++++++--- test/recon_trace_use_dbg_SUITE.erl | 177 +++++++++++++++-------------- 2 files changed, 139 insertions(+), 101 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index ccdc67e..b5e1bf9 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -116,11 +116,22 @@ calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> %% {Mod, Fun, Args} where Args is a function. [case Args of '_' -> {Mod, Fun, fun pass_all/1}; + N when is_integer(N) -> + ArgsNoFun = args_no_fun(N), + {Mod, Fun, ArgsNoFun}; _ -> TSpec end || {Mod, Fun, Args} = TSpec <- TSpecs]. pass_all(V) -> V. +args_no_fun(N) -> + fun(V) -> + case erlang:length(V) of + N -> V; + _ -> throw(arity_no_match) + end + end. + %%%%%%%%%%%%%%%%%%%%%%% %%% PRIVATE EXPORTS %%% %%%%%%%%%%%%%%%%%%%%%%% @@ -134,25 +145,34 @@ generate_pattern_filter(count_tracer, %% @private Stops when N trace messages have been received count_tracer(Max, {M, F, PatternFun}, IoServer, Formatter) -> -fun - (_Trace, N) when N > Max -> - io:format("Recon tracer rate limit tripped.~n"), - dbg:stop(); - (Trace, N) when (N =< Max) and is_tuple(Trace) -> - %% Type = element(1, Trace), - Print = filter_call(Trace, M, F, PatternFun), - case Print of - reject -> N; - print -> - Output = Formatter(Trace), - io:format(IoServer, Output, []), - N+1; - _ -> N - end - %%(Trace, N) when N =< Max -> - %% io:format("Unexpexted trace~p", [Trace]), N -end. + fun + (_Trace, N) when N > Max -> + io:format("Recon tracer rate limit tripped.~n"), + dbg:stop(); + (Trace, N) when (N =< Max) and is_tuple(Trace) -> + %% Type = element(1, Trace), + Print = filter_call(Trace, M, F, PatternFun), + case Print of + reject -> N; + print -> + case Formatter(Trace) of + "" -> ok; + Formatted -> + case is_process_alive(IoServer) of + true -> io:format(IoServer, Formatted, []); + false -> io:format("Recon tracer formater stopped.~n"), + dbg:stop() + end + end, + N+1; + _ -> N + end + %%(Trace, N) when N =< Max -> + %% io:format("Unexpexted trace~p", [Trace]), N + end. +trace_function_type('_') -> standard_fun; +trace_function_type(N) when is_integer(N) -> standard_fun; trace_function_type(Patterns) when is_list(Patterns) -> trace_function_type(Patterns, trace_fun); trace_function_type(PatternFun) when is_function(PatternFun, 1) -> @@ -218,6 +238,8 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> catch error:function_clause -> reject; + error:arity_no_match -> + reject; error:E -> reject end @@ -343,7 +365,12 @@ validate_io_server(Opts) -> %%%%%%%%%%%%%%%%%%%%%%%% %% Thanks Geoff Cant for the foundations for this. format(TraceMsg) -> + io:format("ffffffffffffffffff ~n~p ~n", + [TraceMsg]), {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), + + io:format("aaaffffffffffffffffff ~n~p ~n", + [{Type, Pid, {Hour,Min,Sec}, TraceInfo}]), {FormatStr, FormatArgs} = case {Type, TraceInfo} of %% {trace, Pid, 'receive', Msg} {'receive', [Msg]} -> diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index a572d9d..5ebea88 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -13,17 +13,17 @@ -compile({parse_transform, ms_transform}). -export([ - all/0, groups/0, - init_per_suite/1, end_per_suite/1, - init_per_group/2, end_per_group/2, - init_per_testcase/2, end_per_testcase/2 - ]). + all/0, groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2 + ]). -export([ - count_trace_match/3, - assert_trace_match/2, - assert_trace_no_match/2 - ]). + count_trace_match/3, + assert_trace_match/2, + assert_trace_no_match/2 + ]). %% Test cases -export([ @@ -55,22 +55,22 @@ all() -> groups() -> [ {basic_test, [sequence], [ - dummy_basic_test - ]}, + dummy_basic_test + ]}, {doc_based_test, [sequence], [ - trace_full_module_test, - trace_one_function_test, - trace_rate_limit_test, - trace_even_arg_test, - trace_iolist_to_binary_with_binary_test, - trace_specific_pid_test, - trace_arity_test, - trace_spec_list_new_procs_only_test, - trace_handle_call_new_and_custom_registry_test, - trace_return_shellfun_test, - trace_return_matchspec_test, - trace_return_shorthand_test - ] + trace_full_module_test, + trace_one_function_test, + trace_rate_limit_test, + trace_even_arg_test, + trace_iolist_to_binary_with_binary_test, + trace_specific_pid_test, + trace_arity_test, + trace_spec_list_new_procs_only_test, + trace_handle_call_new_and_custom_registry_test, + trace_return_shellfun_test, + trace_return_matchspec_test, + trace_return_shorthand_test + ] } ]. @@ -98,7 +98,7 @@ init_per_group(_GroupName, Config) -> end_per_group(_GroupName, Config) -> Config. -init_per_testcase(TestName, Config) -> +init_per_testcase(TestName, Config) -> LogFileName = "test_statem_"++atom_to_list(TestName)++".log", {ok, FH} = file:open(LogFileName, [write]), [{file, {FH, LogFileName}} | Config]. @@ -145,8 +145,8 @@ dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), recon_trace_use_dbg:calls({test_statem, light_state, fun([cast, switch_state, _]) -> anything end}, 10, - [{io_server, FH},{use_dbg, true}, {scope,local}]), - + [{io_server, FH}, {use_dbg, true}, {scope,local}]), + test_statem:switch_state(), S = test_statem:get_state(), ct:log("State: ~p", [S]), @@ -160,7 +160,7 @@ dummy_basic_trace_test(Config) -> MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), recon_trace_use_dbg:calls({test_statem, light_state, MatchSpec}, 10, - [{io_server, FH},{use_dbg, true}, {scope,local}]), + [{io_server, FH}, {use_dbg, true}, {scope,local}]), test_statem:switch_state(), S = test_statem:get_state(), @@ -188,7 +188,8 @@ dummy_basic_trace_test(Config) -> %%====================================================================== trace_full_module_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, '_', '_'}, 100, [{io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, '_', '_'}, 100, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(100), ok = test_statem:switch_state(), @@ -199,7 +200,7 @@ trace_full_module_test(Config) -> lists:foreach(fun(_)->test_statem:get_state(), timer:sleep(50) end, lists:seq(1, 7)), % Call get_state multiple times {ok, TraceOutput} = file:read_file(FileName), - + %% there are race conditions when test ends, so count_trace_match("test_statem:get_state\\(\\)", TraceOutput,8), count_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), count_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), @@ -213,7 +214,8 @@ trace_full_module_test(Config) -> %%====================================================================== trace_one_function_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, get_state, 0}, 100, [{io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, get_state, 0}, 100, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(100), ok = test_statem:switch_state(), @@ -237,21 +239,22 @@ trace_one_function_test(Config) -> %%====================================================================== trace_rate_limit_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); {ok,heavy_state,_} -> ok end, - recon_trace_use_dbg:calls({test_statem, heavy_state, 3}, {1, 1000}, [ {io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, heavy_state, 3}, {1, 1000}, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(2200), % Allow more time for potential rate limiting delays recon_trace:clear(), {ok, TraceOutput} = file:read_file(FileName), - count_trace_match("test_statem:heavy_state", TraceOutput, 2), + count_trace_match("test_statem:heavy_state", TraceOutput, 2), ok. %%====================================================================== @@ -264,7 +267,7 @@ trace_rate_limit_test(Config) -> %%====================================================================== trace_even_arg_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); {ok,heavy_state,_} -> ok @@ -272,7 +275,8 @@ trace_even_arg_test(Config) -> end, MatchSpec = dbg:fun2ms(fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace() end), - recon_trace_use_dbg:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, heavy_state, MatchSpec}, 10, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(1900), recon_trace:clear(), {ok, TraceOutput} = file:read_file(FileName), @@ -291,10 +295,11 @@ trace_even_arg_test(Config) -> %%====================================================================== trace_iolist_to_binary_with_binary_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), - MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), - recon_trace_use_dbg:calls({erlang, iolist_to_binary, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), + MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), + recon_trace_use_dbg:calls({erlang, iolist_to_binary, MatchSpec}, 10, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace @@ -313,7 +318,7 @@ trace_iolist_to_binary_with_binary_test(Config) -> ok. %%====================================================================== -%% Documentation: Calls to the queue module only in a given process Pid, +%% Documentation: Calls to the queue module only in a given process Pid, %% at a rate of 50 per second at most: recon_trace_use_dbg:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) %%--- %% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most. @@ -321,17 +326,18 @@ trace_iolist_to_binary_with_binary_test(Config) -> %% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}]) %%====================================================================== trace_specific_pid_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), %%default statem state is heavy_state case test_statem:get_state() of {ok,light_state,_} -> test_statem:switch_state(); {ok,heavy_state,_} -> ok - end, + end, %% new statem in light state {ok, Pid} = gen_statem:start(test_statem, [], []), - recon_trace_use_dbg:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}, {io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, '_', '_'}, {10,1000}, + [{pid, Pid}, {io_server, FH}, {use_dbg, true}, {scope,local}]), gen_statem:call(Pid, get_value), gen_statem:call(Pid, get_value), @@ -359,9 +365,10 @@ trace_specific_pid_test(Config) -> %% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10, [{args, arity}]) %%====================================================================== trace_arity_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10, + [{args, arity}, {io_server, FH}, {use_dbg, true}, {scope,local}]), test_statem:get_state(), ok = test_statem:switch_state(), @@ -386,14 +393,15 @@ trace_arity_test(Config) -> %% Function: recon_trace_use_dbg:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) %%====================================================================== trace_spec_list_new_procs_only_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of - {ok,light_state,_} -> test_statem:switch_state(); - {ok,heavy_state,_} -> ok - end, + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, - recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, [{pid, new}, {io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + [{pid, new}, {io_server, FH}, {use_dbg, true}, {scope,local}]), {ok, heavy_state,_} = test_statem:get_state(), %% Call from a *new* process - should trace @@ -412,7 +420,7 @@ trace_spec_list_new_procs_only_test(Config) -> {ok, TraceOutput} = file:read_file(FileName), %% Check calls from the new process ARE traced count_trace_match("test_statem:light_state", TraceOutput, 2), - assert_trace_no_match("test_statem:heavy_state", TraceOutput), + assert_trace_no_match("test_statem:heavy_state", TraceOutput), is_process_alive(NewPid) andalso exit(NewPid, kill), % Cleanup spawned proc ok. %%====================================================================== @@ -424,35 +432,35 @@ trace_spec_list_new_procs_only_test(Config) -> %% Function: recon_trace_use_dbg:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, fake_reg, ts_test}, new]}]) %%====================================================================== trace_handle_call_new_and_custom_registry_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), try - case test_statem:get_state() of - {ok,light_state,_} -> test_statem:switch_state(); - {ok,heavy_state,_} -> ok - end, - fake_reg:start(), - {ok, NewPid} = gen_statem:start({via, fake_reg, ts_test}, test_statem, [], []), - - recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, - [{pid, [{via, fake_reg, ts_test}, new]}, {io_server, FH},{scope,local}]), - - gen_statem:call({via, fake_reg, ts_test}, get_value), - gen_statem:call(NewPid, get_value), - - %% Call from old process - should NOT trace - test_statem:get_state(), - test_statem:get_state(), - test_statem:get_state(), - timer:sleep(100), - recon_trace:clear(), - - {ok, TraceOutput} = file:read_file(FileName), - %% Check calls from the new process ARE traced - count_trace_match("test_statem:light_state", TraceOutput, 2), - assert_trace_no_match("test_statem:heavy_state", TraceOutput) + case test_statem:get_state() of + {ok,light_state,_} -> test_statem:switch_state(); + {ok,heavy_state,_} -> ok + end, + fake_reg:start(), + {ok, NewPid} = gen_statem:start({via, fake_reg, ts_test}, test_statem, [], []), + + recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + [{pid, [{via, fake_reg, ts_test}, new]}, {io_server, FH}, {use_dbg, true}, {scope,local}]), + + gen_statem:call({via, fake_reg, ts_test}, get_value), + gen_statem:call(NewPid, get_value), + + %% Call from old process - should NOT trace + test_statem:get_state(), + test_statem:get_state(), + test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + %% Check calls from the new process ARE traced + count_trace_match("test_statem:light_state", TraceOutput, 2), + assert_trace_no_match("test_statem:heavy_state", TraceOutput) after - gen_statem:stop({via, fake_reg, ts_test}), - fake_reg:stop() + gen_statem:stop({via, fake_reg, ts_test}), + fake_reg:stop() end. %%====================================================================== %% Documentation: Show the result of a given function call: recon_trace_use_dbg:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) @@ -472,7 +480,8 @@ trace_return_shellfun_test(Config) -> MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), - recon_trace_use_dbg:calls({test_statem, get_state, MatchSpec}, 10, [ {io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, get_state, MatchSpec}, 10, + [{io_server, FH}, use_dbg, {scope,local}]), {ok,light_state, N} = test_statem:get_state(), @@ -499,7 +508,8 @@ trace_return_matchspec_test(Config) -> {ok,light_state,_} -> test_statem:switch_state() end, %% Trace the API function test_statem:get_state/1 using match spec - recon_trace_use_dbg:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10, [ {io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, get_state, + [{'_', [], [{return_trace}]}]}, 10, [ {io_server, FH}, {use_dbg, true}, {scope,local}]), {ok,heavy_state, N} = test_statem:get_state(), timer:sleep(100), @@ -518,13 +528,14 @@ trace_return_matchspec_test(Config) -> %% Function: recon_trace_use_dbg:calls({test_statem, get_state, return_trace}, 10). %%====================================================================== trace_return_shorthand_test(Config) -> - {FH, FileName} = proplists:get_value(file, Config), + {FH, FileName} = proplists:get_value(file, Config), case test_statem:get_state() of {ok,light_state,_} -> ok; {ok,heavy_state,_} -> test_statem:switch_state() end, %% Trace the API function test_statem:get_state/1 using shorthand - recon_trace_use_dbg:calls({test_statem, get_state, return_trace}, 10, [ {io_server, FH},{scope,local}]), + recon_trace_use_dbg:calls({test_statem, get_state, return_trace}, 10, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), {ok,light_state, N} = test_statem:get_state(), timer:sleep(100), recon_trace:clear(), @@ -534,4 +545,4 @@ trace_return_shorthand_test(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), ok. - +%%====================================================================== From 082df0b722675fd64d6857fc5a8478a06dc48a01 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Tue, 13 May 2025 01:46:21 +0200 Subject: [PATCH 12/35] use_dbg rate trace limit --- src/recon_trace_use_dbg.erl | 93 ++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index b5e1bf9..7093e59 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -90,39 +90,43 @@ calls(TSpecs, Max, Opts) -> % trace_calls(TSpecs, Pid, Opts); -calls_dbg(TSpecs = [{M,F,A}|_], Max, Opts) -> +calls_dbg(TSpecs = [{M,F,A}|_], Boundaries, Opts) -> case trace_function_type(A) of trace_fun -> io:format("Warning: TSpecs contain erlang trace template function"++ "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), - recon_trace:calls(TSpecs, Max, proplists:delete(use_dbg, Opts)); + recon_trace:calls(TSpecs, Boundaries, proplists:delete(use_dbg, Opts)); standard_fun -> FunTSpecs = tspecs_normalization(TSpecs), Formatter = validate_formatter(Opts), IoServer = validate_io_server(Opts), PatternsFun = - generate_pattern_filter(count_tracer, FunTSpecs, - Max, IoServer, Formatter), + generate_pattern_filter(FunTSpecs, Boundaries, IoServer, Formatter), - dbg:tracer(process,{PatternsFun, 0}), + dbg:tracer(process,{PatternsFun, startValue(Boundaries)}), dbg:p(all,[c]), dbg:tpl({M, F, '_'},[{'_', [], []}]), dbg:tp({M, F, '_'},[{'_', [], []}]) end. - tspecs_normalization(TSpecs) -> +startValue({_, _}) -> + {0, os:timestamp()}; +startValue(_Max) -> + 0. + +tspecs_normalization(TSpecs) -> %% Normalizes the TSpecs to be a list of tuples %% {Mod, Fun, Args} where Args is a function. - [case Args of - '_' -> {Mod, Fun, fun pass_all/1}; - N when is_integer(N) -> - ArgsNoFun = args_no_fun(N), - {Mod, Fun, ArgsNoFun}; - _ -> TSpec - end || {Mod, Fun, Args} = TSpec <- TSpecs]. + [case Args of + '_' -> {Mod, Fun, fun pass_all/1}; + N when is_integer(N) -> + ArgsNoFun = args_no_fun(N), + {Mod, Fun, ArgsNoFun}; + _ -> TSpec + end || {Mod, Fun, Args} = TSpec <- TSpecs]. -pass_all(V) -> V. +pass_all(V) -> V. args_no_fun(N) -> fun(V) -> @@ -137,11 +141,14 @@ args_no_fun(N) -> %%%%%%%%%%%%%%%%%%%%%%% %% starts the tracer and formatter processes, and %% cleans them up before each call. -generate_pattern_filter(count_tracer, - [{M,F,PatternFun}] = TSpecs, Max, IoServer, Formater) -> + +generate_pattern_filter([{M,F,PatternFun}] = TSpecs, {Max, Time}, IoServer, Formater) -> + clear(), + rate_tracer({Max, Time}, {M,F,PatternFun}, IoServer, Formater); +generate_pattern_filter([{M,F,PatternFun}] = TSpecs, Max, IoServer, Formater) -> clear(), - %%Ref = make_ref(), count_tracer(Max, {M,F,PatternFun},IoServer, Formater). + %% @private Stops when N trace messages have been received count_tracer(Max, {M, F, PatternFun}, IoServer, Formatter) -> @@ -151,25 +158,45 @@ count_tracer(Max, {M, F, PatternFun}, IoServer, Formatter) -> dbg:stop(); (Trace, N) when (N =< Max) and is_tuple(Trace) -> %% Type = element(1, Trace), - Print = filter_call(Trace, M, F, PatternFun), - case Print of - reject -> N; - print -> - case Formatter(Trace) of - "" -> ok; - Formatted -> - case is_process_alive(IoServer) of - true -> io:format(IoServer, Formatted, []); - false -> io:format("Recon tracer formater stopped.~n"), - dbg:stop() - end - end, - N+1; - _ -> N + handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter) + end. + +rate_tracer({Max, Time}, {M, F, PatternFun}, IoServer, Formatter) -> + fun(Trace, {N, Timestamp}) -> + Now = os:timestamp(), + Delay = timer:now_diff(Now, Timestamp) div 1000, + + if Delay > Time -> + NewN = handle_trace(Trace, 0, M, F, PatternFun, IoServer, Formatter), + {NewN, Now}; + Max >= N -> + NewN = handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter), + {NewN, Timestamp}; + true -> + io:format("Recon tracer rate limit tripped.~n"), + dbg:stop() end + end. + +handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter) -> + Print = filter_call(Trace, M, F, PatternFun), + case Print of + reject -> N; + print -> + case Formatter(Trace) of + "" -> ok; + Formatted -> + case is_process_alive(IoServer) of + true -> io:format(IoServer, Formatted, []); + false -> io:format("Recon tracer formater stopped.~n"), + dbg:stop() + end + end, + N+1; + _ -> N + end. %%(Trace, N) when N =< Max -> %% io:format("Unexpexted trace~p", [Trace]), N - end. trace_function_type('_') -> standard_fun; trace_function_type(N) when is_integer(N) -> standard_fun; From 74771bbfd37ebb2955c529350d403e1abf6b3194 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Tue, 13 May 2025 02:51:32 +0200 Subject: [PATCH 13/35] use_dgb added support for scope and PidSpecs --- src/recon_trace_use_dbg.erl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 7093e59..8660e02 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -97,17 +97,22 @@ calls_dbg(TSpecs = [{M,F,A}|_], Boundaries, Opts) -> "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), recon_trace:calls(TSpecs, Boundaries, proplists:delete(use_dbg, Opts)); standard_fun -> + clear(), FunTSpecs = tspecs_normalization(TSpecs), Formatter = validate_formatter(Opts), IoServer = validate_io_server(Opts), + {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), PatternsFun = generate_pattern_filter(FunTSpecs, Boundaries, IoServer, Formatter), dbg:tracer(process,{PatternsFun, startValue(Boundaries)}), - dbg:p(all,[c]), - dbg:tpl({M, F, '_'},[{'_', [], []}]), - dbg:tp({M, F, '_'},[{'_', [], []}]) + dbg:p(hd(PidSpecs), [c]), + dbg:tp({M, F, '_'},[{'_', [], []}]), + case MatchOpts of + [global] -> ok; + [local] -> dbg:tpl({M, F, '_'},[{'_', [], []}]) + end end. startValue({_, _}) -> From 9c3079890d7cd8e82b7ca8bd5eca7a003b9c474f Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Fri, 16 May 2025 03:01:03 +0200 Subject: [PATCH 14/35] Enhance tracing functionality by adding arity handling and improving trace message formatting --- src/recon_trace_use_dbg.erl | 216 +++++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 92 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 8660e02..eada042 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -93,7 +93,7 @@ calls(TSpecs, Max, Opts) -> calls_dbg(TSpecs = [{M,F,A}|_], Boundaries, Opts) -> case trace_function_type(A) of trace_fun -> - io:format("Warning: TSpecs contain erlang trace template function"++ + io:format("Warning: TSpecs contain erlang trace template function "++ "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), recon_trace:calls(TSpecs, Boundaries, proplists:delete(use_dbg, Opts)); standard_fun -> @@ -120,6 +120,12 @@ startValue({_, _}) -> startValue(_Max) -> 0. + +preprocess_traces(Trace, TraceOpts) -> + case proplists:get_bool(arity, TraceOpts) of + true -> {return_to, Trace}; + _ -> Trace + end. tspecs_normalization(TSpecs) -> %% Normalizes the TSpecs to be a list of tuples %% {Mod, Fun, Args} where Args is a function. @@ -147,18 +153,18 @@ args_no_fun(N) -> %% starts the tracer and formatter processes, and %% cleans them up before each call. -generate_pattern_filter([{M,F,PatternFun}] = TSpecs, {Max, Time}, IoServer, Formater) -> +generate_pattern_filter([{M,F,PatternFun}] = TSpecs, {Max, Time}, IoServer, Formatter) -> clear(), - rate_tracer({Max, Time}, {M,F,PatternFun}, IoServer, Formater); -generate_pattern_filter([{M,F,PatternFun}] = TSpecs, Max, IoServer, Formater) -> + rate_tracer({Max, Time}, {M,F,PatternFun}, IoServer, Formatter); +generate_pattern_filter([{M,F,PatternFun}] = TSpecs, Max, IoServer, Formatter) -> clear(), - count_tracer(Max, {M,F,PatternFun},IoServer, Formater). + count_tracer(Max, {M,F,PatternFun}, IoServer, Formatter). %% @private Stops when N trace messages have been received count_tracer(Max, {M, F, PatternFun}, IoServer, Formatter) -> fun - (_Trace, N) when N > Max -> + (_Trace, {N, _}) when N > Max -> io:format("Recon tracer rate limit tripped.~n"), dbg:stop(); (Trace, N) when (N =< Max) and is_tuple(Trace) -> @@ -203,6 +209,7 @@ handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter) -> %%(Trace, N) when N =< Max -> %% io:format("Unexpexted trace~p", [Trace]), N + trace_function_type('_') -> standard_fun; trace_function_type(N) when is_integer(N) -> standard_fun; trace_function_type(Patterns) when is_list(Patterns) -> @@ -369,7 +376,7 @@ validate_tspec(Mod, Fun, Args) when is_function(Args) -> validate_tspec(Mod, Fun, return_trace) -> validate_tspec(Mod, Fun, [{'_', [], [{return_trace}]}]); validate_tspec(Mod, Fun, Args) -> - BannedMods = ['_', ?MODULE, io, lists], + BannedMods = ['_', ?MODULE, io, lists, dbg, recon_trace], %% The banned mod check can be bypassed by using %% match specs if you really feel like being dumb. case {lists:member(Mod, BannedMods), Args} of @@ -384,8 +391,14 @@ validate_tspec(Mod, Fun, Args) -> end. validate_formatter(Opts) -> - case proplists:get_value(formatter, Opts) of - F when is_function(F, 1) -> F; + Formatter = proplists:get_value(formatter, Opts), + ArgsOrArity = proplists:get_value(args, Opts), + case {ArgsOrArity, Formatter} of + {arity, Formatter} when is_function(Formatter, 1) -> + io:format("Custom formater, arity option ignored ~n"), + Formatter; + {_args, Formatter} when is_function(Formatter, 1) -> Formatter; + {arity, Formatter} -> generate_formatter(Formatter, arity); _ -> fun format/1 end. @@ -395,95 +408,114 @@ validate_io_server(Opts) -> %%%%%%%%%%%%%%%%%%%%%%%% %%% TRACE FORMATTING %%% %%%%%%%%%%%%%%%%%%%%%%%% +generate_formatter(Formatter, arity) -> + fun(TraceMsg) -> + {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), + {_, UpdTrace} = trace_calls_to_arity({Type, TraceInfo}), + {FormatStr, FormatArgs} = + trace_to_io(Type, UpdTrace), + io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", + [Hour, Min, Sec, Pid] ++ FormatArgs) + end; +generate_formatter(_Formatter, _) -> + fun format/1. + +trace_calls_to_arity(TypeTraceInfo) -> + case TypeTraceInfo of + {call, [{M,F,Args}]} -> + {call, [{M,F,length(Args)}]}; + {call, [{M,F,Args}, Msg]} -> + {call, [{M,F,length(Args)}, Msg]}; + Trace -> Trace + end. + +trace_to_io(Type, TraceInfo) -> + case {Type, TraceInfo} of + %% {trace, Pid, 'receive', Msg} + {'receive', [Msg]} -> + {"< ~p", [Msg]}; + %% {trace, Pid, send, Msg, To} + {send, [Msg, To]} -> + {" > ~p: ~p", [To, Msg]}; + %% {trace, Pid, send_to_non_existing_process, Msg, To} + {send_to_non_existing_process, [Msg, To]} -> + {" > (non_existent) ~p: ~p", [To, Msg]}; + %% {trace, Pid, call, {M, F, Args}} + {call, [{M,F,Args}]} -> + {"~p:~p~s", [M,F,format_args(Args)]}; + %% {trace, Pid, call, {M, F, Args}, Msg} + {call, [{M,F,Args}, Msg]} -> + {"~p:~p~s ~s", [M,F,format_args(Args), format_trace_output(Msg)]}; + %% {trace, Pid, return_to, {M, F, Arity}} + {return_to, [{M,F,Arity}]} -> + {" '--> ~p:~p/~p", [M,F,Arity]}; + %% {trace, Pid, return_from, {M, F, Arity}, ReturnValue} + {return_from, [{M,F,Arity}, Return]} -> + {"~p:~p/~p --> ~s", [M,F,Arity, format_trace_output(Return)]}; + %% {trace, Pid, exception_from, {M, F, Arity}, {Class, Value}} + {exception_from, [{M,F,Arity}, {Class,Val}]} -> + {"~p:~p/~p ~p ~p", [M,F,Arity, Class, Val]}; + %% {trace, Pid, spawn, Spawned, {M, F, Args}} + {spawn, [Spawned, {M,F,Args}]} -> + {"spawned ~p as ~p:~p~s", [Spawned, M, F, format_args(Args)]}; + %% {trace, Pid, exit, Reason} + {exit, [Reason]} -> + {"EXIT ~p", [Reason]}; + %% {trace, Pid, link, Pid2} + {link, [Linked]} -> + {"link(~p)", [Linked]}; + %% {trace, Pid, unlink, Pid2} + {unlink, [Linked]} -> + {"unlink(~p)", [Linked]}; + %% {trace, Pid, getting_linked, Pid2} + {getting_linked, [Linker]} -> + {"getting linked by ~p", [Linker]}; + %% {trace, Pid, getting_unlinked, Pid2} + {getting_unlinked, [Unlinker]} -> + {"getting unlinked by ~p", [Unlinker]}; + %% {trace, Pid, register, RegName} + {register, [Name]} -> + {"registered as ~p", [Name]}; + %% {trace, Pid, unregister, RegName} + {unregister, [Name]} -> + {"no longer registered as ~p", [Name]}; + %% {trace, Pid, in, {M, F, Arity} | 0} + {in, [{M,F,Arity}]} -> + {"scheduled in for ~p:~p/~p", [M,F,Arity]}; + {in, [0]} -> + {"scheduled in", []}; + %% {trace, Pid, out, {M, F, Arity} | 0} + {out, [{M,F,Arity}]} -> + {"scheduled out from ~p:~p/~p", [M, F, Arity]}; + {out, [0]} -> + {"scheduled out", []}; + %% {trace, Pid, gc_start, Info} + {gc_start, [Info]} -> + HeapSize = proplists:get_value(heap_size, Info), + OldHeapSize = proplists:get_value(old_heap_size, Info), + MbufSize = proplists:get_value(mbuf_size, Info), + {"gc beginning -- heap ~p bytes", + [HeapSize + OldHeapSize + MbufSize]}; + %% {trace, Pid, gc_end, Info} + {gc_end, [Info]} -> + HeapSize = proplists:get_value(heap_size, Info), + OldHeapSize = proplists:get_value(old_heap_size, Info), + MbufSize = proplists:get_value(mbuf_size, Info), + {"gc finished -- heap ~p bytes", + [HeapSize + OldHeapSize + MbufSize]}; + _ -> + {"unknown trace type ~p -- ~p", [Type, TraceInfo]} + end. + + %% Thanks Geoff Cant for the foundations for this. format(TraceMsg) -> - io:format("ffffffffffffffffff ~n~p ~n", - [TraceMsg]), {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), - - io:format("aaaffffffffffffffffff ~n~p ~n", - [{Type, Pid, {Hour,Min,Sec}, TraceInfo}]), - {FormatStr, FormatArgs} = case {Type, TraceInfo} of - %% {trace, Pid, 'receive', Msg} - {'receive', [Msg]} -> - {"< ~p", [Msg]}; - %% {trace, Pid, send, Msg, To} - {send, [Msg, To]} -> - {" > ~p: ~p", [To, Msg]}; - %% {trace, Pid, send_to_non_existing_process, Msg, To} - {send_to_non_existing_process, [Msg, To]} -> - {" > (non_existent) ~p: ~p", [To, Msg]}; - %% {trace, Pid, call, {M, F, Args}} - {call, [{M,F,Args}]} -> - {"~p:~p~s", [M,F,format_args(Args)]}; - %% {trace, Pid, call, {M, F, Args}, Msg} - {call, [{M,F,Args}, Msg]} -> - {"~p:~p~s ~s", [M,F,format_args(Args), format_trace_output(Msg)]}; - %% {trace, Pid, return_to, {M, F, Arity}} - {return_to, [{M,F,Arity}]} -> - {" '--> ~p:~p/~p", [M,F,Arity]}; - %% {trace, Pid, return_from, {M, F, Arity}, ReturnValue} - {return_from, [{M,F,Arity}, Return]} -> - {"~p:~p/~p --> ~s", [M,F,Arity, format_trace_output(Return)]}; - %% {trace, Pid, exception_from, {M, F, Arity}, {Class, Value}} - {exception_from, [{M,F,Arity}, {Class,Val}]} -> - {"~p:~p/~p ~p ~p", [M,F,Arity, Class, Val]}; - %% {trace, Pid, spawn, Spawned, {M, F, Args}} - {spawn, [Spawned, {M,F,Args}]} -> - {"spawned ~p as ~p:~p~s", [Spawned, M, F, format_args(Args)]}; - %% {trace, Pid, exit, Reason} - {exit, [Reason]} -> - {"EXIT ~p", [Reason]}; - %% {trace, Pid, link, Pid2} - {link, [Linked]} -> - {"link(~p)", [Linked]}; - %% {trace, Pid, unlink, Pid2} - {unlink, [Linked]} -> - {"unlink(~p)", [Linked]}; - %% {trace, Pid, getting_linked, Pid2} - {getting_linked, [Linker]} -> - {"getting linked by ~p", [Linker]}; - %% {trace, Pid, getting_unlinked, Pid2} - {getting_unlinked, [Unlinker]} -> - {"getting unlinked by ~p", [Unlinker]}; - %% {trace, Pid, register, RegName} - {register, [Name]} -> - {"registered as ~p", [Name]}; - %% {trace, Pid, unregister, RegName} - {unregister, [Name]} -> - {"no longer registered as ~p", [Name]}; - %% {trace, Pid, in, {M, F, Arity} | 0} - {in, [{M,F,Arity}]} -> - {"scheduled in for ~p:~p/~p", [M,F,Arity]}; - {in, [0]} -> - {"scheduled in", []}; - %% {trace, Pid, out, {M, F, Arity} | 0} - {out, [{M,F,Arity}]} -> - {"scheduled out from ~p:~p/~p", [M, F, Arity]}; - {out, [0]} -> - {"scheduled out", []}; - %% {trace, Pid, gc_start, Info} - {gc_start, [Info]} -> - HeapSize = proplists:get_value(heap_size, Info), - OldHeapSize = proplists:get_value(old_heap_size, Info), - MbufSize = proplists:get_value(mbuf_size, Info), - {"gc beginning -- heap ~p bytes", - [HeapSize + OldHeapSize + MbufSize]}; - %% {trace, Pid, gc_end, Info} - {gc_end, [Info]} -> - HeapSize = proplists:get_value(heap_size, Info), - OldHeapSize = proplists:get_value(old_heap_size, Info), - MbufSize = proplists:get_value(mbuf_size, Info), - {"gc finished -- heap ~p bytes", - [HeapSize + OldHeapSize + MbufSize]}; - _ -> - {"unknown trace type ~p -- ~p", [Type, TraceInfo]} - end, + {FormatStr, FormatArgs} = + trace_to_io(Type, TraceInfo), io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", [Hour, Min, Sec, Pid] ++ FormatArgs). - - extract_info(TraceMsg) -> case tuple_to_list(TraceMsg) of [trace_ts, Pid, Type | Info] -> From 48ce0548749dcb4c8606091e4d3df5d17d5fbf51 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 17 May 2025 12:08:38 +0200 Subject: [PATCH 15/35] all legacy type testcase passed for use_dbg --- src/recon_trace_use_dbg.erl | 119 ++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 39 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index eada042..9f5e98b 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -90,9 +90,9 @@ calls(TSpecs, Max, Opts) -> % trace_calls(TSpecs, Pid, Opts); -calls_dbg(TSpecs = [{M,F,A}|_], Boundaries, Opts) -> - case trace_function_type(A) of - trace_fun -> +calls_dbg(TSpecs, Boundaries, Opts) -> + case trace_function_types(TSpecs) of + shell_fun -> io:format("Warning: TSpecs contain erlang trace template function "++ "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), recon_trace:calls(TSpecs, Boundaries, proplists:delete(use_dbg, Opts)); @@ -105,14 +105,9 @@ calls_dbg(TSpecs = [{M,F,A}|_], Boundaries, Opts) -> {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), PatternsFun = generate_pattern_filter(FunTSpecs, Boundaries, IoServer, Formatter), - dbg:tracer(process,{PatternsFun, startValue(Boundaries)}), dbg:p(hd(PidSpecs), [c]), - dbg:tp({M, F, '_'},[{'_', [], []}]), - case MatchOpts of - [global] -> ok; - [local] -> dbg:tpl({M, F, '_'},[{'_', [], []}]) - end + dbg_tp(TSpecs, MatchOpts) end. startValue({_, _}) -> @@ -120,7 +115,14 @@ startValue({_, _}) -> startValue(_Max) -> 0. - +dbg_tp(MFAList, MatchOpts) -> + Res = [dbg:tp({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], + case MatchOpts of + [local] -> + [dbg:tpl({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList]; + [global] -> Res + end. + preprocess_traces(Trace, TraceOpts) -> case proplists:get_bool(arity, TraceOpts) of true -> {return_to, Trace}; @@ -153,35 +155,34 @@ args_no_fun(N) -> %% starts the tracer and formatter processes, and %% cleans them up before each call. -generate_pattern_filter([{M,F,PatternFun}] = TSpecs, {Max, Time}, IoServer, Formatter) -> +generate_pattern_filter(TSpecs, {Max, Time}, IoServer, Formatter) -> clear(), - rate_tracer({Max, Time}, {M,F,PatternFun}, IoServer, Formatter); -generate_pattern_filter([{M,F,PatternFun}] = TSpecs, Max, IoServer, Formatter) -> + rate_tracer({Max, Time}, TSpecs, IoServer, Formatter); +generate_pattern_filter(TSpecs, Max, IoServer, Formatter) -> clear(), - count_tracer(Max, {M,F,PatternFun}, IoServer, Formatter). - + count_tracer(Max, TSpecs, IoServer, Formatter). %% @private Stops when N trace messages have been received -count_tracer(Max, {M, F, PatternFun}, IoServer, Formatter) -> +count_tracer(Max, TSpecs, IoServer, Formatter) -> fun (_Trace, {N, _}) when N > Max -> io:format("Recon tracer rate limit tripped.~n"), dbg:stop(); (Trace, N) when (N =< Max) and is_tuple(Trace) -> %% Type = element(1, Trace), - handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter) + handle_trace(Trace, N, TSpecs, IoServer, Formatter) end. -rate_tracer({Max, Time}, {M, F, PatternFun}, IoServer, Formatter) -> +rate_tracer({Max, Time}, TSpecs, IoServer, Formatter) -> fun(Trace, {N, Timestamp}) -> Now = os:timestamp(), Delay = timer:now_diff(Now, Timestamp) div 1000, if Delay > Time -> - NewN = handle_trace(Trace, 0, M, F, PatternFun, IoServer, Formatter), + NewN = handle_trace(Trace, 0, TSpecs, IoServer, Formatter), {NewN, Now}; Max >= N -> - NewN = handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter), + NewN = handle_trace(Trace, N, TSpecs, IoServer, Formatter), {NewN, Timestamp}; true -> io:format("Recon tracer rate limit tripped.~n"), @@ -189,8 +190,8 @@ rate_tracer({Max, Time}, {M, F, PatternFun}, IoServer, Formatter) -> end end. -handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter) -> - Print = filter_call(Trace, M, F, PatternFun), +handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> + Print = filter_call(Trace, TSpecs), case Print of reject -> N; print -> @@ -208,39 +209,73 @@ handle_trace(Trace, N, M, F, PatternFun, IoServer, Formatter) -> end. %%(Trace, N) when N =< Max -> %% io:format("Unexpexted trace~p", [Trace]), N +%%----------------------------------------------------------------------- +%% In the list of TSpecs, the third element of the tuple +%% is responsible for matching arguments. +%% In standard recon_trace, the third element is a template function +%% transformed to a matchspec. +%% If use_dbg is set to true, the third element is used as actual functor and interpreted +%% in a totally different way. +%% The function is used to determine the type of the functions to be traced. +%% They should be possible to interpret all functions in the same way. +%% For example '_' can be interpreted as a dbg trace function or a functor. +%% Since use_dbg is set to true, the function is considered by default a functor. +%% The function is considered a trace function if its every clause +%% ends with a functions like return_trace(), +%% that are translated into a {return_trace} in the matchspecs list. +%% ---------------------------------------------------- +trace_function_types(TSpecs) -> +FunTypes= [trace_function_type(Args) || {_, _, Args} <- TSpecs], + + HasShell = lists:any(fun(T) -> T == shell_fun end, FunTypes), + HasStandard = lists:all(fun(T) -> T == standard_fun end, FunTypes), + case {HasShell, HasStandard} of + {true, true} -> exit(mixed_function_types); + {true, false} -> shell_fun; + {false, _true_or_false} -> standard_fun + end. +%% in case of fun_to_ms, the function is transformed to '_' +%% for this implementation it is transformed to fun(A) -> A end +trace_function_type('_') -> undecided_fun; +trace_function_type(N) when is_integer(N) -> undecided_fun; -trace_function_type('_') -> standard_fun; -trace_function_type(N) when is_integer(N) -> standard_fun; +%% shorthand used by shell functions +trace_function_type(return_trace) -> shell_fun; +%% if the function is a matchspec, we check if every clause has *_trace() trace_function_type(Patterns) when is_list(Patterns) -> - trace_function_type(Patterns, trace_fun); + trace_function_type(Patterns, shell_fun); + +%% if function transforms it still can be proper functor, +%% check if the is *_trace() is absent +%% if every clause has *_trace() it is a shell function trace_function_type(PatternFun) when is_function(PatternFun, 1) -> try fun_to_ms(PatternFun) of - Patterns -> trace_function_type(Patterns, not_decided) + Patterns -> trace_function_type(Patterns, undecided) catch _:_ -> standard_fun end. %% all function clauses are '_' -trace_function_type([], trace_fun) -> trace_fun; +trace_function_type([], shell_fun) -> shell_fun; trace_function_type([], standard_fun) -> standard_fun; +trace_function_type([], undecided_fun) -> undecided_fun; trace_function_type([Clause | Rest], Type) -> ClauseType = clause_type(Clause), case Type of - not_decided -> + undecided -> trace_function_type(Rest, ClauseType); _ -> if ClauseType =/= Type -> exit(mixed_clauses_types); true -> trace_function_type(Rest, Type) end end. -%% actually, it should not be possible since the thirdlist has at least return value -clause_type({_head,_guard, []}) -> standard_fun; + clause_type({_head,_guard, Return}) -> case lists:last(Return) of %% can return_trace, current_stacktrace, exception_trace - {_return_trace} -> trace_fun; + {_return_trace} -> shell_fun; _ -> standard_fun end. @@ -251,14 +286,20 @@ clause_type({_head,_guard, Return}) -> %% and calls the pattern function %% @end -filter_call(TraceMsg, M, F, PatternFun) -> - case extract_info(TraceMsg) of - {call, _, _, [{TraceM,TraceF, Args}]} -> - test_match(M, F, TraceM, TraceF, Args, PatternFun); - {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> - test_match(M, F, TraceM, TraceF, Args, PatternFun); - _ -> print - end. +filter_call(TraceMsg, TSpecs) -> + filter_call(TraceMsg, TSpecs, reject). + +filter_call(TraceMsg, _, print) -> print; +filter_call(TraceMsg, [], Answer) -> Answer; +filter_call(TraceMsg, [{M, F, PatternFun} | TSpecs], reject) -> + NewAnswer = case extract_info(TraceMsg) of + {call, _, _, [{TraceM,TraceF, Args}]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + _ -> print + end, + filter_call(TraceMsg, TSpecs, NewAnswer). test_match(M, F, TraceM, TraceF, Args, PatternFun) -> Match = From ecfc5cdbe9109ebddc42c44eadb3fd6473f9bf36 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 17 May 2025 14:40:37 +0200 Subject: [PATCH 16/35] Add support for new tracing functions in recon_trace and recon_trace_use_dbg - refactoring mostly referencing recon_trace instead duplicating - fixed return number of matches --- src/recon_trace.erl | 16 +- src/recon_trace_use_dbg.erl | 393 +++--------------------------------- 2 files changed, 36 insertions(+), 373 deletions(-) diff --git a/src/recon_trace.erl b/src/recon_trace.erl index 62b3b67..35f1cd6 100644 --- a/src/recon_trace.erl +++ b/src/recon_trace.erl @@ -187,6 +187,9 @@ %% Internal exports -export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]). +%% For use_dbg feature +-export([trace_to_io/2, extract_info/1, validate_opts/1, fun_to_ms/1]). + -type matchspec() :: [{[term()] | '_', [term()], [term()]}]. -type shellfun() :: fun((_) -> term()). -type formatterfun() :: fun((_) -> iodata()). @@ -486,7 +489,7 @@ validate_tspec(Mod, Fun, Args) when is_function(Args) -> validate_tspec(Mod, Fun, return_trace) -> validate_tspec(Mod, Fun, [{'_', [], [{return_trace}]}]); validate_tspec(Mod, Fun, Args) -> - BannedMods = ['_', ?MODULE, io, lists], + BannedMods = ['_', ?MODULE, io, lists, recon_trace_use_dbg], %% The banned mod check can be bypassed by using %% match specs if you really feel like being dumb. case {lists:member(Mod, BannedMods), Args} of @@ -515,7 +518,12 @@ validate_io_server(Opts) -> %% Thanks Geoff Cant for the foundations for this. format(TraceMsg) -> {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), - {FormatStr, FormatArgs} = case {Type, TraceInfo} of + {FormatStr, FormatArgs} = trace_to_io(Type, TraceInfo), + io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", + [Hour, Min, Sec, Pid] ++ FormatArgs). + +trace_to_io(Type, TraceInfo) -> + case {Type, TraceInfo} of %% {trace, Pid, 'receive', Msg} {'receive', [Msg]} -> {"< ~p", [Msg]}; @@ -590,9 +598,7 @@ format(TraceMsg) -> [HeapSize + OldHeapSize + MbufSize]}; _ -> {"unknown trace type ~p -- ~p", [Type, TraceInfo]} - end, - io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", - [Hour, Min, Sec, Pid] ++ FormatArgs). + end. extract_info(TraceMsg) -> case tuple_to_list(TraceMsg) of diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 9f5e98b..514725a 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -5,15 +5,16 @@ %%% Erlang nodes, currently for function calls only. Functionality includes: %%% @end -module(recon_trace_use_dbg). --compile(export_all). -include_lib("stdlib/include/ms_transform.hrl"). + +-import(recon_trace, [formatter/5, validate_opts/1, trace_to_io/2, + format/1, extract_info/1, fun_to_ms/1]). + %% API -export([clear/0, calls/2, calls/3]). - --export([format/1]). %% Internal exports --export([count_tracer/4, formatter/5, format_trace_output/1, format_trace_output/2]). +-export([count_tracer/4, rate_tracer/4]). -type matchspec() :: [{[term()] | '_', [term()], [term()]}]. -type shellfun() :: fun((_) -> term()). @@ -51,15 +52,10 @@ %% @doc Stops all tracing at once. -spec clear() -> ok. clear() -> + recon_trace:clear(), dbg:p(all,clear), dbg:ctp('_'), dbg:stop(), - erlang:trace(all, false, [all]), - erlang:trace_pattern({'_','_','_'}, false, [local,meta,call_count,call_time]), - erlang:trace_pattern({'_','_','_'}, false, []), % unsets global - maybe_kill(recon_trace_tracer), - maybe_kill(recon_trace_formatter), - maybe_kill(recon_trace_dbg_printer), ok. %% @equiv calls({Mod, Fun, Args}, Max, []) @@ -84,12 +80,6 @@ calls(TSpecs, Max, Opts) -> _ -> recon_trace:calls(TSpecs, Max, Opts) end. -% calls_dbg(TSpecs = [_|_], {Max, Time}, Opts) -> -% Pid = setup(rate_tracer, [Max, Time], -% validate_formatter(Opts), validate_io_server(Opts)), -% trace_calls(TSpecs, Pid, Opts); - - calls_dbg(TSpecs, Boundaries, Opts) -> case trace_function_types(TSpecs) of shell_fun -> @@ -102,7 +92,7 @@ calls_dbg(TSpecs, Boundaries, Opts) -> Formatter = validate_formatter(Opts), IoServer = validate_io_server(Opts), - {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), + {PidSpecs, _TraceOpts, MatchOpts} = validate_opts(Opts), PatternsFun = generate_pattern_filter(FunTSpecs, Boundaries, IoServer, Formatter), dbg:tracer(process,{PatternsFun, startValue(Boundaries)}), @@ -116,18 +106,15 @@ startValue(_Max) -> 0. dbg_tp(MFAList, MatchOpts) -> - Res = [dbg:tp({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], case MatchOpts of - [local] -> - [dbg:tpl({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList]; - [global] -> Res - end. - -preprocess_traces(Trace, TraceOpts) -> - case proplists:get_bool(arity, TraceOpts) of - true -> {return_to, Trace}; - _ -> Trace + [local] -> + Matches = [dbg:tpl({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], + lists:sum([Cnt || {ok,[{_,_,Cnt},_]}<- Matches]); + [global] -> + Matches = [dbg:tp({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], + lists:sum([Cnt || {ok,[{_,_,Cnt},_]}<- Matches]) end. + tspecs_normalization(TSpecs) -> %% Normalizes the TSpecs to be a list of tuples %% {Mod, Fun, Args} where Args is a function. @@ -162,12 +149,11 @@ generate_pattern_filter(TSpecs, Max, IoServer, Formatter) -> clear(), count_tracer(Max, TSpecs, IoServer, Formatter). -%% @private Stops when N trace messages have been received count_tracer(Max, TSpecs, IoServer, Formatter) -> fun (_Trace, {N, _}) when N > Max -> io:format("Recon tracer rate limit tripped.~n"), - dbg:stop(); + clear(); (Trace, N) when (N =< Max) and is_tuple(Trace) -> %% Type = element(1, Trace), handle_trace(Trace, N, TSpecs, IoServer, Formatter) @@ -185,8 +171,8 @@ rate_tracer({Max, Time}, TSpecs, IoServer, Formatter) -> NewN = handle_trace(Trace, N, TSpecs, IoServer, Formatter), {NewN, Timestamp}; true -> - io:format("Recon tracer rate limit tripped.~n"), - dbg:stop() + io:format("Recon tracer rate limit tripped.~n"), + clear() end end. @@ -201,14 +187,12 @@ handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> case is_process_alive(IoServer) of true -> io:format(IoServer, Formatted, []); false -> io:format("Recon tracer formater stopped.~n"), - dbg:stop() + clear() end end, N+1; _ -> N end. - %%(Trace, N) when N =< Max -> - %% io:format("Unexpexted trace~p", [Trace]), N %%----------------------------------------------------------------------- %% In the list of TSpecs, the third element of the tuple %% is responsible for matching arguments. @@ -289,8 +273,8 @@ clause_type({_head,_guard, Return}) -> filter_call(TraceMsg, TSpecs) -> filter_call(TraceMsg, TSpecs, reject). -filter_call(TraceMsg, _, print) -> print; -filter_call(TraceMsg, [], Answer) -> Answer; +filter_call(_TraceMsg, _, print) -> print; +filter_call(_TraceMsg, [], Answer) -> Answer; filter_call(TraceMsg, [{M, F, PatternFun} | TSpecs], reject) -> NewAnswer = case extract_info(TraceMsg) of {call, _, _, [{TraceM,TraceF, Args}]} -> @@ -325,111 +309,9 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> end end. -%% @private Formats traces to be output -formatter(Tracer, Parent, Ref, FormatterFun, IOServer) -> - process_flag(trap_exit, true), - link(Tracer), - Parent ! {Ref, linked}, - formatter(Tracer, IOServer, FormatterFun). - -formatter(Tracer, IOServer, FormatterFun) -> - receive - {'EXIT', Tracer, normal} -> - io:format("Recon tracer rate limit tripped.~n"), - exit(normal); - {'EXIT', Tracer, Reason} -> - exit(Reason); - TraceMsg -> - case FormatterFun(TraceMsg) of - "" -> ok; - Formatted -> io:format(IOServer, Formatted, []) - end, - formatter(Tracer, IOServer, FormatterFun) - end. - - -%%%%%%%%%%%%%%%%%%%%%%% -%%% SETUP FUNCTIONS %%% -%%%%%%%%%%%%%%%%%%%%%%% -%% Sets the traces in action -trace_calls(TSpecs, Pid, Opts) -> - {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), - Matches = [begin - {Arity, Spec} = validate_tspec(Mod, Fun, Args), - erlang:trace_pattern({Mod, Fun, Arity}, Spec, MatchOpts) - end || {Mod, Fun, Args} <- TSpecs], - [erlang:trace(PidSpec, true, [call, {tracer, Pid} | TraceOpts]) - || PidSpec <- PidSpecs], - lists:sum(Matches). - - -%%%%%%%%%%%%%%%%%% -%%% VALIDATION %%% -%%%%%%%%%%%%%%%%%% - -validate_opts(Opts) -> - PidSpecs = validate_pid_specs(proplists:get_value(pid, Opts, all)), - Scope = proplists:get_value(scope, Opts, global), - TraceOpts = case proplists:get_value(timestamp, Opts, formatter) of - formatter -> []; - trace -> [timestamp] - end ++ - case proplists:get_value(args, Opts, args) of - args -> []; - arity -> [arity] - end ++ - case proplists:get_value(return_to, Opts, undefined) of - true when Scope =:= local -> - [return_to]; - true when Scope =:= global -> - io:format("Option return_to only works with option {scope, local}~n"), - %% Set it anyway - [return_to]; - _ -> - [] - end, - MatchOpts = [Scope], - {PidSpecs, TraceOpts, MatchOpts}. - -%% Support the regular specs, but also allow `recon:pid_term()' and lists -%% of further pid specs. --spec validate_pid_specs(pidspec() | [pidspec(),...]) -> - [all | new | existing | pid(), ...]. -validate_pid_specs(all) -> [all]; -validate_pid_specs(existing) -> [existing]; -validate_pid_specs(new) -> [new]; -validate_pid_specs([Spec]) -> validate_pid_specs(Spec); -validate_pid_specs(PidTerm = [Spec|Rest]) -> - %% can be "" or [pidspec()] - try - [recon_lib:term_to_pid(PidTerm)] - catch - error:function_clause -> - validate_pid_specs(Spec) ++ validate_pid_specs(Rest) - end; -validate_pid_specs(PidTerm) -> - %% has to be `recon:pid_term()'. - [recon_lib:term_to_pid(PidTerm)]. - -validate_tspec(Mod, Fun, Args) when is_function(Args) -> - validate_tspec(Mod, Fun, fun_to_ms(Args)); -%% helper to save typing for common actions -validate_tspec(Mod, Fun, return_trace) -> - validate_tspec(Mod, Fun, [{'_', [], [{return_trace}]}]); -validate_tspec(Mod, Fun, Args) -> - BannedMods = ['_', ?MODULE, io, lists, dbg, recon_trace], - %% The banned mod check can be bypassed by using - %% match specs if you really feel like being dumb. - case {lists:member(Mod, BannedMods), Args} of - {true, '_'} -> error({dangerous_combo, {Mod,Fun,Args}}); - {true, []} -> error({dangerous_combo, {Mod,Fun,Args}}); - _ -> ok - end, - case Args of - '_' -> {'_', true}; - _ when is_list(Args) -> {'_', Args}; - _ when Args >= 0, Args =< 255 -> {Args, true} - end. +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% VALIDATE FUNCTIONS %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% validate_formatter(Opts) -> Formatter = proplists:get_value(formatter, Opts), @@ -440,7 +322,7 @@ validate_formatter(Opts) -> Formatter; {_args, Formatter} when is_function(Formatter, 1) -> Formatter; {arity, Formatter} -> generate_formatter(Formatter, arity); - _ -> fun format/1 + _ -> fun recon_trace:format/1 end. validate_io_server(Opts) -> @@ -459,7 +341,7 @@ generate_formatter(Formatter, arity) -> [Hour, Min, Sec, Pid] ++ FormatArgs) end; generate_formatter(_Formatter, _) -> - fun format/1. + fun recon_trace:format/1. trace_calls_to_arity(TypeTraceInfo) -> case TypeTraceInfo of @@ -470,229 +352,4 @@ trace_calls_to_arity(TypeTraceInfo) -> Trace -> Trace end. -trace_to_io(Type, TraceInfo) -> - case {Type, TraceInfo} of - %% {trace, Pid, 'receive', Msg} - {'receive', [Msg]} -> - {"< ~p", [Msg]}; - %% {trace, Pid, send, Msg, To} - {send, [Msg, To]} -> - {" > ~p: ~p", [To, Msg]}; - %% {trace, Pid, send_to_non_existing_process, Msg, To} - {send_to_non_existing_process, [Msg, To]} -> - {" > (non_existent) ~p: ~p", [To, Msg]}; - %% {trace, Pid, call, {M, F, Args}} - {call, [{M,F,Args}]} -> - {"~p:~p~s", [M,F,format_args(Args)]}; - %% {trace, Pid, call, {M, F, Args}, Msg} - {call, [{M,F,Args}, Msg]} -> - {"~p:~p~s ~s", [M,F,format_args(Args), format_trace_output(Msg)]}; - %% {trace, Pid, return_to, {M, F, Arity}} - {return_to, [{M,F,Arity}]} -> - {" '--> ~p:~p/~p", [M,F,Arity]}; - %% {trace, Pid, return_from, {M, F, Arity}, ReturnValue} - {return_from, [{M,F,Arity}, Return]} -> - {"~p:~p/~p --> ~s", [M,F,Arity, format_trace_output(Return)]}; - %% {trace, Pid, exception_from, {M, F, Arity}, {Class, Value}} - {exception_from, [{M,F,Arity}, {Class,Val}]} -> - {"~p:~p/~p ~p ~p", [M,F,Arity, Class, Val]}; - %% {trace, Pid, spawn, Spawned, {M, F, Args}} - {spawn, [Spawned, {M,F,Args}]} -> - {"spawned ~p as ~p:~p~s", [Spawned, M, F, format_args(Args)]}; - %% {trace, Pid, exit, Reason} - {exit, [Reason]} -> - {"EXIT ~p", [Reason]}; - %% {trace, Pid, link, Pid2} - {link, [Linked]} -> - {"link(~p)", [Linked]}; - %% {trace, Pid, unlink, Pid2} - {unlink, [Linked]} -> - {"unlink(~p)", [Linked]}; - %% {trace, Pid, getting_linked, Pid2} - {getting_linked, [Linker]} -> - {"getting linked by ~p", [Linker]}; - %% {trace, Pid, getting_unlinked, Pid2} - {getting_unlinked, [Unlinker]} -> - {"getting unlinked by ~p", [Unlinker]}; - %% {trace, Pid, register, RegName} - {register, [Name]} -> - {"registered as ~p", [Name]}; - %% {trace, Pid, unregister, RegName} - {unregister, [Name]} -> - {"no longer registered as ~p", [Name]}; - %% {trace, Pid, in, {M, F, Arity} | 0} - {in, [{M,F,Arity}]} -> - {"scheduled in for ~p:~p/~p", [M,F,Arity]}; - {in, [0]} -> - {"scheduled in", []}; - %% {trace, Pid, out, {M, F, Arity} | 0} - {out, [{M,F,Arity}]} -> - {"scheduled out from ~p:~p/~p", [M, F, Arity]}; - {out, [0]} -> - {"scheduled out", []}; - %% {trace, Pid, gc_start, Info} - {gc_start, [Info]} -> - HeapSize = proplists:get_value(heap_size, Info), - OldHeapSize = proplists:get_value(old_heap_size, Info), - MbufSize = proplists:get_value(mbuf_size, Info), - {"gc beginning -- heap ~p bytes", - [HeapSize + OldHeapSize + MbufSize]}; - %% {trace, Pid, gc_end, Info} - {gc_end, [Info]} -> - HeapSize = proplists:get_value(heap_size, Info), - OldHeapSize = proplists:get_value(old_heap_size, Info), - MbufSize = proplists:get_value(mbuf_size, Info), - {"gc finished -- heap ~p bytes", - [HeapSize + OldHeapSize + MbufSize]}; - _ -> - {"unknown trace type ~p -- ~p", [Type, TraceInfo]} - end. - - -%% Thanks Geoff Cant for the foundations for this. -format(TraceMsg) -> - {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), - {FormatStr, FormatArgs} = - trace_to_io(Type, TraceInfo), - io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", - [Hour, Min, Sec, Pid] ++ FormatArgs). - -extract_info(TraceMsg) -> - case tuple_to_list(TraceMsg) of - [trace_ts, Pid, Type | Info] -> - {TraceInfo, [Timestamp]} = lists:split(length(Info)-1, Info), - {Type, Pid, to_hms(Timestamp), TraceInfo}; - [trace, Pid, Type | TraceInfo] -> - {Type, Pid, to_hms(os:timestamp()), TraceInfo} - end. - -to_hms(Stamp = {_, _, Micro}) -> - {_,{H, M, Secs}} = calendar:now_to_local_time(Stamp), - Seconds = Secs rem 60 + (Micro / 1000000), - {H,M,Seconds}; -to_hms(_) -> - {0,0,0}. - -format_args(Arity) when is_integer(Arity) -> - [$/, integer_to_list(Arity)]; -format_args(Args) when is_list(Args) -> - [$(, join(", ", [format_trace_output(Arg) || Arg <- Args]), $)]. - - -%% @doc formats call arguments and return values - most types are just printed out, except for -%% tuples recognised as records, which mimic the source code syntax -%% @end -format_trace_output(Args) -> - format_trace_output(recon_rec:is_active(), recon_map:is_active(), Args). - -format_trace_output(Recs, Args) -> - format_trace_output(Recs, recon_map:is_active(), Args). - -format_trace_output(true, _, Args) when is_tuple(Args) -> - recon_rec:format_tuple(Args); -format_trace_output(false, true, Args) when is_tuple(Args) -> - format_tuple(false, true, Args); -format_trace_output(Recs, Maps, Args) when is_list(Args), Recs orelse Maps -> - case io_lib:printable_list(Args) of - true -> - io_lib:format("~p", [Args]); - false -> - format_maybe_improper_list(Recs, Maps, Args) - end; -format_trace_output(Recs, true, Args) when is_map(Args) -> - {Label, Map} = case recon_map:process_map(Args) of - {L, M} -> {atom_to_list(L), M}; - M -> {"", M} - end, - ItemList = maps:to_list(Map), - [Label, - "#{", - join(", ", [format_kv(Recs, true, Key, Val) || {Key, Val} <- ItemList]), - "}"]; -format_trace_output(Recs, false, Args) when is_map(Args) -> - ItemList = maps:to_list(Args), - ["#{", - join(", ", [format_kv(Recs, false, Key, Val) || {Key, Val} <- ItemList]), - "}"]; -format_trace_output(_, _, Args) -> - io_lib:format("~p", [Args]). - -format_kv(Recs, Maps, Key, Val) -> - [format_trace_output(Recs, Maps, Key), "=>", format_trace_output(Recs, Maps, Val)]. - - -format_tuple(Recs, Maps, Tup) -> - [${ | format_tuple_(Recs, Maps, tuple_to_list(Tup))]. - -format_tuple_(_Recs, _Maps, []) -> - "}"; -format_tuple_(Recs, Maps, [H|T]) -> - [format_trace_output(Recs, Maps, H), $,, - format_tuple_(Recs, Maps, T)]. - - -format_maybe_improper_list(Recs, Maps, List) -> - [$[ | format_maybe_improper_list_(Recs, Maps, List)]. - -format_maybe_improper_list_(_, _, []) -> - "]"; -format_maybe_improper_list_(Recs, Maps, [H|[]]) -> - [format_trace_output(Recs, Maps, H), $]]; -format_maybe_improper_list_(Recs, Maps, [H|T]) when is_list(T) -> - [format_trace_output(Recs, Maps, H), $,, - format_maybe_improper_list_(Recs, Maps, T)]; -format_maybe_improper_list_(Recs, Maps, [H|T]) when not is_list(T) -> - %% Handling improper lists - [format_trace_output(Recs, Maps, H), $|, - format_trace_output(Recs, Maps, T), $]]. - - -%%%%%%%%%%%%%%% -%%% HELPERS %%% -%%%%%%%%%%%%%%% - -maybe_kill(Name) -> - case whereis(Name) of - undefined -> - ok; - Pid -> - unlink(Pid), - exit(Pid, kill), - wait_for_death(Pid, Name) - end. - -wait_for_death(Pid, Name) -> - case is_process_alive(Pid) orelse whereis(Name) =:= Pid of - true -> - timer:sleep(10), - wait_for_death(Pid, Name); - false -> - ok - end. - -%% Borrowed from dbg -fun_to_ms(ShellFun) when is_function(ShellFun) -> - case erl_eval:fun_data(ShellFun) of - {fun_data,ImportList,Clauses} -> - case ms_transform:transform_from_shell( - dbg,Clauses,ImportList) of - {error,[{_,[{_,_,Code}|_]}|_],_} -> - io:format("Error: ~s~n", - [ms_transform:format_error(Code)]), - {error,transform_error}; - Else -> - Else - end; - false -> - exit(shell_funs_only) - end. --ifdef(OTP_RELEASE). --spec join(term(), [term()]) -> [term()]. -join(Sep, List) -> - lists:join(Sep, List). --else. --spec join(string(), [string()]) -> string(). -join(Sep, List) -> - string:join(List, Sep). --endif. From ad21953c3790c6ddea14cd80f78a9eca1356d9a8 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 18 May 2025 00:26:40 +0200 Subject: [PATCH 17/35] Refactor tracing calls to unify recon_trace and recon_trace_use_dbg functionality --- src/recon_trace.erl | 11 ++++- src/recon_trace_use_dbg.erl | 21 +-------- test/recon_trace_use_dbg_SUITE.erl | 76 +++++++++++++++--------------- 3 files changed, 49 insertions(+), 59 deletions(-) diff --git a/src/recon_trace.erl b/src/recon_trace.erl index 35f1cd6..304532a 100644 --- a/src/recon_trace.erl +++ b/src/recon_trace.erl @@ -339,11 +339,18 @@ calls(TSpecs = [_|_], Max) -> calls({Mod, Fun, Args}, Max, Opts) -> calls([{Mod,Fun,Args}], Max, Opts); -calls(TSpecs = [_|_], {Max, Time}, Opts) -> + +calls(TSpecs = [_|_], Boundaries, Opts) -> + case proplists:get_bool(use_dbg, Opts) of + true -> recon_trace_use_dbg:calls_dbg(TSpecs, Boundaries, Opts); + false -> spec_calls(TSpecs, Boundaries, Opts) + end. + +spec_calls(TSpecs = [_|_], {Max, Time}, Opts) -> Pid = setup(rate_tracer, [Max, Time], validate_formatter(Opts), validate_io_server(Opts)), trace_calls(TSpecs, Pid, Opts); -calls(TSpecs = [_|_], Max, Opts) -> +spec_calls(TSpecs = [_|_], Max, Opts) -> Pid = setup(count_tracer, [Max], validate_formatter(Opts), validate_io_server(Opts)), trace_calls(TSpecs, Pid, Opts). diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 514725a..aba5509 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -11,7 +11,7 @@ format/1, extract_info/1, fun_to_ms/1]). %% API --export([clear/0, calls/2, calls/3]). +-export([clear/0, calls_dbg/3]). %% Internal exports -export([count_tracer/4, rate_tracer/4]). @@ -58,28 +58,11 @@ clear() -> dbg:stop(), ok. -%% @equiv calls({Mod, Fun, Args}, Max, []) --spec calls(tspec() | [tspec(),...], max()) -> num_matches(). -calls({Mod, Fun, Args}, Max) -> - calls([{Mod,Fun,Args}], Max, []); -calls(TSpecs = [_|_], Max) -> - calls(TSpecs, Max, []). - - %% @doc Allows to set trace patterns and pid specifications to trace %% function calls. %% %% @end --spec calls(tspec() | [tspec(),...], max(), options()) -> num_matches(). - -calls({Mod, Fun, Args}, Max, Opts) -> - calls([{Mod,Fun,Args}], Max, Opts); -calls(TSpecs, Max, Opts) -> - case proplists:get_bool(use_dbg, Opts) of - true -> calls_dbg(TSpecs, Max, Opts); - _ -> recon_trace:calls(TSpecs, Max, Opts) - end. - +-spec calls_dbg(tspec() | [tspec(),...], max(), options()) -> num_matches(). calls_dbg(TSpecs, Boundaries, Opts) -> case trace_function_types(TSpecs) of shell_fun -> diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 5ebea88..d25a9cc 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -144,7 +144,7 @@ assert_trace_no_match(RegexString, TraceOutput) -> dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, light_state, fun([cast, switch_state, _]) -> anything end}, 10, + recon_trace:calls({test_statem, light_state, fun([cast, switch_state, _]) -> anything end}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), test_statem:switch_state(), @@ -159,7 +159,7 @@ dummy_basic_trace_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), - recon_trace_use_dbg:calls({test_statem, light_state, MatchSpec}, 10, + recon_trace:calls({test_statem, light_state, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), test_statem:switch_state(), @@ -180,15 +180,15 @@ dummy_basic_trace_test(Config) -> %%====================================================================== %% Documentation: All calls from the queue module, with 10 calls printed at most: -%% recon_trace_use_dbg:calls({queue, '_', '_'}, 10) +%% recon_trace:calls({queue, '_', '_'}, 10) %%--- %% Test: All calls from the test_statem module, with 10 calls printed at most. %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10) +%% Function: recon_trace:calls({test_statem, '_', '_'}, 10) %%====================================================================== trace_full_module_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, '_', '_'}, 100, + recon_trace:calls({test_statem, '_', '_'}, 100, [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(100), @@ -206,15 +206,15 @@ trace_full_module_test(Config) -> count_trace_match("test_statem:heavy_state\\(cast, switch_state, #{iterator=>", TraceOutput,1), ok. %%====================================================================== -%% Documentation: All calls to lists:seq(A,B), with 100 calls printed at most: recon_trace_use_dbg:calls({lists, seq, 2}, 100) +%% Documentation: All calls to lists:seq(A,B), with 100 calls printed at most: recon_trace:calls({lists, seq, 2}, 100) %%--- %% Test: All calls from the test_statem:get_state module, with 10 calls printed at most. %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, get_state, '_'}, 10) +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10) %%====================================================================== trace_one_function_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, get_state, 0}, 100, + recon_trace:calls({test_statem, get_state, 0}, 100, [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(100), @@ -231,11 +231,11 @@ trace_one_function_test(Config) -> ok. %%====================================================================== -%% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace_use_dbg:calls({lists, seq, 2}, {100, 1000}) +%% Documentation: All calls to lists:seq(A,B), with 100 calls per second at most: recon_trace:calls({lists, seq, 2}, {100, 1000}) %%--- %% Test: All calls to test_statem:heavy_state(A,B), with 1 call per second printed at most: %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, heavy_state, 2}, 10) +%% Function: recon_trace:calls({test_statem, heavy_state, 2}, 10) %%====================================================================== trace_rate_limit_test(Config) -> @@ -246,7 +246,7 @@ trace_rate_limit_test(Config) -> {ok,heavy_state,_} -> ok end, - recon_trace_use_dbg:calls({test_statem, heavy_state, 3}, {1, 1000}, + recon_trace:calls({test_statem, heavy_state, 3}, {1, 1000}, [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(2200), % Allow more time for potential rate limiting delays @@ -259,11 +259,11 @@ trace_rate_limit_test(Config) -> %%====================================================================== %% Documentation: All calls to lists:seq(A,B,2) (all sequences increasing by two) with 100 calls at most: -%% recon_trace_use_dbg:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) +%% recon_trace:calls({lists, seq, fun([_,_,2]) -> ok end}, 100) %%--- %% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) +%% Function: recon_trace:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) %%====================================================================== trace_even_arg_test(Config) -> @@ -275,7 +275,7 @@ trace_even_arg_test(Config) -> end, MatchSpec = dbg:fun2ms(fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace() end), - recon_trace_use_dbg:calls({test_statem, heavy_state, MatchSpec}, 10, + recon_trace:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), timer:sleep(1900), recon_trace:clear(), @@ -285,11 +285,11 @@ trace_even_arg_test(Config) -> ok. %%====================================================================== %% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): -%% recon_trace_use_dbg:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) %%--- %% Test: All calls to iolist_to_binary/1 made with a binary argument. %%--- -%% Function: recon_trace_use_dbg:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) %%--- %% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell %%====================================================================== @@ -298,7 +298,7 @@ trace_iolist_to_binary_with_binary_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), - recon_trace_use_dbg:calls({erlang, iolist_to_binary, MatchSpec}, 10, + recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace @@ -319,11 +319,11 @@ trace_iolist_to_binary_with_binary_test(Config) -> %%====================================================================== %% Documentation: Calls to the queue module only in a given process Pid, -%% at a rate of 50 per second at most: recon_trace_use_dbg:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) +%% at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) %%--- %% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most. %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}]) +%% Function: recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}]) %%====================================================================== trace_specific_pid_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -336,7 +336,7 @@ trace_specific_pid_test(Config) -> %% new statem in light state {ok, Pid} = gen_statem:start(test_statem, [], []), - recon_trace_use_dbg:calls({test_statem, '_', '_'}, {10,1000}, + recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}, {io_server, FH}, {use_dbg, true}, {scope,local}]), gen_statem:call(Pid, get_value), @@ -358,16 +358,16 @@ trace_specific_pid_test(Config) -> %%====================================================================== %% Documentation: Print the traces with the function arity instead of literal arguments: -%% recon_trace_use_dbg:calls(TSpec, Max, [{args, arity}]) +%% recon_trace:calls(TSpec, Max, [{args, arity}]) %%--- %% Test: Print traces for test_statem calls with arity instead of arguments. %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10, [{args, arity}]) +%% Function: recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}]) %%====================================================================== trace_arity_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - recon_trace_use_dbg:calls({test_statem, '_', '_'}, 10, + recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}, {io_server, FH}, {use_dbg, true}, {scope,local}]), test_statem:get_state(), @@ -386,11 +386,11 @@ trace_arity_test(Config) -> %%====================================================================== %% Documentation: Matching the filter/2 functions of both dict and lists modules, across new processes only: -%% recon_trace_use_dbg:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) +%% recon_trace:calls([{dict,filter,2},{lists,filter,2}], 10, [{pid, new}]) %%--- %% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only. %%--- -%% Function: recon_trace_use_dbg:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) +%% Function: recon_trace:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) %%====================================================================== trace_spec_list_new_procs_only_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -400,7 +400,7 @@ trace_spec_list_new_procs_only_test(Config) -> {ok,heavy_state,_} -> ok end, - recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, [{pid, new}, {io_server, FH}, {use_dbg, true}, {scope,local}]), {ok, heavy_state,_} = test_statem:get_state(), @@ -425,11 +425,11 @@ trace_spec_list_new_procs_only_test(Config) -> ok. %%====================================================================== %% Documentation: Tracing the handle_call/3 functions of a given module for all new processes, and those of an existing one registered with gproc: -%% recon_trace_use_dbg:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) +%% recon_trace:calls({Mod,handle_call,3}, {10,100}, [{pid, [{via, gproc, Name}, new]}]) %%--- %% Test: Tracing test_statem for new processes and one via custom process register. %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, fake_reg, ts_test}, new]}]) +%% Function: recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, fake_reg, ts_test}, new]}]) %%====================================================================== trace_handle_call_new_and_custom_registry_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -441,7 +441,7 @@ trace_handle_call_new_and_custom_registry_test(Config) -> fake_reg:start(), {ok, NewPid} = gen_statem:start({via, fake_reg, ts_test}, test_statem, [], []), - recon_trace_use_dbg:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, [{pid, [{via, fake_reg, ts_test}, new]}, {io_server, FH}, {use_dbg, true}, {scope,local}]), gen_statem:call({via, fake_reg, ts_test}, get_value), @@ -463,11 +463,11 @@ trace_handle_call_new_and_custom_registry_test(Config) -> fake_reg:stop() end. %%====================================================================== -%% Documentation: Show the result of a given function call: recon_trace_use_dbg:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) +%% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) %%--- %% Test: Show the result of test_statem:get_state/0 calls. %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) +%% Function: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) %%--- %% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell %%====================================================================== @@ -480,7 +480,7 @@ trace_return_shellfun_test(Config) -> MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), - recon_trace_use_dbg:calls({test_statem, get_state, MatchSpec}, 10, + recon_trace:calls({test_statem, get_state, MatchSpec}, 10, [{io_server, FH}, use_dbg, {scope,local}]), {ok,light_state, N} = test_statem:get_state(), @@ -494,11 +494,11 @@ trace_return_shellfun_test(Config) -> ok. %%====================================================================== %% Documentation: Show the result of a given function call: -%% recon_trace_use_dbg:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), +%% recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), %%--- %% Test: Show the result of test_statem:get_state/0 calls (using match spec). %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) +%% Function: recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) %%====================================================================== trace_return_matchspec_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -508,7 +508,7 @@ trace_return_matchspec_test(Config) -> {ok,light_state,_} -> test_statem:switch_state() end, %% Trace the API function test_statem:get_state/1 using match spec - recon_trace_use_dbg:calls({test_statem, get_state, + recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10, [ {io_server, FH}, {use_dbg, true}, {scope,local}]), {ok,heavy_state, N} = test_statem:get_state(), @@ -521,11 +521,11 @@ trace_return_matchspec_test(Config) -> ok. %%====================================================================== %% Documentation: A short-hand version for this pattern of 'match anything, trace everything' -%% for a function is recon_trace_use_dbg:calls({Mod, Fun, return_trace}). +%% for a function is recon_trace:calls({Mod, Fun, return_trace}). %%--- %% Test: Show the result of test_statem:get_state/0 calls (shorthand). %%--- -%% Function: recon_trace_use_dbg:calls({test_statem, get_state, return_trace}, 10). +%% Function: recon_trace:calls({test_statem, get_state, return_trace}, 10). %%====================================================================== trace_return_shorthand_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -534,7 +534,7 @@ trace_return_shorthand_test(Config) -> {ok,heavy_state,_} -> test_statem:switch_state() end, %% Trace the API function test_statem:get_state/1 using shorthand - recon_trace_use_dbg:calls({test_statem, get_state, return_trace}, 10, + recon_trace:calls({test_statem, get_state, return_trace}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), {ok,light_state, N} = test_statem:get_state(), timer:sleep(100), From d3619a6da74479c4e83286d351fae39067ae5eb9 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 18 May 2025 17:10:47 +0200 Subject: [PATCH 18/35] Enhance tracing functionality by adding clear support for recon_trace_use_dbg and integrating new test cases for timestamp and binary pattern matching. --- src/recon_trace.erl | 4 + src/recon_trace_use_dbg.erl | 50 ++++----- test/recon_trace_SUITE.erl | 33 +++++- test/recon_trace_use_dbg_SUITE.erl | 172 +++++++++++++++++++++++++++-- 4 files changed, 221 insertions(+), 38 deletions(-) diff --git a/src/recon_trace.erl b/src/recon_trace.erl index 304532a..d747aa5 100644 --- a/src/recon_trace.erl +++ b/src/recon_trace.erl @@ -231,6 +231,10 @@ clear() -> erlang:trace_pattern({'_','_','_'}, false, []), % unsets global maybe_kill(recon_trace_tracer), maybe_kill(recon_trace_formatter), + %% for recon_trace_use_dbg + dbg:p(all,clear), + dbg:ctp('_'), + dbg:stop(), ok. %% @equiv calls({Mod, Fun, Args}, Max, []) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index aba5509..b05c47f 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -1,17 +1,18 @@ -%%% @author Fred Hebert -%%% [http://ferd.ca/] +%%% @author +%%% [https://flmath.github.io] %%% @doc -%%% `recon_trace' is a module that handles tracing in a safe manner for single -%%% Erlang nodes, currently for function calls only. Functionality includes: +%%% `recon_trace_use_dbg' is a module that allows API of recon use dbg module +%%% The added value of this solution is more flexibility in the pattern matching +%%% you can pattern match any structure you BEAM can put into function guard. %%% @end -module(recon_trace_use_dbg). -include_lib("stdlib/include/ms_transform.hrl"). -import(recon_trace, [formatter/5, validate_opts/1, trace_to_io/2, - format/1, extract_info/1, fun_to_ms/1]). + format/1, extract_info/1, fun_to_ms/1, clear/0]). %% API --export([clear/0, calls_dbg/3]). +-export([calls_dbg/3]). %% Internal exports -export([count_tracer/4, rate_tracer/4]). @@ -48,16 +49,6 @@ %%%%%%%%%%%%%% %%% PUBLIC %%% %%%%%%%%%%%%%% - -%% @doc Stops all tracing at once. --spec clear() -> ok. -clear() -> - recon_trace:clear(), - dbg:p(all,clear), - dbg:ctp('_'), - dbg:stop(), - ok. - %% @doc Allows to set trace patterns and pid specifications to trace %% function calls. %% @@ -75,11 +66,13 @@ calls_dbg(TSpecs, Boundaries, Opts) -> Formatter = validate_formatter(Opts), IoServer = validate_io_server(Opts), - {PidSpecs, _TraceOpts, MatchOpts} = validate_opts(Opts), + {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), PatternsFun = generate_pattern_filter(FunTSpecs, Boundaries, IoServer, Formatter), dbg:tracer(process,{PatternsFun, startValue(Boundaries)}), - dbg:p(hd(PidSpecs), [c]), + %% we want to receive full traces to match them then we can calculate arity + ProcessOpts = [c]++proplists:delete(arity, TraceOpts), + dbg:p(hd(PidSpecs), ProcessOpts), dbg_tp(TSpecs, MatchOpts) end. @@ -160,6 +153,9 @@ rate_tracer({Max, Time}, TSpecs, IoServer, Formatter) -> end. handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> + io:format("aaaaaaaaaaaaaaaaaaaaaaaRecon tracer: ~p~n", [Trace]), + io:format("aaaaaaaaaaaaaaaaaaaaaaaReconssss tracer: ~p~n", [TSpecs]), + Print = filter_call(Trace, TSpecs), case Print of reject -> N; @@ -195,7 +191,7 @@ trace_function_types(TSpecs) -> FunTypes= [trace_function_type(Args) || {_, _, Args} <- TSpecs], HasShell = lists:any(fun(T) -> T == shell_fun end, FunTypes), - HasStandard = lists:all(fun(T) -> T == standard_fun end, FunTypes), + HasStandard = lists:any(fun(T) -> T == standard_fun end, FunTypes), case {HasShell, HasStandard} of {true, true} -> exit(mixed_function_types); {true, false} -> shell_fun; @@ -246,8 +242,6 @@ clause_type({_head,_guard, Return}) -> _ -> standard_fun end. - - % @doc %% @private Filters the trace messages %% and calls the pattern function @@ -281,13 +275,18 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> false -> reject; check -> try erlang:apply(PatternFun, [Args]) of - _ -> print + + _ -> io:format("abRecon tracer: ~p~n", [Args]), + print catch error:function_clause -> + io:format("acRecon tracer: ~p~n", [Args]), reject; error:arity_no_match -> + io:format("adRecon tracer: ~p~n", [Args]), reject; error:E -> + io:format("aeRecon tracer: ~p~n", [E]), reject end end. @@ -304,8 +303,7 @@ validate_formatter(Opts) -> io:format("Custom formater, arity option ignored ~n"), Formatter; {_args, Formatter} when is_function(Formatter, 1) -> Formatter; - {arity, Formatter} -> generate_formatter(Formatter, arity); - _ -> fun recon_trace:format/1 + {Args, _Formatter} -> default_formatter(Args) end. validate_io_server(Opts) -> @@ -314,7 +312,7 @@ validate_io_server(Opts) -> %%%%%%%%%%%%%%%%%%%%%%%% %%% TRACE FORMATTING %%% %%%%%%%%%%%%%%%%%%%%%%%% -generate_formatter(Formatter, arity) -> +default_formatter(arity) -> fun(TraceMsg) -> {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), {_, UpdTrace} = trace_calls_to_arity({Type, TraceInfo}), @@ -323,7 +321,7 @@ generate_formatter(Formatter, arity) -> io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", [Hour, Min, Sec, Pid] ++ FormatArgs) end; -generate_formatter(_Formatter, _) -> +default_formatter(_) -> fun recon_trace:format/1. trace_calls_to_arity(TypeTraceInfo) -> diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl index 8cb9fca..987188a 100644 --- a/test/recon_trace_SUITE.erl +++ b/test/recon_trace_SUITE.erl @@ -39,7 +39,8 @@ trace_handle_call_new_and_custom_registry_test/1, trace_return_shellfun_test/1, trace_return_matchspec_test/1, - trace_return_shorthand_test/1 + trace_return_shorthand_test/1, + trace_timestamp_test/1 ]). %%-------------------------------------------------------------------- @@ -68,7 +69,8 @@ groups() -> trace_handle_call_new_and_custom_registry_test, trace_return_shellfun_test, trace_return_matchspec_test, - trace_return_shorthand_test + trace_return_shorthand_test, + trace_timestamp_test ] } ]. @@ -488,6 +490,7 @@ trace_return_matchspec_test(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state/0 --> {ok,heavy_state,"++integer_to_list(N)++"}", TraceOutput), ok. + %%====================================================================== %% Documentation: A short-hand version for this pattern of 'match anything, trace everything' %% for a function is recon_trace:calls({Mod, Fun, return_trace}). @@ -514,3 +517,29 @@ trace_return_shorthand_test(Config) -> assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), ok. +%%====================================================================== +%% Documentation: The timestamp option adds a timestamp to the trace output. +%% recon_trace:calls({Mod, Fun, return_trace}, 10, [{timestamp, true}]). +%%--- +%% Test: Show the result of test_statem:get_state/0 calls. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). +%%====================================================================== +trace_timestamp_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, + %% Trace the API function test_statem:get_state/1 using shorthand + recon_trace:calls({test_statem, get_state, '_'}, 10, + [ {io_server, FH},{scope,local}, {timestamp, trace}]), + {ok,light_state, N} = test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + + %% Check for the call and its return value + assert_trace_match("test_statem:get_state\\(", TraceOutput), + ok. diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index d25a9cc..a8ac633 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -31,7 +31,10 @@ dummy_basic_trace_test/1, trace_full_module_test/1, trace_one_function_test/1, - trace_rate_limit_test/1, + trace_map_match_test/1, + trace_binary_all_pattern_test/1, + trace_binary_patterns_test/1, + trace_rate_limit_test/1, trace_even_arg_test/1, trace_iolist_to_binary_with_binary_test/1, trace_specific_pid_test/1, @@ -40,7 +43,8 @@ trace_handle_call_new_and_custom_registry_test/1, trace_return_shellfun_test/1, trace_return_matchspec_test/1, - trace_return_shorthand_test/1 + trace_return_shorthand_test/1, + trace_timestamp_test/1 ]). %%-------------------------------------------------------------------- @@ -49,14 +53,11 @@ %% @doc Returns list of test cases and/or groups to be executed. all() -> - [{group, basic_test}, {group, doc_based_test}]. + [{group, use_dbg_test}, {group, doc_based_test}]. %% @doc Defines the test groups. groups() -> [ - {basic_test, [sequence], [ - dummy_basic_test - ]}, {doc_based_test, [sequence], [ trace_full_module_test, trace_one_function_test, @@ -71,6 +72,16 @@ groups() -> trace_return_matchspec_test, trace_return_shorthand_test ] + }, + {use_dbg_test, [sequence], [ + dummy_basic_trace_test, + trace_map_match_test, + trace_binary_patterns_test, + trace_binary_all_pattern_test, + dummy_basic_test, + trace_map_match_test, + trace_timestamp_test + ] } ]. @@ -143,20 +154,30 @@ assert_trace_no_match(RegexString, TraceOutput) -> dummy_basic_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, - recon_trace:calls({test_statem, light_state, fun([cast, switch_state, _]) -> anything end}, 10, + Num = recon_trace:calls({test_statem, light_state, fun([cast, switch_state, _]) -> true end}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), + + ct:log("Number of traces: ~p", [Num]), test_statem:switch_state(), S = test_statem:get_state(), ct:log("State: ~p", [S]), timer:sleep(100), {ok, TraceOutput} = file:read_file(FileName), - assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), + assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>", TraceOutput), ok. dummy_basic_trace_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, MatchSpec = dbg:fun2ms(fun(_) -> return_trace() end), recon_trace:calls({test_statem, light_state, MatchSpec}, 10, @@ -273,7 +294,7 @@ trace_even_arg_test(Config) -> {ok,heavy_state,_} -> ok end, - MatchSpec = dbg:fun2ms(fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace() end), + MatchSpec = fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace end, recon_trace:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -283,7 +304,41 @@ trace_even_arg_test(Config) -> count_trace_match("test_statem:heavy_state\\(timeout", TraceOutput, 3), ok. +%%%====================================================================== +%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): +%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% Test: All calls to iolist_to_binary/1 made with a binary argument. +%%--- +%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell %%====================================================================== + +trace_map_match_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = fun([#{a:=b}]) -> return_trace end, + recon_trace:calls({maps, to_list, MatchSpec}, 10, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), + + _ = maps:to_list(#{}), % Should NOT trace + _ = maps:to_list(#{a=>b}), % Should trace + _ = maps:to_list(#{a=>c}), % Should NOT trace + _ = maps:to_list(#{a=>b, c=>d}), % Should trace + + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + + recon_trace:clear(), + + assert_trace_match("maps:to_list\\(#{a=>b}\\)", TraceOutput), + assert_trace_match("maps:to_list\\(#{c=>d", TraceOutput), + assert_trace_no_match("maps:to_list\\(#{a=>c}\\)", TraceOutput), + assert_trace_no_match("maps:to_list\\(#{}\\)", TraceOutput), + ok. + +%====================================================================== %% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): %% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) %%--- @@ -297,7 +352,7 @@ trace_even_arg_test(Config) -> trace_iolist_to_binary_with_binary_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - MatchSpec = dbg:fun2ms(fun([X]) when is_binary(X) -> return_trace() end), + MatchSpec = fun([X]) when is_binary(X) -> return_trace end, recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -317,6 +372,76 @@ trace_iolist_to_binary_with_binary_test(Config) -> assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), ok. +%%====================================================================== +%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): +%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% Test: Use dbg flag to pattern match binaries. +%%--- +%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% +%%=========================================== + +trace_binary_all_pattern_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = fun([<>]) -> X end, + recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), + + _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace + _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace + _ = erlang:iolist_to_binary([<<"mix">>, "ed"]), % Should NOT trace + _ = erlang:iolist_to_binary(<<"another binary">>), % Should trace + + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + + recon_trace:clear(), + + assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), + assert_trace_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), + ok. + + +%%====================================================================== +%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): +%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% Test: Use dbg flag to pattern match binaries. +%%--- +%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% +%%=========================================== + +trace_binary_patterns_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = fun([<<"already",_/binary>>]) -> return_trace end, + recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), + + _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace + _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace + _ = erlang:iolist_to_binary([<<"mix">>, "ed"]), % Should NOT trace + _ = erlang:iolist_to_binary(<<"another binary">>), % Should NOT trace + + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + + recon_trace:clear(), + + assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), + assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), + ok. + + %%====================================================================== %% Documentation: Calls to the queue module only in a given process Pid, %% at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) @@ -545,4 +670,31 @@ trace_return_shorthand_test(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), ok. + +%%====================================================================== +%% Documentation: The timestamp option adds a timestamp to the trace output. +%% recon_trace:calls({Mod, Fun, return_trace}, 10, [{timestamp, true}]). +%%--- +%% Test: Show the result of test_statem:get_state/0 calls. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). %%====================================================================== +trace_timestamp_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, + %% Trace the API function test_statem:get_state/1 using shorthand + recon_trace:calls({test_statem, get_state, '_'}, 10, + [ {io_server, FH},{scope,local}, {timestamp, trace}, {use_dbg, true}]), + {ok,light_state, N} = test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + + %% Check for the call and its return value + assert_trace_match("test_statem:get_state\\(", TraceOutput), + ok. + From d07bbab85815d64ca38798947e3fd1b57c264678 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 18 May 2025 19:02:18 +0200 Subject: [PATCH 19/35] Enhance tracing functionality by adding return_to option and related test cases for recon_trace and recon_trace_use_dbg modules. --- src/recon_trace_use_dbg.erl | 10 +--- test/recon_trace_SUITE.erl | 40 ++++++++++++++-- test/recon_trace_use_dbg_SUITE.erl | 73 ++++++++++++++++++++++++++++-- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index b05c47f..433a19f 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -153,9 +153,6 @@ rate_tracer({Max, Time}, TSpecs, IoServer, Formatter) -> end. handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> - io:format("aaaaaaaaaaaaaaaaaaaaaaaRecon tracer: ~p~n", [Trace]), - io:format("aaaaaaaaaaaaaaaaaaaaaaaReconssss tracer: ~p~n", [TSpecs]), - Print = filter_call(Trace, TSpecs), case Print of reject -> N; @@ -275,18 +272,13 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> false -> reject; check -> try erlang:apply(PatternFun, [Args]) of - - _ -> io:format("abRecon tracer: ~p~n", [Args]), - print + _ -> print catch error:function_clause -> - io:format("acRecon tracer: ~p~n", [Args]), reject; error:arity_no_match -> - io:format("adRecon tracer: ~p~n", [Args]), reject; error:E -> - io:format("aeRecon tracer: ~p~n", [E]), reject end end. diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl index 987188a..254ed14 100644 --- a/test/recon_trace_SUITE.erl +++ b/test/recon_trace_SUITE.erl @@ -40,7 +40,8 @@ trace_return_shellfun_test/1, trace_return_matchspec_test/1, trace_return_shorthand_test/1, - trace_timestamp_test/1 + trace_timestamp_test/1, + trace_return_to_test/1 ]). %%-------------------------------------------------------------------- @@ -70,8 +71,9 @@ groups() -> trace_return_shellfun_test, trace_return_matchspec_test, trace_return_shorthand_test, - trace_timestamp_test - ] + trace_timestamp_test, + trace_return_to_test + ] } ]. @@ -516,7 +518,6 @@ trace_return_shorthand_test(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state/0 --> {ok,light_state,"++integer_to_list(N)++"}", TraceOutput), ok. - %%====================================================================== %% Documentation: The timestamp option adds a timestamp to the trace output. %% recon_trace:calls({Mod, Fun, return_trace}, 10, [{timestamp, true}]). @@ -534,7 +535,35 @@ trace_timestamp_test(Config) -> %% Trace the API function test_statem:get_state/1 using shorthand recon_trace:calls({test_statem, get_state, '_'}, 10, [ {io_server, FH},{scope,local}, {timestamp, trace}]), - {ok,light_state, N} = test_statem:get_state(), + {ok,light_state, _} = test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + + %% Check for the call and its return value + assert_trace_match("test_statem:get_state\\(", TraceOutput), + ok. + +%%====================================================================== +%% Documentation: The return_to option adds a traces for calls pointing where +%% local functions return result to. +%% recon_trace:calls({Mod, Fun, return_trace}, 10, [{return_to, true}]). +%%--- +%% Test: Show the result of test_statem:get_state/0 calls and the return to trace. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). +%%====================================================================== +trace_return_to_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, + %% Trace the API function test_statem:get_state/1 using shorthand + recon_trace:calls({test_statem, get_state, '_'}, 10, + [ {io_server, FH},{scope,local}, {return_to, true}]), + {ok,light_state, _} = test_statem:get_state(), timer:sleep(100), recon_trace:clear(), @@ -542,4 +571,5 @@ trace_timestamp_test(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state\\(", TraceOutput), + assert_trace_match("--> "++atom_to_list(?MODULE)++":trace_return_to_test/1", TraceOutput), ok. diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index a8ac633..05b53c2 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -44,9 +44,13 @@ trace_return_shellfun_test/1, trace_return_matchspec_test/1, trace_return_shorthand_test/1, - trace_timestamp_test/1 + trace_timestamp_test/1, + trace_return_to_test/1, + trace_no_return_to_test/1 ]). + + %%-------------------------------------------------------------------- %% Suite Configuration %%-------------------------------------------------------------------- @@ -80,7 +84,9 @@ groups() -> trace_binary_all_pattern_test, dummy_basic_test, trace_map_match_test, - trace_timestamp_test + trace_timestamp_test, + trace_return_to_test, + trace_no_return_to_test ] } ]. @@ -525,7 +531,7 @@ trace_spec_list_new_procs_only_test(Config) -> {ok,heavy_state,_} -> ok end, - recon_trace:calls([{test_statem, light_state, '_'}, {test_statem, heavy_state, '_'}], 10, + recon_trace:calls([{test_statem, light_state, fun(_) -> return_trace end}, {test_statem, heavy_state, '_'}], 10, [{pid, new}, {io_server, FH}, {use_dbg, true}, {scope,local}]), {ok, heavy_state,_} = test_statem:get_state(), @@ -594,7 +600,6 @@ trace_handle_call_new_and_custom_registry_test(Config) -> %%--- %% Function: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) %%--- -%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell %%====================================================================== trace_return_shellfun_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -688,7 +693,35 @@ trace_timestamp_test(Config) -> %% Trace the API function test_statem:get_state/1 using shorthand recon_trace:calls({test_statem, get_state, '_'}, 10, [ {io_server, FH},{scope,local}, {timestamp, trace}, {use_dbg, true}]), - {ok,light_state, N} = test_statem:get_state(), + {ok,light_state, _} = test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + + %% Check for the call and its return value + assert_trace_match("test_statem:get_state\\(", TraceOutput), + ok. + +%%====================================================================== +%% Documentation: The return_to option adds a traces for calls pointing where +%% local functions return result to. +%% recon_trace:calls({Mod, Fun, return_trace}, 10, [{return_to, true}]). +%%--- +%% Test: Show the result of test_statem:get_state/0 calls and the return to trace. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). +%%====================================================================== +trace_return_to_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, + %% Trace the API function test_statem:get_state/1 using shorthand + recon_trace:calls({test_statem, get_state, '_'}, 10, + [{use_dbg, true}, {io_server, FH},{scope,local}, {return_to, true}]), + {ok,light_state, _} = test_statem:get_state(), timer:sleep(100), recon_trace:clear(), @@ -696,5 +729,35 @@ trace_timestamp_test(Config) -> %% Check for the call and its return value assert_trace_match("test_statem:get_state\\(", TraceOutput), + assert_trace_match("--> "++atom_to_list(?MODULE)++":trace_return_to_test/1", TraceOutput), + ok. + +%%====================================================================== +%% Documentation: The return_to option adds a traces for calls, ensure it do not send +%% return to traces for not matching calls. +%% recon_trace:calls({Mod, Fun, return_trace}, 10, [{return_to, true}]). +%%--- +%% Test: Show no result of test_statem:get_state/0 calls and the return to trace. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). +%%====================================================================== +trace_no_return_to_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + case test_statem:get_state() of + {ok,light_state,_} -> ok; + {ok,heavy_state,_} -> test_statem:switch_state() + end, + %% Trace the API function test_statem:get_state/1 using shorthand + recon_trace:calls({test_statem, not_get_state, '_'}, 10, + [{use_dbg, true}, {io_server, FH},{scope,local}, {return_to, true}]), + {ok,light_state, _} = test_statem:get_state(), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + + %% Check for the call and its return value + assert_trace_no_match("test_statem:get_state\\(", TraceOutput), + assert_trace_no_match("--> "++atom_to_list(?MODULE)++":trace_return_to_test/1", TraceOutput), ok. From 7a627999546bc495e4d7fb034c23339118399418 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 18 May 2025 22:16:30 +0200 Subject: [PATCH 20/35] Add suppress print for recon_trace_use_dbg --- src/recon_trace_use_dbg.erl | 5 +++-- test/recon_trace_use_dbg_SUITE.erl | 36 ++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 433a19f..216d91c 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -272,13 +272,14 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> false -> reject; check -> try erlang:apply(PatternFun, [Args]) of - _ -> print + suppress -> reject; + _ -> print catch error:function_clause -> reject; error:arity_no_match -> reject; - error:E -> + error:_E -> reject end end. diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 05b53c2..ce3ccc7 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -46,7 +46,8 @@ trace_return_shorthand_test/1, trace_timestamp_test/1, trace_return_to_test/1, - trace_no_return_to_test/1 + trace_no_return_to_test/1, + trace_suppress_print_test/1 ]). @@ -86,7 +87,8 @@ groups() -> trace_map_match_test, trace_timestamp_test, trace_return_to_test, - trace_no_return_to_test + trace_no_return_to_test, + trace_suppress_print_test ] } ]. @@ -760,4 +762,34 @@ trace_no_return_to_test(Config) -> assert_trace_no_match("test_statem:get_state\\(", TraceOutput), assert_trace_no_match("--> "++atom_to_list(?MODULE)++":trace_return_to_test/1", TraceOutput), ok. +%%====================================================================== +%% Documentation: The return_to option adds a traces for calls, ensure it do not send +%% return to traces for not matching calls. +%% recon_trace:calls({Mod, Fun, return_trace}, 10, [{return_to, true}]). +%%--- +%% Test: Show no result of test_statem:get_state/0 calls and the return to trace. +%%--- +%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). +%%====================================================================== +trace_suppress_print_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = fun([enter_heavy_state,_]) -> suppress; + ([enter_light_state,_]) -> return_some end, + recon_trace:calls({test_statem, traced_function, MatchSpec}, 100, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), + + timer:sleep(100), + ok = test_statem:switch_state(), + _ = test_statem:get_state(), + timer:sleep(100), + ok = test_statem:switch_state(), + timer:sleep(100), + + {ok, TraceOutput} = file:read_file(FileName), + %% there are race conditions when test ends, so + assert_trace_no_match("test_statem:traced_function\\(enter_heavy_state", TraceOutput), + assert_trace_match("test_statem:traced_function\\(enter_light_state", TraceOutput), + ok. + \ No newline at end of file From b8dc7d942c2fd47b840c3f55288ad1f6cf02b132 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Mon, 19 May 2025 00:25:12 +0200 Subject: [PATCH 21/35] Enhance documentation and test cases for recon_trace_use_dbg module, clarifying usage of dbg flag and pattern matching for maps and binaries. --- src/recon_trace_use_dbg.erl | 234 +++++++++++++++-------------- test/recon_trace_use_dbg_SUITE.erl | 70 +++------ 2 files changed, 144 insertions(+), 160 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 216d91c..8067478 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -49,9 +49,9 @@ %%%%%%%%%%%%%% %%% PUBLIC %%% %%%%%%%%%%%%%% -%% @doc Allows to set trace patterns and pid specifications to trace -%% function calls. -%% +%% @doc +%% Allows to set trace patterns and pid specifications to trace +%% function calls using dbg module functions. %% @end -spec calls_dbg(tspec() | [tspec(),...], max(), options()) -> num_matches(). calls_dbg(TSpecs, Boundaries, Opts) -> @@ -76,20 +76,13 @@ calls_dbg(TSpecs, Boundaries, Opts) -> dbg_tp(TSpecs, MatchOpts) end. -startValue({_, _}) -> - {0, os:timestamp()}; -startValue(_Max) -> - 0. +%%%%%%%%%%%%%%%%%%%%%%% +%%% PRIVATE EXPORTS %%% +%%%%%%%%%%%%%%%%%%%%%%% -dbg_tp(MFAList, MatchOpts) -> - case MatchOpts of - [local] -> - Matches = [dbg:tpl({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], - lists:sum([Cnt || {ok,[{_,_,Cnt},_]}<- Matches]); - [global] -> - Matches = [dbg:tp({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], - lists:sum([Cnt || {ok,[{_,_,Cnt},_]}<- Matches]) - end. +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% TSPECS NORMALIZATION %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% tspecs_normalization(TSpecs) -> %% Normalizes the TSpecs to be a list of tuples @@ -112,12 +105,51 @@ args_no_fun(N) -> end end. -%%%%%%%%%%%%%%%%%%%%%%% -%%% PRIVATE EXPORTS %%% -%%%%%%%%%%%%%%%%%%%%%%% -%% starts the tracer and formatter processes, and -%% cleans them up before each call. +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% VALIDATE FORMATTER %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% + +validate_formatter(Opts) -> + Formatter = proplists:get_value(formatter, Opts), + ArgsOrArity = proplists:get_value(args, Opts), + case {ArgsOrArity, Formatter} of + {arity, Formatter} when is_function(Formatter, 1) -> + io:format("Custom formater, arity option ignored ~n"), + Formatter; + {_args, Formatter} when is_function(Formatter, 1) -> Formatter; + {Args, _Formatter} -> default_formatter(Args) + end. +default_formatter(arity) -> + fun(TraceMsg) -> + {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), + {_, UpdTrace} = trace_calls_to_arity({Type, TraceInfo}), + {FormatStr, FormatArgs} = + trace_to_io(Type, UpdTrace), + io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", + [Hour, Min, Sec, Pid] ++ FormatArgs) + end; +default_formatter(_) -> + fun recon_trace:format/1. + +trace_calls_to_arity(TypeTraceInfo) -> + case TypeTraceInfo of + {call, [{M,F,Args}]} -> + {call, [{M,F,length(Args)}]}; + {call, [{M,F,Args}, Msg]} -> + {call, [{M,F,length(Args)}, Msg]}; + Trace -> Trace + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% VALIDATE IO SERVER %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% +validate_io_server(Opts) -> + proplists:get_value(io_server, Opts, group_leader()). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% GENERATE PATTERN FILTER %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% generate_pattern_filter(TSpecs, {Max, Time}, IoServer, Formatter) -> clear(), rate_tracer({Max, Time}, TSpecs, IoServer, Formatter); @@ -169,7 +201,78 @@ handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> N+1; _ -> N end. -%%----------------------------------------------------------------------- + +filter_call(TraceMsg, TSpecs) -> + filter_call(TraceMsg, TSpecs, reject). + +filter_call(_TraceMsg, _, print) -> print; +filter_call(_TraceMsg, [], Answer) -> Answer; +filter_call(TraceMsg, [{M, F, PatternFun} | TSpecs], reject) -> + NewAnswer = case extract_info(TraceMsg) of + {call, _, _, [{TraceM,TraceF, Args}]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun); + _ -> print + end, + filter_call(TraceMsg, TSpecs, NewAnswer). + +test_match(M, F, TraceM, TraceF, Args, PatternFun) -> + Match = + case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of + {true, true, '_'} -> true; + {true, true, _} -> check; + _ -> false + end, + + case Match of + true -> print; + false -> reject; + check -> + try erlang:apply(PatternFun, [Args]) of + suppress -> reject; + _ -> print + catch + error:function_clause -> + reject; + error:arity_no_match -> + reject; + error:_E -> + reject + end + end. + +%%% Start value for the dbg tracer process state +startValue({_, _}) -> + {0, os:timestamp()}; +startValue(_Max) -> + 0. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% DEBUG TRACE PATTERN %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% @doc +%%% The function is used to establish call trace patterns +%%% If the matchspec {M,F,Args} is on the list all calls module M +%%% and function F will be sending traces to the tracer process which will use +%%% the function to print the matching traces. +%%% @end + +-spec dbg_tp([{atom(),atom(),term()}], [local] | [global]) -> num_matches(). +dbg_tp(MFAList, MatchOpts) -> + case MatchOpts of + [local] -> + Matches = [dbg:tpl({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], + lists:sum([Cnt || {ok,[{_,_,Cnt},_]}<- Matches]); + [global] -> + Matches = [dbg:tp({M, F, '_'}, [{'_', [], []}]) || {M, F, _A} <- MFAList], + lists:sum([Cnt || {ok,[{_,_,Cnt},_]}<- Matches]) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% CHECK IF FUNCTION IS NOT STANDARD RECON %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% In the list of TSpecs, the third element of the tuple %% is responsible for matching arguments. %% In standard recon_trace, the third element is a template function @@ -239,91 +342,4 @@ clause_type({_head,_guard, Return}) -> _ -> standard_fun end. -% @doc -%% @private Filters the trace messages -%% and calls the pattern function -%% @end - -filter_call(TraceMsg, TSpecs) -> - filter_call(TraceMsg, TSpecs, reject). - -filter_call(_TraceMsg, _, print) -> print; -filter_call(_TraceMsg, [], Answer) -> Answer; -filter_call(TraceMsg, [{M, F, PatternFun} | TSpecs], reject) -> - NewAnswer = case extract_info(TraceMsg) of - {call, _, _, [{TraceM,TraceF, Args}]} -> - test_match(M, F, TraceM, TraceF, Args, PatternFun); - {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> - test_match(M, F, TraceM, TraceF, Args, PatternFun); - _ -> print - end, - filter_call(TraceMsg, TSpecs, NewAnswer). - -test_match(M, F, TraceM, TraceF, Args, PatternFun) -> - Match = - case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of - {true, true, '_'} -> true; - {true, true, _} -> check; - _ -> false - end, - - case Match of - true -> print; - false -> reject; - check -> - try erlang:apply(PatternFun, [Args]) of - suppress -> reject; - _ -> print - catch - error:function_clause -> - reject; - error:arity_no_match -> - reject; - error:_E -> - reject - end - end. - -%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% VALIDATE FUNCTIONS %%% -%%%%%%%%%%%%%%%%%%%%%%%%%% - -validate_formatter(Opts) -> - Formatter = proplists:get_value(formatter, Opts), - ArgsOrArity = proplists:get_value(args, Opts), - case {ArgsOrArity, Formatter} of - {arity, Formatter} when is_function(Formatter, 1) -> - io:format("Custom formater, arity option ignored ~n"), - Formatter; - {_args, Formatter} when is_function(Formatter, 1) -> Formatter; - {Args, _Formatter} -> default_formatter(Args) - end. - -validate_io_server(Opts) -> - proplists:get_value(io_server, Opts, group_leader()). - -%%%%%%%%%%%%%%%%%%%%%%%% -%%% TRACE FORMATTING %%% -%%%%%%%%%%%%%%%%%%%%%%%% -default_formatter(arity) -> - fun(TraceMsg) -> - {Type, Pid, {Hour,Min,Sec}, TraceInfo} = extract_info(TraceMsg), - {_, UpdTrace} = trace_calls_to_arity({Type, TraceInfo}), - {FormatStr, FormatArgs} = - trace_to_io(Type, UpdTrace), - io_lib:format("~n~p:~p:~9.6.0f ~p " ++ FormatStr ++ "~n", - [Hour, Min, Sec, Pid] ++ FormatArgs) - end; -default_formatter(_) -> - fun recon_trace:format/1. - -trace_calls_to_arity(TypeTraceInfo) -> - case TypeTraceInfo of - {call, [{M,F,Args}]} -> - {call, [{M,F,length(Args)}]}; - {call, [{M,F,Args}, Msg]} -> - {call, [{M,F,length(Args)}, Msg]}; - Trace -> Trace - end. - diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index ce3ccc7..87da94c 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -199,21 +199,12 @@ dummy_basic_trace_test(Config) -> assert_trace_match("test_statem:light_state\\(cast, switch_state, #{iterator=>0", TraceOutput), ok. - - -%%====================================================================== -%% --------------------------------------------------------------------------- -%% Test cases based on https://ferd.github.io/recon/recon_trace.html#calls/3 -%% --------------------------------------------------------------------------- -%%====================================================================== - %%====================================================================== %% Documentation: All calls from the queue module, with 10 calls printed at most: %% recon_trace:calls({queue, '_', '_'}, 10) %%--- %% Test: All calls from the test_statem module, with 10 calls printed at most. %%--- -%% Function: recon_trace:calls({test_statem, '_', '_'}, 10) %%====================================================================== trace_full_module_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -239,7 +230,6 @@ trace_full_module_test(Config) -> %%--- %% Test: All calls from the test_statem:get_state module, with 10 calls printed at most. %%--- -%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10) %%====================================================================== trace_one_function_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -264,7 +254,6 @@ trace_one_function_test(Config) -> %%--- %% Test: All calls to test_statem:heavy_state(A,B), with 1 call per second printed at most: %%--- -%% Function: recon_trace:calls({test_statem, heavy_state, 2}, 10) %%====================================================================== trace_rate_limit_test(Config) -> @@ -292,7 +281,6 @@ trace_rate_limit_test(Config) -> %%--- %% Test: All calls to test_statem:heavy_state(A,B) where B is even, with 10 calls at most: %%--- -%% Function: recon_trace:calls({test_statem, heavy_state, fun([_, B]) when is_integer(B), B rem 2 == 0 -> ok end}, 10) %%====================================================================== trace_even_arg_test(Config) -> @@ -313,14 +301,11 @@ trace_even_arg_test(Config) -> count_trace_match("test_statem:heavy_state\\(timeout", TraceOutput, 3), ok. %%%====================================================================== -%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): -%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%% Documentation: Test matching maps with a specific pattern. %%--- -%% Test: All calls to iolist_to_binary/1 made with a binary argument. +%% Test: Use dbg flag to pattern match maps with a specific pattern. %%--- -%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -%%--- -%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell +%% NOTE: Possible only with the use_dbg flag. %%====================================================================== trace_map_match_test(Config) -> @@ -347,14 +332,10 @@ trace_map_match_test(Config) -> ok. %====================================================================== -%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): -%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -%%--- -%% Test: All calls to iolist_to_binary/1 made with a binary argument. +%% Documentation: Test matching binaries with binary guard. %%--- -%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%% Test: Use dbg flag to pattern match binaries with guard. %%--- -%% NOTE: Maybe there is a way to transform fun_shell directly in recon as in erlang shell %%====================================================================== trace_iolist_to_binary_with_binary_test(Config) -> @@ -381,14 +362,11 @@ trace_iolist_to_binary_with_binary_test(Config) -> ok. %%====================================================================== -%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): -%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%% Documentation: Test matching binaries. %%--- %% Test: Use dbg flag to pattern match binaries. %%--- -%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -%%--- -%% +%% NOTE: Possible only with the use_dbg flag. %%=========================================== trace_binary_all_pattern_test(Config) -> @@ -416,14 +394,13 @@ trace_binary_all_pattern_test(Config) -> %%====================================================================== -%% Documentation: All calls to iolist_to_binary/1 made with a binary as an argument already (kind of useless conversion!): -%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) -%%--- -%% Test: Use dbg flag to pattern match binaries. +%% Documentation: Test matching binaries with a specific pattern. +%% +%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) %%--- -%% Function: recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%% Test: Use dbg flag to pattern match parts of binaries. %%--- -%% +%% NOTE: Possible only with the use_dbg flag. %%=========================================== trace_binary_patterns_test(Config) -> @@ -456,7 +433,6 @@ trace_binary_patterns_test(Config) -> %%--- %% Test: Calls to the test_statem module only in the test_statem process Pid, at a rate of 10 per second at most. %%--- -%% Function: recon_trace:calls({test_statem, '_', '_'}, {10,1000}, [{pid, Pid}]) %%====================================================================== trace_specific_pid_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -495,7 +471,6 @@ trace_specific_pid_test(Config) -> %%--- %% Test: Print traces for test_statem calls with arity instead of arguments. %%--- -%% Function: recon_trace:calls({test_statem, '_', '_'}, 10, [{args, arity}]) %%====================================================================== trace_arity_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -523,7 +498,6 @@ trace_arity_test(Config) -> %%--- %% Test: Matching light_state/2 and heavy_state/2 calls in test_statem across new processes only. %%--- -%% Function: recon_trace:calls([{test_statem, light_state, 2}, {test_statem, heavy_state, 2}], 10, [{pid, new}]) %%====================================================================== trace_spec_list_new_procs_only_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -562,7 +536,6 @@ trace_spec_list_new_procs_only_test(Config) -> %%--- %% Test: Tracing test_statem for new processes and one via custom process register. %%--- -%% Function: recon_trace:calls({test_statem, handle_call, 3}, {10, 100}, [{pid, [{via, fake_reg, ts_test}, new]}]) %%====================================================================== trace_handle_call_new_and_custom_registry_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -599,8 +572,7 @@ trace_handle_call_new_and_custom_registry_test(Config) -> %% Documentation: Show the result of a given function call: recon_trace:calls({Mod,Fun,fun(_) -> return_trace() end}, Max, Opts) %%--- %% Test: Show the result of test_statem:get_state/0 calls. -%%--- -%% Function: recon_trace:calls({test_statem, get_state, fun(_) -> return_trace() end}, 10) +%% The recon_trace:calls calls back default implementation. %%--- %%====================================================================== trace_return_shellfun_test(Config) -> @@ -629,8 +601,8 @@ trace_return_shellfun_test(Config) -> %% recon_trace:calls({Mod,Fun,[{'_', [], [{return_trace}]}]}, Max, Opts), %%--- %% Test: Show the result of test_statem:get_state/0 calls (using match spec). +%% The recon_trace:calls calls back default implementation. %%--- -%% Function: recon_trace:calls({test_statem, get_state, [{'_', [], [{return_trace}]}]}, 10) %%====================================================================== trace_return_matchspec_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -657,7 +629,6 @@ trace_return_matchspec_test(Config) -> %%--- %% Test: Show the result of test_statem:get_state/0 calls (shorthand). %%--- -%% Function: recon_trace:calls({test_statem, get_state, return_trace}, 10). %%====================================================================== trace_return_shorthand_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -682,9 +653,9 @@ trace_return_shorthand_test(Config) -> %% Documentation: The timestamp option adds a timestamp to the trace output. %% recon_trace:calls({Mod, Fun, return_trace}, 10, [{timestamp, true}]). %%--- -%% Test: Show the result of test_statem:get_state/0 calls. +%% Test: Show the result of test_statem:get_state/0 calls, timestamp has different source +%% but same format. %%--- -%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). %%====================================================================== trace_timestamp_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -741,7 +712,6 @@ trace_return_to_test(Config) -> %%--- %% Test: Show no result of test_statem:get_state/0 calls and the return to trace. %%--- -%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). %%====================================================================== trace_no_return_to_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), @@ -763,13 +733,11 @@ trace_no_return_to_test(Config) -> assert_trace_no_match("--> "++atom_to_list(?MODULE)++":trace_return_to_test/1", TraceOutput), ok. %%====================================================================== -%% Documentation: The return_to option adds a traces for calls, ensure it do not send -%% return to traces for not matching calls. -%% recon_trace:calls({Mod, Fun, return_trace}, 10, [{return_to, true}]). +%% Documentation: The clauses of the match spec can be used to suppress the trace output. %%--- -%% Test: Show no result of test_statem:get_state/0 calls and the return to trace. +%% Test: Show no result of calls that return suppress. %%--- -%% Function: recon_trace:calls({test_statem, get_state, '_'}, 10). +%% Note: The suppress is right now the only special feature, but it is easy to add more. %%====================================================================== trace_suppress_print_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), From 84946c20fc44c366e0b22706ad052e6c97fd3127 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Mon, 19 May 2025 01:16:57 +0200 Subject: [PATCH 22/35] Update author information and documentation in test suites for clarity and consistency --- test/fake_reg.erl | 4 ++++ test/recon_trace_SUITE.erl | 4 ++-- test/recon_trace_use_dbg_SUITE.erl | 7 ++++--- test/test_statem.erl | 7 ++----- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/test/fake_reg.erl b/test/fake_reg.erl index 1152ea5..ec41a78 100644 --- a/test/fake_reg.erl +++ b/test/fake_reg.erl @@ -1,8 +1,12 @@ +%% @author +%% [https://flmath.github.io] +%% @doc %% Minimal fake registration module for testing %% purposes. This module simulates the behavior of the %% registration in Erlang, allowing us to %% test the functionality of our code without %% relying on the actual Erlang registry. +%% @end -module(fake_reg). -behaviour(gen_server). diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl index 254ed14..37f9ae2 100644 --- a/test/recon_trace_SUITE.erl +++ b/test/recon_trace_SUITE.erl @@ -1,5 +1,5 @@ -%%%------------------------------------------------------------------- -%%% @author 2023, Mathias Green +%%% @author +%%% [https://flmath.github.io] %%% @doc %%% Common Test Suite for recon_trace functionality. %%% Tests various scenarios based on the recon_trace documentation. diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 87da94c..c5bcdf2 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -1,8 +1,9 @@ %%%------------------------------------------------------------------- -%%% @author 2023, Mathias Green +%%% @author +%%% [https://flmath.github.io] %%% @doc -%%% Common Test Suite for recon_trace functionality. -%%% Tests various scenarios based on the recon_trace documentation. +%%% Common Test Suite for recon_trace_use_dbg functionality. +%%% Tests various scenarios related to use_dbg flag. %%% @end %%%------------------------------------------------------------------- diff --git a/test/test_statem.erl b/test/test_statem.erl index 9a9a912..caa7254 100644 --- a/test/test_statem.erl +++ b/test/test_statem.erl @@ -1,15 +1,12 @@ -%%%------------------------------------------------------------------- -%%% @copyright (C) 2019, Mathias Green +%%% @author +%%% [https://flmath.github.io] %%% @doc %%% Basic statem for testing purposes %%% @end -%%% Created : 08 Jun 2019 by Mathias Green (flmath) %%%------------------------------------------------------------------- -module(test_statem). - -behaviour(gen_statem). - -include_lib("eunit/include/eunit.hrl"). From 60b16bdf464b51659567db3a52488664f1632da0 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Wed, 2 Jul 2025 00:49:08 +0200 Subject: [PATCH 23/35] Add print_accutator function and enhance rate limiting in recon_trace_use_dbg module --- src/recon_trace_use_dbg.erl | 45 ++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 8067478..5b5361a 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -15,7 +15,7 @@ -export([calls_dbg/3]). %% Internal exports --export([count_tracer/4, rate_tracer/4]). +-export([count_tracer/4, rate_tracer/4, print_accutator/2]). -type matchspec() :: [{[term()] | '_', [term()], [term()]}]. -type shellfun() :: fun((_) -> term()). @@ -144,8 +144,29 @@ trace_calls_to_arity(TypeTraceInfo) -> %%%%%%%%%%%%%%%%%%%%%%%%%% %%% VALIDATE IO SERVER %%% %%%%%%%%%%%%%%%%%%%%%%%%%% + validate_io_server(Opts) -> - proplists:get_value(io_server, Opts, group_leader()). + IoServer = proplists:get_value(io_server, Opts, group_leader()), + IoDelay = proplists:get_value(io_delay, Opts, 1), + proc_lib:spawn_link(?MODULE, print_accutator, [IoServer, IoDelay]). + +print_accutator(IoServer, IoDelay) -> + receive + {msg, Msg} -> + try io:format(IoServer, Msg, []) + catch + error:_ -> + io:format("Recon output process stopped.~n"), + clear() + end; + rate_limit_tripped -> + io:format("Recon tracer rate limit tripped.~n"), + clear(), exit(normal) + end, + receive + after IoDelay -> print_accutator(IoServer, IoDelay) + end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% GENERATE PATTERN FILTER %%% @@ -159,14 +180,13 @@ generate_pattern_filter(TSpecs, Max, IoServer, Formatter) -> count_tracer(Max, TSpecs, IoServer, Formatter) -> fun - (_Trace, {N, _}) when N > Max -> - io:format("Recon tracer rate limit tripped.~n"), - clear(); + (_Trace, N) when N > Max -> + IoServer ! rate_limit_tripped, clear(); (Trace, N) when (N =< Max) and is_tuple(Trace) -> %% Type = element(1, Trace), handle_trace(Trace, N, TSpecs, IoServer, Formatter) end. - +%% recon_trace:calls({queue,in, fun(A) -> print end}, 3, [use_dbg]). rate_tracer({Max, Time}, TSpecs, IoServer, Formatter) -> fun(Trace, {N, Timestamp}) -> Now = os:timestamp(), @@ -179,8 +199,7 @@ rate_tracer({Max, Time}, TSpecs, IoServer, Formatter) -> NewN = handle_trace(Trace, N, TSpecs, IoServer, Formatter), {NewN, Timestamp}; true -> - io:format("Recon tracer rate limit tripped.~n"), - clear() + IoServer ! rate_limit_tripped, clear() end end. @@ -191,12 +210,7 @@ handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> print -> case Formatter(Trace) of "" -> ok; - Formatted -> - case is_process_alive(IoServer) of - true -> io:format(IoServer, Formatted, []); - false -> io:format("Recon tracer formater stopped.~n"), - clear() - end + Formatted -> IoServer ! {msg, Formatted} end, N+1; _ -> N @@ -231,7 +245,8 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> check -> try erlang:apply(PatternFun, [Args]) of suppress -> reject; - _ -> print + print -> print; + _ -> print catch error:function_clause -> reject; From cc705bf2853050069b19b28d8e8bdef08debf39c Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Wed, 2 Jul 2025 03:35:05 +0200 Subject: [PATCH 24/35] Restrict tracing functionality in recon_trace_use_dbg module, now it is surpassed by default, add custom value print and test case for it --- src/recon_trace_use_dbg.erl | 25 ++++++++++---- test/recon_trace_use_dbg_SUITE.erl | 53 +++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 5b5361a..e4ab70b 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -95,12 +95,12 @@ tspecs_normalization(TSpecs) -> _ -> TSpec end || {Mod, Fun, Args} = TSpec <- TSpecs]. -pass_all(V) -> V. +pass_all(V) -> print. args_no_fun(N) -> fun(V) -> case erlang:length(V) of - N -> V; + N -> print; _ -> throw(arity_no_match) end end. @@ -156,8 +156,15 @@ print_accutator(IoServer, IoDelay) -> try io:format(IoServer, Msg, []) catch error:_ -> - io:format("Recon output process stopped.~n"), - clear() + io:format("Recon output process stopped when trace was sent.~n"), + clear(), exit(normal) + end; + {print_value, Value} -> + try io:format(IoServer, "Print value: ~p~n", [Value]) + catch + error:_ -> + io:format("Recon output process stopped when value was sent.~n"), + clear(), exit(normal) end; rate_limit_tripped -> io:format("Recon tracer rate limit tripped.~n"), @@ -186,7 +193,7 @@ count_tracer(Max, TSpecs, IoServer, Formatter) -> %% Type = element(1, Trace), handle_trace(Trace, N, TSpecs, IoServer, Formatter) end. -%% recon_trace:calls({queue,in, fun(A) -> print end}, 3, [use_dbg]). + rate_tracer({Max, Time}, TSpecs, IoServer, Formatter) -> fun(Trace, {N, Timestamp}) -> Now = os:timestamp(), @@ -207,6 +214,9 @@ handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> Print = filter_call(Trace, TSpecs), case Print of reject -> N; + {print, Value} -> + IoServer ! {print_value, Value}, + N+1; print -> case Formatter(Trace) of "" -> ok; @@ -219,6 +229,7 @@ handle_trace(Trace, N, TSpecs, IoServer, Formatter) -> filter_call(TraceMsg, TSpecs) -> filter_call(TraceMsg, TSpecs, reject). +filter_call(_TraceMsg, _, {print, Value}) -> {print, Value}; filter_call(_TraceMsg, _, print) -> print; filter_call(_TraceMsg, [], Answer) -> Answer; filter_call(TraceMsg, [{M, F, PatternFun} | TSpecs], reject) -> @@ -227,6 +238,7 @@ filter_call(TraceMsg, [{M, F, PatternFun} | TSpecs], reject) -> test_match(M, F, TraceM, TraceF, Args, PatternFun); {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> test_match(M, F, TraceM, TraceF, Args, PatternFun); + %% if the trace is not a call, we just print it _ -> print end, filter_call(TraceMsg, TSpecs, NewAnswer). @@ -246,7 +258,8 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> try erlang:apply(PatternFun, [Args]) of suppress -> reject; print -> print; - _ -> print + {print, Value} -> {print, Value}; + _ -> reject catch error:function_clause -> reject; diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index c5bcdf2..c429812 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -48,7 +48,8 @@ trace_timestamp_test/1, trace_return_to_test/1, trace_no_return_to_test/1, - trace_suppress_print_test/1 + trace_suppress_print_test/1, + trace_custom_value_print_test/1 ]). @@ -89,7 +90,9 @@ groups() -> trace_timestamp_test, trace_return_to_test, trace_no_return_to_test, - trace_suppress_print_test + trace_suppress_print_test, + trace_custom_value_print_test + ] } ]. @@ -168,7 +171,7 @@ dummy_basic_test(Config) -> {ok,heavy_state,_} -> test_statem:switch_state() end, - Num = recon_trace:calls({test_statem, light_state, fun([cast, switch_state, _]) -> true end}, 10, + Num = recon_trace:calls({test_statem, light_state, fun([cast, switch_state, _]) -> print end}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), ct:log("Number of traces: ~p", [Num]), @@ -291,7 +294,7 @@ trace_even_arg_test(Config) -> {ok,heavy_state,_} -> ok end, - MatchSpec = fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace end, + MatchSpec = fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> print end, recon_trace:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -312,7 +315,7 @@ trace_even_arg_test(Config) -> trace_map_match_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - MatchSpec = fun([#{a:=b}]) -> return_trace end, + MatchSpec = fun([#{a:=b}]) -> print end, recon_trace:calls({maps, to_list, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -342,7 +345,7 @@ trace_map_match_test(Config) -> trace_iolist_to_binary_with_binary_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - MatchSpec = fun([X]) when is_binary(X) -> return_trace end, + MatchSpec = fun([X]) when is_binary(X) -> print end, recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -373,7 +376,7 @@ trace_iolist_to_binary_with_binary_test(Config) -> trace_binary_all_pattern_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - MatchSpec = fun([<>]) -> X end, + MatchSpec = fun([<<_X/binary>>]) -> print end, recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -407,7 +410,7 @@ trace_binary_all_pattern_test(Config) -> trace_binary_patterns_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), - MatchSpec = fun([<<"already",_/binary>>]) -> return_trace end, + MatchSpec = fun([<<"already",_/binary>>]) -> print end, recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -508,7 +511,7 @@ trace_spec_list_new_procs_only_test(Config) -> {ok,heavy_state,_} -> ok end, - recon_trace:calls([{test_statem, light_state, fun(_) -> return_trace end}, {test_statem, heavy_state, '_'}], 10, + recon_trace:calls([{test_statem, light_state, fun(_) -> print end}, {test_statem, heavy_state, '_'}], 10, [{pid, new}, {io_server, FH}, {use_dbg, true}, {scope,local}]), {ok, heavy_state,_} = test_statem:get_state(), @@ -738,13 +741,12 @@ trace_no_return_to_test(Config) -> %%--- %% Test: Show no result of calls that return suppress. %%--- -%% Note: The suppress is right now the only special feature, but it is easy to add more. %%====================================================================== trace_suppress_print_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), MatchSpec = fun([enter_heavy_state,_]) -> suppress; - ([enter_light_state,_]) -> return_some end, + ([enter_light_state,_]) -> print end, recon_trace:calls({test_statem, traced_function, MatchSpec}, 100, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -761,4 +763,31 @@ trace_suppress_print_test(Config) -> assert_trace_match("test_statem:traced_function\\(enter_light_state", TraceOutput), ok. - \ No newline at end of file +%%====================================================================== +%% Documentation: The clauses of the match spec can be used to write custom value +%% the trace output. +%%--- +%% Test: Show custom value result of calls that return {print, custom_value}. +%%--- +%%====================================================================== +trace_custom_value_print_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = fun([enter_heavy_state,_]) -> suppress; + ([enter_light_state,_]) -> {print, [custom_value]} end, + recon_trace:calls({test_statem, traced_function, MatchSpec}, 100, + [{io_server, FH}, {use_dbg, true}, {scope,local}]), + + timer:sleep(100), + ok = test_statem:switch_state(), + _ = test_statem:get_state(), + timer:sleep(100), + ok = test_statem:switch_state(), + timer:sleep(100), + + {ok, TraceOutput} = file:read_file(FileName), + %% there are race conditions when test ends, so + assert_trace_no_match("test_statem:traced_function\\(enter_heavy_state", TraceOutput), + assert_trace_no_match("test_statem:traced_function\\(enter_light_state", TraceOutput), + assert_trace_match("Print value: \\[custom_value\\]", TraceOutput), + ok. \ No newline at end of file From c6923101e1a144b6603b4b1d6dbfe28068863448 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Wed, 2 Jul 2025 09:41:20 +0200 Subject: [PATCH 25/35] Fix pattern matching in pass_all function and adjust count_tracer conditions for rate limiting --- src/recon_trace_use_dbg.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index e4ab70b..c28f316 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -95,7 +95,7 @@ tspecs_normalization(TSpecs) -> _ -> TSpec end || {Mod, Fun, Args} = TSpec <- TSpecs]. -pass_all(V) -> print. +pass_all(_) -> print. args_no_fun(N) -> fun(V) -> @@ -187,9 +187,9 @@ generate_pattern_filter(TSpecs, Max, IoServer, Formatter) -> count_tracer(Max, TSpecs, IoServer, Formatter) -> fun - (_Trace, N) when N > Max -> + (_Trace, N) when N >= Max -> IoServer ! rate_limit_tripped, clear(); - (Trace, N) when (N =< Max) and is_tuple(Trace) -> + (Trace, N) when (N < Max) and is_tuple(Trace) -> %% Type = element(1, Trace), handle_trace(Trace, N, TSpecs, IoServer, Formatter) end. From 67d627cc98568e0ac7b8ae7d22855bc29ec2c972 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Thu, 3 Jul 2025 03:24:28 +0200 Subject: [PATCH 26/35] trace_function_type to introduce silent_fun_to_ms for improved shell_fun handling in function transformation --- src/recon_trace_use_dbg.erl | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index c28f316..dbf7f50 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -9,7 +9,7 @@ -include_lib("stdlib/include/ms_transform.hrl"). -import(recon_trace, [formatter/5, validate_opts/1, trace_to_io/2, - format/1, extract_info/1, fun_to_ms/1, clear/0]). + format/1, extract_info/1, clear/0]). %% API -export([calls_dbg/3]). @@ -341,12 +341,28 @@ trace_function_type(Patterns) when is_list(Patterns) -> %% check if the is *_trace() is absent %% if every clause has *_trace() it is a shell function trace_function_type(PatternFun) when is_function(PatternFun, 1) -> - try fun_to_ms(PatternFun) of + try silent_fun_to_ms(PatternFun) of + {error,transform_error} -> standard_fun; Patterns -> trace_function_type(Patterns, undecided) catch _:_ -> standard_fun end. +silent_fun_to_ms(ShellFun) when is_function(ShellFun) -> + case erl_eval:fun_data(ShellFun) of + {fun_data,ImportList,Clauses} -> + case ms_transform:transform_from_shell( + dbg,Clauses,ImportList) of + {error,[{_,[{_,_,Code}|_]}|_],_} -> + {error,transform_error}; + Else -> + Else + end; + false -> + exit(shell_funs_only) + end. + + %% all function clauses are '_' trace_function_type([], shell_fun) -> shell_fun; trace_function_type([], standard_fun) -> standard_fun; From a1e3fb3d7ecf7ad816be1ff871807754075d50ed Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Fri, 4 Jul 2025 04:41:42 +0200 Subject: [PATCH 27/35] Refactor calls_dbg to handle multiple PIDs and add trace_multi_pid_test for enhanced tracing functionality --- src/recon_trace_use_dbg.erl | 4 +-- test/recon_trace_use_dbg_SUITE.erl | 42 +++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index dbf7f50..765c147 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -72,7 +72,7 @@ calls_dbg(TSpecs, Boundaries, Opts) -> dbg:tracer(process,{PatternsFun, startValue(Boundaries)}), %% we want to receive full traces to match them then we can calculate arity ProcessOpts = [c]++proplists:delete(arity, TraceOpts), - dbg:p(hd(PidSpecs), ProcessOpts), + lists:foreach(fun(Pid) -> dbg:p(Pid, ProcessOpts) end, PidSpecs), dbg_tp(TSpecs, MatchOpts) end. @@ -353,7 +353,7 @@ silent_fun_to_ms(ShellFun) when is_function(ShellFun) -> {fun_data,ImportList,Clauses} -> case ms_transform:transform_from_shell( dbg,Clauses,ImportList) of - {error,[{_,[{_,_,Code}|_]}|_],_} -> + {error,[{_,[{_,_,_Code}|_]}|_],_} -> {error,transform_error}; Else -> Else diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index c429812..b9b0cb4 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -39,6 +39,7 @@ trace_even_arg_test/1, trace_iolist_to_binary_with_binary_test/1, trace_specific_pid_test/1, + trace_multi_pid_test/1, trace_arity_test/1, trace_spec_list_new_procs_only_test/1, trace_handle_call_new_and_custom_registry_test/1, @@ -72,6 +73,7 @@ groups() -> trace_even_arg_test, trace_iolist_to_binary_with_binary_test, trace_specific_pid_test, + trace_multi_pid_test, trace_arity_test, trace_spec_list_new_procs_only_test, trace_handle_call_new_and_custom_registry_test, @@ -91,7 +93,7 @@ groups() -> trace_return_to_test, trace_no_return_to_test, trace_suppress_print_test, - trace_custom_value_print_test + trace_custom_value_print_test ] } @@ -469,6 +471,44 @@ trace_specific_pid_test(Config) -> is_process_alive(Pid) andalso exit(Pid, kill), % Cleanup spawned proc ok. +%%====================================================================== +%% Documentation: Calls to the queue module only in given 2 processes Pid, +%% at a rate of 50 per second at most: recon_trace:calls({queue, '_', '_'}, {50,1000}, [{pid, Pid}]) +%%--- +%% Test: Calls to the test_statem module only in the test_statem 2 processes Pids, at a rate of 10 per second at most. +%%--- +%%====================================================================== +trace_multi_pid_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + %% new statem in light state + {ok, Pid} = gen_statem:start(test_statem, [], []), + {ok, Pid2} = gen_statem:start(test_statem, [], []), + + %% the second statem state is heavy_state + gen_statem:cast(Pid2, switch_state), + + recon_trace:calls({test_statem, '_', '_'}, {10,1000}, + [{pid, [Pid, Pid2]}, {io_server, FH}, {use_dbg, true}, {scope,local}]), + + gen_statem:call(Pid, get_value), + gen_statem:call(Pid, get_value), + gen_statem:call(Pid2, get_value), + gen_statem:call(Pid2, get_value), + gen_statem:call(Pid2, get_value), + timer:sleep(100), + recon_trace:clear(), + + {ok, TraceOutput} = file:read_file(FileName), + %% Check calls originating from are traced (e.g., handle_event) + count_trace_match(".*test_statem:light_state", TraceOutput,2), + %% Check calls from the other process are NOT traced + count_trace_match(".*test_statem:heavy_state", TraceOutput,3), + is_process_alive(Pid) andalso exit(Pid, kill), % Cleanup spawned proc + is_process_alive(Pid2) andalso exit(Pid2, kill), % Cleanup spawned proc + ok. + + %%====================================================================== %% Documentation: Print the traces with the function arity instead of literal arguments: %% recon_trace:calls(TSpec, Max, [{args, arity}]) From 583ca72f5ffc9a17965ed9be3bffd6121fcf641b Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Fri, 4 Jul 2025 05:11:04 +0200 Subject: [PATCH 28/35] Enhance print_accutator output format and update trace_custom_value_print_test for new value structure --- src/recon_trace_use_dbg.erl | 2 +- test/recon_trace_use_dbg_SUITE.erl | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 765c147..0e8d099 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -160,7 +160,7 @@ print_accutator(IoServer, IoDelay) -> clear(), exit(normal) end; {print_value, Value} -> - try io:format(IoServer, "Print value: ~p~n", [Value]) + try io:format(IoServer, " ~p~n", [Value]) catch error:_ -> io:format("Recon output process stopped when value was sent.~n"), diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index b9b0cb4..54aedb6 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -814,7 +814,8 @@ trace_custom_value_print_test(Config) -> {FH, FileName} = proplists:get_value(file, Config), MatchSpec = fun([enter_heavy_state,_]) -> suppress; - ([enter_light_state,_]) -> {print, [custom_value]} end, + ([enter_light_state,_]) -> + {print, {"printed", [custom_value]}} end, recon_trace:calls({test_statem, traced_function, MatchSpec}, 100, [{io_server, FH}, {use_dbg, true}, {scope,local}]), @@ -829,5 +830,5 @@ trace_custom_value_print_test(Config) -> %% there are race conditions when test ends, so assert_trace_no_match("test_statem:traced_function\\(enter_heavy_state", TraceOutput), assert_trace_no_match("test_statem:traced_function\\(enter_light_state", TraceOutput), - assert_trace_match("Print value: \\[custom_value\\]", TraceOutput), + assert_trace_match("{\"printed\",\\[custom_value\\]}", TraceOutput), ok. \ No newline at end of file From 43489169c82da287a254441b024f4671137db58b Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Fri, 4 Jul 2025 23:35:48 +0200 Subject: [PATCH 29/35] fixing problems related to OTP24 --- src/recon_trace.erl | 2 ++ src/recon_trace_use_dbg.erl | 4 +++- test/recon_trace_SUITE.erl | 2 +- test/recon_trace_use_dbg_SUITE.erl | 5 ++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/recon_trace.erl b/src/recon_trace.erl index d747aa5..5252f7d 100644 --- a/src/recon_trace.erl +++ b/src/recon_trace.erl @@ -231,7 +231,9 @@ clear() -> erlang:trace_pattern({'_','_','_'}, false, []), % unsets global maybe_kill(recon_trace_tracer), maybe_kill(recon_trace_formatter), + %% for recon_trace_use_dbg + maybe_kill(recon_trace_tracer), dbg:p(all,clear), dbg:ctp('_'), dbg:stop(), diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index 0e8d099..ae87460 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -148,7 +148,9 @@ trace_calls_to_arity(TypeTraceInfo) -> validate_io_server(Opts) -> IoServer = proplists:get_value(io_server, Opts, group_leader()), IoDelay = proplists:get_value(io_delay, Opts, 1), - proc_lib:spawn_link(?MODULE, print_accutator, [IoServer, IoDelay]). + Pid = proc_lib:spawn_link(?MODULE, print_accutator, [IoServer, IoDelay]), + register(recon_trace_printer, Pid), + Pid. print_accutator(IoServer, IoDelay) -> receive diff --git a/test/recon_trace_SUITE.erl b/test/recon_trace_SUITE.erl index 37f9ae2..77be571 100644 --- a/test/recon_trace_SUITE.erl +++ b/test/recon_trace_SUITE.erl @@ -253,7 +253,7 @@ trace_even_arg_test(Config) -> {ok,heavy_state,_} -> ok end, - MatchSpec = dbg:fun2ms(fun([_,_,#{iterator:=N}]) when N rem 2 == 0 -> return_trace() end), + MatchSpec = dbg:fun2ms(fun([_,_,#{iterator=>N}]) when N rem 2 == 0 -> return_trace() end), recon_trace:calls({test_statem, heavy_state, MatchSpec}, 10, [{io_server, FH},{scope,local}]), timer:sleep(1900), diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 54aedb6..37d6c68 100644 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -88,7 +88,6 @@ groups() -> trace_binary_patterns_test, trace_binary_all_pattern_test, dummy_basic_test, - trace_map_match_test, trace_timestamp_test, trace_return_to_test, trace_no_return_to_test, @@ -273,7 +272,7 @@ trace_rate_limit_test(Config) -> recon_trace:calls({test_statem, heavy_state, 3}, {1, 1000}, [{io_server, FH}, {use_dbg, true}, {scope,local}]), - timer:sleep(2200), % Allow more time for potential rate limiting delays + timer:sleep(4000), % Allow more time for potential rate limiting delays recon_trace:clear(), {ok, TraceOutput} = file:read_file(FileName), @@ -332,7 +331,7 @@ trace_map_match_test(Config) -> recon_trace:clear(), assert_trace_match("maps:to_list\\(#{a=>b}\\)", TraceOutput), - assert_trace_match("maps:to_list\\(#{c=>d", TraceOutput), + assert_trace_match("maps:to_list\\(#{.*c=>d", TraceOutput), assert_trace_no_match("maps:to_list\\(#{a=>c}\\)", TraceOutput), assert_trace_no_match("maps:to_list\\(#{}\\)", TraceOutput), ok. From 7f41ff4435c90dc1df3417e51904a65c399d1be3 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 5 Jul 2025 11:46:40 +0200 Subject: [PATCH 30/35] Remove unused rebar.lock and recon.mermaid files --- rebar.lock | 1 - recon.mermaid | 49 ------------------------------------------------- 2 files changed, 50 deletions(-) delete mode 100644 rebar.lock delete mode 100644 recon.mermaid diff --git a/rebar.lock b/rebar.lock deleted file mode 100644 index 57afcca..0000000 --- a/rebar.lock +++ /dev/null @@ -1 +0,0 @@ -[]. diff --git a/recon.mermaid b/recon.mermaid deleted file mode 100644 index 7f23279..0000000 --- a/recon.mermaid +++ /dev/null @@ -1,49 +0,0 @@ -graph TD - subgraph recon_trace Internal Function Flow - - API_calls3["calls/3 (API)"] --> Internal_setup_trace(setup_trace) - API_calls2["calls/2 (API)"] --> API_calls3 - - API_clear["clear/0 (API)"] --> Internal_stop_trace(stop_trace) - - API_format["format/1 (API)"] --> Internal_do_format(do_format) - API_format_output1["format_trace_output/1 (API)"] --> Internal_do_format_output(do_format_output) - API_format_output2["format_trace_output/2 (API)"] --> Internal_do_format_output - - %% Internal Logic Functions - Internal_setup_trace --> Internal_validate_args(validate_args) - Internal_setup_trace --> Internal_start_tracer(start_tracer_process) - Internal_setup_trace --> Internal_start_formatter(start_formatter_process) - Internal_setup_trace --> Erlang_trace_pattern["erlang:trace_pattern/3"] - Internal_setup_trace --> Erlang_trace["erlang:trace/3"] - - Internal_start_tracer --> TracerLoop(tracer_loop) - Internal_start_formatter --> FormatterLoop(formatter_loop) - - TracerLoop -- receives trace --> Internal_check_limits(check_limits) - TracerLoop -- forwards trace --> FormatterLoop - TracerLoop -- calls --> RL["recon_lib (utils)"] - - - FormatterLoop -- receives trace --> Internal_do_format - Internal_do_format --> Internal_do_format_output - Internal_do_format_output -- uses --> RR["recon_rec:format/1"] - - Internal_stop_trace --> Internal_stop_process(stop_process: Tracer) - Internal_stop_trace --> Internal_stop_process(stop_process: Formatter) - Internal_stop_trace --> Erlang_trace["erlang:trace/3"] - - end - - %% Styling - classDef api fill:#BD93F9,stroke:#333,stroke-width:2px,color:#fff; - classDef internal fill:#f8f8f2,stroke:#50FA7B,stroke-width:1px,color:#282a36; - classDef external fill:#FF79C6,stroke:#333,stroke-width:1px; - classDef lib fill:#F1FA8C,stroke:#333,stroke-width:1px; - classDef rec fill:#FFB86C,stroke:#333,stroke-width:1px; - - class API_calls3,API_calls2,API_clear,API_format,API_format_output1,API_format_output2 api; - class Internal_setup_trace,Internal_validate_args,Internal_start_tracer,Internal_start_formatter,TracerLoop,FormatterLoop,Internal_check_limits,Internal_do_format,Internal_do_format_output,Internal_stop_trace,Internal_stop_process internal; - class Erlang_trace_pattern,Erlang_trace external; - class RL lib; - class RR rec; From c70b87ba20245e338f66e36835d8b3d1de2cb8ff Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sun, 6 Jul 2025 01:21:17 +0200 Subject: [PATCH 31/35] Fix print actuator naming and improve unregister logic in recon_trace_use_dbg --- src/recon_trace.erl | 1 + src/recon_trace_use_dbg.erl | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/recon_trace.erl b/src/recon_trace.erl index 5252f7d..e687701 100644 --- a/src/recon_trace.erl +++ b/src/recon_trace.erl @@ -234,6 +234,7 @@ clear() -> %% for recon_trace_use_dbg maybe_kill(recon_trace_tracer), + catch unregister(recon_trace_tracer), dbg:p(all,clear), dbg:ctp('_'), dbg:stop(), diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl index ae87460..a9c36cf 100644 --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -15,7 +15,7 @@ -export([calls_dbg/3]). %% Internal exports --export([count_tracer/4, rate_tracer/4, print_accutator/2]). +-export([count_tracer/4, rate_tracer/4, print_actuator/2]). -type matchspec() :: [{[term()] | '_', [term()], [term()]}]. -type shellfun() :: fun((_) -> term()). @@ -148,11 +148,11 @@ trace_calls_to_arity(TypeTraceInfo) -> validate_io_server(Opts) -> IoServer = proplists:get_value(io_server, Opts, group_leader()), IoDelay = proplists:get_value(io_delay, Opts, 1), - Pid = proc_lib:spawn_link(?MODULE, print_accutator, [IoServer, IoDelay]), - register(recon_trace_printer, Pid), + Pid = proc_lib:spawn_link(?MODULE, print_actuator, [IoServer, IoDelay]), + reregister(recon_trace_printer, Pid), Pid. -print_accutator(IoServer, IoDelay) -> +print_actuator(IoServer, IoDelay) -> receive {msg, Msg} -> try io:format(IoServer, Msg, []) @@ -173,7 +173,7 @@ print_accutator(IoServer, IoDelay) -> clear(), exit(normal) end, receive - after IoDelay -> print_accutator(IoServer, IoDelay) + after IoDelay -> print_actuator(IoServer, IoDelay) end. @@ -388,4 +388,12 @@ clause_type({_head,_guard, Return}) -> _ -> standard_fun end. - +reregister(Name, Pid) -> + case whereis(Name) of + undefined -> ok; + OldPid -> + unlink(OldPid), + exit(OldPid, kill), + unregister(Name) + end, + register(Name, Pid). \ No newline at end of file From 58cd92794da3794baca9e348112d9d54db280d96 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Wed, 20 Aug 2025 22:25:20 +0200 Subject: [PATCH 32/35] Implement plugin_tracker module and enhance recon_trace_use_dbg with plugin support --- src/plugins/plugin_tracker.erl | 97 ++++++++++++++++++++++++++++++ src/recon_trace_use_dbg.erl | 41 +++++++++---- test/recon_trace_use_dbg_SUITE.erl | 67 ++++++++++++++++++++- 3 files changed, 191 insertions(+), 14 deletions(-) create mode 100644 src/plugins/plugin_tracker.erl mode change 100644 => 100755 src/recon_trace_use_dbg.erl mode change 100644 => 100755 test/recon_trace_use_dbg_SUITE.erl diff --git a/src/plugins/plugin_tracker.erl b/src/plugins/plugin_tracker.erl new file mode 100644 index 0000000..83284c7 --- /dev/null +++ b/src/plugins/plugin_tracker.erl @@ -0,0 +1,97 @@ +-module(plugin_tracker). + +-export([filter_fun/5, start_value/2, is_plugin/0]). +-import(recon_trace, [formatter/5, validate_opts/1, trace_to_io/2, + format/1, extract_info/1, clear/0]). + +is_plugin() -> + true. + +filter_fun(TSpecs, Boundaries, IoServer, Formatter, _Opts) -> + generate_pattern_filter(Boundaries, TSpecs, IoServer, Formatter). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% GENERATE PATTERN FILTER %%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +generate_pattern_filter(TSpecs, {Max, Time}, IoServer, Formatter) -> + clear(), + rate_tracer({Max, Time}, TSpecs, IoServer, Formatter); +generate_pattern_filter(TSpecs, Max, IoServer, Formatter) -> + clear(), + count_tracer(Max, TSpecs, IoServer, Formatter). + +count_tracer(Max, _TSpecs, IoServer, Formatter) -> + fun + (_Trace, {_, _, N}) when N >= Max -> + IoServer ! rate_limit_tripped, clear(); + (Trace, {TSpecs, Session,N}) when (N < Max) and is_tuple(Trace) -> + %% Type = element(1, Trace), + handle_trace(Trace, N, TSpecs, IoServer, Formatter, Session) + end. + +rate_tracer({_Max, _Time}, _TSpecs, _IoServer, _Formatter) -> + io:format("Rate tracer is not supported for module: ~p~n", [?MODULE]). + + +handle_trace(Trace, N, [TSpec | TSpecRest] = TSpecs, IoServer, Formatter, Session) -> + Print = filter_call(Trace, TSpec, Session), + case Print of + reject -> {TSpecs, Session, N}; + {print, Value, NewSession} -> + IoServer ! {print_value, Value}, + {TSpecRest, NewSession, N+1}; + {print, NewSession} -> + case Formatter(Trace) of + "" -> ok; + Formatted -> IoServer ! {msg, Formatted} + end, + {TSpecRest, NewSession, N+1}; + _ -> {TSpecs, Session, N} + end. + + +filter_call(TraceMsg, {M, F, PatternFun}, Session) -> + case extract_info(TraceMsg) of + {call, _, _, [{TraceM,TraceF, Args}]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun, Session); + {call, _, _, [{TraceM, TraceF, Args}, _Msg]} -> + test_match(M, F, TraceM, TraceF, Args, PatternFun, Session); + %% if the trace is not a call, we just print it + _ -> {print, Session} + end. + +test_match(M, F, TraceM, TraceF, Args, PatternFun, Session) -> + Match = + case {M==TraceM, ((F=='_') or (F==TraceF)), PatternFun} of + {true, true, '_'} -> true; + {true, true, _} -> check; + _ -> false + end, + + case Match of + true -> {print, Session}; + false -> reject; + check -> + try erlang:apply(PatternFun, [Args, Session]) of + suppress -> reject; + print -> {print, Session}; + {print, Value} -> {print, Value, Session}; + {print_session, NewSession} -> {print, maps:merge(Session, NewSession)}; + {print_session, Value, NewSession} -> {print, Value, maps:merge(Session, NewSession)}; + _ -> reject + catch + error:function_clause -> + reject; + error:arity_no_match -> + reject; + error:_E -> + reject + end + end. + +%%% Start value for the dbg tracer process state +start_value(_, {_, _}) -> + {0, undefined, 0}; +start_value(Specs, _Max) -> + {Specs, #{}, 0}. + diff --git a/src/recon_trace_use_dbg.erl b/src/recon_trace_use_dbg.erl old mode 100644 new mode 100755 index a9c36cf..37d08da --- a/src/recon_trace_use_dbg.erl +++ b/src/recon_trace_use_dbg.erl @@ -55,21 +55,26 @@ %% @end -spec calls_dbg(tspec() | [tspec(),...], max(), options()) -> num_matches(). calls_dbg(TSpecs, Boundaries, Opts) -> - case trace_function_types(TSpecs) of - shell_fun -> + case trace_function_types(TSpecs, Opts) of + {shell_fun, _} -> io:format("Warning: TSpecs contain erlang trace template function "++ "use_dbg flag ignored, falling back to default recon_trace behaviour~n"), recon_trace:calls(TSpecs, Boundaries, proplists:delete(use_dbg, Opts)); - standard_fun -> + {standard_fun, Plugin} -> clear(), FunTSpecs = tspecs_normalization(TSpecs), Formatter = validate_formatter(Opts), IoServer = validate_io_server(Opts), {PidSpecs, TraceOpts, MatchOpts} = validate_opts(Opts), - PatternsFun = - generate_pattern_filter(FunTSpecs, Boundaries, IoServer, Formatter), - dbg:tracer(process,{PatternsFun, startValue(Boundaries)}), + {PatternsFun, StartValue} = + if Plugin == default -> + {generate_pattern_filter(FunTSpecs, Boundaries, IoServer, Formatter), + start_value(FunTSpecs, Boundaries)}; + true -> {Plugin:filter_fun(FunTSpecs, Boundaries, IoServer, Formatter, Opts), + Plugin:start_value(FunTSpecs, Boundaries)} + end, + dbg:tracer(process,{PatternsFun, StartValue}), %% we want to receive full traces to match them then we can calculate arity ProcessOpts = [c]++proplists:delete(arity, TraceOpts), lists:foreach(fun(Pid) -> dbg:p(Pid, ProcessOpts) end, PidSpecs), @@ -273,9 +278,9 @@ test_match(M, F, TraceM, TraceF, Args, PatternFun) -> end. %%% Start value for the dbg tracer process state -startValue({_, _}) -> +start_value(_, {_, _}) -> {0, os:timestamp()}; -startValue(_Max) -> +start_value(_, _Max) -> 0. @@ -307,16 +312,30 @@ dbg_tp(MFAList, MatchOpts) -> %% is responsible for matching arguments. %% In standard recon_trace, the third element is a template function %% transformed to a matchspec. -%% If use_dbg is set to true, the third element is used as actual functor and interpreted +%% If use_dbg is set to true, the third element is used as actual function and interpreted %% in a totally different way. %% The function is used to determine the type of the functions to be traced. %% They should be possible to interpret all functions in the same way. -%% For example '_' can be interpreted as a dbg trace function or a functor. -%% Since use_dbg is set to true, the function is considered by default a functor. +%% For example '_' can be interpreted as a dbg trace function or a function. +%% Since use_dbg is set to true, the function is considered by default a function. %% The function is considered a trace function if its every clause %% ends with a functions like return_trace(), %% that are translated into a {return_trace} in the matchspecs list. %% ---------------------------------------------------- +trace_function_types(TSpecs, Opts) -> + case proplists:get_value(plugin, Opts, default) of + default -> {trace_function_types(TSpecs), default}; + Plugin -> + try Plugin:is_plugin() of + true -> {standard_fun, Plugin} + catch + _:_ -> + io:format("Warning: Plugin ~p does not implement is_plugin/0 function,~n", [Plugin]), + recon_trace:clear(), + fail + end + end. + trace_function_types(TSpecs) -> FunTypes= [trace_function_type(Args) || {_, _, Args} <- TSpecs], diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl old mode 100644 new mode 100755 index 37d6c68..e14e55f --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -50,7 +50,8 @@ trace_return_to_test/1, trace_no_return_to_test/1, trace_suppress_print_test/1, - trace_custom_value_print_test/1 + trace_custom_value_print_test/1, + trace_plugin_tracker_test/1 ]). @@ -92,7 +93,8 @@ groups() -> trace_return_to_test, trace_no_return_to_test, trace_suppress_print_test, - trace_custom_value_print_test + trace_custom_value_print_test, + trace_plugin_tracker_test ] } @@ -830,4 +832,63 @@ trace_custom_value_print_test(Config) -> assert_trace_no_match("test_statem:traced_function\\(enter_heavy_state", TraceOutput), assert_trace_no_match("test_statem:traced_function\\(enter_light_state", TraceOutput), assert_trace_match("{\"printed\",\\[custom_value\\]}", TraceOutput), - ok. \ No newline at end of file + ok. + +%%====================================================================== +%% Documentation: Test matching binaries with a specific pattern. +%% +%% recon_trace:calls({erlang, iolist_to_binary, fun([X]) when is_binary(X) -> ok end}, 10) +%%--- +%% Test: Use dbg flag to pattern match parts of binaries. +%%--- +%% NOTE: Possible only with the use_dbg flag. +%%=========================================== + +trace_plugin_tracker_test(Config) -> + {FH, FileName} = proplists:get_value(file, Config), + + MatchSpec = {erlang, iolist_to_binary, + fun([ <<"request",A/binary>>], #{}) -> {print_session, #{session => A}} end}, + MatchSpec2 = {maps, to_list, + fun([#{response := A, value := V }], #{session := A}) -> {print_session, V, #{}} end}, + recon_trace:calls([MatchSpec, MatchSpec2], 10, + [{io_server, FH}, {use_dbg, true}, {scope,local}, {plugin, plugin_tracker}]), + + _ = erlang:iolist_to_binary(<<"request right">>), % Should trace + _ = erlang:iolist_to_binary(<<"already wrong">>), % Should NOT trace + _ = maps:to_list(#{ response => <<" wrong">>, value => bad }), % Should NOT trace + _ = maps:to_list(#{ response => <<" right">>, value => good }), % Should trace + timer:sleep(100), + {ok, TraceOutput} = file:read_file(FileName), + + recon_trace:clear(), + + assert_trace_match("erlang:iolist_to_binary\\(<<\"request right\">>\\)", TraceOutput), + assert_trace_no_match("wrong", TraceOutput), + assert_trace_match("good", TraceOutput), + ok. + + +% trace_binary_patterns_test(Config) -> +% {FH, FileName} = proplists:get_value(file, Config), + +% MatchSpec = fun([<<"already",_/binary>>]) -> print end, +% recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, +% [{io_server, FH}, {use_dbg, true}, {scope,local}]), + +% _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace +% _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace +% _ = erlang:iolist_to_binary([<<"mix">>, "ed"]), % Should NOT trace +% _ = erlang:iolist_to_binary(<<"another binary">>), % Should NOT trace + +% timer:sleep(100), +% {ok, TraceOutput} = file:read_file(FileName), + +% recon_trace:clear(), + +% assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), +% assert_trace_no_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), +% assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), +% assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), +% ok. + From fa6131b692c4c21eaa7f702f8ba718633fa3e2b0 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Thu, 21 Aug 2025 00:55:44 +0200 Subject: [PATCH 33/35] Add recon_plugins module with type definitions and callbacks --- src/plugins/plugin_tracker.erl | 1 + src/recon_plugins.erl | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/recon_plugins.erl diff --git a/src/plugins/plugin_tracker.erl b/src/plugins/plugin_tracker.erl index 83284c7..5565ca4 100644 --- a/src/plugins/plugin_tracker.erl +++ b/src/plugins/plugin_tracker.erl @@ -1,4 +1,5 @@ -module(plugin_tracker). +-behaviour(recon_plugins). -export([filter_fun/5, start_value/2, is_plugin/0]). -import(recon_trace, [formatter/5, validate_opts/1, trace_to_io/2, diff --git a/src/recon_plugins.erl b/src/recon_plugins.erl new file mode 100644 index 0000000..e191b10 --- /dev/null +++ b/src/recon_plugins.erl @@ -0,0 +1,21 @@ +%%% @author +%%% [https://flmath.github.io] +%%% @doc +%%% `recon_trace_use_dbg' is a module that allows API of recon use dbg module +%%% The added value of this solution is more flexibility in the pattern matching +%%% you can pattern match any structure you BEAM can put into function guard. +-module(recon_plugins). +%% API + +-type tspec() :: recon_trace_use_dbg:tspec(). +-type device() :: atom() | pid() | standard_io | standard_error | user. +-type property() :: atom() | tuple(). +-type proplist() :: [property()]. +-type init() :: recon_trace_use_dbg:max_traces(). +-type matchspecs() :: recon_trace_use_dbg:matchspecs(). + +-type filter_fun_type() :: fun((tspec() | [tspec()], init()) -> init()). + +-callback filter_fun(matchspecs(), init(), device(), pid(), proplist()) -> filter_fun_type(). +-callback is_plugin() -> boolean(). +-callback start_value(matchspecs(), init()) -> {matchspecs(), map(), init()}. From b96dfac0f99ba71bd9b11d3ed9373fac05ef0fcc Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 23 Aug 2025 13:42:04 +0200 Subject: [PATCH 34/35] Remove commented-out test case for binary patterns in recon_trace_use_dbg_SUITE --- test/recon_trace_use_dbg_SUITE.erl | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index e14e55f..3c3f8d4 100755 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -868,27 +868,3 @@ trace_plugin_tracker_test(Config) -> assert_trace_match("good", TraceOutput), ok. - -% trace_binary_patterns_test(Config) -> -% {FH, FileName} = proplists:get_value(file, Config), - -% MatchSpec = fun([<<"already",_/binary>>]) -> print end, -% recon_trace:calls({erlang, iolist_to_binary, MatchSpec}, 10, -% [{io_server, FH}, {use_dbg, true}, {scope,local}]), - -% _ = erlang:iolist_to_binary(<<"already binary">>), % Should trace -% _ = erlang:iolist_to_binary(["not binary"]), % Should NOT trace -% _ = erlang:iolist_to_binary([<<"mix">>, "ed"]), % Should NOT trace -% _ = erlang:iolist_to_binary(<<"another binary">>), % Should NOT trace - -% timer:sleep(100), -% {ok, TraceOutput} = file:read_file(FileName), - -% recon_trace:clear(), - -% assert_trace_match("erlang:iolist_to_binary\\(<<\"already binary\">>\\)", TraceOutput), -% assert_trace_no_match("erlang:iolist_to_binary\\(<<\"another binary\">>\\)", TraceOutput), -% assert_trace_no_match("erlang:iolist_to_binary\\(\\[\"not binary\"\\]\\)", TraceOutput), -% assert_trace_no_match("erlang:iolist_to_binary\\(\\[<<\"mix\">>,\"ed\"\\]\\)", TraceOutput), -% ok. - From 75371fb7c01be158a17c296d9f6cdf2b10b75f56 Mon Sep 17 00:00:00 2001 From: Mathias Green Date: Sat, 23 Aug 2025 16:13:58 +0200 Subject: [PATCH 35/35] Fix formatting in plugin_tracker and update test case for recon_trace_use_dbg_SUITE --- src/plugins/plugin_tracker.erl | 3 ++- test/recon_trace_use_dbg_SUITE.erl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/plugin_tracker.erl b/src/plugins/plugin_tracker.erl index 5565ca4..9b58866 100644 --- a/src/plugins/plugin_tracker.erl +++ b/src/plugins/plugin_tracker.erl @@ -33,7 +33,8 @@ count_tracer(Max, _TSpecs, IoServer, Formatter) -> rate_tracer({_Max, _Time}, _TSpecs, _IoServer, _Formatter) -> io:format("Rate tracer is not supported for module: ~p~n", [?MODULE]). - +handle_trace(_, _, [], IoServer, _, _) -> + IoServer ! rate_limit_tripped, clear(); handle_trace(Trace, N, [TSpec | TSpecRest] = TSpecs, IoServer, Formatter, Session) -> Print = filter_call(Trace, TSpec, Session), case Print of diff --git a/test/recon_trace_use_dbg_SUITE.erl b/test/recon_trace_use_dbg_SUITE.erl index 3c3f8d4..c683a7b 100755 --- a/test/recon_trace_use_dbg_SUITE.erl +++ b/test/recon_trace_use_dbg_SUITE.erl @@ -852,7 +852,7 @@ trace_plugin_tracker_test(Config) -> MatchSpec2 = {maps, to_list, fun([#{response := A, value := V }], #{session := A}) -> {print_session, V, #{}} end}, recon_trace:calls([MatchSpec, MatchSpec2], 10, - [{io_server, FH}, {use_dbg, true}, {scope,local}, {plugin, plugin_tracker}]), + [{io_server, FH}, {use_dbg, true}, {scope, local}, {plugin, plugin_tracker}]), _ = erlang:iolist_to_binary(<<"request right">>), % Should trace _ = erlang:iolist_to_binary(<<"already wrong">>), % Should NOT trace