Current section
Files
Jump to
Current section
Files
lib/mix/tasks/codecov.ex
defmodule Mix.Tasks.Codecov do
@moduledoc """
Creates `./codecov.json` report based on the exported test coverage
## Usage examples
* `mix codecov` - if no arguments supplied, the task imports all `*.coverdata` files
from the `./cover` directory.
* `mix codecov _build/test/cover cover` - any supplied argument is treated as a directory
name, the task imports all `*.coverdata` files from these directories.
"""
@shortdoc "Builds a JSON coverage report from exported *.coverdata files"
use Mix.Task
@requirements ["loadpaths"]
@impl Mix.Task
def run(args) do
Mix.ensure_application!(:tools)
project_config = Mix.Project.config()
test_coverage_config = project_config[:test_coverage]
ignore_modules = test_coverage_config[:ignore_modules]
cover_paths =
case args do
[] -> ["cover"]
_ -> args
end
{:ok, pid} = :cover.start()
{:ok, string_io} = StringIO.open("")
Process.group_leader(pid, string_io)
case Enum.flat_map(cover_paths, &Path.wildcard(Path.join(&1, "*.coverdata"))) do
[] ->
Mix.shell().error(
"Could not find .coverdata file in any of the paths: " <>
Enum.join(cover_paths, ", ")
)
entries ->
for entry <- entries do
Mix.shell().info("Importing cover results: #{entry}")
:ok = :cover.import(String.to_charlist(entry))
end
end
modules =
for m <- :cover.imported_modules(),
## analyze only loaded modules
Code.ensure_loaded?(m),
not Enum.member?(ignore_modules, m),
reduce: %{} do
acc ->
{:ok, coverage} = :cover.analyse(m, :calls, :line)
compile_info = m.module_info(:compile)
source_file =
compile_info[:source]
|> List.to_string()
|> Path.relative_to(File.cwd!())
cover_info =
case Map.get(acc, source_file) do
nil ->
source_code = File.read!(source_file)
number_of_lines =
(Regex.scan(~r"\n", source_code, return: :index) |> length()) + 1
## element with index 0 is dropped later, hence adding 1 to the number_of_lines
coverage_array = :array.new(number_of_lines + 1, default: nil)
%{name: source_file, source: source_code, coverage: coverage_array}
map ->
map
end
coverage =
for {{^m, line}, calls} <- coverage,
## elixir modules have a weird 0 calls coverage reporting for line 0, ignore it
line !== 0,
reduce: cover_info.coverage do
array -> :array.set(line, calls, array)
end
Map.put(acc, source_file, %{cover_info | coverage: coverage})
end
|> Map.values()
|> Enum.map(fn cover_info ->
## array always starts with index 0, so drop the first element.
%{cover_info | coverage: tl(:array.to_list(cover_info.coverage))}
end)
|> Enum.sort_by(& &1.name)
json_data = Jason.encode!(%{source_files: modules})
File.write!("codecov.json", json_data)
end
end