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