فهرست منبع

feat: add new module emqx_cover.erl

Zaiming (Stone) Shi 3 سال پیش
والد
کامیت
ba65cf48c3

+ 214 - 0
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, "").

+ 1 - 1
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]},

+ 2 - 0
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.

+ 2 - 0
changes/v5.0.18/fix-9966.zh.md

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

+ 3 - 0
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,

+ 1 - 0
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"}}}

+ 2 - 0
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,