1%%% Test suite for the recon module. Because many of the tests in
2%%% here depend on runtime properties and this is *not* transparent,
3%%% the tests are rather weak and more or less check for interface
4%%% conformance and obvious changes more than anything.
5-module(recon_SUITE).
6-include_lib("common_test/include/ct.hrl").
7-include_lib("eunit/include/eunit.hrl").
8-compile(export_all).
9
10-ifdef(OTP_RELEASE).
11-define(FILES_IMPL, nif).
12-define(ERROR_LOGGER_MATCH(_),  ).
13-define(REDUCTIONS_MATCH(X), X).
14-else.
15-define(FILES_IMPL, port).
16-define(ERROR_LOGGER_MATCH(X), X,).
17-define(REDUCTIONS_MATCH(_), []).
18-endif.
19
20all() -> [{group,info}, proc_count, proc_window, bin_leak,
21          node_stats_list, get_state, source, tcp, udp, files, port_types,
22          inet_count, inet_window, binary_memory, scheduler_usage].
23
24groups() -> [{info, [], [info3, info4, info1, info2,
25                         port_info1, port_info2]}].
26
27init_per_group(info, Config) ->
28    Self = self(),
29    Pid = spawn(fun() ->
30         {ok, TCP} = gen_tcp:listen(0, []),
31         {ok, UDP} = gen_udp:open(0),
32         Self ! {TCP, UDP},
33         timer:sleep(infinity)
34     end),
35    receive
36        {TCP, UDP} -> [{pid, Pid}, {tcp, TCP}, {udp, UDP} | Config]
37    end.
38
39end_per_group(info, Config) ->
40    exit(?config(pid, Config), kill).
41
42init_per_testcase(files, Config) ->
43    case ?FILES_IMPL of
44        nif -> {skip, "files can no longer be listed in OTP-21 and above"};
45        port -> Config
46    end;
47init_per_testcase(_, Config) ->
48    Config.
49
50end_per_testcase(_, Config) ->
51    Config.
52
53%%%%%%%%%%%%%
54%%% TESTS %%%
55%%%%%%%%%%%%%
56
57info3(Config) ->
58    Pid = ?config(pid, Config),
59    {A,B,C} = pid_to_triple(Pid),
60    Info1 = recon:info(Pid),
61    Info2 = recon:info(A,B,C),
62    %% Reduction count is unreliable
63    ?assertMatch(?REDUCTIONS_MATCH(
64                    [{work, [{reductions,_}]}, {work, [{reductions,_}]}]
65                 ), (Info1 -- Info2) ++ (Info2 -- Info1)).
66
67info4(Config) ->
68    Pid = ?config(pid, Config),
69    Keys = [meta, signals, location, memory_used,
70            links, monitors, messages,
71            [links, monitors, messages]],
72    {A,B,C} = pid_to_triple(Pid),
73    lists:map(fun(Key) ->
74                  Info = recon:info(Pid, Key),
75                  Info = recon:info(A,B,C, Key)
76              end,
77              Keys).
78
79info1(Config) ->
80    Pid = ?config(pid, Config),
81    Categories = [{meta, [registered_name, dictionary, group_leader, status]},
82                  {signals, [links, monitors, monitored_by, trap_exit]},
83                  {location, [initial_call, current_stacktrace]},
84                  {memory_used, [memory, message_queue_len, heap_size,
85                                 total_heap_size, garbage_collection]},
86                  {work, [reductions]}],
87    [] = lists:flatten(
88        [K || {Cat,List} <- Categories,
89              K <- List,
90              Info <- [recon:info(Pid)],
91              undefined == proplists:get_value(K, proplists:get_value(Cat,Info))
92        ]),
93    register(info1, Pid),
94    Res1 = recon:info(info1),
95    Res2 = recon:info(whereis(info1)),
96    Res3 = recon:info(pid_to_triple(whereis(info1))),
97    Res4 = recon:info(lists:flatten(io_lib:format("~p",[Pid]))),
98    unregister(info1),
99    L = lists:usort(Res1 ++ Res2 ++ Res3 ++ Res4),
100    ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]},
101                                    {work,[{reductions,_}]},
102                                    {work,[{reductions,_}]}]), L -- Res1),
103    ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]},
104                                    {work,[{reductions,_}]},
105                                    {work,[{reductions,_}]}]), L -- Res2),
106    ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]},
107                                    {work,[{reductions,_}]},
108                                    {work,[{reductions,_}]}]), L -- Res3),
109    ?assertMatch(?REDUCTIONS_MATCH([{work,[{reductions,_}]},
110                                    {work,[{reductions,_}]},
111                                    {work,[{reductions,_}]}]), L -- Res4),
112    ok.
113
114info2(Config) ->
115    Pid = ?config(pid, Config),
116    Categories = [{meta, [registered_name, dictionary, group_leader, status]},
117                  {signals, [links, monitors, monitored_by, trap_exit]},
118                  {location, [initial_call, current_stacktrace]},
119                  {memory_used, [memory, message_queue_len, heap_size,
120                                 total_heap_size, garbage_collection]},
121                  {work, [reductions]}],
122    %% registered_name is special -- only returns
123    %% [] when passed through by info/2. Because we pass terms through
124    %% according to the docs, we have to respect that
125    [] = recon:info(Pid, registered_name),
126    %% Register to get the expected tuple
127    register(info2, Pid),
128    Keys = lists:flatten([K || {_,L} <- Categories, K <- L]),
129    %% check that individual category call works for all terms
130    [] = lists:flatten(
131        [K || {Cat, List} <- Categories,
132              K <- List,
133              {GetCat,Info} <- [recon:info(Pid, Cat)],
134              Cat =:= GetCat,
135              undefined =:= proplists:get_value(K, Info)]
136    ),
137    %% Can get a list of arguments
138    true = lists:sort(Keys)
139           =:=
140           lists:sort(proplists:get_keys(recon:info(Pid, Keys))),
141    true = length(Keys)
142           =:=
143           length([1 || K1 <- Keys, {K2,_} <- [recon:info(Pid, K1)],
144                        K1 == K2]),
145    unregister(info2).
146
147proc_count(_Config) ->
148    Res = recon:proc_count(memory, 10),
149    true = proc_attrs(Res),
150    %% greatest to smallest
151    true = lists:usort(fun({P1,V1,_},{P2,V2,_}) -> {V1,P1} >= {V2,P2} end,
152                       Res) =:= Res,
153    10 = length(Res),
154    15 = length(recon:proc_count(reductions, 15)).
155
156proc_window(_Config) ->
157    Res = recon:proc_window(reductions, 10, 100),
158    true = proc_attrs(Res),
159    %% we can't check order easily because stuff doesn't move
160    %% fast enough on a test node to show up here
161    10 = length(Res),
162    15 = length(recon:proc_window(memory, 15, 100)).
163
164bin_leak(_Config) ->
165    Res = recon:bin_leak(5),
166    5 = length(Res),
167    true = proc_attrs(Res),
168    true = lists:any(fun({_,Val,_})-> Val =/= 0  end, Res),
169    %% all results are =< 0, and ordered from smallest to biggest
170    lists:foldl(fun({_,Current,_}, Next) when Current =< Next -> Current end,
171                0,
172                lists:reverse(Res)).
173
174%% This function implicitly tests node_stats/4
175node_stats_list(_Config) ->
176    Res = recon:node_stats_list(2,100),
177    2 = length([1 || {[{process_count,_},
178                       {run_queue,_},
179                       ?ERROR_LOGGER_MATCH({error_logger_queue_len,_})
180                       {memory_total,_},
181                       {memory_procs,_},
182                       {memory_atoms,_},
183                       {memory_bin,_},
184                       {memory_ets,_}|_],
185                      [{bytes_in,_},
186                       {bytes_out,_},
187                       {gc_count,_},
188                       {gc_words_reclaimed,_},
189                       {reductions,_},
190                       {scheduler_usage,[_|_]}|_]} <- Res]).
191
192get_state(_Config) ->
193    Res = recon:get_state(kernel_sup),
194    Res = recon:get_state(whereis(kernel_sup)),
195    Res = recon:get_state(pid_to_triple(whereis(kernel_sup))),
196    state = element(1,Res).
197
198%% Skip on remote-loading, too hard
199
200source(_Config) ->
201    Pat = <<"find this sentence in this file's source">>,
202    {_,_} = binary:match(iolist_to_binary(recon:source(?MODULE)), Pat).
203
204tcp(_Config) ->
205    {ok, Listen} = gen_tcp:listen(0, []),
206    true = lists:member(Listen, recon:tcp()).
207
208udp(_Config) ->
209    {ok, Port} = gen_udp:open(0),
210    true = lists:member(Port, recon:udp()).
211
212%% SCTP not supported everywhere, skipped.
213
214files(Config) ->
215    Len = length(recon:files()),
216    {ok, _IoDevice} = file:open(filename:join(?config(priv_dir, Config), "a"),
217                                [write]),
218    true = Len + 1 =:= length(recon:files()).
219
220port_types(_Config) ->
221    lists:all(fun({[_|_],N}) when is_integer(N), N > 0 -> true end,
222              recon:port_types()).
223
224inet_count(_Config) ->
225    [gen_tcp:listen(0,[]) || _ <- lists:seq(1,100)],
226    Res = recon:inet_count(oct, 10),
227    %% all results are =< 0, and ordered from biggest to smaller
228    lists:foldl(fun({_,Current,_}, Next) when Current >= Next -> Current end,
229                0,
230                lists:reverse(Res)),
231    true = inet_attrs(Res),
232    %% greatest to smallest
233    10 = length(Res),
234    15 = length(recon:inet_count(cnt, 15)).
235
236inet_window(_Config) ->
237    [gen_tcp:listen(0,[]) || _ <- lists:seq(1,100)],
238    Res = recon:inet_window(oct, 10, 100),
239    %% all results are =< 0, and ordered from biggest to smaller
240    lists:foldl(fun({_,Current,_}, Next) when Current >= Next -> Current end,
241                0,
242                lists:reverse(Res)),
243    true = inet_attrs(Res),
244    %% greatest to smallest
245    10 = length(Res),
246    15 = length(recon:inet_window(cnt, 15, 100)).
247
248%% skip RPC
249
250port_info1(Config) ->
251    TCP = ?config(tcp, Config),
252    UDP = ?config(tcp, Config),
253    TCPInfo = recon:port_info(TCP),
254    UDPInfo = recon:port_info(UDP),
255    %% type is the only specific value supported for now
256    Cats = lists:sort([meta, signals, io, memory_used, type]),
257    %% Too many options for inet stuff.
258    Cats = lists:sort(proplists:get_keys(TCPInfo)),
259    Cats = lists:sort(proplists:get_keys(UDPInfo)).
260
261port_info2(Config) ->
262    TCP = ?config(tcp, Config),
263    UDP = ?config(tcp, Config),
264    %% Not testing the whole set, but heeeh. Good enough.
265    {io, [{input,_},{output,_}]} = recon:port_info(TCP, io),
266    {io, [{input,_},{output,_}]} = recon:port_info(UDP, io).
267
268%% binary_memory is a created attribute that counts the amount
269%% of memory held by refc binaries, usable in info/2-4 and
270%% in proc_count/proc_window.
271binary_memory(_Config) ->
272    %% we just don't want them to crash like it happens with
273    %% non-existing attributes.
274    ?assertError(_, recon:proc_count(fake_attribute, 10)),
275    ?assertError(_, recon:proc_window(fake_attribute, 10, 100)),
276    recon:proc_count(binary_memory, 10),
277    recon:proc_window(binary_memory, 10, 100),
278    %% And now for info, it should work in lists, which can contain
279    %% duplicates, or in single element calls.
280    %% Note: we allocate the binary before spawning the process but
281    %%       use it in a closure to avoid race conditions on allocation for
282    %%       the test.
283    Bin = <<1:999999>>,
284    Pid1 = spawn_link(fun() -> timer:sleep(100000) end),
285    Pid2 = spawn_link(fun() -> timer:sleep(100000), Bin end),
286    {binary_memory, 0} = recon:info(Pid1, binary_memory),
287    {binary_memory, N} = recon:info(Pid2, binary_memory),
288    true = N > 0,
289    Res1 = recon:info(Pid1, [binary, binary_memory, binary]),
290    Res2 = recon:info(Pid2, [binary_memory, binary, binary_memory]),
291    %% we expect everything to look as a single call to process_info/2
292    [{binary,X}, {binary_memory,_}, {binary,X}] = Res1,
293    [{binary_memory,Y}, {binary,_}, {binary_memory,Y}] = Res2.
294
295%% Just check that we get many schedulers and that the usage values are all
296%% between 0 and 1 inclusively. We don't care for edge cases like a
297%% scheduler disappearing halfway through a run.
298scheduler_usage(_Config) ->
299    List = recon:scheduler_usage(100),
300    ?assertEqual(length(List), length(
301                 [1 || {Id,Rate} <- List,
302                       is_integer(Id), Id > 0,
303                       Rate >= 0, Rate =< 1])
304    ).
305
306%%%%%%%%%%%%%%%
307%%% HELPERS %%%
308%%%%%%%%%%%%%%%
309
310pid_to_triple(Pid) when is_pid(Pid) ->
311    "<0."++Rest = lists:flatten(io_lib:format("~p",[Pid])),
312    {B,C} = lists:foldl(fun($>, Acc) -> Acc;
313                           ($., B) -> {B,0};
314                           (N, {B,C}) -> {B,(C*10)+(N-$0)};
315                           (N, B) -> (B*10)+(N-$0)
316                        end,
317                        0,
318                        Rest),
319    {0,B,C}.
320
321proc_attrs(L) ->
322    lists:all(fun({Pid,_Val,List}) -> is_pid(Pid) andalso is_list(List) end,
323              L).
324
325inet_attrs(L) ->
326    lists:all(fun({Port,_Val,List}) -> is_port(Port) andalso is_list(List) end,
327              L).
328