Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ Zero-dependency Elixir coverage tool built for AI-assisted development. Wraps Er

## Why Six

Erlang's `:cover` counts every executable line - including `defmodule`, `use`, `alias`, and other boilerplate that nobody considers "untested code." It also has no concept of ignoring specific functions or code blocks, and its output is designed for humans reading a terminal, not agents reading a file.
Erlang's `:cover` counts every executable line, including `defmodule`, `use`, `alias`, and other boilerplate that nobody considers "untested code." It also has no concept of ignoring specific functions or code blocks, and its output is designed for humans reading a terminal, not agents reading a file.

Six fixes all of that: smart defaults that exclude structural declarations, `@six :ignore` for function-level exclusions, comment directives for block-level control, and a structured markdown report at `.six/coverage.md` that tells an AI agent exactly which functions have untested branches - with source snippets and context. Zero dependencies beyond OTP.
Six fixes all of that: smart defaults that exclude structural declarations, `@six :ignore` for function-level exclusions, comment directives for block-level control, and a structured markdown report at `.six/coverage.md` that tells an AI agent which functions have untested branches, with source snippets and context. Zero dependencies beyond OTP.

## Installation

Expand Down Expand Up @@ -70,14 +70,27 @@ This produces two things:

## Ignoring code

Three mechanisms, from automatic to explicit:
Four mechanisms, from automatic to explicit:

### Default pattern filters

Lines matching these patterns are automatically excluded from coverage - no configuration needed:
Lines matching these patterns are automatically excluded from coverage, with no configuration needed:

`defmodule`, `defprotocol`, `defimpl`, `defrecord`, `defdelegate`, `defstruct`, `defexception`, `@moduledoc`, `@doc`, `@impl`, `@behaviour`, `@callback`, `use`, `import`, `alias`, `require`, `plug`, `end`

### Log-level filtering

`Logger` macros check the configured level before evaluating their message and metadata. Test suites usually run with `config :logger, level: :warning`, so an `info` or `debug` call never evaluates its arguments, and `:cover` counts those lines as missed even though no test could cover them. `warning` and `error` calls still emit, so they stay covered.

List the levels you don't want counted, and Six excludes every matching `Logger` call from coverage, including multi-line calls and their metadata:

```elixir
# config/test.exs
config :six, ignore_log_levels: [:info, :debug]
```

This is off by default. It recognizes the standard `Logger` level calls (`debug`, `info`, `notice`, `warning`, `error`, and so on), the `Logger.log(level, ...)` form, and the deprecated `Logger.warn` alias.

### Function-level attribute

