1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2010-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%
22%% Tests based on the contents of the diameter app file.
23%%
24
25-module(diameter_app_SUITE).
26
27-export([suite/0,
28         all/0,
29	 init_per_suite/1,
30         end_per_suite/1]).
31
32%% testcases
33-export([keys/1,
34         vsn/1,
35	 modules/1,
36	 exports/1,
37         release/1,
38	 xref/1,
39         relup/1]).
40
41-include("diameter_ct.hrl").
42
43-define(A, list_to_atom).
44
45%% Modules not in the app and that should not have dependencies on it
46%% for build reasons.
47-define(COMPILER_MODULES, [diameter_codegen,
48                           diameter_dict_scanner,
49                           diameter_dict_parser,
50                           diameter_dict_util,
51                           diameter_exprecs,
52                           diameter_make]).
53
54-define(INFO_MODULES, [diameter_dbg,
55                       diameter_info]).
56
57%% ===========================================================================
58
59suite() ->
60    [{timetrap, {seconds, 60}}].
61
62all() ->
63    [keys,
64     vsn,
65     modules,
66     exports,
67     release,
68     xref,
69     relup].
70
71init_per_suite(Config) ->
72    [{application, ?APP, App}] = diameter_util:consult(?APP, app),
73    [{app, App} | Config].
74
75end_per_suite(_Config) ->
76    ok.
77
78%% ===========================================================================
79%% # keys/1
80%%
81%% Ensure that the app file contains selected keys. Some of these would
82%% also be caught by other testcases.
83%% ===========================================================================
84
85keys(Config) ->
86    App = fetch(app, Config),
87    [] = lists:filter(fun(K) -> not lists:keymember(K, 1, App) end,
88                      [vsn, description, modules, registered, applications]).
89
90%% ===========================================================================
91%% # vsn/1
92%%
93%% Ensure that our app version sticks to convention.
94%% ===========================================================================
95
96vsn(Config) ->
97    true = is_vsn(fetch(vsn, fetch(app, Config))).
98
99%% ===========================================================================
100%% # modules/1
101%%
102%% Ensure that the app file modules and installed modules differ by
103%% compiler/info modules.
104%% ===========================================================================
105
106modules(Config) ->
107    Mods = fetch(modules, fetch(app, Config)),
108    Installed = code_mods(),
109    Help = lists:sort(?INFO_MODULES ++ ?COMPILER_MODULES),
110
111    {[], Help} = {Mods -- Installed, lists:sort(Installed -- Mods)}.
112
113code_mods() ->
114    Dir  = code:lib_dir(?APP, ebin),
115    {ok, Files} = file:list_dir(Dir),
116    [?A(lists:reverse(R)) || N <- Files, "maeb." ++ R <- [lists:reverse(N)]].
117
118%% ===========================================================================
119%% # exports/1
120%%
121%% Ensure that no module does export_all.
122%% ===========================================================================
123
124exports(Config) ->
125    Mods = fetch(modules, fetch(app, Config)),
126    [] = [M || M <- Mods, exports_all(M)].
127
128exports_all(Mod) ->
129    Opts = fetch(options, Mod:module_info(compile)),
130
131    is_list(Opts) andalso lists:member(export_all, Opts).
132
133%% ===========================================================================
134%% # release/1
135%%
136%% Ensure that it's possible to build a minimal release with our app file.
137%% ===========================================================================
138
139release(Config) ->
140    App = fetch(app, Config),
141    Rel = {release,
142           {"diameter test release", fetch(vsn, App)},
143           {erts, erlang:system_info(version)},
144           [{A, appvsn(A)} || A <- [sasl | fetch(applications, App)]]},
145    Dir = fetch(priv_dir, Config),
146    ok = write_file(filename:join([Dir, "diameter_test.rel"]), Rel),
147    {ok, _, []} = systools:make_script("diameter_test", [{path, [Dir]},
148                                                         {outdir, Dir},
149                                                         silent]).
150
151%% sasl need to be included to avoid a missing_sasl warning, error
152%% in the case of relup/1.
153
154appvsn(Name) ->
155    [{application, Name, App}] = diameter_util:consult(Name, app),
156    fetch(vsn, App).
157
158%% ===========================================================================
159%% # xref/1
160%%
161%% Ensure that no function in our application calls an undefined function
162%% or one in an application we haven't declared as a dependency. (Almost.)
163%% ===========================================================================
164
165xref(Config) ->
166    App = fetch(app, Config),
167    Mods = fetch(modules, App),  %% modules listed in the app file
168
169    %% List of application names extracted from runtime_dependencies.
170    Deps = lists:map(fun unversion/1, fetch(runtime_dependencies, App)),
171
172    {ok, XRef} = xref:start(make_name(xref_test_name)),
173    ok = xref:set_default(XRef, [{verbose, false}, {warnings, false}]),
174
175    %% Only add our application and those it's dependent on according
176    %% to the app file. Well, almost. erts beams are also required to
177    %% stop xref from complaining about calls to module erlang, which
178    %% was previously in kernel. Erts isn't an application however, in
179    %% the sense that there's no .app file, and isn't listed in
180    %% applications.
181    ok = lists:foreach(fun(A) -> add_application(XRef, A) end,
182                       [?APP, erts | fetch(applications, App)]),
183
184    {ok, Undefs} = xref:analyze(XRef, undefined_function_calls),
185    {ok, RTmods} = xref:analyze(XRef, {module_use, Mods}),
186    {ok, CTmods} = xref:analyze(XRef, {module_use, ?COMPILER_MODULES}),
187    {ok, RTdeps} = xref:analyze(XRef, {module_call, Mods}),
188
189    xref:stop(XRef),
190
191    Rel = release(),  %% otp_release-ish
192
193    %% Only care about calls from our own application.
194    [] = lists:filter(fun({{F,_,_} = From, {_,_,_} = To}) ->
195                              lists:member(F, Mods)
196                                  andalso not ignored(From, To, Rel)
197                      end,
198                      Undefs),
199
200    %% Ensure that only runtime or info modules call runtime modules.
201    %% It's not strictly necessary that diameter compiler modules not
202    %% depend on other diameter modules but it's a simple source of
203    %% build errors if not properly encoded in the makefile so guard
204    %% against it.
205    [] = (RTmods -- Mods) -- ?INFO_MODULES,
206
207    %% Ensure that runtime modules don't call compiler modules.
208    CTmods = CTmods -- Mods,
209
210    %% Ensure that runtime modules only call other runtime modules, or
211    %% applications declared in runtime_dependencies in the app file.
212    %% The declared application versions are ignored since we only
213    %% know what we see now.
214    [] = lists:filter(fun(M) -> not lists:member(app(M), Deps) end,
215                      RTdeps -- Mods).
216
217ignored({FromMod,_,_}, {ToMod,_,_} = To, Rel)->
218    %% diameter_tcp does call ssl despite the latter not being listed
219    %% as a dependency in the app file since ssl is only required for
220    %% TLS security: it's up to a client who wants TLS to start ssl.
221    %% The OTP 18 time api is also called if it exists, so that the
222    %% same code can be run on older releases.
223    {FromMod, ToMod} == {diameter_tcp, ssl}
224        orelse (FromMod == diameter_lib
225                andalso Rel < 18
226                andalso lists:member(To, time_api())).
227
228%% New time api in OTP 18.
229time_api() ->
230    [{erlang, F, A} || {F,A} <- [{convert_time_unit,3},
231                                 {monotonic_time,0},
232                                 {monotonic_time,1},
233                                 {system_time,0},
234                                 {system_time,1},
235                                 {time_offset,0},
236                                 {time_offset,1},
237                                 {timestamp,0},
238                                 {unique_integer,0},
239                                 {unique_integer,1}]]
240        ++ [{os, system_time, 0},
241            {os, system_time, 1}].
242
243release() ->
244    Rel = erlang:system_info(otp_release),
245    try list_to_integer(Rel) of
246        N -> N
247    catch
248        error:_ ->
249            0  %% aka < 17
250    end.
251
252unversion(App) ->
253    {Name, [$-|Vsn]} = lists:splitwith(fun(C) -> C /= $- end, App),
254    true = is_app(Name), %% assert
255    Vsn = vsn_str(Vsn),  %%
256    Name.
257
258app('$M_EXPR') -> %% could be anything but assume it's ok
259    "erts";
260app(Mod) ->
261    case code:which(Mod) of
262        preloaded ->
263            "erts";
264        Reason when is_atom(Reason) ->
265            error({Reason, Mod});
266        Path ->
267            %% match to identify an unexpectedly short path
268            {_, _, [_,_,_|_] = Split} = {Mod, Path, filename:split(Path)},
269            unversion(lists:nth(3, lists:reverse(Split)))
270    end.
271
272add_application(XRef, App) ->
273    add_application(XRef, App, code:lib_dir(App)).
274
275%% erts will not be in the lib directory before installation.
276add_application(XRef, erts, {error, _}) ->
277    Dir = filename:join([code:root_dir(), "erts", "preloaded", "ebin"]),
278    {ok, _} = xref:add_directory(XRef, Dir, []);
279add_application(XRef, App, Dir)
280  when is_list(Dir) ->
281    {ok, App} = xref:add_application(XRef, Dir, []).
282
283make_name(Suf) ->
284    list_to_atom(atom_to_list(?APP) ++ "_" ++ atom_to_list(Suf)).
285
286%% ===========================================================================
287%% # relup/1
288%%
289%% Ensure that we can generate release upgrade files using our appup file.
290%% ===========================================================================
291
292relup(Config) ->
293    [{Vsn, Up, Down}] = diameter_util:consult(?APP, appup),
294    true = is_vsn(Vsn),
295
296    App = fetch(app, Config),
297    Rel = [{erts, erlang:system_info(version)}
298           | [{A, appvsn(A)} || A <- [sasl | fetch(applications, App)]]],
299
300    Dir = fetch(priv_dir, Config),
301
302    Name = write_rel(Dir, Rel, Vsn),
303    UpFrom = acc_rel(Dir, Rel, Up),
304    DownTo = acc_rel(Dir, Rel, Down),
305
306    {[Name], [Name], [], []}  %% no current in up/down and go both ways
307        = {[Name] -- UpFrom,
308           [Name] -- DownTo,
309           UpFrom -- DownTo,
310           DownTo -- UpFrom},
311
312    [[], []] = [S -- sets:to_list(sets:from_list(S))
313                || S <- [UpFrom, DownTo]],
314
315    {ok, _, _, []} = systools:make_relup(Name, UpFrom, DownTo, [{path, [Dir]},
316                                                                {outdir, Dir},
317                                                                silent]).
318
319acc_rel(Dir, Rel, List) ->
320    lists:foldl(fun(T,A) -> acc_rel(Dir, Rel, T, A) end,
321                [],
322                List).
323
324acc_rel(Dir, Rel, {Vsn, _}, Acc) ->
325    [write_rel(Dir, Rel, Vsn) | Acc].
326
327%% Write a rel file and return its name.
328write_rel(Dir, [Erts | Apps], Vsn) ->
329    VS = vsn_str(Vsn),
330    Name = "diameter_test_" ++ VS,
331    ok = write_file(filename:join([Dir, Name ++ ".rel"]),
332                    {release,
333                     {"diameter " ++ VS ++ " test release", VS},
334                     Erts,
335                     Apps}),
336    Name.
337
338%% ===========================================================================
339%% ===========================================================================
340
341fetch(Key, List) ->
342    {Key, {Key, Val}} = {Key, lists:keyfind(Key, 1, List)}, %% useful badmatch
343    Val.
344
345write_file(Path, T) ->
346    file:write_file(Path, io_lib:format("~p.", [T])).
347
348%% Is a version string of the expected form?
349is_vsn(V) ->
350    V = vsn_str(V),
351    true.
352
353%% Turn a from/to version in appup to a version string after ensuring
354%% that it's valid version number of regexp. In the regexp case, the
355%% regexp itself becomes the version string since there's no
356%% requirement that a version in appup be anything but a string. The
357%% restrictions placed on string-valued version numbers (that they be
358%% '.'-separated integers) are our own.
359
360vsn_str(S)
361  when is_list(S) ->
362    {_, match}   = {S, match(S, "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*$")},
363    {_, nomatch} = {S, match(S, "\\.0\\.0$")},
364    S;
365
366vsn_str(B)
367  when is_binary(B) ->
368    {ok, _} = re:compile(B),
369    binary_to_list(B).
370
371match(S, RE) ->
372    re:run(S, RE, [{capture, none}]).
373
374%% Is an application name of the expected form?
375is_app(S)
376  when is_list(S) ->
377    {_, match} = {S, match(S, "^([a-z]([a-z_]*|[a-zA-Z]*))$")},
378    true.
379