emqx/lib/mix/tasks/emqx.ct.ex

261 lines
6.7 KiB
Elixir

defmodule Mix.Tasks.Emqx.Ct do
use Mix.Task
# todo: invoke the equivalent of `make merge-config` as a requirement...
@requirements ["compile", "loadpaths"]
@impl true
def run(args) do
Mix.debug(true)
opts = parse_args!(args)
Enum.each([:common_test, :eunit, :mnesia], &add_to_path_and_cache/1)
# app_to_debug = :emqx_conf
# Mix.Dep.Umbrella.cached()
# |> Enum.find(& &1.app == app_to_debug)
# |> Mix.Dep.in_dependency(fn _dep_mix_project_mod ->
# config = Mix.Project.config()
# |> IO.inspect(label: app_to_debug)
# end)
ensure_whole_emqx_project_is_loaded!()
unload_emqx_applications!()
load_common_helpers!()
hack_test_data_dirs!(opts.suites)
# ensure_suites_are_loaded!(opts.suites)
{_, 0} = System.cmd("epmd", ["-daemon"])
node_name = :"test@127.0.0.1"
:net_kernel.start([node_name, :longnames])
logdir = Path.join([Mix.Project.build_path(), "logs"])
File.mkdir_p!(logdir)
{:ok, _} = Application.ensure_all_started(:cth_readable)
# unmangle PROFILE env because some places (`:emqx_conf.resolve_schema_module`) expect
# the version without the `-test` suffix.
System.fetch_env!("PROFILE")
|> String.replace_suffix("-test", "")
|> then(& System.put_env("PROFILE", &1))
# {_, _, _} = ["test_server:do_init_tc_call -> return"] |> Enum.map(&to_charlist/1) |> :redbug.start()
maybe_start_cover()
results = :ct.run_test(
abort_if_missing_suites: true,
auto_compile: false,
suite: opts |> Map.fetch!(:suites) |> Enum.map(&to_charlist/1),
testcase: opts |> Map.fetch!(:cases) |> Enum.map(&to_charlist/1),
readable: 'true',
name: node_name,
ct_hooks: [:cth_readable_shell],
logdir: to_charlist(logdir)
)
symlink_to_last_run(logdir)
case results do
{_success, failed, {_user_skipped, auto_skipped}} when failed > 0 or auto_skipped > 0 ->
Mix.raise("failures running tests: #{failed} failed, #{auto_skipped} auto skipped")
{success, _failed = 0, {user_skipped, _auto_skipped = 0}} ->
Mix.shell().info("Test suites ran successfully. #{success} succeeded, #{user_skipped} skipped by the user")
end
end
@doc """
Here we ensure the _modules_ are loaded, not the applications.
Specially useful for suites that use utilities such as `emqx_common_test_helpers` and
similar.
This needs to run before we unload the applications, otherwise we lose their `:modules`
attribute.
"""
def ensure_whole_emqx_project_is_loaded!() do
apps_path =
Mix.Project.project_file()
|> Path.dirname()
|> Path.join("apps")
apps_path
|> File.ls!()
|> Stream.filter(fn app_name ->
apps_path
|> Path.join(app_name)
|> File.dir?()
end)
|> Stream.map(&String.to_atom/1)
|> Enum.flat_map(fn app ->
case :application.get_key(app, :modules) do
{:ok, mods} ->
mods
other ->
# IO.inspect({app, other}, label: :bad_mods)
[]
end
end)
|> Code.ensure_all_loaded!()
end
@doc """
We do this because some callbacks rely on the side-effect of the application being
loaded. We leave starting them to `:emqx_cth_suite:start`.
For example, `:emqx_dashboard:apps/0` uses that to decide which specs to load.
"""
def unload_emqx_applications!() do
for {app, _, _} <- Application.loaded_applications(),
to_string(app) =~ ~r/^emqx/ do
:ok = Application.unload(app)
end
end
defp load_common_helpers!() do
Code.ensure_all_loaded!([
:emqx_common_test_helpers,
:emqx_bridge_testlib,
:emqx_bridge_v2_testlib,
:emqx_authn_http_test_server,
])
end
defp hack_test_data_dirs!(suites) do
project_root = Path.dirname(Mix.Project.project_file())
suites
|> Stream.map(fn suite_path ->
src_data_dir =
project_root
|> Path.join(Path.rootname(suite_path) <> "_data")
data_dir_exists? = File.dir?(src_data_dir)
IO.inspect(binding())
if data_dir_exists? do
suite_mod =
suite_path
|> Path.basename(".erl")
|> String.to_atom()
ebin_path = get_mod_ebin_path(suite_mod)
data_dir = Path.basename(src_data_dir)
dest_data_path =
[ebin_path, "..", data_dir]
|> Path.join()
|> Path.expand()
File.rm(dest_data_path)
case File.ln_s(src_data_dir, dest_data_path) do
:ok ->
:ok
{:error, :eexist} ->
:ok
end
IO.inspect(binding())
end
end)
|> Stream.run()
end
defp get_mod_ebin_path(mod) do
case :code.which(mod) do
:cover_compiled ->
Mix.raise("todo (see test_server_ctrl:get_data_dir/2)")
full_path when is_list(full_path) ->
full_path
end
end
defp ensure_suites_are_loaded!(suites) do
suites
|> Enum.map(fn suite_path ->
["apps", app_name | _] = Path.split(suite_path)
String.to_atom(app_name)
end)
|> Enum.uniq()
|> Enum.flat_map(fn app ->
{:ok, mods} = :application.get_key(app, :modules)
mods
end)
|> Code.ensure_all_loaded!()
end
def add_to_path_and_cache(lib_name) do
:code.lib_dir()
|> Path.join("#{lib_name}-*")
|> Path.wildcard()
|> hd()
|> Path.join("ebin")
|> to_charlist()
|> :code.add_path(:cache)
end
defp parse_args!(args) do
{opts, _rest} = OptionParser.parse!(
args,
strict: [
suites: :string,
groups: :string,
cases: :string])
|> IO.inspect(label: :opts)
suites = get_name_list(opts, :suites)
groups = get_name_list(opts, :groups)
cases = get_name_list(opts, :cases)
if suites == [] do
Mix.raise("must define at least one suite using --suites path/to/suite1.erl,path/to/suite2.erl")
end
%{
suites: suites,
groups: groups,
cases: cases
}
end
defp get_name_list(opts, key) do
opts
|> Keyword.get(key, "")
|> String.split(",", trim: true)
end
def cover_enabled?() do
case System.get_env("ENABLE_COVER_COMPILE") do
"1" -> true
"true" -> true
_ -> false
end
end
defp maybe_start_cover() do
if cover_enabled?() do
{:ok, _} = start_cover()
end
end
defp start_cover() do
case :cover.start() do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
end
end
defp symlink_to_last_run(log_dir) do
last_dir =
log_dir
|> File.ls!()
|> Stream.filter(& &1 =~ ~r/^ct_run/)
|> Enum.sort()
|> List.last()
if last_dir do
dest = Path.join(log_dir, "last")
File.rm(dest)
File.ln_s!(last_dir, dest)
end
end
end