diff --git a/apps/emqx_machine/src/emqx_cover.erl b/apps/emqx_machine/src/emqx_cover.erl new file mode 100644 index 000000000..c6f610746 --- /dev/null +++ b/apps/emqx_machine/src/emqx_cover.erl @@ -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, ""). diff --git a/apps/emqx_machine/src/emqx_machine.app.src b/apps/emqx_machine/src/emqx_machine.app.src index c805fdd25..4acb3cb84 100644 --- a/apps/emqx_machine/src/emqx_machine.app.src +++ b/apps/emqx_machine/src/emqx_machine.app.src @@ -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]}, diff --git a/changes/v5.0.18/fix-9966.en.md b/changes/v5.0.18/fix-9966.en.md new file mode 100644 index 000000000..cc3a0bb8f --- /dev/null +++ b/changes/v5.0.18/fix-9966.en.md @@ -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. diff --git a/changes/v5.0.18/fix-9966.zh.md b/changes/v5.0.18/fix-9966.zh.md new file mode 100644 index 000000000..df5b7cff7 --- /dev/null +++ b/changes/v5.0.18/fix-9966.zh.md @@ -0,0 +1,2 @@ +在发布包中增加了2个新的 Erlang app,分别是 ‘tools’ 和 ‘covertool’。 +这两个 app 可以用于性能和测试覆盖率的分析。 diff --git a/mix.exs b/mix.exs index a2df76701..baa5750f0 100644 --- a/mix.exs +++ b/mix.exs @@ -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, diff --git a/rebar.config b/rebar.config index ffdb7407a..7997f2c4b 100644 --- a/rebar.config +++ b/rebar.config @@ -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"}}} diff --git a/rebar.config.erl b/rebar.config.erl index 4ff94bd78..14b84213b 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -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,