# 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: []
endRun: mix test.qlover
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
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.
- Establish a baseline. Run the full suite and meet the 100% coverage threshold. qlover records the executable lines, each test file's runtime hits, compiled-code and source-map identities, and compiler references.
- Look for changes. On the next run, compare the current project with that baseline. If nothing relevant changed, no tests need to run.
- Replace changed contributions. Editing a test reruns that file and replaces its old line hits. Unchanged files' hits remain valid for unchanged code. Deleting a test removes its hits without running unrelated tests.
- Gate the union. Every executable line still needs a current owner. Changed application modules require fresh evidence; old-version hits cannot fill their gaps. A test or coverage failure preserves the old baseline.
Changes to configuration, dependencies, migrations, test helpers, or test
fixtures can trigger a full run. test_helper.exs is a suite-wide input;
coverage from suite-level execution is kept separately and conservatively
refreshed after test edits. Legacy reference-only baselines receive one
attributed full refresh.
Use mix test.qlover --no-stale to force a full suite run and refresh the
baseline.
Use mix test.qlover --dry to compile pending changes and print the exact test
files qlover would select without running tests or updating the coverage
baseline. Focused selections distinguish edited files from conservative
referencer refreshes and report retained coverage rows.
Runtime attribution currently requires OTP 29's :sys_coverage transform
and Elixir 1.20.2–1.20.x. Other supported Elixir runtimes keep the legacy,
conservative native-cover path rather than committing unverified attributed
evidence. Requires a 100% coverage policy. Ordinary
mix test and mix test test/my_test.exs remain available for your usual
test workflow.
Changed application code still uses compiler references alongside recorded
runtime owners. Long-lived shared servers and external inputs without
request-scoped ownership cannot be reused as a particular test file's hits.
For a new module with no known test owner, qlover can inspect its executable
lines directly from the BEAM and report missing coverage with zero test
executions. If a dynamic call or application startup actually covers it,
mix test.qlover --no-stale runs the full suite to establish that evidence.
When attribution remains ambiguous, force a full check:
mix test --no-stale --coverA 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.qloverThe 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.
From a clone of this repository:
cd examples/demo
QLOVER_CACHE_DIR="" ./compare.shThe 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.
