Current section
Files
Jump to
Current section
Files
README.md
# qlover

## Install
```elixir
# 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](https://github.com/qforge-dev/qlover/tree/main/examples/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:
```text
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](docs/assets/comparison.png) · [All results and reproduction](https://github.com/qforge-dev/qlover/tree/main/examples/demo)
## 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 records the executable lines, each test file's runtime
hits, compiled-code and source-map identities, and compiler 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. **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.
4. **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:
```sh
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:
```sh
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:
```sh
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](https://github.com/qforge-dev/qlover/tree/main/examples/demo) · [Changelog](CHANGELOG.md) · [Apache-2.0 license](https://github.com/qforge-dev/qlover/blob/main/LICENSE)