Packages

Generate coverage reports for the code in a git diff

Current section

Files

Jump to
cover_diff lib cover_diff.ex
Raw

lib/cover_diff.ex

defmodule CoverDiff do
@moduledoc """
Generates a coverage report for the code in a git diff.
To use it as a coverage module:
```
def project() do
[
...
test_coverage: [tool: CoverDiff, base_branch: "my_base_branch"]
...
]
end
```
The diff will span between your current HEAD and the specified branch in
your `:test_coverage` options:
* `:base_branch` - branch to diff against, defaults to `"master"`
* `:context` - how many lines to show around uncovered lines in reports,
defaults to 2
CoverDiff currently does not work with umbrella applications.
CoverDiff can also be used as a Mix task without polluting your project
configuration. This lets you use any cover tool capable of exporting
.coverdata files during tests and analyze them afterwards using CoverDiff.
Check Mix.Tasks.CoverDiff for details.
"""
alias CoverDiff.{Cover, Diff, Output}
@default_threshold 90
@default_base_branch "master"
@default_context_size 2
@default_output "cover"
def run(opts) do
opts =
Mix.Project.config()
|> Keyword.get(:test_coverage, [])
|> format_opts()
|> Map.merge(Map.new(opts))
with {:ok, diff} <- Diff.get_diff(opts[:base_branch], opts[:context]),
_result <- Cover.compile_from_diff(diff),
:ok <- Cover.import_coverage(opts[:output]) do
generate_cover_results(diff, opts)
else
_ -> System.at_exit(fn _ -> exit({:shutdown, 3}) end)
end
end
@doc false
def start(_compile_path, opts) do
opts = format_opts(opts)
diff = Diff.get_diff(opts[:base_branch], opts[:context])
CoverDiff.Cover.compile_from_diff(diff)
fn -> generate_cover_results(diff, opts) end
end
defp format_opts(opts) do
%{
base_branch: opts[:base_branch] || @default_base_branch,
context: opts[:context] || @default_context_size,
output:
case opts[:output] do
false -> false
output -> output || @default_output
end,
summary:
case opts[:summary] do
false -> false
_ -> true
end,
threshold:
case opts[:summary] do
false -> nil
[threshold: threshold] -> threshold
_ -> @default_threshold
end
}
end
# If we remove a test without changing the code, coverage is not impacted :(
def generate_cover_results(diff, opts) do
Mix.shell().info("Generating cover results ...")
diff
|> add_coverage()
|> Output.export(opts)
end
defp add_coverage(changes) do
coverage = gather_coverage(changes)
changes
|> Enum.map(fn {filename, lines} ->
lines =
for {line_number, type, text} <- lines do
coverage =
case type do
:context -> nil
:add -> coverage[{filename, line_number}]
end
{line_number, text, coverage}
end
{filename, lines}
end)
end
defp gather_coverage(changes) do
results = Cover.analyze()
modules = Cover.modules()
filenames = Map.new(modules, fn module -> {module, Cover.module_path(module)} end)
changed_modules = Enum.filter(modules, fn module -> changes[filenames[module]] != nil end)
for {{module, line}, cov} <- results, module in changed_modules, line != 0, reduce: %{} do
acc ->
case cov do
{1, 0} -> Map.put(acc, {filenames[module], line}, true)
{0, 1} -> Map.put_new(acc, {filenames[module], line}, false)
end
end
end
end