Packages

Elixir coverage diffing by code block, not by line number. Certitudo snapshots test coverage and compares snapshots block by block, separating real coverage changes from ordinary source movement.

Current section

Files

Jump to
certitudo lib mix tasks certitudo.ex
Raw

lib/mix/tasks/certitudo.ex

defmodule Mix.Tasks.Certitudo do
@moduledoc """
Run tests, capture coverage, and compare against a previous snapshot.
Each run produces an **impressio** — the current coverage snapshot. If an
identical impressio already exists (same fingerprint), it is reused and
labelled *Deja vu*. The snapshot we compare against is the **retentio** —
either the most recent prior run or an explicit `--since` target.
Output written to `.certitudo/<run_id>/`:
- `snapshot.json` — the impressio
- `coverage.coverdata` — raw Erlang cover data
- `diff.prev.json` — differentia between retentio and impressio (when available)
```
mix certitudo
mix certitudo --example
mix certitudo --no-color
mix certitudo --label scenario -- --only scenario
mix certitudo --from-coverdata cover/covprobe.coverdata
mix certitudo --no-auto-diff
mix certitudo --since 20260605T190458Z
```
Pass `--no-color` to disable ANSI colors in Certitudo's own output.
Pass `--example` to render the bundled demonstration diff with all statuses,
without running tests or comparing snapshots.
## Related tasks
- `mix certitudo.discrimen` — compare any two snapshots explicitly
- `mix certitudo.inspectio` — inspect and prune `.certitudo/` artifacts
- `mix certitudo.lacunae` — show uncovered lines
- `mix certitudo.speculum` — print a snapshot as a coverage table
"""
use Mix.Task
alias Certitudo.Coverage
alias Certitudo.Coverage.Runtime
@shortdoc "Create coverage snapshot (+ optional diff.prev)"
@impl Mix.Task
def run(args) do
{opts, passthrough, _invalid} =
OptionParser.parse(args,
strict: [
example: :boolean,
label: :string,
run_id: :string,
since: :string,
from_coverdata: :string,
auto_diff: :boolean,
color: :boolean,
prefix: :keep
],
allow_nonexistent_atoms: true
)
shell_info("Signa obductionis quaerimus", opts)
if Keyword.get(opts, :example, false) do
print_example(opts)
else
run_id = opts[:run_id] || Coverage.timestamp_id()
opts =
opts
|> Keyword.put(:run_id, run_id)
|> put_runtime_defaults()
coverdata_path =
case opts[:from_coverdata] do
nil ->
run_tests_and_export_coverdata!(run_id, passthrough)
path ->
path
end
result = Certitudo.obducere(coverdata_path, opts)
shell_info("", opts)
case result.kind do
:new ->
"impressio: #{Path.basename(result.impressio_run_dir)}"
|> shell_info(opts)
:known ->
"Deja vu! seen at #{Path.basename(result.impressio_run_dir)}"
|> shell_info(opts, :yellow)
"impressio: #{Path.basename(result.impressio_run_dir)}"
|> shell_info(opts)
end
print_diff(result.diff, opts)
"Certitudo: #{coverage_label(result.snapshot["totals"]["coverage_percent"])} for #{result.snapshot["totals"]["modules"]} modules"
|> shell_info(opts)
shell_info("", opts)
maybe_hint_since(Path.basename(result.impressio_run_dir), opts)
end
end
defp coverage_label(nil), do: "coverage not measured (no modules matched)"
defp coverage_label(percent), do: "#{percent}%"
defp run_tests_and_export_coverdata!(run_id, passthrough) do
export_name = "export_#{run_id}"
test_args =
["test", "--cover", "--export-coverage", export_name] ++ passthrough
{_, status} =
System.cmd("mix", test_args,
into: IO.stream(:stdio, :line),
stderr_to_stdout: true,
env: [{"MIX_ENV", "test"}]
)
if status != 0 do
Mix.raise("mix test failed with status #{status}")
end
Path.join("cover", "#{export_name}.coverdata")
end
defp put_runtime_defaults(opts) do
app = Mix.Project.config()[:app]
build_path = Mix.Project.build_path()
beam_dirs = [Path.join([build_path, "lib", to_string(app), "ebin"])]
prefixes = Keyword.get_values(opts, :prefix)
own_modules =
case prefixes do
# no explicit --prefix: scope to what's actually compiled for this
# app (Runtime.own_module_names/1 — physical .beam files under
# beam_dirs, not a guess from the app name).
[] -> Runtime.own_module_names(beam_dirs)
# explicit --prefix given: caller's intent is authoritative, don't
# additively widen it back out with every compiled module.
_ -> MapSet.new()
end
ignore_modules =
case Mix.Project.config()[:test_coverage] do
cfg when is_list(cfg) -> Keyword.get(cfg, :ignore_modules, [])
_ -> []
end
opts
|> Keyword.put(:prefixes, prefixes)
|> Keyword.put(:own_modules, own_modules)
|> Keyword.put(:ignore_modules, ignore_modules)
|> Keyword.put(:beam_dirs, beam_dirs)
end
defp print_example(opts) do
diff =
example_diff_path()
|> File.read!()
|> Jason.decode!()
shell_info("", opts)
print_auto_diff_modules(diff, opts)
shell_info(example_summary(), opts)
shell_info("", opts)
end
defp example_diff_path do
Path.join([File.cwd!(), "examples", "example.diff.json"])
end
defp example_summary do
"\nCertitudo: 89.23% for 42 modules"
end
defp print_diff({:ok, %{retentio_path: retentio_path, diff: diff}}, opts) do
"retentio: #{Path.basename(Path.dirname(retentio_path))}"
|> shell_info(opts)
shell_info("", opts)
print_auto_diff_modules(diff, opts)
shell_info("", opts)
end
defp print_diff({:since_not_found, since}, opts) do
shell_info(
"warning: --since #{since} not found, skipping diff",
opts,
:yellow
)
end
defp print_diff(:skipped, _opts), do: :ok
defp maybe_hint_since(run_id, opts) do
if is_nil(Keyword.get(opts, :since)) do
shell_info(
"since: to compare future runs against this snapshot:",
opts
)
shell_info(" mix certitudo --since #{run_id}", opts)
end
end
defp print_auto_diff_modules(%{"changed_modules" => changed}, opts)
when is_list(changed) do
if changed == [] do
shell_info("Identical: coverage not changed", opts, :blue)
else
shell_info("Differentia (#{length(changed)} modules):", opts, :blue)
Enum.each(changed, fn mod ->
module = mod["module"]
source_changed = mod["source_changed"] || false
block_diffs = mod["block_diffs"] || []
shell_info(
" #{module}: block_diffs=#{length(block_diffs)} source_changed=#{source_changed}",
opts,
:blue
)
print_block_diffs(block_diffs, opts)
end)
end
end
defp print_block_diffs(block_diffs, opts) when is_list(block_diffs) do
block_diffs
|> Enum.filter(&printable_block_diff?/1)
|> Enum.take(20)
|> Enum.chunk_by(& &1["status"])
|> Enum.each(&print_block_chunk(&1, opts))
end
defp print_block_chunk([%{"status" => "moved_unchanged"} = diff], opts) do
shell_info(" moved_unchanged: #{diff_range(diff)}", opts, :magenta)
end
defp print_block_chunk(
[%{"status" => "moved_unchanged"} | _] = chunk,
opts
) do
ranges = Enum.map_join(chunk, ", ", &diff_range/1)
shell_info(
" moved_unchanged (#{length(chunk)}): #{ranges}",
opts,
:magenta
)
end
defp print_block_chunk(chunk, opts) do
Enum.each(chunk, fn diff ->
color = status_color(diff["status"])
shell_info(
" #{diff["status"]}: #{diff_range(diff)}",
opts,
color
)
print_residue(diff["residue"], opts)
end)
end
defp printable_block_diff?(diff) do
diff["status"] != "unchanged" and diff_range(diff) != "none"
end
defp print_residue(%{"entries" => entries}, opts) when is_list(entries) do
Enum.each(entries, fn entry ->
status = entry["status"]
line = entry["right"] || entry["left"]
shell_info(
" #{line_symbol(status)} #{line["number"]} | #{line["text"]}",
opts,
status_color(status)
)
end)
end
defp print_residue(_residue, _opts), do: :ok
defp block_range(nil), do: "none"
defp block_range(%{"range" => %{"first" => first, "last" => last}}),
do: "#{first}-#{last}"
defp diff_range(%{"left" => left, "right" => right}) do
left_range = block_range(left)
right_range = block_range(right)
if left_range == right_range do
left_range
else
"#{left_range} -> #{right_range}"
end
end
defp status_color("new_covered"), do: :green
defp status_color("existing_coverage_gained"), do: :green
defp status_color("new_uncovered"), do: :orange
defp status_color("removed_uncovered"), do: :yellow
defp status_color("removed_covered"), do: :red
defp status_color("existing_coverage_lost"), do: :red
defp status_color("existing_coverage_mixed"), do: :purple
defp status_color("moved_unchanged"), do: :magenta
defp status_color("moved_coverage_changed"), do: :purple
defp status_color("ambiguous_moved"), do: :cyan
defp status_color(status),
do: Mix.raise("unknown coverage status: #{inspect(status)}")
defp line_symbol("new_covered"), do: "+"
defp line_symbol("new_uncovered"), do: "+"
defp line_symbol("removed_covered"), do: "-"
defp line_symbol("removed_uncovered"), do: "-"
defp line_symbol("existing_coverage_gained"), do: "~"
defp line_symbol("existing_coverage_lost"), do: "~"
defp line_symbol(status),
do: Mix.raise("unknown coverage line status: #{inspect(status)}")
defp shell_info(text, opts, color \\ :blue)
when is_binary(text) and is_list(opts) do
case Keyword.get(opts, :color, true) do
false ->
Mix.shell().info(text)
true ->
Mix.shell().info(colorize(text, color))
end
end
defp colorize(text, color) when is_atom(color) do
color
|> ansi_color()
|> Kernel.++([text, :reset])
|> IO.ANSI.format()
|> IO.iodata_to_binary()
end
defp ansi_color(:orange), do: ["\e[38;5;208m"]
defp ansi_color(:purple), do: ["\e[38;5;183m"]
defp ansi_color(color), do: [color]
end