emqx/apps/emqx_machine/src/emqx_cover.erl

215 lines
6.2 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. 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.
%%--------------------------------------------------------------------
%% @doc This module is NOT used in production.
%% It is used to collect coverage data when running blackbox test
-module(emqx_cover).
-include_lib("covertool/include/covertool.hrl").
-ifdef(EMQX_ENTERPRISE).
-define(OUTPUT_APPNAME, 'EMQX Enterprise').
-else.
-define(OUTPUT_APPNAME, 'EMQX').
-endif.
-export([
start/0,
start/1,
abort/0,
export_and_stop/1,
lookup_source/1
]).
%% This is a ETS table to keep a mapping of module name (atom) to
%% .erl file path (relative path from project root)
%% We needed this ETS table because the source file information
%% is missing from the .beam metadata sicne we are using 'deterministic'
%% compile flag.
-define(SRC, emqx_cover_module_src).
%% @doc Start cover.
%% All emqx_ modules will be cover-compiled, this may cause
%% some excessive RAM consumption and result in warning logs.
start() ->
start(#{}).
%% @doc Start cover.
%% All emqx_ modules will be cover-compiled, this may cause
%% some excessive RAM consumption and result in warning logs.
%% Supported options:
%% - project_root: the directory to search for .erl source code
%% - debug_secret_file: only applicable to EMQX Enterprise
start(Opts) ->
ok = abort(),
DefaultDir = os_env("EMQX_PROJECT_ROOT"),
ProjRoot = maps:get(project_root, Opts, DefaultDir),
case ProjRoot =:= "" of
true ->
io:format("Project source code root dir is not provided.~n"),
io:format(
"You may either start EMQX node with environment variable EMQX_PROJECT_ROOT set~n"
),
io:format("Or provide #{project_root => \"/path/to/emqx/\"} as emqx_cover:start arg~n"),
exit(project_root_is_not_set);
false ->
ok
end,
%% spawn a ets table owner
%% this implementation is kept dead-simple
%% because there is no concurrency requirement
Parent = self(),
{Pid, Ref} =
erlang:spawn_monitor(
fun() ->
true = register(?SRC, self()),
_ = ets:new(?SRC, [named_table, public]),
_ = Parent ! {started, self()},
receive
stop ->
ok
end
end
),
receive
{started, Pid} ->
ok;
{'DOWN', Ref, process, Pid, Reason} ->
throw({failed_to_start, Reason})
after 1000 ->
throw({failed_to_start, timeout})
end,
Modules = modules(Opts),
case cover:start() of
{ok, _Pid} ->
ok;
{error, {already_started, _Pid}} ->
ok;
Other ->
throw(Other)
end,
ok = cover_compile(Modules),
io:format("cover-compiled ~p modules~n", [length(Modules)]),
ok = put_project_root(ProjRoot),
ok = do_build_source_mapping(ProjRoot, Modules),
CachedModulesCount = ets:info(?SRC, size),
io:format("source-cached ~p modules~n", [CachedModulesCount]),
ok.
%% @doc Abort cover data collection without exporting.
abort() ->
_ = cover:stop(),
case whereis(?SRC) of
undefined ->
ok;
Pid ->
Ref = monitor(process, Pid),
exit(Pid, kill),
receive
{'DOWN', Ref, process, Pid, _} ->
ok
end
end,
ok.
%% @doc Export coverage report (xml) format.
%% e.g. `emqx_cover:export_and_stop("/tmp/cover.xml").'
export_and_stop(Path) when is_list(Path) ->
ProjectRoot = get_project_root(),
Config = #config{
appname = ?OUTPUT_APPNAME,
sources = [ProjectRoot],
output = Path,
lookup_source = fun ?MODULE:lookup_source/1
},
covertool:generate_report(Config, cover:modules()).
get_project_root() ->
[{_, Dir}] = ets:lookup(?SRC, {root, ?OUTPUT_APPNAME}),
Dir.
put_project_root(Dir) ->
_ = ets:insert(?SRC, {{root, ?OUTPUT_APPNAME}, Dir}),
ok.
do_build_source_mapping(Dir, Modules0) ->
Modules = sets:from_list(Modules0, [{version, 2}]),
All = filelib:wildcard("**/*.erl", Dir),
lists:foreach(
fun(Path) ->
ModuleNameStr = filename:basename(Path, ".erl"),
Module = list_to_atom(ModuleNameStr),
case sets:is_element(Module, Modules) of
true ->
ets:insert(?SRC, {Module, Path});
false ->
ok
end
end,
All
),
ok.
lookup_source(Module) ->
case ets:lookup(?SRC, Module) of
[{_, Path}] ->
Path;
[] ->
false
end.
modules(_Opts) ->
%% TODO better filter based on Opts,
%% e.g. we may want to see coverage info for ehttpc
Filter = fun is_emqx_module/1,
find_modules(Filter).
cover_compile(Modules) ->
Results = cover:compile_beam(Modules),
Errors = lists:filter(
fun
({ok, _}) -> false;
(_) -> true
end,
Results
),
case Errors of
[] ->
ok;
_ ->
io:format("failed_to_cover_compile:~n~p~n", [Errors]),
throw(failed_to_cover_compile)
end.
find_modules(Filter) ->
All = code:all_loaded(),
F = fun({M, _BeamPath}) -> Filter(M) andalso {true, M} end,
lists:filtermap(F, All).
is_emqx_module(?MODULE) ->
%% do not cover-compile self
false;
is_emqx_module(Module) ->
case erlang:atom_to_binary(Module, utf8) of
<<"emqx", _/binary>> ->
true;
_ ->
false
end.
os_env(Name) ->
os:getenv(Name, "").