Add `use Six` to a module and tag functions with `@six :ignore`:
Expand All @@ -99,7 +112,7 @@ defmodule MyApp.CoverBridge do
end
```

`use Six` at the top of a file signals that the module has coverage exclusions - you know to look for `@six :ignore` tags. The attribute applies to the immediately following `def`/`defp`/`defmacro`/`defmacrop`, even with `@doc` or `@impl` in between.
`use Six` at the top of a file signals that the module has coverage exclusions, so you know to look for `@six :ignore` tags. The attribute applies to the immediately following `def`/`defp`/`defmacro`/`defmacrop`, even with `@doc` or `@impl` in between.

### Comment directives

Expand All @@ -120,14 +133,14 @@ Directive comments must be standalone comment lines. Six will not treat strings,

## Tracking ignores

When you're aiming for 100% coverage, you're either testing everything or explicitly ignoring it. Without a tally, ignores quietly accumulate. Enable `track_ignores` to write a `.sixignore` manifest at the project root that lists every explicit exclusion in your codebase:
When you're aiming for 100% coverage, you're either testing everything or explicitly ignoring it. Without a tally, ignores accumulate unnoticed. Enable `track_ignores` to write a `.sixignore` manifest at the project root that lists every explicit exclusion in your codebase:

```elixir
# config/test.exs
config :six, track_ignores: true
```

…or pass `--track-ignores` to `mix six`. Commit `.sixignore` so changes show up in PR diffs reviewers see new exclusions before they land.
Or pass `--track-ignores` to `mix six`. Commit `.sixignore` so changes show up in PR diffs, and reviewers see new exclusions before they land.

```
# Generated by Six. Tracks explicit coverage exclusions.
Expand All @@ -137,7 +150,7 @@ lib/my_app/cover.ex compile_modules
lib/my_app/payments.ex process_refund a1b2c3d4
```

The format is intentionally **stable across line shifts**: `@six :ignore` entries are keyed by function name, and comment directive entries use a content hash of the ignored code. Adding or removing lines elsewhere in a file produces zero diff in `.sixignore` — the only changes you see are real ones (new exclusion, removed exclusion, or modified ignored code).
The format is stable across line shifts: `@six :ignore` entries are keyed by function name, and comment directive entries use a content hash of the ignored code. Adding or removing lines elsewhere in a file produces zero diff in `.sixignore`. The only changes you see are real ones: a new exclusion, a removed exclusion, or modified ignored code.

## Configuration

Expand All @@ -153,6 +166,9 @@ config :six,
# Set to false to ONLY use your patterns, not the built-in defaults
default_patterns: true,

# Exclude log calls at these levels from coverage (default: [])
ignore_log_levels: [:info, :debug],

# Fail CI if coverage drops below this
minimum_coverage: 85.0,

Expand Down Expand Up @@ -196,7 +212,7 @@ mix six --import-cover cover

## Acknowledgments

Six is built on top of Erlang's [:cover](https://www.erlang.org/doc/apps/tools/cover) and is inspired by [ExCoveralls](https://github.com/parroty/excoveralls) and [Coverex](https://github.com/alfert/coverex) - thank you!
Six is built on top of Erlang's [:cover](https://www.erlang.org/doc/apps/tools/cover) and is inspired by [ExCoveralls](https://github.com/parroty/excoveralls) and [Coverex](https://github.com/alfert/coverex). Thank you!

## License

Expand Down
2 changes: 1 addition & 1 deletion guides/ai-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ This runs `mix test --cover`, reads the report, and writes tests for uncovered b

The report also includes an **Ignored** section listing every function and line range excluded from coverage, so you can audit whether ignores are still justified.

For longer-running projects, enable `track_ignores: true` (or pass `--track-ignores`) to write a committable `.sixignore` manifest at the project root. Every new exclusion shows up as a line in your PR diff, making it impossible to add ignores without explicit review. See the README for details.
For longer-running projects, enable `track_ignores: true` (or pass `--track-ignores`) to write a committable `.sixignore` manifest at the project root. Every new exclusion shows up as a line in your PR diff, so no ignore can land without explicit review. See the README for details.
6 changes: 3 additions & 3 deletions guides/reading-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ Rows are sorted worst-first so the files that need attention are at the top.

## Colors

- **Green** — coverage is at or above the threshold (default 90%)
- **Red** — coverage is below the threshold
- **Yellow** — the file has 0 relevant lines, meaning every executable line was filtered out (all `defmodule`, `use`, `alias`, `end`, etc.). There is nothing to cover, so Six cannot score it. This is normal for files that are purely structural, like a module that only defines a struct or delegates.
- Green means coverage is at or above the threshold (default 90%).
- Red means coverage is below the threshold.
- Yellow means the file has 0 relevant lines, so every executable line was filtered out (all `defmodule`, `use`, `alias`, `end`, etc.). There is nothing to cover, so Six cannot score it. This is normal for files that are purely structural, like a module that only defines a struct or delegates.
6 changes: 3 additions & 3 deletions guides/threshold-vs-minimum-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ If total coverage is `82.0%`:

## When to use each

- **Local development**: use `threshold` when you want a visible target without blocking your workflow.
- **CI**: use `minimum_coverage` when you want coverage regressions to fail the build.
- **Both**: use `threshold` as the aspirational goal and `minimum_coverage` as the hard floor. This is usually the most practical setup.
- For local development, use `threshold` when you want a visible target without blocking your workflow.
- For CI, use `minimum_coverage` when you want coverage regressions to fail the build.
- To get both, set `threshold` as the goal you are aiming for and `minimum_coverage` as the hard floor. This is usually the most practical setup.
2 changes: 1 addition & 1 deletion lib/six/formatters/agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ defmodule Six.Formatters.Agent do

@doc false
def detect_branch_context(source_lines, {start_line, _end_line}) do
# Check lines at and near the start of the missed range for branch context
# Scan lines near the start of the missed range for branch context
range_start = max(start_line - 3, 0)
context_lines = Enum.slice(source_lines, range_start, start_line - range_start + 1)
missed_line = Enum.at(source_lines, start_line - 1, "")
Expand Down
14 changes: 1 addition & 13 deletions lib/six/ignore_logs.ex
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
defmodule Six.Ignore.Logs do
@moduledoc """
Excludes log statements from coverage based on their level.

`Logger` macros check the configured level *before* evaluating their message
and metadata. A test suite usually runs with `config :logger, level: :warning`,
so the arguments of an `info`/`debug` call are never executed — and `:cover`
reports those lines as missed even though no test could cover them. (`warning`
and `error` calls still emit, so they stay coverable.)

For every level listed in the `:ignore_log_levels` config, this stage nullifies
the coverage of matching `Logger` calls, spanning the whole statement —
including multi-line metadata — via the AST. It is a no-op by default.
"""
@moduledoc false

@valid_levels [:emergency, :alert, :critical, :error, :warning, :notice, :info, :debug]

Expand Down
4 changes: 4 additions & 0 deletions lib/six/report.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
defmodule Six.Report do
@moduledoc false

@doc """
Runs coverage analysis for the given options: builds the summary, emits each
configured formatter, and enforces the minimum coverage threshold.
"""
def run(opts \\ []) do
config =
Six.Config.read()
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Six.MixProject do
use Mix.Project

@version "0.3.0"
@version "0.3.1"
@source_url "https://github.com/typicalpixel/six"

def project do
Expand Down
Loading