|
|
@@ -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, "").
|