qlover

Side-by-side terminal illustration: qlover on the left runs 0 tests for an unchanged rerun, 8 after a module edit, and 1 when uncovered code is added; full coverage on the right runs 48, 48, and 49. Both reject the uncovered code.

Install

# mix.exs
defp deps do
[
# Your existing dependencies...
{:qlover, github: "qforge-dev/qlover", only: :test, runtime: false}
]
end
def project do
[
# Your existing project settings...
test_coverage: [summary: [threshold: 100]],
elixirc_options: [tracers: qlover_tracers()],
test_elixirc_options: [tracers: qlover_tracers()]
]
end
def cli do
[preferred_envs: [qlover: :test, "test.qlover": :test]]
end
defp qlover_tracers do
if Mix.env() == :test, do: [Qlover.Tracer], else: []
end

Run: mix test.qlover

How much work can you skip?

Stop paying for the same test run.

Your tests passed. You changed one file. Why run everything again?

qlover is an incremental test runner and 100% line-coverage gate for Elixir. It remembers a passing coverage baseline, runs the tests needed for your changes, and checks the affected code again. Less repeated work, less waiting, less compute spent proving what you already know.

Measured in the 48-test demo, comparing a full mix test --no-stale --cover run with mix test.qlover:

Change Without qlover With qlover Test executions avoided Result, both
First run, no baseline or cache 48 48 0% Pass
Run again, nothing changed 48 0 100% Pass
Refactor one module 48 8 83% Pass
Edit one test file 48 8 83% Pass
Add a module and 5 covering tests 53 5 91% Pass
Implement a feature with 3 new tests 51 3 94% Pass
Add code that the tests never cover 49 1 98% Coverage fails

The first run earns the baseline. Later runs reuse it. The zero-test row is an unchanged rerun, not a free first run.

Every mix test.qlover invocation reports the test work:

qlover: ran 8 tests; didn't run 40 tests.

Counts include generated tests and doctests, and follow test additions and deletions. An unchanged rerun reports ran 0 tests; failed runs also print the summary. ExUnit skips and exclusions count as not run and are listed separately. Counts travel with the shared baseline; an older baseline needs one full run to learn them. If compilation or an interrupted run prevents counting, the summary labels unavailable counts as unknown.

These are test-execution savings, not wall-clock speedup percentages. Startup, compilation, and coverage checks still take time; the time saved depends on how expensive your tests are. The animation illustrates these counts after a green baseline; its playback is not a timing benchmark. Still image · All results and reproduction

Why qlover exists

A full test run is valuable when it tells you something new. Repeating the same checks after an unrelated edit spends time and compute answering a question you already answered.

That cost repeats throughout the day: every save-and-check loop, every worktree, every agent asking whether its change is ready. The goal is to make the amount of test work follow the size of the change, rather than the size of the whole project.

Coverage makes this harder. Running a small subset with ordinary mix test --cover can make an otherwise fully covered project look incomplete: the other tests simply did not run. A strict coverage threshold then sends you back to the entire suite.

qlover keeps a record of the coverage already established and asks for fresh evidence where something changed.

How it works

  1. Establish a baseline. Run the full suite and meet the 100% coverage threshold. qlover remembers the compiled code, test files, and their references.
  2. Look for changes. On the next run, compare the current project with that baseline. If nothing relevant changed, no tests need to run.
  3. Run the affected tests. Select the relevant test files and collect fresh coverage. Editing or deleting a test can also require code to be checked again.
  4. Keep the coverage requirement. Code needing a new check must reach 100% in the fresh run. Old coverage cannot fill gaps in changed code. A test or coverage failure stops the run; a successful check advances the baseline.

Changes to configuration, dependencies, migrations, test helpers, or test fixtures can trigger a full run. test_helper.exs is a suite-wide input and needs no per-test attribution. Missing reference data for changed tests also falls back to the full suite.

Requires Elixir 1.18 or newer and a 100% coverage policy. Ordinary mix test and mix test test/my_test.exs remain available for your usual test workflow.

Selection uses recorded module references. Dynamic calls, protocol dispatch, and external inputs outside the tracked paths can escape that map. For changes involving those, use a full check:

mix test --no-stale --cover

Reuse the work across worktrees

A new checkout does not always need to repeat a full run. If identical project content already has a passing baseline in the shared cache, qlover can reuse it after compiling locally.

The cache defaults to ~/.cache/qlover (or $XDG_CACHE_HOME/qlover). Point worktrees at the same directory to share it:

export QLOVER_CACHE_DIR="$HOME/.cache/qlover"
mix test.qlover

The full run still happened somewhere. A cache hit reuses that result; changed content still needs a new check. Agents on separate machines need access to the same cache directory to share it. Set QLOVER_CACHE_DIR="" to disable sharing.

Try the comparison

From a clone of this repository:

cd examples/demo
QLOVER_CACHE_DIR="" ./compare.sh

The script establishes a full baseline, applies independent changes, and compares qlover with full coverage for each one. It prints test counts and pass/fail results, and exits unsuccessfully if the verdicts disagree.

Demo and full results · Changelog · Apache-2.0 license