Affected Test Selection - 2026-10-30¶
Context¶
On a pull request most of a suite tests code the change didn't touch. Running only the tests a change can affect makes CI faster, but a selection that misses a test hides a failure until main breaks. Taltal asked for it (2026-10-30) with three conditions: map changes to tests from past coverage or configurable path-to-test globs, fall back to running everything when unsure, and show why each test was picked. Off by default.
Decision¶
qasight/services/affected_tests.py picks the tests; two opt-in entry points use it:
qasight affectedprints the tests and the reason for each (text,--json, or--listfor other runners' command lines).pytest --qasight-affected(orTESTO_AFFECTED=1) deselects the rest inside pytest, so pytest users need no script. It runs before--qasight-shard, so shards split only the affected tests.
Changed files are the working tree against the merge base with the baseline branch, plus untracked files (so a local run sees uncommitted work). git diff --no-renames lists both names of a moved file. The base is --base, else affected.base, else the pull request's target branch from the CI variables qasight/services/ci_provenance.py already reads, else origin/main.
Mapping, for each changed file, first match wins (affected: in qasight.yaml, parsed by qasight/config/affected.py):
| Step | Source | Result |
|---|---|---|
| 1 | run_all_when (built in: conftest.py, pyproject.toml, lockfiles, qasight.yaml, ...) |
run everything |
| 2 | ignore (built in: __pycache__/, *.pyc, .pytest_cache/, ...) |
no test |
| 3 | test_files name patterns (test_*.py, *.spec.ts, *_test.go, ...) |
that file; nothing if it was deleted |
| 4 | rules: changed globs to tests globs or pytest node ids |
those tests |
| 5 | a coverage.py JSON report with test contexts (pytest --cov --cov-context=test, coverage json --show-contexts) |
the tests that ran a line of the file |
When unsure, run everything. A file no step maps, a file the coverage report lists but no test ran (only imported), a rule whose test globs match no file, a failed git diff, an unknown base and an empty diff each add a reason to run_all_reasons, and any reason means the whole suite. In pytest, a picked Python test that wasn't collected (renamed since the coverage report, or outside the paths pytest was given) also runs everything. Only ignore can make a change run nothing; pytest then exits 0 instead of its "no tests ran" 5.
Why each test. Every picked test carries "<changed file>: <why>" lines; qasight affected prints them, --json returns them, and pytest prints the first 20 in its summary.
Consequences¶
- Nothing changes unless a user passes
--qasight-affectedor callsqasight affected.qasight rundoesn't select by itself: a stage's command line belongs to the user, and--listgives them the tests to put in it. - Run history isn't a mapping source: a run records outcomes, not which files a test touched. Coverage contexts are the "past" signal, and they go stale; the run-all fallbacks (a test that isn't collected, a file the report doesn't list) catch the common cases, and a periodic full run (nightly, or on main) catches the rest.
- Coverage is pytest-only for now (contexts are pytest node ids). Other runners use
rulesand changed test files. - Reference: Command Reference ยง qasight affected.