defmodule Archeometer.Explore.Coverage do
@moduledoc """
Module for collecting test coverage statistics, using Erlang `cover` module.
As it's a tool for Erlang code, instead of using its module and function
capabilities, the line coverage is directly used, and then counted per module,
in the same manner the Mix `test.coverage` task does.
"""
def prepare_code_server() do
_code_server = cover_compile()
run_tests()
end
def calculate_module_coverage() do
module_coverage =
get_line_coverage()
|> mod_coverage_from_lines()
missing_coverage =
module_coverage
|> MapSet.new(fn %{module: mod} -> mod end)
|> missing_mods()
module_coverage
|> Enum.concat(missing_coverage)
|> Enum.sort()
end
def calculate_function_coverage() do
get_function_coverage()
|> Enum.map(fn {{module, name, arity}, coverage} ->
%{name: name, module: module, num_args: arity, coverage: coverage}
end)
end
defp missing_mods(coverage) do
:cover.modules()
|> Enum.filter(fn mod -> mod not in coverage end)
|> Enum.map(fn mod -> %{module: mod, coverage: 1.0} end)
end
defp run_tests() do
unless System.get_env("MIX_ENV") || Mix.env() == :test do
Mix.raise("env: test != #{Mix.env()}")
end
Mix.Task.run("test")
end
defp mod_coverage_from_lines(line_diagnosis) do
line_diagnosis
|> Enum.group_by(fn {{mod, _}, _} -> mod end, fn {_, line_diagnosis} ->
# a line is considere covered if any of its diagnosis shows it as covered
Enum.any?(line_diagnosis)
end)
|> Enum.map(fn {mod, line_coverage} ->
covered = Enum.count(line_coverage, & &1)
%{module: mod, coverage: covered / length(line_coverage)}
end)
end
defp get_line_coverage() do
{:result, raw_coverage, _failed} = :cover.analyse(:coverage, :line)
raw_coverage
|> Enum.filter(fn {{_, line}, _} -> line != 0 end)
|> Enum.group_by(&elem(&1, 0), fn {_, cov} ->
case cov do
{1, 0} -> true
{0, 1} -> false
end
end)
end
defp get_function_coverage() do
{:result, raw_coverage, _failed} = :cover.analyse(:coverage, :function)
raw_coverage
|> Enum.map(fn {function_id, {covered, uncovered}} ->
{function_id, covered / (covered + uncovered)}
end)
end
def cover_compile() do
_ = :cover.stop()
Mix.Task.run("compile")
{:ok, pid} = :cover.start()
compiled_files =
Mix.Project.config()
|> app_paths()
|> Enum.flat_map(&beams(&1))
|> :cover.compile_beam()
case compiled_files do
results when is_list(results) ->
:ok
{:error, reason} ->
Mix.raise("#{inspect(reason)}")
end
pid
end
defp app_paths(config) do
case Mix.Project.apps_paths(config) do
nil ->
[Mix.Project.compile_path()]
paths ->
build_path = Mix.Project.build_path(config)
Enum.map(paths, fn {app, _} ->
Path.join([build_path, "lib", Atom.to_string(app), "ebin"])
end)
end
end
defp beams(dir) do
consolidation_dir = Mix.Project.consolidation_path()
consolidated =
case File.ls(consolidation_dir) do
{:ok, files} -> files
_ -> []
end
for file <- File.ls!(dir), Path.extname(file) == ".beam" do
with true <- file in consolidated,
[_ | _] = path <- :code.which(file |> Path.rootname() |> String.to_atom()) do
path
else
_ -> String.to_charlist(Path.join(dir, file))
end
end
end
end