diff options
Diffstat (limited to 'erts/test/otp_SUITE.erl')
-rw-r--r-- | erts/test/otp_SUITE.erl | 557 |
1 files changed, 221 insertions, 336 deletions
diff --git a/erts/test/otp_SUITE.erl b/erts/test/otp_SUITE.erl index 1690406314..a8424ea46f 100644 --- a/erts/test/otp_SUITE.erl +++ b/erts/test/otp_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2000-2022. All Rights Reserved. +%% Copyright Ericsson AB 2000-2023. 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. @@ -218,17 +218,13 @@ call_to_deprecated(Config) when is_list(Config) -> {comment,integer_to_list(length(DeprecatedCalls))++" calls to deprecated functions"}. call_to_size_1(Config) when is_list(Config) -> - %% Applications that do not call erlang:size/1: - Apps = [asn1,compiler,debugger,kernel,observer,parsetools, - runtime_tools,stdlib,tools], + %% Forbid the use of erlang:size/1 in all applications. + Apps = all_otp_applications(Config), not_recommended_calls(Config, Apps, {erlang,size,1}). call_to_now_0(Config) when is_list(Config) -> - %% Applications that do not call erlang:now/1: - Apps = [asn1,common_test,compiler,debugger,dialyzer, - kernel,mnesia,observer,parsetools,reltool, - runtime_tools,sasl,stdlib,syntax_tools, - tools], + %% Forbid the use of erlang:now/1 in all applications except et. + Apps = all_otp_applications(Config) -- [et], not_recommended_calls(Config, Apps, {erlang,now,0}). not_recommended_calls(Config, Apps0, MFA) -> @@ -281,9 +277,20 @@ not_recommended_calls(Config, Apps0, MFA) -> {comment, Mess} end; _ -> - ct:fail({length(CallsToMFA),calls_to_size_1}) + ct:fail({length(CallsToMFA),calls_to,MFA}) end. +all_otp_applications(Config) -> + Server = proplists:get_value(xref_server, Config), + {ok,AllApplications} = xref:q(Server, "A"), + OtpAppsMap = get_otp_applications(Config), + [App || App <- AllApplications, is_map_key(App, OtpAppsMap)]. + +get_otp_applications(Config) -> + DataDir = proplists:get_value(data_dir, Config), + [Current|_] = read_otp_version_table(DataDir), + read_version_lines([Current]). + is_present_application(Name, Server) -> Q = io_lib:format("~w : App", [Name]), case xref:q(Server, lists:flatten(Q)) of @@ -475,6 +482,189 @@ check_apps_deps([{App, Deps}|AppDeps], IgnoreApps) -> end end. +%% +%% Test that the runtime dependencies have not become stale. +%% + +%% Path of installed OTP releases. +-define(OTP_RELEASES, "/usr/local/otp/releases"). + +%% Wildcard to match all releases from OTP 17. +-define(OTP_RELEASE_WC, "{sles10_64_17_patched,ubuntu16_64_*_patched}"). + +test_runtime_dependencies_versions(Config) -> + DataDir = proplists:get_value(data_dir, Config), + + OtpReleases = ?OTP_RELEASES, + OtpReleasesWc = filename:join(OtpReleases, ?OTP_RELEASE_WC), + + IgnoreApps = [], + IgnoreUndefs = ignore_undefs(), + + FirstVersionForApp = get_first_app_versions(DataDir), + + case {element(1, os:type()) =:= unix, + not is_development_build(DataDir), + filelib:is_dir(OtpReleases)} of + {true, true, true} -> + test_runtime_dependencies_versions_rels( + IgnoreApps, + IgnoreUndefs, + FirstVersionForApp, + OtpReleasesWc); + {false, _, _} -> + {skip, "This test only runs on Unix systems"}; + {_, false, _} -> + {skip, + "This test case is designed to run in the Erlang/OTP teams " + "test system for daily tests. The test case depends on that " + "app versions have been set correctly by scripts that " + "are executed before creating builds for the daily tests."}; + {_, _ ,false} -> + {skip, "Can not do the tests without a proper releases dir. " + "Check that " ++ OtpReleases ++ " is set up correctly."} + end. + +ignore_undefs() -> + Socket = case lists:member(prim_socket, erlang:pre_loaded()) of + true -> + #{}; + false -> + Ignore = #{{prim_socket,'_','_'} => true, + {socket_registry,'_','_'} => true, + {prim_net,'_','_'} => true }, + #{kernel => Ignore, erts => Ignore} + end, + Socket#{eunit => + %% Intentional call to nonexisting function + #{{eunit_test, nonexisting_function, 0} => true}, + diameter => + %% The following functions are optional dependencies for diameter + #{{dbg,ctp,0} => true, + {dbg,p,2} => true, + {dbg,stop,0} => true, + {dbg,trace_port,2} => true, + {dbg,tracer,2} => true, + {erl_prettypr,format,1} => true, + {erl_syntax,form_list,1} => true}, + common_test => + %% ftp:start/0 has been part of the ftp application from + %% the beginning so it is unclear why xref report this + %% as undefined + #{{ftp,start,0} => true}}. + +%% Read the otp_versions.table file and create a mapping from application name +%% the first version for each application. +get_first_app_versions(DataDir) -> + Lines = read_otp_version_table(DataDir), + read_version_lines(Lines). + +test_runtime_dependencies_versions_rels(IgnoreApps, IgnoreUndefs, + FirstVersionForApp, OtpReleasesWc) -> + AppVersionToPath = version_to_path(OtpReleasesWc), + Deps = [Dep || {App,_,_}=Dep <- get_deps(FirstVersionForApp), + not lists:member(App, IgnoreApps)], + case test_deps(Deps, IgnoreUndefs, AppVersionToPath, FirstVersionForApp) of + [] -> + ok; + [_|_]=Undefs -> + _ = [print_undefs(Undef) || Undef <- Undefs], + ct:fail({length(Undefs),errors}) + end. + +print_undefs({_App,AppPath,Deps,Undefs}) -> + App = filename:basename(filename:dirname(AppPath)), + io:format("Undefined functions in ~ts:", [App]), + io:put_chars([io_lib:format(" ~p:~p/~p\n", [M,F,A]) || + {M,F,A} <- Undefs]), + io:format("Dependencies: ~ts\n", [lists:join(" ", Deps)]). + +version_to_path(OtpReleasesWc) -> + CurrentWc = filename:join([code:lib_dir(), "*-*", "ebin"]), + LibWc = filename:join([OtpReleasesWc, "lib", "*-*", "ebin"]), + Dirs = lists:sort(filelib:wildcard(CurrentWc) ++ filelib:wildcard(LibWc)), + All = [{filename:basename(filename:dirname(Dir)),Dir} || Dir <- Dirs], + maps:from_list(All). + +get_deps(FirstVersionForApp) -> + Paths = [begin + Dir = filename:dirname(Path), + [App0 | _] = string:split(filename:basename(Dir), "-"), + App = list_to_atom(App0), + AppFile = filename:join(Path, App0 ++ ".app"), + {Path, App, AppFile} + end || Path <- code:get_path(), + filename:basename(Path) =:= "ebin"], + %% Only keep applications included in OTP. + Apps = [Triple || {_, App, AppFile}=Triple <- Paths, + filelib:is_file(AppFile), + is_map_key(App, FirstVersionForApp)], + [{App, Path, get_runtime_deps(App, AppFile)} || {Path, App, AppFile} <- Apps]. + +get_runtime_deps(App, AppFile) -> + {ok,[{application, App, Info}]} = file:consult(AppFile), + case lists:keyfind(runtime_dependencies, 1, Info) of + {runtime_dependencies, RDeps} -> + RDeps; + false -> + [] + end. + +test_deps([{Name,Path,Deps}|Apps], IgnoreUndefs, AppVersionToPath, FirstVersionForApp) -> + case test_dep(Name, Path, Deps, AppVersionToPath, FirstVersionForApp, IgnoreUndefs) of + ok -> + test_deps(Apps, IgnoreUndefs, AppVersionToPath, FirstVersionForApp); + {error, Error} -> + [Error|test_deps(Apps, IgnoreUndefs, AppVersionToPath, FirstVersionForApp)] + end; +test_deps([], _IgnoreUndefs, _AppVersionToPath, _FirstVersionForApp) -> + []. + +test_dep(App, AppPath, Deps, AppVersionToPath, FirstVersionForApp, IgnoreUndefs) -> + DepPaths = [get_app_path(Dep, AppVersionToPath, FirstVersionForApp, App) || + Dep <- Deps], + + Server = xref_server_test_dep, + {ok, _} = xref:start(Server, []), + xref:set_default(Server, [{verbose,false}, + {warnings,false}, + {builtins,true}]), + ok = xref:set_library_path(Server, DepPaths), + {ok, _} = xref:add_directory(Server, AppPath), + {ok, Undef0} = xref:analyze(Server, undefined_functions), + xref:stop(Server), + + %% Filter out undefined functions that we should ignore. + Ignore = maps:get(App, IgnoreUndefs, #{}), + Undef = [MFA || {M,F,_A}=MFA <- Undef0, + not is_map_key(MFA, Ignore), + not is_map_key({M,'_','_'}, Ignore), + not is_map_key({M,F,'_'}, Ignore)], + case Undef of + [] -> + ok; + [_|_] -> + {error, {App, AppPath, Deps, Undef}} + end. + +get_app_path(App, AppVersionToPath, FirstVersionForApp, ReferencedBy) -> + case AppVersionToPath of + #{App := Path} -> + Path; + #{} -> + [Name0, _Version] = string:split(App, "-"), + Name = list_to_existing_atom(Name0), + First = map_get(Name, FirstVersionForApp), + io:format("WARNING: ~ts referenced by ~ts is too old; using ~ts instead\n", + [App, ReferencedBy, First]), + map_get(First, AppVersionToPath) + end. + +is_development_build(DataDir) -> + OTPVersionTicketsPath = filename:join(DataDir, "otp_version_tickets"), + {ok, FileContentBin} = file:read_file(OTPVersionTicketsPath), + string:trim(binary_to_list(FileContentBin), both, "\n ") =:= "DEVELOPMENT". + %%% %%% Common help functions. %%% @@ -545,329 +735,24 @@ start_xref_server(Server, Mode) -> end, Server. -get_suite_data_dir_path() -> - filename:join(filename:dirname(code:which(?MODULE)), "otp_SUITE_data"). - -get_otp_versions_table_path() -> - filename:join(get_suite_data_dir_path(), "otp_versions.table"). - -get_otp_version_tickets_path() -> - filename:join(get_suite_data_dir_path(), "otp_version_tickets"). - -%% Return a map that maps from app versions to the OTP versions they -%% were last released in. The function makes use of the file -%% "otp_versions.table" and the current code:get_path() to -%% find apps. -get_runtime_dep_to_otp_version_map() -> - %% Find apps in "otp_versions.table" - VersionsTableFile = get_otp_versions_table_path(), - VersionsTableBin = - case file:read_file(VersionsTableFile) of - {ok, Bin} -> Bin; - Error -> ct:fail("Could not read the file ~s which is needed to perform the test. " - "Error: ~p~n", - [VersionsTableFile, Error]) - end, - VersionsTableStr = erlang:binary_to_list(VersionsTableBin), - Lines = lists:reverse(string:tokens(VersionsTableStr, "\n")), - AddVersionsInString = - fun(Map, OTPVersion, AppVersionsString0) -> - AppVersionsString = - lists:flatten(string:replace(AppVersionsString0, "#", "", all)), - AppVersions = string:tokens(AppVersionsString, " "), - lists:foldl( - fun(AppVersion0, MapSoFar) -> - case string:trim(AppVersion0) of - "" -> - MapSoFar; - AppVersion1 -> - maps:put(AppVersion1, OTPVersion, MapSoFar) - end - end, - Map, - AppVersions) - end, - VersionMap0 = - lists:foldl( - fun(Line, MapSoFar) -> - [OTPVersion, AppVersionsString| _] = - string:tokens(Line, ":"), - AddVersionsInString(MapSoFar, - string:trim(OTPVersion), - string:trim(AppVersionsString)) - end, - #{}, - Lines), - %% Find apps in code:get_path() - lists:foldl( - fun(Path, MapSoFar) -> - case filelib:wildcard(filename:join(Path, "*.app")) of - [AppFile] -> - {ok,[{application, App, Info}]} = file:consult(AppFile), - case lists:keyfind(vsn, 1, Info) of - false -> - MapSoFar; - {vsn, VsnStr} -> - AppVsnStr = - erlang:atom_to_list(App) ++ "-" ++ VsnStr, - maps:put(AppVsnStr, {latest, Path}, MapSoFar) - end; - _ -> - MapSoFar - end - end, - VersionMap0, - code:get_path()). - -%% Find runtime dependencies for an app -get_runtime_deps(App) -> - AppFile = code:where_is_file(atom_to_list(App) ++ ".app"), - {ok,[{application, App, Info}]} = file:consult(AppFile), - case lists:keyfind(runtime_dependencies, 1, Info) of - {runtime_dependencies, RDeps} -> - RDeps; - false -> - [] - end. - -%% Given a release dir find the path to the given dependency -find_dep_in_rel_dir(Dep, RelDirRoot) -> - %% The dependencies that we have found are cached to avoid - %% searching through the file system unnecessary many times - CacheId = runtime_dep_test_cache, - DepCache = - case erlang:get(CacheId) of - undefined -> #{}; - M-> M - end, - case maps:get(Dep, DepCache, none) of - none -> - DepPaths = filelib:wildcard(filename:join([RelDirRoot, "lib", "**", Dep, "ebin"])), - case DepPaths of - [Path] -> - erlang:put(CacheId, maps:put(Dep, Path, DepCache)), - Path; - _ -> - ErrorMessage = - io_lib:format("ERROR: Could not find ~p in ~p (where it is supposed to be)." - "Found ~p~n", [Dep, RelDirRoot, DepPaths]), - io:format(lists:flatten(ErrorMessage)), - ct:fail(ErrorMessage) - - end; - Path -> - Path - end. - -%% Get the major OTP version part of an OTP version string -%% Example: OTP-22.0 gives 22 -get_major_version(OtpVersion) -> - [_,MajorVersion|_] = string:tokens(OtpVersion, "-."), - MajorVersion. - -%% Returns the release directory for the oldest available OTP release -%% on the current machine -first_available_otp_rel() -> - %% At least one version less than the current version should be available - SholdBeAvailable = erlang:list_to_integer(erlang:system_info(otp_release)) - 1, - (fun FindOldest(CurrRel, PrevAvailable) -> - case test_server:is_release_available(erlang:integer_to_list(CurrRel)) of - false -> PrevAvailable; - true -> FindOldest(CurrRel-1, erlang:integer_to_list(CurrRel)) - end - end)(SholdBeAvailable, none). - -%% Searches for the oldest available version of Dep -get_oldest_available_version_of_dep(Dep) -> - [DepName, _Version] = string:tokens(Dep, "-"), - FirstAvailableRel = first_available_otp_rel(), - (fun Find(LookInRel) -> - case test_server:is_release_available(LookInRel) of - true -> - RelRoot = find_rel_root(LookInRel), - Options0 = filelib:wildcard(filename:join([RelRoot, "lib", "**", "ebin"])), - Options1 = [Opt || - Opt <- Options0, - string:find(Opt, - filename:join("lib", DepName ++ "-")) =/= nomatch], - GetVersionTuple = - fun(Path) -> - [AppVerStr] = - [C || C <- filename:split(Path), - string:find(C, DepName ++ "-") =/= nomatch], - [_,VerStr] = string:tokens(AppVerStr, "-"), - erlang:list_to_tuple([erlang:list_to_integer(X) || - X <- string:tokens(VerStr, ".")]) - end, - Options2 = - lists:sort(fun(A,B) -> - GetVersionTuple(A) =< GetVersionTuple(B) - end, - Options1), - case Options2 of - [Path|_] -> - Path; - _ -> - NextRelToTry = - erlang:integer_to_list(erlang:list_to_integer(LookInRel)+1), - Find(NextRelToTry) - end; - false -> - ct:fail({could_not_find_dep_anywhere, DepName}) - end - end)(FirstAvailableRel). - -find_rel_root(Rel) -> - case test_server:find_release(Rel) of - not_available -> - not_available; - OtpRelErl -> filename:dirname(filename:dirname(OtpRelErl)) - end. - -%% Find the absolute paths to RuntimeDeps -get_paths_to_dependencies(App, RuntimeDeps) -> - FirstAvailableOTPRel = first_available_otp_rel(), - FirstAvailableRel = erlang:list_to_integer(FirstAvailableOTPRel), - DepToOtpVerMap = get_runtime_dep_to_otp_version_map(), - lists:foldl( - fun(Dep, SoFar) -> - case maps:get(Dep, DepToOtpVerMap, false) of - false -> - ct:fail(io_lib:format( - "The dependency ~s for ~p could not be found. " - "Have you typed a non-existing version?", - [Dep, App])); - {latest, Path} -> - %% The dependency is in Path (one of the paths returned by code:get_paths()) - [Path | SoFar]; - OtpVersionWithDep -> - OtpMajorVersionWithDep = get_major_version(OtpVersionWithDep), - OtpMajorVersionWithDepInt = erlang:list_to_integer(OtpMajorVersionWithDep), - case find_rel_root(OtpMajorVersionWithDep) of - not_available when FirstAvailableRel > OtpMajorVersionWithDepInt -> - io:format("Warning: Could not find runtime dependency ~p for ~p. " - "~p belongs to ~p but ~p is too old to be available on this machine. " - "Trying to find the oldest available version of ~p...", - [Dep, App, Dep, OtpVersionWithDep, OtpVersionWithDep, Dep]), - [get_oldest_available_version_of_dep(Dep) | SoFar]; - RelDirRoot -> - [find_dep_in_rel_dir(Dep, RelDirRoot) | SoFar] - end - end - end, - [], - RuntimeDeps). - -test_app_runtime_deps_versions(AppPath, App, IgnoredUndefinedFunctions) -> - %% Get a list of all runtime dependencies for app - RuntimeDeps = get_runtime_deps(App), - %% Get paths to the found runtime dependencies - DepPaths = get_paths_to_dependencies(App, RuntimeDeps), - XRefSName = test_app_runtime_deps_versions_server, - %% Start xref server and do the test - {ok, _} = xref:start(XRefSName, []), - ok = xref:set_library_path(XRefSName, DepPaths), - Dir = filename:join(AppPath, "ebin"), - {ok, _} = xref:add_directory(XRefSName, Dir), - {ok, UndefinedFunctions0} = xref:analyze(XRefSName, undefined_functions), - xref:stop(XRefSName), - %% Filter out undefined functions that we should ignore - UndefinedFunctions1 = - [F || F <- UndefinedFunctions0, - not maps:get(F, IgnoredUndefinedFunctions, false), - not maps:get({element(1,F),'_','_'}, IgnoredUndefinedFunctions, false), - not maps:get({element(1,F),element(2,F),'_'}, IgnoredUndefinedFunctions, false)], - case UndefinedFunctions1 of - [] -> - ok; - UndefinedFunctions -> - {error_undefined_functions_in_app, App, UndefinedFunctions} - end. - -test_runtime_dependencies_versions_rels(IgnoreApps, AppsToIgnoredUndefinedFunctions) -> - %% Do for every application: - %% 1. Find deps from app file - %% 2. Find where the deps are installed - %% 3. Run xref tests on the apps with the specified dependencies - %% 4. Report error if undefined function - Apps0 = [{Path, list_to_atom(AppName)} - || {match, [Path, AppName]} - <- [re:run(X,"(" ++ code:lib_dir()++"/"++"([^/-]*).*)/ebin", - [{capture,[1,2],list},unicode]) || X <- code:get_path()]], - Apps = [{Path, App} || - {Path, App} <- Apps0, - code:where_is_file(atom_to_list(App) ++ ".app") =/= non_existing], - Res = [test_app_runtime_deps_versions(AppPath, - App, - maps:get(App, AppsToIgnoredUndefinedFunctions, #{})) || - {AppPath, App} <- Apps, not lists:member(App, IgnoreApps)], - BadRes = [R || R <- Res, R =/= ok], - case BadRes =:= [] of - true -> ok; - _ -> - ct:fail(BadRes) - end. - -is_development_build() -> - {ok, FileContentBin} = file:read_file(get_otp_version_tickets_path()), - "DEVELOPMENT" =:= string:trim(erlang:binary_to_list(FileContentBin), both, "\n "). - -test_runtime_dependencies_versions(_Config) -> - ReleasesDir = "/usr/local/otp/releases", - IgnoreApps = [], - SocketIgnore = case lists:member(prim_socket, erlang:pre_loaded()) of - true -> #{}; - false -> - Ignore = #{{prim_socket,'_','_'} => true, - {socket_registry,'_','_'} => true, - {prim_net,'_','_'} => true }, - #{ kernel => Ignore, erts => Ignore } - end, - AppsToIgnoredUndefinedFunctions = - #{eunit => - %% Intentional call to nonexisting function - #{{eunit_test, nonexisting_function, 0} => true}, - diameter => - %% The following functions are optional dependencies for diameter - #{{dbg,ctp,0} => true, - {dbg,p,2} => true, - {dbg,stop_clear,0} => true, - {dbg,trace_port,2} => true, - {dbg,tracer,2} => true, - {erl_prettypr,format,1} => true, - {erl_syntax,form_list,1} => true}, - common_test => - %% ftp:start/0 has been part of the ftp application from - %% the beginning so it is unclear why xref report this - %% as undefined - #{{ftp,start,0} => true}}, - case {erlang:element(1, os:type()) =:= unix, - not is_development_build(), - filelib:is_dir(ReleasesDir), - filelib:is_file(get_otp_versions_table_path()), - first_available_otp_rel() =/= none} of - {true, true, true, true, true} -> - test_runtime_dependencies_versions_rels( - IgnoreApps, - maps:merge(AppsToIgnoredUndefinedFunctions, SocketIgnore)); - {_, _ ,_, false, _} -> {skip, - "Could not find the file \"otp_versions.table\". " - "Check that the test has been built correctly. " - "\"otp_versions.table\" is copied to \"erts/test/otp_SUITE_data\" " - "by the makefile \"erts/test/Makefile\""}; - {_, false , _, _, _} -> {skip, - "This test case is designed to run in the Erlang/OTP teams " - "test system for nightly tests. The test case depend on that " - "app versions have been set correctly by scripts that " - "are executed before creating builds for the nightly tests."}; - {_, _ ,false, _, _} -> {skip, "Can not do the tests without a proper releases dir. " - "Check that " ++ ReleasesDir ++ " is set up correctly."}; - {_, _ , _, _, false} -> - PrevRelNr = erlang:list_to_integer(erlang:system_info(otp_release)) - 1, - PrevRelNrStr = erlang:integer_to_list(PrevRelNr), - {skip, - "Seems like the releases dir is not set up correctly. " - "Is release " ++ PrevRelNrStr ++ " installed in the releases dir? " - "(releases dir = " ++ ReleasesDir ++ ")"}; - {false, _ ,_, _, _} -> {skip, "This test only runs on Unix systems"} - end. +read_otp_version_table(DataDir) -> + VersionTableFile = filename:join(DataDir, "otp_versions.table"), + {ok, Contents} = file:read_file(VersionTableFile), + binary:split(Contents, <<"\n">>, [global,trim]). + +read_version_lines(Lines) -> + read_version_lines(Lines, #{}). + +read_version_lines([Line|Lines], Map0) -> + [<<"OTP-",_/binary>>, <<":">> | Apps] = binary:split(Line, <<" ">>, [global,trim]), + Map = lists:foldl(fun(App, Acc) -> + case binary:split(App, <<"-">>) of + [Name, _Version] -> + Acc#{binary_to_atom(Name) => binary_to_list(App)}; + [_] -> + Acc + end + end, Map0, Apps), + read_version_lines(Lines, Map); +read_version_lines([], Map) -> + Map. |