%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% %% Tests based on the contents of the diameter app file. %% -module(diameter_app_SUITE). -export([suite/0, all/0, init_per_suite/1, end_per_suite/1]). %% testcases -export([keys/1, vsn/1, modules/1, exports/1, release/1, xref/1, relup/1]). -include("diameter_ct.hrl"). -define(A, list_to_atom). %% Modules not in the app and that should not have dependencies on it %% for build reasons. -define(COMPILER_MODULES, [diameter_codegen, diameter_dict_scanner, diameter_dict_parser, diameter_dict_util, diameter_exprecs, diameter_make]). -define(INFO_MODULES, [diameter_dbg, diameter_info]). %% =========================================================================== suite() -> [{timetrap, {seconds, 60}}]. all() -> [keys, vsn, modules, exports, release, xref, relup]. init_per_suite(Config) -> [{application, ?APP, App}] = diameter_util:consult(?APP, app), [{app, App} | Config]. end_per_suite(_Config) -> ok. %% =========================================================================== %% # keys/1 %% %% Ensure that the app file contains selected keys. Some of these would %% also be caught by other testcases. %% =========================================================================== keys(Config) -> App = fetch(app, Config), [] = lists:filter(fun(K) -> not lists:keymember(K, 1, App) end, [vsn, description, modules, registered, applications]). %% =========================================================================== %% # vsn/1 %% %% Ensure that our app version sticks to convention. %% =========================================================================== vsn(Config) -> true = is_vsn(fetch(vsn, fetch(app, Config))). %% =========================================================================== %% # modules/1 %% %% Ensure that the app file modules and installed modules differ by %% compiler/info modules. %% =========================================================================== modules(Config) -> Mods = fetch(modules, fetch(app, Config)), Installed = code_mods(), Help = lists:sort(?INFO_MODULES ++ ?COMPILER_MODULES), {[], Help} = {Mods -- Installed, lists:sort(Installed -- Mods)}. code_mods() -> Dir = code:lib_dir(?APP, ebin), {ok, Files} = file:list_dir(Dir), [?A(lists:reverse(R)) || N <- Files, "maeb." ++ R <- [lists:reverse(N)]]. %% =========================================================================== %% # exports/1 %% %% Ensure that no module does export_all. %% =========================================================================== exports(Config) -> Mods = fetch(modules, fetch(app, Config)), [] = [M || M <- Mods, exports_all(M)]. exports_all(Mod) -> Opts = fetch(options, Mod:module_info(compile)), is_list(Opts) andalso lists:member(export_all, Opts). %% =========================================================================== %% # release/1 %% %% Ensure that it's possible to build a minimal release with our app file. %% =========================================================================== release(Config) -> App = fetch(app, Config), Rel = {release, {"diameter test release", fetch(vsn, App)}, {erts, erlang:system_info(version)}, [{A, appvsn(A)} || A <- [sasl | fetch(applications, App)]]}, Dir = fetch(priv_dir, Config), ok = write_file(filename:join([Dir, "diameter_test.rel"]), Rel), {ok, _, []} = systools:make_script("diameter_test", [{path, [Dir]}, {outdir, Dir}, silent]). %% sasl need to be included to avoid a missing_sasl warning, error %% in the case of relup/1. appvsn(Name) -> [{application, Name, App}] = diameter_util:consult(Name, app), fetch(vsn, App). %% =========================================================================== %% # xref/1 %% %% Ensure that no function in our application calls an undefined function %% or one in an application we haven't declared as a dependency. (Almost.) %% =========================================================================== xref(Config) -> App = fetch(app, Config), Mods = fetch(modules, App), %% modules listed in the app file %% List of application names extracted from runtime_dependencies. Deps = lists:map(fun unversion/1, fetch(runtime_dependencies, App)), {ok, XRef} = xref:start(make_name(xref_test_name)), ok = xref:set_default(XRef, [{verbose, false}, {warnings, false}]), %% Only add our application and those it's dependent on according %% to the app file. Well, almost. erts beams are also required to %% stop xref from complaining about calls to module erlang, which %% was previously in kernel. Erts isn't an application however, in %% the sense that there's no .app file, and isn't listed in %% applications. ok = lists:foreach(fun(A) -> add_application(XRef, A) end, [?APP, erts | fetch(applications, App)]), {ok, Undefs} = xref:analyze(XRef, undefined_function_calls), {ok, RTmods} = xref:analyze(XRef, {module_use, Mods}), {ok, CTmods} = xref:analyze(XRef, {module_use, ?COMPILER_MODULES}), {ok, RTdeps} = xref:analyze(XRef, {module_call, Mods}), xref:stop(XRef), Rel = release(), %% otp_release-ish %% Only care about calls from our own application. [] = lists:filter(fun({{F,_,_} = From, {_,_,_} = To}) -> lists:member(F, Mods) andalso not ignored(From, To, Rel) end, Undefs), %% Ensure that only runtime or info modules call runtime modules. %% It's not strictly necessary that diameter compiler modules not %% depend on other diameter modules but it's a simple source of %% build errors if not properly encoded in the makefile so guard %% against it. [] = (RTmods -- Mods) -- ?INFO_MODULES, %% Ensure that runtime modules don't call compiler modules. CTmods = CTmods -- Mods, %% Ensure that runtime modules only call other runtime modules, or %% applications declared as in runtime_dependencies in the app %% file. Note that the declared application versions are ignored %% since we only know what we can see now. [] = lists:filter(fun(M) -> not lists:member(app(M), Deps) end, RTdeps -- Mods). ignored({FromMod,_,_}, {ToMod,_,_} = To, Rel)-> %% diameter_tcp does call ssl despite the latter not being listed %% as a dependency in the app file since ssl is only required for %% TLS security: it's up to a client who wants TLS to start ssl. %% The OTP 18 time api is also called if it exists, so that the %% same code can be run on older releases. {FromMod, ToMod} == {diameter_tcp, ssl} orelse (FromMod == diameter_lib andalso Rel < 18 andalso lists:member(To, time_api())). %% New time api in OTP 18. time_api() -> [{erlang, F, A} || {F,A} <- [{convert_time_unit,3}, {monotonic_time,0}, {monotonic_time,1}, {system_time,0}, {system_time,1}, {time_offset,0}, {time_offset,1}, {timestamp,0}, {unique_integer,0}, {unique_integer,1}]] ++ [{os, system_time, 0}, {os, system_time, 1}]. release() -> Rel = erlang:system_info(otp_release), try list_to_integer(Rel) of N -> N catch error:_ -> 0 %% aka < 17 end. unversion(App) -> {Name, [$-|Vsn]} = lists:splitwith(fun(C) -> C /= $- end, App), true = is_app(Name), %% assert Vsn = vsn_str(Vsn), %% Name. app('$M_EXPR') -> %% could be anything but assume it's ok "erts"; app(Mod) -> case code:which(Mod) of preloaded -> "erts"; Path -> %% match to identify an unexpectedly short path {_, _, [_,_,_|_] = Split} = {Mod, Path, filename:split(Path)}, unversion(lists:nth(3, lists:reverse(Split))) end. add_application(XRef, App) -> add_application(XRef, App, code:lib_dir(App)). %% erts will not be in the lib directory before installation. add_application(XRef, erts, {error, _}) -> Dir = filename:join([code:root_dir(), "erts", "preloaded", "ebin"]), {ok, _} = xref:add_directory(XRef, Dir, []); add_application(XRef, App, Dir) when is_list(Dir) -> {ok, App} = xref:add_application(XRef, Dir, []). make_name(Suf) -> list_to_atom(atom_to_list(?APP) ++ "_" ++ atom_to_list(Suf)). %% =========================================================================== %% # relup/1 %% %% Ensure that we can generate release upgrade files using our appup file. %% =========================================================================== relup(Config) -> [{Vsn, Up, Down}] = diameter_util:consult(?APP, appup), true = is_vsn(Vsn), App = fetch(app, Config), Rel = [{erts, erlang:system_info(version)} | [{A, appvsn(A)} || A <- [sasl | fetch(applications, App)]]], Dir = fetch(priv_dir, Config), Name = write_rel(Dir, Rel, Vsn), UpFrom = acc_rel(Dir, Rel, Up), DownTo = acc_rel(Dir, Rel, Down), {[Name], [Name], [], []} %% no current in up/down and go both ways = {[Name] -- UpFrom, [Name] -- DownTo, UpFrom -- DownTo, DownTo -- UpFrom}, [[], []] = [S -- sets:to_list(sets:from_list(S)) || S <- [UpFrom, DownTo]], {ok, _, _, []} = systools:make_relup(Name, UpFrom, DownTo, [{path, [Dir]}, {outdir, Dir}, silent]). acc_rel(Dir, Rel, List) -> lists:foldl(fun(T,A) -> acc_rel(Dir, Rel, T, A) end, [], List). acc_rel(Dir, Rel, {Vsn, _}, Acc) -> [write_rel(Dir, Rel, Vsn) | Acc]. %% Write a rel file and return its name. write_rel(Dir, [Erts | Apps], Vsn) -> VS = vsn_str(Vsn), Name = "diameter_test_" ++ VS, ok = write_file(filename:join([Dir, Name ++ ".rel"]), {release, {"diameter " ++ VS ++ " test release", VS}, Erts, Apps}), Name. %% =========================================================================== %% =========================================================================== fetch(Key, List) -> {Key, {Key, Val}} = {Key, lists:keyfind(Key, 1, List)}, %% useful badmatch Val. write_file(Path, T) -> file:write_file(Path, io_lib:format("~p.", [T])). %% Is a version string of the expected form? is_vsn(V) -> V = vsn_str(V), true. %% Turn a from/to version in appup to a version string after ensuring %% that it's valid version number of regexp. In the regexp case, the %% regexp itself becomes the version string since there's no %% requirement that a version in appup be anything but a string. The %% restrictions placed on string-valued version numbers (that they be %% '.'-separated integers) are our own. vsn_str(S) when is_list(S) -> {_, match} = {S, match(S, "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*$")}, {_, nomatch} = {S, match(S, "\\.0\\.0$")}, S; vsn_str(B) when is_binary(B) -> {ok, _} = re:compile(B), binary_to_list(B). match(S, RE) -> re:run(S, RE, [{capture, none}]). %% Is an application name of the expected form? is_app(S) when is_list(S) -> {_, match} = {S, match(S, "^([a-z]([a-z_]*|[a-zA-Z]*))$")}, true.