feat: add new module emqx_cover.erl

This commit is contained in:
Zaiming (Stone) Shi 2023-02-13 16:45:48 +01:00
parent 43aab61a3a
commit ba65cf48c3
7 changed files with 225 additions and 1 deletions

View File

@ -0,0 +1,214 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 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, "").

View File

@ -3,7 +3,7 @@
{id, "emqx_machine"},
{description, "The EMQX Machine"},
% strict semver, bump manually!
{vsn, "0.1.3"},
{vsn, "0.2.0"},
{modules, []},
{registered, []},
{applications, [kernel, stdlib]},

View File

@ -0,0 +1,2 @@
Add two new Erlang apps 'tools' and 'covertool' to the release.
So we can run profiling and test coverage analysis on release packages.

View File

@ -0,0 +1,2 @@
在发布包中增加了2个新的 Erlang app分别是 toolscovertool
这两个 app 可以用于性能和测试覆盖率的分析。

View File

@ -46,6 +46,7 @@ defmodule EMQXUmbrella.MixProject do
[
{:lc, github: "emqx/lc", tag: "0.3.2", override: true},
{:redbug, "2.0.8"},
{:covertool, github: "zmstone/covertool", tag: "2.0.4.1", override: true},
{:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", override: true},
{:ehttpc, github: "emqx/ehttpc", tag: "0.4.6", override: true},
{:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true},
@ -222,6 +223,8 @@ defmodule EMQXUmbrella.MixProject do
emqx_plugin_libs: :load,
esasl: :load,
observer_cli: :permanent,
tools: :load,
covertool: :load,
system_monitor: :load,
emqx_http_lib: :permanent,
emqx_resource: :permanent,

View File

@ -46,6 +46,7 @@
{deps,
[ {lc, {git, "https://github.com/emqx/lc.git", {tag, "0.3.2"}}}
, {redbug, "2.0.8"}
, {covertool, {git, "https://github.com/zmstone/covertool", {tag, "2.0.4.1"}}}
, {gpb, "4.19.5"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps
, {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.9.1"}}}
, {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.9"}}}

View File

@ -378,6 +378,8 @@ relx_apps(ReleaseType, Edition) ->
{emqx_plugin_libs, load},
{esasl, load},
observer_cli,
{tools, load},
{covertool, load},
% started by emqx_machine
{system_monitor, load},
emqx_http_lib,