| %%% Test suite for the recon module. Because many of the tests in |
| %%% here depend on runtime properties and this is *not* transparent, |
| %%% the tests are rather weak and more or less check for interface |
| %%% conformance and obvious changes more than anything. |
| -module(recon_SUITE). |
| -include_lib("common_test/include/ct.hrl"). |
| -include_lib("eunit/include/eunit.hrl"). |
| -compile(export_all). |
| |
| -ifdef(OTP_RELEASE). |
| -define(FILES_IMPL, nif). |
| -define(ERROR_LOGGER_MATCH(_), ). |
| -define(REDUCTIONS_MATCH(X), X). |
| -else. |
| -define(FILES_IMPL, port). |
| -define(ERROR_LOGGER_MATCH(X), X,). |
| -define(REDUCTIONS_MATCH(_), []). |
| -endif. |
| |
| all() -> [{group,info}, proc_count, proc_window, bin_leak, |
| node_stats_list, get_state, source, tcp, udp, files, port_types, |
| inet_count, inet_window, binary_memory, scheduler_usage]. |
| |
| groups() -> [{info, [], [info3, info4, info1, info2, info_dead, |
| port_info1, port_info2]}]. |
| |
| init_per_group(info, Config) -> |
| Self = self(), |
| Pid = spawn(fun() -> |
| {ok, TCP} = gen_tcp:listen(0, []), |
| {ok, UDP} = gen_udp:open(0), |
| Self ! {TCP, UDP}, |
| timer:sleep(infinity) |
| end), |
| receive |
| {TCP, UDP} -> [{pid, Pid}, {tcp, TCP}, {udp, UDP} | Config] |
| end. |
| |
| end_per_group(info, Config) -> |
| exit(?config(pid, Config), kill). |
| |
| init_per_testcase(files, Config) -> |
| case ?FILES_IMPL of |
| nif -> {skip, "files can no longer be listed in OTP-21 and above"}; |
| port -> Config |
| end; |
| init_per_testcase(_, Config) -> |
| Config. |
| |
| end_per_testcase(_, Config) -> |
| Config. |
| |
| %%%%%%%%%%%%% |
| %%% TESTS %%% |
| %%%%%%%%%%%%% |
| |
| info3(Config) -> |
| Pid = ?config(pid, Config), |
| {A,B,C} = pid_to_triple(Pid), |
| Info1 = recon:info(Pid), |
| Info2 = recon:info(A,B,C), |
| %% Reduction count is unreliable |
| ?assertMatch(?REDUCTIONS_MATCH( |
| [{work, [{reductions,_}]}, {work, [{reductions,_}]}] |
| ), (Info1 -- Info2) ++ (Info2 -- Info1)). |
| |
| info4(Config) -> |
| Pid = ?config(pid, Config), |
| Keys = [meta, signals, location, memory_used, |
| links, monitors, messages, |
| [links, monitors, messages]], |
| {A,B,C} = pid_to_triple(Pid), |
| lists:map(fun(Key) -> |
| Info = recon:info(Pid, Key), |
| Info = recon:info(A,B,C, Key) |
| end, |
| Keys). |
| |
| info1(Config) -> |
| Pid = ?config(pid, Config), |
| Categories = [{meta, [registered_name, dictionary, group_leader, status]}, |
| {signals, [links, monitors, monitored_by, trap_exit]}, |
| {location, [initial_call, current_stacktrace]}, |
| {memory_used, [memory, message_queue_len, heap_size, |
| total_heap_size, garbage_collection]}, |
| {work, [reductions]}], |
| [] = lists:flatten( |
| [K || {Cat,List} <- Categories, |
| K <- List, |
| Info <- [recon:info(Pid)], |
| undefined == proplists:get_value(K, proplists:get_value(Cat,Info)) |
| ]), |
| register(info1, Pid), |
| Res1 = recon:info(info1), |
| Res2 = recon:info(whereis(info1)), |
| Res3 = recon:info(pid_to_triple(whereis(info1))), |
| Res4 = recon:info(lists:flatten(io_lib:format("~p",[Pid]))), |
| unregister(info1), |
| L = lists:usort(Res1 ++ Res2 ++ Res3 ++ Res4), |
| ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, |
| {work,[{reductions,_}]}, |
| {work,[{reductions,_}]}]), L -- Res1), |
| ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, |
| {work,[{reductions,_}]}, |
| {work,[{reductions,_}]}]), L -- Res2), |
| ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, |
| {work,[{reductions,_}]}, |
| {work,[{reductions,_}]}]), L -- Res3), |
| ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]}, |
| {work,[{reductions,_}]}, |
| {work,[{reductions,_}]}]), L -- Res4), |
| ok. |
| |
| info2(Config) -> |
| Pid = ?config(pid, Config), |
| Categories = [{meta, [registered_name, dictionary, group_leader, status]}, |
| {signals, [links, monitors, monitored_by, trap_exit]}, |
| {location, [initial_call, current_stacktrace]}, |
| {memory_used, [memory, message_queue_len, heap_size, |
| total_heap_size, garbage_collection]}, |
| {work, [reductions]}], |
| %% registered_name is special -- only returns |
| %% [] when passed through by info/2. Because we pass terms through |
| %% according to the docs, we have to respect that |
| [] = recon:info(Pid, registered_name), |
| %% Register to get the expected tuple |
| register(info2, Pid), |
| Keys = lists:flatten([K || {_,L} <- Categories, K <- L]), |
| %% check that individual category call works for all terms |
| [] = lists:flatten( |
| [K || {Cat, List} <- Categories, |
| K <- List, |
| {GetCat,Info} <- [recon:info(Pid, Cat)], |
| Cat =:= GetCat, |
| undefined =:= proplists:get_value(K, Info)] |
| ), |
| %% Can get a list of arguments |
| true = lists:sort(Keys) |
| =:= |
| lists:sort(proplists:get_keys(recon:info(Pid, Keys))), |
| true = length(Keys) |
| =:= |
| length([1 || K1 <- Keys, {K2,_} <- [recon:info(Pid, K1)], |
| K1 == K2]), |
| unregister(info2). |
| |
| info_dead(_Config) -> |
| Pid = spawn(fun() -> ok end), |
| timer:sleep(10), |
| Categories = [{meta, [registered_name, dictionary, group_leader, status]}, |
| {signals, [links, monitors, monitored_by, trap_exit]}, |
| {location, [initial_call, current_stacktrace]}, |
| {memory_used, [memory, message_queue_len, heap_size, |
| total_heap_size, garbage_collection]}, |
| {work, [reductions]}], |
| undefined = recon:info(Pid, registered_name), |
| Keys = lists:flatten([K || {_,L} <- Categories, K <- L]), |
| %% check that individual category calls return undefined values |
| [] = lists:flatten( |
| [GetCat || {Cat, _List} <- Categories, |
| {GetCat,Info} <- [recon:info(Pid, Cat)], |
| Cat =:= GetCat, |
| undefined =/= Info] |
| ), |
| true = lists:all(fun(X) -> X == undefined end, |
| [recon:info(Pid, K) || K <- Keys]), |
| undefined = recon:info(Pid, Keys), |
| %% Special virtual record. |
| undefined = recon:info(Pid, binary_memory), |
| ok. |
| |
| proc_count(_Config) -> |
| Res = recon:proc_count(memory, 10), |
| true = proc_attrs(Res), |
| %% greatest to smallest |
| true = lists:usort(fun({P1,V1,_},{P2,V2,_}) -> {V1,P1} >= {V2,P2} end, |
| Res) =:= Res, |
| 10 = length(Res), |
| 15 = length(recon:proc_count(reductions, 15)). |
| |
| proc_window(_Config) -> |
| Res = recon:proc_window(reductions, 10, 100), |
| true = proc_attrs(Res), |
| %% we can't check order easily because stuff doesn't move |
| %% fast enough on a test node to show up here |
| 10 = length(Res), |
| 15 = length(recon:proc_window(memory, 15, 100)). |
| |
| bin_leak(_Config) -> |
| Res = recon:bin_leak(5), |
| 5 = length(Res), |
| true = proc_attrs(Res), |
| true = lists:any(fun({_,Val,_})-> Val =/= 0 end, Res), |
| %% all results are =< 0, and ordered from smallest to biggest |
| lists:foldl(fun({_,Current,_}, Next) when Current =< Next -> Current end, |
| 0, |
| lists:reverse(Res)). |
| |
| %% This function implicitly tests node_stats/4 |
| node_stats_list(_Config) -> |
| Res = recon:node_stats_list(2,100), |
| 2 = length([1 || {[{process_count,_}, |
| {run_queue,_}, |
| ?ERROR_LOGGER_MATCH({error_logger_queue_len,_}) |
| {memory_total,_}, |
| {memory_procs,_}, |
| {memory_atoms,_}, |
| {memory_bin,_}, |
| {memory_ets,_}|_], |
| [{bytes_in,_}, |
| {bytes_out,_}, |
| {gc_count,_}, |
| {gc_words_reclaimed,_}, |
| {reductions,_}, |
| {scheduler_usage,[_|_]}|_]} <- Res]). |
| |
| get_state(_Config) -> |
| Res = recon:get_state(kernel_sup), |
| Res = recon:get_state(whereis(kernel_sup)), |
| Res = recon:get_state(pid_to_triple(whereis(kernel_sup))), |
| state = element(1,Res). |
| |
| %% Skip on remote-loading, too hard |
| |
| source(_Config) -> |
| Pat = <<"find this sentence in this file's source">>, |
| {_,_} = binary:match(iolist_to_binary(recon:source(?MODULE)), Pat). |
| |
| tcp(_Config) -> |
| {ok, Listen} = gen_tcp:listen(0, []), |
| true = lists:member(Listen, recon:tcp()). |
| |
| udp(_Config) -> |
| {ok, Port} = gen_udp:open(0), |
| true = lists:member(Port, recon:udp()). |
| |
| %% SCTP not supported everywhere, skipped. |
| |
| files(Config) -> |
| Len = length(recon:files()), |
| {ok, _IoDevice} = file:open(filename:join(?config(priv_dir, Config), "a"), |
| [write]), |
| true = Len + 1 =:= length(recon:files()). |
| |
| port_types(_Config) -> |
| lists:all(fun({[_|_],N}) when is_integer(N), N > 0 -> true end, |
| recon:port_types()). |
| |
| inet_count(_Config) -> |
| [gen_tcp:listen(0,[]) || _ <- lists:seq(1,100)], |
| Res = recon:inet_count(oct, 10), |
| %% all results are =< 0, and ordered from biggest to smaller |
| lists:foldl(fun({_,Current,_}, Next) when Current >= Next -> Current end, |
| 0, |
| lists:reverse(Res)), |
| true = inet_attrs(Res), |
| %% greatest to smallest |
| 10 = length(Res), |
| 15 = length(recon:inet_count(cnt, 15)). |
| |
| inet_window(_Config) -> |
| [gen_tcp:listen(0,[]) || _ <- lists:seq(1,100)], |
| Res = recon:inet_window(oct, 10, 100), |
| %% all results are =< 0, and ordered from biggest to smaller |
| lists:foldl(fun({_,Current,_}, Next) when Current >= Next -> Current end, |
| 0, |
| lists:reverse(Res)), |
| true = inet_attrs(Res), |
| %% greatest to smallest |
| 10 = length(Res), |
| 15 = length(recon:inet_window(cnt, 15, 100)). |
| |
| %% skip RPC |
| |
| port_info1(Config) -> |
| TCP = ?config(tcp, Config), |
| UDP = ?config(tcp, Config), |
| TCPInfo = recon:port_info(TCP), |
| UDPInfo = recon:port_info(UDP), |
| %% type is the only specific value supported for now |
| Cats = lists:sort([meta, signals, io, memory_used, type]), |
| %% Too many options for inet stuff. |
| Cats = lists:sort(proplists:get_keys(TCPInfo)), |
| Cats = lists:sort(proplists:get_keys(UDPInfo)). |
| |
| port_info2(Config) -> |
| TCP = ?config(tcp, Config), |
| UDP = ?config(tcp, Config), |
| %% Not testing the whole set, but heeeh. Good enough. |
| {io, [{input,_},{output,_}]} = recon:port_info(TCP, io), |
| {io, [{input,_},{output,_}]} = recon:port_info(UDP, io). |
| |
| %% binary_memory is a created attribute that counts the amount |
| %% of memory held by refc binaries, usable in info/2-4 and |
| %% in proc_count/proc_window. |
| binary_memory(_Config) -> |
| %% we just don't want them to crash like it happens with |
| %% non-existing attributes. |
| ?assertError(_, recon:proc_count(fake_attribute, 10)), |
| ?assertError(_, recon:proc_window(fake_attribute, 10, 100)), |
| recon:proc_count(binary_memory, 10), |
| recon:proc_window(binary_memory, 10, 100), |
| %% And now for info, it should work in lists, which can contain |
| %% duplicates, or in single element calls. |
| %% Note: we allocate the binary before spawning the process but |
| %% use it in a closure to avoid race conditions on allocation for |
| %% the test. |
| Bin = <<1:999999>>, |
| Pid1 = spawn_link(fun() -> timer:sleep(100000) end), |
| Pid2 = spawn_link(fun() -> timer:sleep(100000), Bin end), |
| {binary_memory, 0} = recon:info(Pid1, binary_memory), |
| {binary_memory, N} = recon:info(Pid2, binary_memory), |
| true = N > 0, |
| Res1 = recon:info(Pid1, [binary, binary_memory, binary]), |
| Res2 = recon:info(Pid2, [binary_memory, binary, binary_memory]), |
| %% we expect everything to look as a single call to process_info/2 |
| [{binary,X}, {binary_memory,_}, {binary,X}] = Res1, |
| [{binary_memory,Y}, {binary,_}, {binary_memory,Y}] = Res2. |
| |
| %% Just check that we get many schedulers and that the usage values are all |
| %% between 0 and 1 inclusively. We don't care for edge cases like a |
| %% scheduler disappearing halfway through a run. |
| scheduler_usage(_Config) -> |
| List = recon:scheduler_usage(100), |
| ?assertEqual(length(List), length( |
| [1 || {Id,Rate} <- List, |
| is_integer(Id), Id > 0, |
| Rate >= 0, Rate =< 1]) |
| ). |
| |
| %%%%%%%%%%%%%%% |
| %%% HELPERS %%% |
| %%%%%%%%%%%%%%% |
| |
| pid_to_triple(Pid) when is_pid(Pid) -> |
| "<0."++Rest = lists:flatten(io_lib:format("~p",[Pid])), |
| {B,C} = lists:foldl(fun($>, Acc) -> Acc; |
| ($., B) -> {B,0}; |
| (N, {B,C}) -> {B,(C*10)+(N-$0)}; |
| (N, B) -> (B*10)+(N-$0) |
| end, |
| 0, |
| Rest), |
| {0,B,C}. |
| |
| proc_attrs(L) -> |
| lists:all(fun({Pid,_Val,List}) -> is_pid(Pid) andalso is_list(List) end, |
| L). |
| |
| inet_attrs(L) -> |
| lists:all(fun({Port,_Val,List}) -> is_port(Port) andalso is_list(List) end, |
| L). |