Repository navigation
Description
CI could run only the tests a pull request's changes can reach, chosen from a map of which source files each test used on main. This builds on the path filter in #368, which skips test jobs when the docs, examples or tools alone change.
Measurement (2026-11-05)
How it was measured
- The map: the whole CPU suite on
main(dc4ca7a2), recorded per test. Each test's map lists:- the repository functions it ran, recorded with
sys.monitoring(PY_START, each location disabled after its first hit and re-enabled at the next test); - the files of every kernel it launched, and of everything lowering inlined into those kernels. Python never runs kernel bodies, so these come from hooks on
Kernel.__call__,Kernel._lowerandCallBindings.resolve; - what its subprocesses ran, through a
sitecustomize.
- the repository functions it ran, recorded with
- Fixture setup is credited to every test in its file, since a module-scoped fixture is built during whichever test needs it first.
- The recording run: 6755 passed, 371 skipped, 6 min 17 s on 8 workers on an M1 Max. The overhead is negligible.
- The replay:
- the 278 PRs merged from 2026-10-14 to 2026-11-05, each diffed from its merge commit's first parent;
- every CI run on a pull request in that period that failed with named tests: 18 runs, each diffed against
mainas it was when the run started.
- Weights:
test-durations.json, with each file's seconds spread over its tests by their local durations. - Rules:
- a changed test file selects all of its tests;
- the build and CI files (
pyproject.toml,uv.lock,conftest.py,.github/,test-durations.json) run everything; - a file under
packages/*/src/that no test used runs everything; - a file the PR adds selects nothing by itself; the changed files that use it select the tests.
Results
| Average test time vs today, 278 PRs | |
|---|---|
| Path filter alone (#368) | 87% |
| Plus selection by test file | 68% |
| Plus selection by test | 63% |
With selection by test, the 278 PRs split as follows:
| PRs | |
|---|---|
| No test job | 37 |
| Full run | 81 |
| Partial, under 20% of suite time | 50 |
| Partial, 20%–90% | 32 |
| Partial, 90% or more | 78 |
Failures: none missed. 9 of the 18 failing runs got a partial selection, with 13 failing tests between them, and every one was selected. The vis failures would have been found by running 12–31% of the suite:
| Run | Share of suite selected |
|---|---|
| polygon-fan-area | 17% |
| merge-points-polyhedra | 12% |
| vtk-polyhedral-split | 12% |
| vtk/algorithms | 12% |
| polylines failure, in polyhedra-page and project-cell-averages | 31% |
The core and runtime failures were selected at 99–100%. The other 9 runs would have run everything.
What keeps runs broad
-
Files nearly every test really uses. In the 90%+ PRs, the broadest changed file was:
File PRs Share of suite that uses it dataset/views.py16 96% runtime/cpu.py11 99% dataset/topology.py10 96% lang/array.py9 99% The rest were
dataset.py,shapes.py,_common.py,ghosts.pyandunits.py. Narrowing these would need function granularity: map a diff's changed lines to the functions they're in, and select the tests that ran those functions. -
Full runs came from
ci.yml(28 PRs, this period's CI work) and fromtack/dataset/__init__.py(29 PRs). That file holds only module-level code, so no test is credited with it; usually the change is a new algorithm's export, whose implementing files are selected anyway. A rule for module-only files could recover most of those.
What it would take
- The recorder, as a test-only pytest plugin. The prototype is about 150 lines:
sys.monitoring, plus the three Tack hooks above. - A map refreshed on every push to
main, for example by one Linux share uploading it as an artifact, or as a committed file liketest-durations.json. Pushes tomainkeep running everything, as the safety net that refreshes the map. - Selection by node ID in the shares (conftest's
--shard), with each share dealt the selected tests by their weights. - Rules for unmapped files, module-only files and data files under
tests/. - Metal: run it on the same selection, or only when Metal's backend or the core changes. The map comes from CPU runs, so GPU-only files are unmapped and force a full run, which is safe.
Risks
- A result cached in Python (an
lru_cache, a module-level dataset) credits only the first test that computes it, so a later test that depends on it could be missed. None of the 13 failures hit this, but the sample is small. - A dependency added by a PR is unknown to
main's map. The full run on the push tomaincatches it, after the merge. - Wall time is set mostly by Metal's runner queue and the longest share, so it falls less than test time does.
Not before 0.7.
Activity
berkgeveci commentedon Nov 5, 2026
The prototype is on branch
ci/test-selection-prototypeof berkgeveci/tack-fork (https://github.com/berkgeveci/tack-fork/tree/ci/test-selection-prototype/tools/test_selection), on top of #368. It holds the recorder, the replays and this period's history; its README says how to record a map and replay it.