Packages

Zero-dependency Elixir coverage tool built for AI-assisted development

Current section

Files

Jump to
six lib mix tasks six.ex
Raw

lib/mix/tasks/six.ex

defmodule Mix.Tasks.Six do
@shortdoc "Runs tests with coverage analysis"
@moduledoc """
Runs `mix test --cover` with Six as the coverage tool.
mix six [options] [-- test_args]
## Options
* `--threshold N` - Report threshold for formatter output (default: 90)
* `--minimum-coverage N` - Fail if total coverage drops below N
* `--output-dir DIR` - Output directory (default: .six/)
* `--skip FILE` - Additional file pattern to skip (repeatable)
* `--import-cover DIR` - Import .coverdata files from DIR and generate
a report without running tests. Used to merge coverage from
partitioned CI runs.
Any arguments after `--` are passed through to `mix test`.
"""
use Mix.Task
Module.register_attribute(__MODULE__, :six, accumulate: true)
@six :ignore
@impl true
def run(args) do
{opts, test_args} = split_args(args)
Mix.env(:test)
if import_dir = opts[:import_cover] do
import_and_report(import_dir, opts)
else
run_tests(opts, test_args)
end
end
@six :ignore
defp run_tests(opts, test_args) do
project = Mix.Project.config()
test_coverage = Keyword.get(project, :test_coverage, [])
test_coverage = Keyword.put(test_coverage, :tool, Six)
test_coverage = merge_cli_opts(test_coverage, opts)
Application.put_env(:mix, :test_coverage, test_coverage)
Mix.Task.run("test", ["--cover" | test_args])
end
@six :ignore
defp import_and_report(dir, opts) do
unless File.dir?(dir) do
Mix.raise("--import-cover directory does not exist: #{dir}")
end
:cover.start()
compile_path = Mix.Project.compile_path()
Six.Cover.compile_modules(compile_path)
{imported, errors} = Six.Cover.import_all_coverdata(dir)
Enum.each(errors, fn {file, reason} ->
IO.warn("Failed to import #{file}: #{inspect(reason)}")
end)
if imported == 0 do
Mix.raise("No .coverdata files found in #{dir}")
end
IO.puts("Imported #{imported} coverdata file(s) from #{dir}")
Six.Report.run(opts)
end
@doc false
def split_args(args) do
{args_before, args_after} =
case Enum.split_while(args, &(&1 != "--")) do
{before, ["--" | after_]} -> {before, after_}
{before, []} -> {before, []}
end
{parsed, _, _} =
OptionParser.parse(args_before,
strict: [
threshold: :integer,
minimum_coverage: :float,
output_dir: :string,
skip: :keep,
import_cover: :string
],
aliases: [t: :threshold, o: :output_dir]
)
{parsed, args_after}
end
@doc false
def merge_cli_opts(test_coverage, opts) do
Enum.reduce(opts, test_coverage, fn
{:threshold, val}, acc ->
summary = Keyword.get(acc, :summary, [])
Keyword.put(acc, :summary, Keyword.put(summary, :threshold, val))
{:minimum_coverage, val}, acc ->
Keyword.put(acc, :minimum_coverage, val)
{:output_dir, val}, acc ->
Keyword.put(acc, :output_dir, val)
{:skip, val}, acc ->
acc ++ [skip: val]
_, acc ->
acc
end)
end
end