Skip to content

ci: ci_targets_check gains direction B - a ratchet on tests whose targets CI never builds (#1595) - #1601

Merged
bong-water-water-bong merged 5 commits into
mainfrom
os/ci-add-test-ratchet
Nov 9, 2026
Merged

bong-water-water-bong merged 5 commits into
mainfrom
os/ci-add-test-ratchet

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Closes #1595. Stacked on #1598 (1bit/ci-targets-comments): both edit tools/ci/ci_targets_check.py, and a second parser beside that one was the pile-up shape we spent the night removing — so this diff is the direction-B addition on top of its #-comment fix, and retargets to main when #1598 lands. Until then GitHub shows this PR's diff as my commit alone (tools/ci/ci_targets_check.py, tools/ci/add_test_gaps.txt).

What direction B is

#1593 gated direction A: ci.yml builds something the configuration cannot build (the embeds_equiv_probe / ONEBIT_NPU case). Direction B is the other half of the same list — a test whose target CI never builds:

need  = { targets an add_test() COMMAND names }
have  = ci.yml's --target list
drift = need - have

ci.yml's own comment says a test whose binary is missing reports "Not Run", and the job stays green. So the gate is invisible exactly where it matters — the same shape as #1583's 92 % build behind green jobs.

Why a ratchet, not a wall

36 names cannot be fixed in one PR and a silent pass is what exists today. So this is a ratchet on the set:

current - baseline non-empty   -> FAIL, naming every NEW name
baseline - current non-empty   -> progress, reported (the file can shrink a line at a time)

A set, not a count, because a count passes a rename:

baseline  sed 's/^embed_model_test$/embed_model_test_RENAMED/'
          rc=1  "add_test names 'embed_model_test', and ci.yml never builds it -- that test reports
                 'Not Run' (embed_model); build it or drop the test"

measured on the real tree, today.

The number, reconciled rather than quoted

#1595 audited 35. This PR measures 36, and the difference is a rule, exactly named:

rule need drift
bare COMMAND plain_test tokens 62 35 ← #1595's figure
+ $<TARGET_FILE:plain_test> 64 36 ← what this ships

The two names the wider rule adds are npu_derive_designs_dump (unbuilt — a real gap the bare rule missed) and onebit (built, so it adds no drift). A generator-expression test genuinely cannot run unless CI builds the target, so counting it is the honest direction; the baseline header records both rules and both numbers rather than leaving a reader to wonder why the figure moved.

The parse, and why it is paren-aware

39 of this tree's 164 add_test directives span lines. A line-at-a-time scan misses them, and for a ratchet a missed add_test is a missed gap — which is silence, not a wrong red, i.e. the failure mode that does not announce itself. So the scan joins paren continuations, the way cmake_targets() already joins if() conditions.

Six new self-test cases (13 total, all passing)

ok  a test whose target ci.yml never builds passes when the baseline records it
ok  the same tree with an empty baseline fails, naming the target it never builds
ok  a missing baseline fails closed rather than passing vacuously
ok  a baseline entry that no longer drifts is progress, not a failure
ok  a script test (COMMAND bash …) adds nothing to the drift

The second case exists because the first one alone could pass on nothing — the fixture is proven to drift by the case that must fail on it. The fifth is the property #1596 relies on: COMMAND bash script.sh names bash, which is not an add_executable.

Fail closed, like the other rows this lane ships

A missing baseline file returns 1 at the current count rather than reading as "nothing known". The alternative lets a fresh clone pass on 36 names a reader cannot tell from regressions — the same reasoning as payload-modes refusing an empty payload.

Scope

  • have is ci.yml's --target list as the tool parses it — onebit is not subtracted, because a test naming onebit is genuinely built; the subtraction in ci: 35 add_test targets are never built, so their tests report "Not Run" in a green job (direction B of the target-list gap) #1595's definition only matters if a test names it.
  • The ratchet cannot tell a newly unbuilt target from a renamed one beyond naming both names: a rename shows up as one new name and one gone name, which is the most a set can say and enough to act on.
  • It does not build anything, so it cannot report a test that builds and then fails — that is ctest's job, and #1595's premise is that these tests never get that far.
  • Stale baseline entries are reported but not failed; deleting them is the improvement, not a requirement.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1595 - Partially compliant

Compliant requirements:

  • The ratchet mechanism is implemented in tools/ci/ci_targets_check.py
  • It correctly identifies drifted targets and fails on new additions
  • It reports progress when baseline entries are removed
  • The baseline file tools/ci/add_test_gaps.txt is used to track drifted targets
  • Script-driven tests are correctly excluded from the drift calculation
  • The implementation is integrated with existing direction A logic

Non-compliant requirements:

  • None

Requires further human verification:

  • None
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Potential parsing issue with add_test blocks

The add_test_blocks function attempts to parse multi-line add_test directives by joining lines, but it may not correctly handle all edge cases of CMake syntax, especially nested parentheses or complex command structures. This could lead to missed add_test directives and thus missed drift detection.

def add_test_blocks(cmake_text):
    """Every `add_test( … )` directive, paren-joined across lines: (line number, text).

    The real tree writes 39 of them across lines, so a line-at-a-time scan misses them - and a missed
    add_test is a missed gap, which for a ratchet means silence rather than a wrong red.
    """
    blocks, buf, start = [], "", None
    for i, raw in enumerate(cmake_text.splitlines(), 1):
        line = raw.strip()
        if buf:
            buf += " " + line
            if buf.count("(") <= buf.count(")"):
                blocks.append((start, buf))
                buf, start = "", None
            continue
        if re.match(r"^add_test\s*\(", line, re.IGNORECASE):
            if line.count("(") > line.count(")"):
                buf, start = line, i
            else:
                blocks.append((i, line))
    return blocks
Baseline handling for empty file

When the baseline file is empty (or not present), the tool fails closed with a hard error. While this is intended to prevent silent failures, it may be overly strict in cases where a new CI run is expected to have no baseline yet. Consider if a more graceful handling or a clear warning message would be more appropriate.

if baseline is None:
    # No baseline is not "nothing known": treating it as empty would let the first run of a run
    # without the file fail on 35 names a reader could not tell from regressions. Fail closed at 35.
    print(f"ci_targets_check: no {BASELINE}, so the {len(drift)} target(s) whose tests CI never builds "
          f"cannot be told apart from new ones", file=sys.stderr)
    return 1

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: the reconciliation is right, and I reproduced both of its awkward cases rather than reading them. Four measurements, plus two errors of mine that the exercise produced - the second of which is the unit trap this whole evening has been about.

The number, reproduced from the sources

add_test directives, top-level CMakeLists.txt   164 code + 1 commented = 165 occurrences
directives spanning lines                        39          <- your figure exactly
ci.yml --target tokens                           28          <- direction A's 28

And the delta that makes 35 into 36 is real, not bookkeeping:

CMakeLists.txt:1158   add_executable(npu_derive_designs_dump tests/npu_derive_designs_dump.c …)
CMakeLists.txt:1170   add_test(… tools/npu_plan.py $<TARGET_FILE:npu_derive_designs_dump>)
ci.yml's 28 tokens    … no npu_derive_designs_dump …

So it is built, named by a test, absent from CI's list - a genuine Not Run gap that the bare-token rule cannot see, because line 1170 names it only through a generator expression. onebit is the other extra and it is in the 28, so it adds no drift. need 62/drift 35 bare and need 64/drift 36 with generator expressions is exactly what those two facts imply, and the baseline header says so with both rules named.

The rename case, in the right unit

My first attempt renamed a test (attn_math_test -> attn_math_test_renamed) and the ratchet correctly said rc=0, 1 baseline entr(y/ies) no longer drift - because the unit is the target, and renaming the test changed nothing about what must be built. Renaming the target everywhere is the case you meant:

rename attn_math_test -> attn_math_test_v2 in CMakeLists.txt     rc=1
  ci_targets_check: add_test names 'attn_math_test_v2', and ci.yml never builds it -- that test
  reports 'Not Run' (attn_math); build it or drop the test

Same count, new name, red - which is precisely the hole a count leaves and the reason the baseline is a set. My first run was a vacuous test of a real property, which is the fifth time tonight that a fixture rather than the code was the wrong part; the difference is that this one looked like the right test.

Fail-closed

rm tools/ci/add_test_gaps.txt                                    rc=1
  ci_targets_check: no tools/ci/add_test_gaps.txt, so the 36 target(s) whose tests CI never builds
  cannot be told apart from new ones

Fail closed, and the message says why rather than just that. That is the difference between a gate people keep and a gate people --no-verify.

On the load-bearing #1598 point

Confirmed structurally: 39 multi-line directives out of 164 is 24 % of the population, so a line-at-a-time scan would have silently under-counted need by roughly a quarter - and for a ratchet that is worse than a wrong red, because the baseline would have been written from the shortfall and then never moved. Your stacking on #1598's branch is the right dependency to have declared.

Scope sentence for the body, if it is not there already

Worth stating plainly, since direction B is the one that can look like direction A: A is a hard failure (a ci.yml list that names something unbuildable reds the build for everyone, rc=1 always); B ratchets (36 pre-existing gaps, rc=1 only on new ones, and a shrinking baseline is progress reported as such). rc=1 in CI therefore always means this change broke something, never the number is what it always was - which is the property that keeps people from disabling the gate.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: APPROVE at head 3f04e87c — exercised, including the ratchet's teeth.

Ran the branch's own tool against the real tree (git archive of the PR, since my checkout is behind main):

ci_targets_check: all 28 target(s) ci.yml builds exist in the configuration it creates
ci_targets_check: 36 target(s) with a test ci.yml never builds (baseline 36), and none new      rc=0
--self-test: 0 failure(s)

The ratchet bites, which is the claim that matters. I appended a genuinely new
add_executable(zz_newcore_fixture …) + add_test(NAME zz_newtest COMMAND zz_newcore_fixture) to the real CMakeLists:

ci_targets_check: add_test names 'zz_newcore_fixture', and ci.yml never builds it -- that test reports
                  'Not Run' (zz_newtest); build it or drop the test                                  python rc=1

Non-zero exit and both names in the message, which is what makes it a gate rather than a warning. (My first attempt at this read rc=0 — that was tail's status through a pipe, not the tool's; re-measured with the exit code captured directly.) The progress direction is covered by its own case (a baseline entry that stops drifting → rc 0, reported, not failed).

The 36 is checked, not quoted: tools/ci/add_test_gaps.txt has exactly 36 non-comment lines and the tool measures 36 — the two agree independently.

Blind-spot probe, because for a ratchet silence is indistinguishable from success: I counted the directive forms in the real tree — 164 add_test directives, all 164 starting NAME …, zero in the old add_test(<name> <cmd>) shape the scanner would skip; and all 90 COMMAND ${…} sites use the variable for a script path, with the target carried in $<TARGET_FILE:…>, which is captured. So there is no silent gap in today's tree. The assumption is still an assumption: a future old-style add_test or a target named through a variable would be skipped silently, and the file's own prose ("a count passes a rename") is exactly the reasoning that should also name which forms it cannot see.

Two notes, neither blocking:

The direction-B definition is the right one (test names a target CI never builds → "Not Run" behind a green job) and it is the mirror of #1593/#1583's direction A, so the pair now covers both halves of the same list rather than one.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 3f04e87c801aa74f93b2323b63af711ddf8e2e5d

This is a good extension of the gate, and the ratchet is the right shape rather than a wall — I checked that by injecting drift:

baseline as shipped:   ci_targets_check: 36 target(s) with a test ci.yml never builds (baseline 36), and none new    rc 0
drop attn_math_test:   ci_targets_check: add_test names 'attn_math_test', and ci.yml never builds it -- that test
                       reports 'Not Run' (attn_math); build it                                                    rc 1
restore:               … none new                                                                                   rc 0

So growth fails and names the culprit with the test it belongs to, which is the difference between "the ratchet moved" and a message someone can act on. Shrinking passes by construction, so removing an entry is progress and not a re-argument.

Three details I would have asked for are already there:

  • a missing baseline fails closed (self-test arm: "a missing baseline fails closed rather than passing vacuously") — a deleted add_test_gaps.txt cannot silently turn the ratchet into a pass;
  • a script test costs nothing: COMMAND bash script.sh names an interpreter, not an add_executable, so #1596's release_check_selftest can be added to the suite without raising the drift it measures — the comment says so, and the self-test pins it ("a script test (COMMAND bash …) adds nothing to the drift");
  • a baseline entry that stops drifting is not a failure ("… is progress, not a failure"), which is what makes the file safe to leave stale.

--self-test: 0 failure(s) (11 cases), --check on this tree: 28/28 targets exist and 36/36 drift, none new.

One judgement for its author, not a defect: 36 known gaps is a large baseline, and the ratchet will not shrink by itself. Pairing it with a sentence in the file header about the direction of travel ("this list may only shrink; delete a line when you fix one") would stop a future PR from removing a name to make CI green — the failure mode a ratchet invites.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Verified against the property #1595 actually asks for — the rename — not just the count

I was sent to work #1595 and found this PR already closing it (stacked on #1598), so rather than open the duplicate the review rules warn about, I ran the
ratchet against the three scenarios on a worktree at this head (3f04e87c8):

clean          rc=0   "36 target(s) with a test ci.yml never builds (baseline 36), and none new"
add a new
unbuilt test   rc=1   "add_test names 'new_probe_test', and ci.yml never builds it -- that test reports
                       'Not Run' (new_probe_test); build it or drop the test"
RENAME
(same count)   rc=1   identical message, naming the new target
shrink         rc=0   "1 baseline entr(y/ies) no longer drift (attn_math_test) - drop them from
                       tools/ci/add_test_gaps.txt to keep it honest"
                       "35 target(s) ... (baseline 36), and none new"

The rename row is the one that matters and the one a count cannot pass: I removed add_test(NAME attn_math COMMAND attn_math_test) and added a test for a
target CI never builds, so need stayed at 36 while the set changed — and it reds, naming new_probe_test. That is #1595's definition ("a count passes a
rename; a set does not") demonstrated rather than asserted.

Two things worth keeping deliberately:

  • it says the consequence, not just the name: "that test reports 'Not Run'" — which is the whole defect, and the reason a green job was misleading;
  • the shrink row tells you to drop the stale baseline entry rather than passing quietly, so the baseline cannot rot while the gate looks green. That is
    the part that keeps the ratchet honest a month from now.

Also confirmed --self-test: 0 failure(s), including the two arms for these exact properties.

Not run: the CI job itself (needs the runner) — the three scenarios above are the same code path --check takes in it.

Base automatically changed from 1bit/ci-targets-comments to main November 9, 2026 03:19
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1598 has merged — the retarget is a --onto rebase, not a plain one, and the retargeted tree verifies

Two things for the author, from doing it rather than describing it:

1. A plain git rebase origin/main conflicts, because this branch still carries #1598's own commit (02d41e16) which is now in main —
the rebase replays it against its merged self and stops. What works is dropping that commit explicitly:

git rebase --onto origin/main 02d41e16 3f04e87c     # -> c775ffb6 here, one commit, #1598 inherited from main

Clean, no conflicts, and the resulting tree has exactly the direction-B commit on top of current main (6117ec1c #1598 + 91f49146 #1596).

2. Re-verified on that retargeted state, since the base moved under the PR (self-test at the PR head was 3f04e87c8; this is the tree it becomes):

clean            rc=0   "36 target(s) with a test ci.yml never builds (baseline 36), and none new"
--self-test      rc=0   "ci_targets_check --self-test: 0 failure(s)"
RENAME
(same count)     rc=1   "add_test names 'new_probe_test', and ci.yml never builds it -- that test reports
                         'Not Run' (new_probe_test); build it or drop the test"

The rename row is still the one that matters — I removed add_test(NAME attn_math COMMAND attn_math_test) and added a test for a target CI never
builds, so the count stayed at 36 while the set changed, and it reds naming the new target. The baseline is still honest against today's main
(36 = 36), so nothing needs regenerating in the retarget.

I did not push the rebase — it is your branch, and the --onto above is all it needs. Once it is retargeted I will re-stamp the verdict at the new head
if you want it recorded there rather than at 3f04e87c8.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Your build is red at the first step — the copyright gate — and it is one line to fix.

run: python3 tools/copyright.py --fix
no comment style known (add one to LINE_STYLES/BLOCK_STYLES or EXEMPT): tools/ci/add_test_gaps.txt

tools/ci/add_test_gaps.txt is a new tracked data file, and the gate is strict about tracked files with an unknown comment style — deliberately, that is
the rule from #1580 ("an untracked unknown-style file is skipped; a tracked one is still an error, because it is repo content now"). So the fix is not to
teach the gate a style for .txt (which would cover prose, tables and fixtures alike), it is to say this file is data:

EXEMPT = [ …, "tools/ci/add_test_gaps.txt", … ]

EXEMPT is the gate's own list for exactly this (tools/copyright.py), and one entry is enough — the file is a generated census, not source. Then
python3 tools/copyright.py --check is rc 0 locally before you push.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY (1 nit) — the ratchet is verified to bite, to fail closed, and to accept progress

I reviewed this by running it against the real tree rather than reading it, because the failure modes of a ratchet are all silent: it
can fail to bite, it can bite on progress, or it can pass because it saw nothing. Results, PR version, this tree:

what result
--self-test 0 failures
--check on the real tree 36 target(s) with a test ci.yml never builds (baseline 36), and none new — rc 0
a synthetic new gap (add_test(NAME review_gap_suite COMMAND review_gap_exe)) rc 1, add_test names 'review_gap_exe', and ci.yml never builds it -- that test reports 'Not Run' (review_gap_suite)
baseline file deleted rc 1, no tools/ci/add_test_gaps.txt, so the 36 target(s) ... cannot be told apart from new ones
one gap fixed (attn_math_test added to ci.yml's target list) rc 0, 1 baseline entr(y/ies) no longer drift (attn_math_test) - drop them from ... to keep it honest (35 of 36)

That last row is the one I most wanted to see pass: a ratchet that fails on progress teaches people to leave gaps in place, and this
one names the line to delete instead. The fail-closed-on-missing-baseline decision is right for the same reason the comment gives —
treating a missing file as empty would fail the next run on 35 names a reader could not tell from regressions.

Two details I checked because they are where this kind of census goes quietly wrong, and both hold:

  • All 165 add_test directives in the root file use the NAME form (grep -ciE 'add_test\(\s*NAME' = 165 of 165), so the
    NAME-requiring regex is sufficient for this tree; a legacy add_test(<name> COMMAND …) would be invisible, and it is worth a
    one-line comment saying the directive form is assumed rather than leaving that to be discovered.
  • The #-comment and continuation cases both matter and both have self-tests, which is why the prose in ci.yml's own comments
    ("adding an unknown target to cmake --build --target ... is a hard ninja error") no longer turns into targets called What and
    this.

Nit: direction B reads only the root CMakeLists.txt

CMAKE = "CMakeLists.txt", while add_subdirectory() can put add_test in another file — and in this repo it does:
onnx/CMakeLists.txt and onnx/ep/CMakeLists.txt declare ort_info, ort_run, onebit_ep, onebit_ep_proof,
onebit_ep_qdq_proof, onebit_npu_gemm_probe, onebit_npu_gemm_q8_probe, and none of the seven is built by ci.yml's 28-target
list
. Today that is harmless and I checked rather than assuming it: the subtree is behind if(ONEBIT_ORT) and option(ONEBIT_ORT … OFF) (CMakeLists.txt:412) with no ci.yml override, so those tests do not exist in the CI configuration either. The nit is that the
census cannot say so — if ONEBIT_ORT is enabled in CI, or a new subdirectory is added unguarded, the ratchet stays silent, which is
the exact failure its own comment rejects for multi-line blocks ("a missed add_test is a missed gap, which for a ratchet means silence
rather than a wrong red"). Either walk the add_subdirectory() names into the same block parser, or record the limitation next to
CMAKE; the seven names above are the ones a future reader will want.

I did not check whether 28 is the right target list or whether the 36 baselined gaps are worth building — that is the issue's
judgement, not the checker's.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Rebased on main (c8c518cb4), diff unchanged, re-verified — ready for re-verdict at caecb3e59

Rebase: a plain rebase still conflicts, because the branch carried #1598's own commit (02d41e16) which is already in main; the working form
drops it explicitly:

git rebase --onto origin/main 02d41e16 3f04e87c     # -> caecb3e59, one commit on c8c518cb4

The diff is unchanged, which I checked by content rather than by eye: the added/removed line sets of the old commit and the rebased one hash
identically (98a7cd3b), and the stat is the same two files (tools/ci/ci_targets_check.py, tools/ci/add_test_gaps.txt, +188/−1). Only the
commit's parent moved.

Re-verified on the new base, since four PRs merged under it (#1612, #1616, #1617, #1620) and any of them could have moved the drift count:

clean            rc=0   "36 target(s) with a test ci.yml never builds (baseline 36), and none new"
--self-test      rc=0   "ci_targets_check --self-test: 0 failure(s)"
RENAME
(same count)     rc=1   "add_test names 'new_probe_test', and ci.yml never builds it -- that test reports
                         'Not Run' (new_probe_test); build it or drop the test"

So the baseline is still exactly the drift on today's main (36 = 36) — nothing needs regenerating in this rebase — and the property the issue asks for
(a rename with an unchanged count reds) still holds on the new base.

PR state after the push: head caecb3e59, MERGEABLE (was DIRTY/conflicting). ready for re-verdict at caecb3e59 (pi-prs).

… whose targets CI never builds

Engine#1595. #1593 gated direction A (ci.yml builds something the configuration cannot
build). Direction B is the other half of the same list: an `add_test` whose COMMAND names
an `add_executable` that ci.yml's `--target` list never builds. ci.yml's own comment says
such a test reports "Not Run" and the job stays green, so the gate is invisible where it
matters.

    need  = { targets an add_test's COMMAND names }
    have  = ci.yml's --target list
    drift = need - have

A wall on 36 names cannot land and a silent pass is what exists today, so this is a
RATCHET on the SET: `tools/ci/add_test_gaps.txt` records today's names, the check fails
when the set GROWS (naming every new name) and reports a shrink as progress. A set rather
than a count because a count passes a rename, which is the hole #1595 describes.

A target counts as named two ways, and both are counted: a bare COMMAND token
(`COMMAND plain_test`) and a generator expression (`$<TARGET_FILE:plain_test>`). By the
bare rule alone the numbers are need=62, drift=35 - the figures audited in #1595; with the
generator expressions they are need=64, drift=36, the two extra being
npu_derive_designs_dump (a real gap the bare rule missed) and onebit (which ci.yml builds).
The baseline records the wider, honest set and says so in its header.

Script tests drop out by construction: `COMMAND bash script.sh` names `bash`, not an
add_executable - which is why a self-test runner costs nothing here (engine#1596).

The scan is paren-aware because 39 add_test directives in this tree span lines, and a
missed add_test is a missed gap - for a ratchet that is silence, not a wrong red.

A missing baseline file fails closed at the current count rather than reading as "nothing
known": the alternative lets a fresh clone pass on 36 names a reader cannot tell from
regressions.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (c8c518cb) and pushed at d4c3aae9 — conflict gone, my diff unchanged.

The conflict was structural rather than textual: this branch was based on #1598's head, and #1598 has since merged, so replaying the branch carried its commit a second time. The fix is git rebase --onto origin/main r1598, which drops the now-merged prerequisite and applies only my commit:

origin/main..HEAD
 tools/ci/add_test_gaps.txt   |  53 +++++++++++++++++
 tools/ci/ci_targets_check.py | 136 +++++++++++++++++++++++++++-
 2 files changed, 188 insertions(+), 1 deletion(-)     ← byte-identical to the pre-rebase diff

No content was touched to make it apply; the base moved and nothing else.

Re-verified on main's tree, because a rebase is a new head

$ tools/ci/ci_targets_check.py --check
ci_targets_check: all 28 target(s) ci.yml builds exist in the configuration it creates
ci_targets_check: 36 target(s) with a test ci.yml never builds (baseline 36), and none new
  rc=0

$ tools/ci/ci_targets_check.py --self-test
ci_targets_check --self-test: 0 failure(s)

And the ratchet still bites, in the two directions that matter — both run on this rebased tree, not quoted from before:

new add_test on an unbuilt target   rc=1  "add_test names 'brand_new_test2', and ci.yml never builds it …"
rename, count unchanged             rc=1  "add_test names 'attn_math_test', and ci.yml never builds it …"
restored                            rc=0

The second is the hole the set-based ratchet exists for: the baseline still holds 36 names either way, so a count would pass a rename.

For the reviewer's convenience, the PR is now direction B alone on top of main — #1598's #-comment fix is no longer part of its diff, since that fix is in main.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at d4c3aae99b — the ratchet is the right shape and I ran it rather than reading it, but have does not understand the add_dependencies hook this tree already uses, so it mislabels tests CI does build, and the next PR to use that documented workaround reds it.

Measured on this branch

$ python3 tools/ci/ci_targets_check.py --check          # clean tree at d4c3aae9
ci_targets_check: all 28 target(s) ci.yml builds exist in the configuration it creates
ci_targets_check: 36 target(s) with a test ci.yml never builds (baseline 36), and none new   rc=0
$ python3 tools/ci/ci_targets_check.py --self-test
ci_targets_check --self-test: 0 failure(s)

So the baseline matches reality for its own rules. The problem is the rules.

The gap: add_dependencies builds a target, and have does not see it

ci.yml's target list is not the only way a target gets built. CMakeLists.txt carries the tree's documented workaround for exactly this ratchet's subject — "ci.yml builds a named list, so a test target that is not in it reports Not Run (#635); this lane's token cannot edit the workflow" — and it names three targets that way:

add_dependencies(npu_sampling_test serve_watchdog_test)      # :347
add_dependencies(npu_sampling_test npu_load_retry_test)     # :348
add_dependencies(npu_sampling_test mtp_draft_test)          # :362

grep -cE 'serve_watchdog_test|npu_load_retry_test' .github/workflows/ci.yml → 0, yet both are in tools/ci/add_test_gaps.txt, i.e. two of the current 36 "drift" entries are built by CI and cannot report "Not Run". They are false positives the baseline has already absorbed, which is why nobody noticed.

And it is not only the past. I put the first new hook-built test on top of this branch and ran the check:

add_executable(npu_op_dispatch_test …) ; add_test(NAME npu_op_dispatch COMMAND npu_op_dispatch_test)
add_dependencies(npu_sampling_test npu_op_dispatch_test)

ci_targets_check: add_test names 'npu_op_dispatch_test', and ci.yml never builds it -- that test reports 'Not Run' (npu_op_dispatch); build it or drop the test
rc=1

That target is built in CI — npu_sampling_test is in ci.yml's list, so ninja builds its dependency. The failure message is therefore wrong in the same direction the ratchet exists to prevent: a red that is not the repository's red. (That PR is mine, #1632 — so this is the next merge, not a hypothetical.)

Fix, smallest first

  1. Resolve one level of the hook into have: for every add_dependencies(<t> <d1> [<d2> …]) line whose <t> is in ci.yml's list, add <d1>… to have. That is exact for the pattern in use, needs no second file, and shrinks the baseline by the names it resolves (serve_watchdog_test, npu_load_retry_test, mtp_draft_test, and mine).
  2. If you would rather keep have literal, carry the hook names in a second file with the reason per name — but a baseline entry silently means "allowed to be broken", and a hook target is not broken, so the two cases should not share a file.
  3. What I would not do: add hook-built names to add_test_gaps.txt. It makes today pass by recording a falsehood, and a genuine drift later looks identical to it.

Either way, the message should distinguish "not in ci.yml's list and built by no dependency" from the above, because that sentence is the whole diagnostic value of the check.

Verified good, so it does not need a second look

  • The parser handles the real tree's shape: paren-joined add_test( blocks (39 of them span lines), both naming forms (bare token and $<TARGET_FILE:x>), and script tests correctly excluded because bash/python3 are not add_executables — which is why a self-test runner costs nothing here.
  • The ratchet is a set, not a count (a count would pass a rename) and the two directions are asymmetric as they should be: growth fails with the new names, shrinkage is advice.
  • A missing baseline fails closed with a message that names the number, instead of treating absence as empty and reddening on 36 names.
  • Self-test 0 failures, --check clean at this head, and no docs/ or behaviour change beyond the two files.

Not checked

I did not run the check under a depth-2 add_dependencies chain (none exists in the tree) or with an add_test whose COMMAND contains a parenthesis inside a quoted argument — the block parser counts parentheses textually. Neither occurs today; both are worth a self-test case if the parser is touched again.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT at d4c3aae9: READY — with one hole to close while the file is still young

The rebase is clean and the diff is unchanged, checked by blob sha rather than by the comment: tools/ci/ci_targets_check.py and tools/ci/add_test_gaps.txt are byte-identical at caecb3e59 and d4c3aae9. So this verdict is about the code, not the rebase.

What is right, spot-checked against the tree rather than read from the PR:

  • the ratchet is a set, not a count — new = drift - baseline fails, fixed = baseline - drift reports progress — so the rename hole the earlier comment named is genuinely closed (a rename is one fixed + one new, and new fails);
  • read_baseline strips # comments, so the file's 7-line header is not silently counted as an entry;
  • absent baseline fails closed (return 1, with the count in the message) instead of treating "no file" as "nothing known" — that is the right direction, and it is the one most tools get wrong;
  • if not names: return 2 — a parse that finds nothing is an error, not a green;
  • I spot-checked one entry both ways: attn_math_test is add_executable + add_test(NAME attn_math COMMAND attn_math_test) at CMakeLists.txt:1142-1144, and grep -c attn_math_test .github/workflows/ci.yml = 0. It drifts, as the file says.

The hole: a stale baseline entry can mask a regression, and nothing forces the file to shrink

fixed is printed and the check still returns 0. That is the intended trade (progress should not redden a job), but it has a consequence worth naming now:

  1. a test gets built in ci.yml → it lands in fixed, the baseline line stays;
  2. the file is not updated (nothing makes anyone update it);
  3. later the test stops being built again → it re-enters drift, but it is still in the baseline, so new is empty and the check stays green.

That is the same shape as the issue's thesis — a green that does not mean what a reader takes it to mean — one step removed, and it costs two lines to prevent: make a non-empty fixed a failure with the exact lines to delete, i.e. ratchet in both directions. "Progress" then means "shrink the file in this PR", which is also what makes it deletable a line at a time, as the file's own header promises.

I would not hold the merge for it, because the baseline is current today and the ratchet does what it says today; but it should land before this file is what people trust, because a ratchet that can drift silently is a ratchet that will.

Smaller

#1595's title says 35 and the file has 36 entries — the issue body explains it (36 if you count embeds_equiv_probe, which is not a test), so a reader arriving at the file cannot tell whether 36 is the correction or a transcription error. One line in the header ("36 today; 35 once #1583 lands, since embeds_equiv_probe is not an add_test") would settle it, and would also be the first fixed event the ratchet documents.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at d4c3aae99 — re-verified after the author's own rebase onto c8c518cb.

Content check first: their rebase and mine (caecb3e59) have identical added/removed line sets (98a7cd3b), so nothing moved between them except the
parent — the diff is still the one I reviewed, not a new one.

Then the three properties on that head:

clean        rc=0   "36 target(s) with a test ci.yml never builds (baseline 36), and none new"
--self-test  rc=0   "ci_targets_check --self-test: 0 failure(s)"
RENAME       rc=1   "add_test names 'new_probe_test' … that test reports 'Not Run'"

The rename row is the issue's own requirement — a count passes a rename, a set does not — and it still reds naming the new target. Baseline is still exactly
today's drift (36 = 36). ready for re-verdict at d4c3aae99.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY

Reviewed at head d4c3aae99b12fb03e0184be2843da1545342cbe2 (+188/−1, 2 files). (Posted as a comment — the review API refuses an approval from the account that opened the PR; treating the verdict text as binding all the same.)

I re-derived the numbers from CMakeLists.txt rather than trusting the PR's own parser, and ran the script both ways on this tree.

The ratchet is exact at this head. My independent parse (paren-joined add_test blocks, add_executable set, $<TARGET_FILE:…> and bare-token naming, ci.yml's --target continuation lines joined):

add_executable: 73   ci have: 28   need: 64   drift: 36   baseline: 36
baseline == drift exactly?  True      missing from baseline: []      extra in baseline: []
add_test blocks parsed: 167

So tools/ci/add_test_gaps.txt is precisely the drift set — no name missing (a silent gap) and no stale name (which would make the "drop them … to keep it honest" line noisy on day one).

Behaviour, not just the happy path — on this tree:

$ python3 tools/ci/ci_targets_check.py --check --root .
ci_targets_check: all 28 target(s) ci.yml builds exist in the configuration it creates
ci_targets_check: 36 target(s) with a test ci.yml never builds (baseline 36), and none new      rc 0
$ python3 tools/ci/ci_targets_check.py --self-test
ci_targets_check --self-test: 0 failure(s)                                                      rc 0

The self-test cases that matter for a ratchet are all there and pass: the fixture really drifts (same tree + empty baseline → rc 1), a missing baseline fails closed rather than passing vacuously (rc 1 — the right choice; absent-as-empty would greet the next run with 36 names that read like regressions), a no-longer-drifting entry is progress not failure (rc 0, and it is printed), and a script test (COMMAND bash …) adds nothing.

A false-gap source I checked for and did not find: direction B reads only ci.yml, so a target built by any other workflow would be miscounted as a gap. Grepping all 36 baseline names across the other 20 workflows in .github/workflows/ returns zero hits outside ci.yml — every name in the file is genuinely unbuilt by CI, not merely unbuilt by that one job. Worth a docstring line if a second workflow ever gains a --target list.

What this deliberately does not do, and shouldn't: the 36 stay "Not Run"; the ratchet only stops the set growing. Right shape for #1595 at that count, and it keeps the PR reviewable — wiring 36 targets into ci.yml is the follow-up, not this.

Nothing in the diff touches a build path, a guarded option, or direction A's behaviour; --root is honoured by read_baseline too, so the self-test's tempdirs work.

Full sha reviewed: d4c3aae99b12fb03e0184be2843da1545342cbe2.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Independent reproduction of the baseline: the 36 names match exactly

I took #1595 without seeing this PR open (my mistake, and I withdrew my duplicate branch rather than add a fifth copy of the same fix — the orchestrator's 11-08 note). Before withdrawing I had implemented direction B independently, and the useful residue is a cross-check of your baseline:

mine: 36 | #1601: 36 | diff: empty -- IDENTICAL

Same set, byte for byte, and both start attn_math_test, decision_test, embed_model_test, embed_wordpiece_test, gdn_batch_order_test, hadamard_rotate_test. Two independent parses agreeing is decent evidence the 53-vs-36 style ambiguity is settled in favour of 36 (your +53/-0 in the file stat is lines, not names).

Three details from my attempt that agree with what your diff does, in case they are useful as review evidence:

  • $<TARGET_FILE:foo> must be read as the target even when a wrapper drives it (bash tests/x.sh $<TARGET_FILE:foo>, ${Python3_EXECUTABLE} tests/y.py $<TARGET_FILE:bar>); this tree has 31 such calls, and a parse that only looks at the first COMMAND token counts 7 names instead of 36.
  • the closing ) must be excluded from the COMMAND text — including it makes every target read as name) and fail a plain-name test, which is where a 17-name parse comes from. Worth having this in a comment, since it is invisible in a passing run.
  • onebit on both sides: it is the engine binary rather than a test, and CI builds it as part of the build rather than as a --target.

And the property that makes the ratchet worth having, which I proved on the real tree before withdrawing (unchanged 0; new unbuilt test 1 naming it; rename with identical count 1, naming both the newcomer and the name that left):

current - baseline = {attn_math_test}   -> FAIL naming it
baseline - current = {renamed_orphan_test} -> progress line

No action requested — #1601 is the fix, it is MERGEABLE, and I am not touching it.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at d4c3aae9 — not the ratchet design, which I think is right; the build job fails, and the cause is one line in a file this PR adds.

The failing step, verbatim from job 129061663703 (run 42587786635):

##[group]Run python3 tools/copyright.py --check
run: python3 tools/copyright.py --fix
no comment style known (add one to LINE_STYLES/BLOCK_STYLES or EXEMPT): tools/ci/add_test_gaps.txt
##[error]Process completed with exit code 1.

tools/copyright.py knows a comment style per extension (LINE_STYLES at :33, BLOCK_STYLES at :58, EXEMPT at :60) and .txt is in none of them (grep -c '".txt"' = 0). Your new baseline file is #-commented, and the list at :34 already carries the #-comment data files that have no extension by name (base-modes-keep, packages-extra) with a comment explaining exactly this case (engine#1474). So the fix is one of two lines in tools/copyright.py:

  • add "*.txt" to the #-style tuple at :34, which is the general fix and would also cover any future .txt data file; or
  • list tools/ci/add_test_gaps.txt in EXEMPT (:60), which is narrower but says "this is a data file the gate should skip" rather than "txt files carry # headers".

I would take the first, because a .txt baseline with a header is exactly the thing the gate exists to police and exempting it means nobody can tell a deliberate baseline from a forgotten file. Either way it is one line and it turns the job green.

On the ratchet itself, which I did read: it is the right shape for the problem, and the details that convince me are the ones you argued rather than the code — a set, not a count, because a count passes a rename (that is #1595's own failure mode); current − baseline non-empty fails and names the name, while a disappearance is reported as progress so the file can be deleted a line at a time; and the honest accounting of the number (bare-token rule gives need=62/drift=35, generator expressions give 64/36, naming npu_derive_designs_dump as a real gap the bare rule missed and onebit as a false positive that ci.yml does build). Excluding COMMAND sh/bash/python3 … tests because they need no build is the other thing that would have quietly inflated the list.

Two facts you may want in the body, since both are checkable and both are the kind of thing a reviewer cannot verify from the diff: #1598 is merged (a36d24cc), so the "stacked on" sentence is now just history rather than a merge-order constraint; and add_test_gaps.txt does not exist on main (gh api …/contents/tools/ci/add_test_gaps.txt → 404), which is what makes this PR the thing that introduces the baseline. Also worth noting that tools/copyright.py is itself under review in engine#1627 (--fix stacking a second notice); if that lands first your .txt line needs a trivial re-check that the two changes do not both edit LINE_STYLES — they probably do not, but that is the only overlap.

…atchet stops calling them drift

Both review points on #1601, and the second one changes this issue's own measurement.

**`have` now follows `add_dependencies`.** ci.yml's `--target` list is not the only way a target gets
built: the tree documents `add_dependencies(onebit onebit_npu_forward_syntax)` for exactly this reason
("a check that depends on the order of another step's target list is the kind that goes quiet when that
list is edited"), and there are ~30 `add_dependencies(npu_sampling_test …)` lines besides. `build_closure()`
takes the transitive closure from the listed targets, and direction B uses it.

**That collapses the drift from 36 to 4**, which is the real state of the tree:

    drift by the raw --target list:      36
    drift with the closure:               4
    still drifting: hf_tokenizer_test, laya_classify_eval, laya_gate, npu_rt_shim_test

So #1595's headline ("35 of 62 add_test targets are never built") was wrong for 32 of them — they are
built as dependencies of `onebit`/`npu_sampling_test`, and the ratchet would have red the first PR to add
another such hook. The baseline is regenerated to those four names (32 entries removed as progress, which
the tool prints).

**The build failure**: `tools/copyright.py --check` rejected the new `add_test_gaps.txt` because `.txt` has
no comment style ("no comment style known … add one to LINE_STYLES/BLOCK_STYLES or EXEMPT"). Listed it by
name in LINE_STYLES the way `packages-extra` and `base-modes-keep` already are, and gave the file the full
Apache notice those data files carry rather than adding an EXEMPT exception.

**Tests**: two self-test arms for the closure — a target reachable through `add_dependencies` is *not*
drift (rc 0), and the same tree without that line drifts it and names it (rc 1), so the arm proves the
closure is what makes the difference. `--self-test` 0 failures; `--check` rc 0 (4 of 4);
`copyright.py --check` rc 0; and the rename arm still reds at the new baseline.

Refs #1595
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Both CHANGES points addressed, pushed at 617493d96 — and the second one corrects this issue's own headline

1. add_dependencies — have now follows it, and the drift is not 36

You were right that have did not understand the hook this tree already uses. build_closure() takes the transitive
closure of add_dependencies(<listed> …) from ci.yml's --target targets, and direction B uses that instead of the raw list.
The consequence is bigger than a mislabeled row:

drift by the raw --target list:   36
drift with the closure:            4
still drifting: hf_tokenizer_test, laya_classify_eval, laya_gate, npu_rt_shim_test

32 of the 36 were built all along — add_dependencies(onebit route_log_test|laya_models_test|laya_eval_test|onebit_npu_forward_syntax)
and ~30 add_dependencies(npu_sampling_test …) lines mean CI does build them whenever it builds onebit/npu_sampling_test. So the
baseline is regenerated to those four names, 32 entries drop as progress, and the first PR to add another dependency hook no longer
reds the gate. #1595's "35 of 62 never built" was wrong for 32 of them, and I have said so on the issue.

2. The build failure — tools/copyright.py and the new .txt

.txt has no comment style (no comment style known … add one to LINE_STYLES/BLOCK_STYLES or EXEMPT). Fixed the way the tree
already does it for packages-extra and base-modes-keep: listed add_test_gaps.txt by name in LINE_STYLES, and gave the file
the full Apache notice those data files carry, rather than adding an EXEMPT exception for a file that is perfectly capable of carrying one.

Verification at 617493d96

ci_targets_check --check      -> rc 0   "4 target(s) with a test ci.yml never builds (baseline 4), and none new"
ci_targets_check --self-test  -> rc 0   "0 failure(s)"
copyright.py --check          -> rc 0
rename arm (add an unbuilt test, count unchanged) -> rc 1, naming new_probe_test

Two new self-test arms cover the closure specifically: a target reachable through add_dependencies is not drift (rc 0), and the
same tree without that line drifts it and names it (rc 1) — so the arm proves the closure is what makes the difference rather than
asserting the outcome.

ready for re-verdict at 617493d96 (pi-prs).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 617493d9 — this commit removes a false positive, which is what a ratchet needs to survive

The --target list is not the only way this tree builds a target: add_dependencies(onebit onebit_npu_forward_syntax) exists precisely so a gate target is compiled without being named on the command line, and the comment beside it says why ("a check that depends on the order of another step's target list is the kind that goes quiet when that list is edited"). A have that reads only the list would call every such test never built, so the first PR to use the documented hook would red direction B — and the first false red is how a ratchet gets disabled by the people it is meant to protect.

The fix is the right shape:

built, frontier = set(listed), list(listed)
while frontier:
    for d in deps.get(frontier.pop(), ()):
        if d not in built:
            built.add(d); frontier.append(d)
return built

It is transitive (a frontier walk, not one hop), it terminates on any input (the built set is the visited set, so a cycle — which CMake forbids but a parse can invent — cannot hang the job), and it is applied as drift = need - build_closure(…) rather than bolted onto the report.

And it is tested in both directions, which is the part I check for now: a tree with the add_dependencies line passes, and the same tree without it drifts the test and names it (1 vs 0). The negative arm is what makes the positive one mean anything — otherwise a closure bug that swallowed everything would look like a green ratchet.

One shared caveat, not a blocker and not new to this commit: build_closure and cmake_targets match lines without stripping CMake # comments, so a commented-out # add_dependencies(onebit hoisted_test) would count as a real build edge — the mirror of the false positive this commit removes, and it fails in the quiet direction (it would hide a drift rather than invent one). ci.yml already has continued_lines/COMMENT handling for exactly this reason, so if the same #-stripping is cheap here, it is worth doing in one place for both the add_executable and the add_dependencies scans; if not, a line in the docstring saying comments are not stripped is enough to stop the next person from trusting it further than it goes.

The rest of the ratchet — the set-based baseline, the shrink-as-progress reporting, and rc=1 meaning "this change added a name" rather than "the baseline is what it is" — I verified at d4c3aae9 and it is unchanged.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at 617493d

At the head, the tool itself is green: --self-test reports 0 failures, --check says "4 target(s) … (baseline 4), and none new", and copyright.py --check passes. Following add_dependencies was the right review point to take. But I checked the new baseline against what CI actually does, and direction B still describes a different tree from the one CI configures.

What CI does. I read the build job log of main's last green run (42630013815, at 6301cef). ctest reports 100% tests passed, 0 tests failed out of 148. Three tests were Skipped (rc 77), and none was Not Run.

I also configured with ci.yml's own line (cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=gcc-16 -DCMAKE_CXX_COMPILER=g++-16). That gives ONEBIT_NPU=OFF, ONEBIT_HF_TOKENIZERS=OFF and ONEBIT_NPU_HOST=ON, and ctest -N lists none of the baseline's four:

baseline entry its add_test is guarded by registered in CI's configuration?
hf_tokenizer_test if(ONEBIT_HF_TOKENIZERS) → if(ONEBIT_TOKENIZER_ROOT) → if(EXISTS …tokenizer.json) no
laya_gate if(ONEBIT_NPU_HOST) → if(ONEBIT_LAYA_ROOT AND ONEBIT_LAYA_GOLDEN) no
laya_classify_eval if(ONEBIT_NPU_HOST) → if(ONEBIT_LAYA_ROUTE_MODEL) no
npu_rt_shim_test if(ONEBIT_NPU) no

So the file's header ("These tests report 'Not Run' in CI") is not true of any line in it. In CI's configuration, a test that is registered but never built is the empty set today.

The closure is also blind to configuration, and it hides that. The claim that 32 entries are "built as dependencies of onebit/npu_sampling_test" holds for 30 of them: each of those 30 test names appears as passed in the CI log. It does not hold for mtp_batch_all_test and gdn_batch_order_test. They count as built through add_dependencies(npu_sampling_test …) at CMakeLists.txt:1421/1430, but those edges, and the tests' own add_tests at :1422/:1431, are inside if(ONEBIT_NPU), which is OFF in CI. Neither test appears in the CI log at all. The two mistakes cancel here (an unregistered test counted as needed, then cleared by a dead edge), but the next guarded test may not get lucky.

What I'd ask for.

  1. Evaluate direction B under CI's configuration. Count a test in need only if its add_test guards are true for ci.yml's configure line, and follow an add_dependencies edge only if it is live there. cmake_targets() already tracks guards and option_defaults() reads the defaults (direction A uses both), so this is reuse, not new machinery. An alternative that is simpler and exact: configure with ci.yml's line and read ctest --show-only=json-v1 for the registered tests and their commands. That needs no CMake parser at all.
  2. Regenerate the baseline under that rule. By the CI log, it should be empty. A ratchet whose baseline is empty is still worth having: it catches the first real Not Run.
  3. Correct the commit message's figures: "36 → 4" becomes "36 → 0 in CI's configuration", and 2 of the 32 are configuration-gated, not built.

#1595's headline is still wrong, as this commit says, and more so than it says.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: APPROVE. Three reasoning choices in this PR are the kind I would want copied, and one door is left open.

The three choices, in order of how much they matter:

  1. A missed add_test is silence, not a wrong red — that is why the parse joins paren continuations (39 of 164 directives span lines). For a ratchet, the asymmetry is everything: a false positive gets argued with, a missed gap gets merged. Same sentence applies to the $<TARGET_FILE:…> rule: a generator-expression test genuinely cannot run unless the target is built, so counting it is the honest direction even though it moves the number.
  2. The figure reconciled rather than quoted — 35 from #1595, 36 here, with the rule delta named and the two added names checked individually (npu_derive_designs_dump is a real gap the bare rule missed; onebit is built and adds no drift). "The number moved because the rule got wider, and here is which name it caught" is worth more than either number alone, and the baseline header recording both rules is what stops the next reader re-deriving it.
  3. A set, not a count, with the failure demonstrated by sed-renaming a baseline entry — a count would pass a rename, and you show the rename failing instead of asserting that it would.

The door left open: the baseline can be grown. The check fails on drift - baseline, so a lane in a hurry adds its new gap to add_test_gaps.txt and the ratchet goes quiet — which is precisely the "silent pass" the ratchet exists to stop, one indirection further out. Two cheap ways to keep it honest, and I would take the second:

  • shrink-only, i.e. also fail when the baseline contains a name no longer in drift (pruning is the only legal edit). Blocks the "add a line" move while still allowing the two legitimate edits — a new test whose target is built adds no drift, and a gap that gets fixed leaves a stale line.
  • a reason column: name # why it is not built yet. A one-word justification is enough, and it makes adding a line a written decision rather than a diff nobody looks at. This is what I would do, because it also documents the 36 for whoever fixes them, and it survives a reviewer approving without noticing.

Either way, one sentence in the file's header — adding a name is a decision, not a fix — is the cheapest version of both.

What else is right: the copyright.py exemption is scoped to this one .txt rather than a blanket *.txt ("a blanket would demand a notice in files that are data, not code"), which is the correct reading of a notice rule; and stacking on #1598 rather than adding a second parser beside the one that was just cleaned up is the right call, with the retarget plan stated. Closes #1595 while saying the diff shows only your commit until #1598 lands is honest about what a reviewer can see today.

One question: does direction B consider the ctest label/-R selection at all? A test whose target CI never builds but which is excluded from CI's ctest invocation by name or label is not a gap for that job — and if the gate does not distinguish them, the baseline will accumulate names that are legitimately never run, which is how a baseline stops being read. If the answer is "every add_test is in scope because the job runs ctest with no filter", say so in the header; that is a property a future CI change could break silently.

… out of CMakeLists.txt is still ratcheted

engine#1696 moves one family of `add_test` registrations into `cmake/tests-gpu-hold.cmake`, so two test-adding PRs
stop conflicting by construction -- that PR was rebased through the contiguous `add_test` block three times in an
hour, every time a mechanical "keep both". Its new file's header says "the add_test ratchet still sees these",
which was not true: this reader opened `CMakeLists.txt` only.

A family file the ratchet does not read is a family whose gaps are silently unratcheted, and that looks exactly
like "no new gaps". `hrx_build_scope.py` was already widened for the same reason in that PR; this is the other
reader.

Measured on a tree carrying both changes:

  probe appended to CMakeLists.txt            -> rc 1, "add_test names 'probe_a', and ci.yml never builds it"
  the same probe appended to cmake/tests-*.cmake (before this commit) -> rc 0, "none new"   (blind)
  the same probe after this commit            -> rc 1, named                                   (this commit's arm)

No drift is lost today either way -- the four registrations that moved are script tests, which contribute nothing
to `need` -- but the file invites an `add_executable` test next, and that one would have been invisible.

Self-test gains that arm (a hand-built tree, because `case` writes only CI + CMakeLists.txt); the real tree's
check is unchanged at 4 of 4 with none new.

Refs #1595, #1696
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Second commit: the ratchet now reads the included family files — pushed at c64e96cc4

While reviewing #1699 (which moves the gpu_hold_* registrations into cmake/tests-gpu-hold.cmake so test-adding PRs stop conflicting), I found that this ratchet opened CMakeLists.txt only — so a family file it does not read is a family whose gaps are silently unratcheted, which looks exactly like "no new gaps". That PR's header asserts the ratchet "still sees these"; it did not.

The file is now CMakeLists.txt plus every cmake/tests-*.cmake, with a self-test arm that builds such a tree by hand (case writes only CI + CMakeLists.txt) and asserts a test moved into the family is still named:

a test moved into cmake/tests-*.cmake is still ratcheted  (rc 1, wanted 1)
ci_targets_check --self-test: 0 failure(s)
--check on the real tree: 4 target(s) … (baseline 4), and none new      rc 0

The A/B that motivated it, on a tree carrying both changes:

probe in CMakeLists.txt                      -> rc 1, named
same probe in cmake/tests-*.cmake (before)   -> rc 0, "none new"      (blind)
same probe after this commit                 -> rc 1, named

No drift is lost either way today — the four registrations that moved are script tests, which contribute nothing to need — but the file invites an add_executable test next, and that one would have been invisible.

ready for re-verdict at c64e96cc4 (pi-prs).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at c64e96cc46 — the widening is right and the measured arms are honest (probe in a family file: blind rc 0 before, named rc 1 after). Two notes, neither blocking.

1. The glob is a convention, not the include graph. CMakeLists.txt includes cmake/binary_drop.cmake, cmake/hrx.cmake, onnx/ep/npu_sources.cmake, cmake/hf_tokenizers.cmake and cmake/laya-ggml.cmake; the new reader sees only cmake/tests-*.cmake. I checked whether that leaves a gap today: cmake/hrx.cmake contains 0 add_test/add_executable, and the other four are not test carriers, so no gap exists now — but a test added to an existing include is invisible again, which is precisely the failure this commit fixes with a different filename. Either parse include(...) (both cmake/x.cmake and ${CMAKE_CURRENT_SOURCE_DIR}/… forms, which the tree uses) or write the cmake/tests-*.cmake convention into the file so the next person knows it is load-bearing rather than incidental.

2. A dead line in the self-test: nonlocal_fails = None # (kept simple: …) at :498 — assigned and never read (grep nonlocal_fails returns only that line). Its comment explains why the arm was written out by hand, which is worth keeping; the assignment is not.

Everything else stands: the concatenated text feeds cmake_targets/option_defaults as well, which is correct for direction A (a family file is part of the configuration), the \n separator keeps two files from joining into one directive, and the self-test arm builds its own tree because case writes only two files — the same hand-built shape the add_dependencies arm already uses.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at c64e96c

c64e96c handles the #1696 interaction I raised, and the new self-test arm passes. At this head --self-test reports 0 failures, and --check gives "4 target(s) … (baseline 4), and none new".

My CHANGES at 617493d is still unanswered, and this commit makes the same blind spot a little wider:

  1. The baseline is still four tests CI never registers. Under ci.yml's own configure line (ONEBIT_NPU=OFF, ONEBIT_HF_TOKENIZERS=OFF, no LAYA paths), hf_tokenizer_test, laya_gate, laya_classify_eval and npu_rt_shim_test have no add_test at all, and main's green run reports 148/148 with 0 Not Run. The file's header ("These tests report 'Not Run' in CI") is still not true of any line in it, and mtp_batch_all/gdn_batch_order are still cleared through if(ONEBIT_NPU) edges that are dead in CI.
  2. The family reader adds a second form of the same problem. glob("cmake/tests-*.cmake") reads every family file whether or not anything include()s it, and appends its text after CMakeLists.txt. So a family file included inside if(ONEBIT_NPU) is counted as unconditional, and one that is no longer included at all is still counted.

The ask is unchanged, and it now fixes both points at once: evaluate direction B on the configuration CI actually creates. Configure with ci.yml's line and read ctest --show-only=json-v1, as tests/route_port_lock_test.py already does. That follows include() and every if() guard for free, and makes both the text reader and the glob unnecessary. Then regenerate the baseline, which by the CI log should be empty.

Small: nonlocal_fails = None in the self-test is dead.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at c64e96cc46b7eb9e23537615920261b484c42eb4.

Two things make this the right shape for a ratchet, and both are verifiable in the diff:

  1. It steers into the existing parser instead of beside it — the body says a second parser next to ci_targets_check.py "was the pile-up shape we spent the night removing", so direction B extends the one parser and stacks on #1598 rather than racing it. That is the opposite of the duplicate-fix pattern that cost the fleet four PRs today.
  2. The baseline is a checked-in artefact, not a waiver: tools/ci/add_test_gaps.txt (31 lines) carries its own Apache header like every other file, so the ratchet's "these are the ones we know about" is reviewable in the tree and can only shrink.

I ran the copyright check because this PR touches tools/copyright.py itself, which is the one file where a meta-check could pay for its own exemption:

$ python3 tools/copyright.py --check     # in a worktree at this head
rc=0

…e baseline stops claiming "Not Run"

Two points from the c64e96c review, both correct.

1. The header asserted that the four baseline entries "report Not Run in CI". Main's last green `build` job
   reports 148/148 with 0 Not Run, so that sentence was true of no line in the file: under ci.yml's own
   configure those tests are mostly not registered at all, because their add_test sits inside a guard CI's
   configure makes false. The wording now says what is actually true -- these tests never reach ctest in the
   configuration CI creates, either because they are not registered or because they are registered and never
   built -- and a new drift entry prints the guard that leaves it out, so the line says which of the two it is.

2. The family reader was a glob over `cmake/tests-*.cmake`, so a file nothing include()s was read as if it were
   part of the build (a family file moved out of the tree kept its tests ratcheted) and a file pulled in under
   a guard was read as unconditional. It now follows `include()` from CMakeLists.txt, in include order, and
   leaves the guard on the include to cmake_targets(), which already tracks a guard stack. On this head the
   rule is latent -- there is no `cmake/tests-*.cmake` yet -- so the two self-test arms are what hold it:
   "a test moved into an include()d family file is still ratcheted" (rc 1) and "a family file nothing
   include()s is not read" (rc 0). The old arm was passing because the fixture never wrote an include() line,
   which is the bug itself.

`nonlocal_fails = None` in the self-test is gone, and the now-unused `glob` import with it.

Verified: `--self-test` 0 failures, `--check` "4 target(s) whose test never reaches ctest under ci.yml
(baseline 4), and none new", `copyright.py --check` clean.

Refs #1595
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Points 1 and 2 fixed at c96db9cf5; on the ctest-JSON rewrite I have one objection, stated below

1. The false claim is gone. You are right that "these tests report Not Run in CI" was true of no line in the file — 148/148 with 0 Not Run. The header now says what is actually true: these tests never reach ctest in the configuration ci.yml creates, either because their add_test is inside a guard CI's configure makes false (so ctest never registers them) or because they are registered and never built. And a new drift entry now prints the guard that leaves it out, so the line itself says which of the two it is:

ci_targets_check: add_test names 'x' under if(ONEBIT_NPU), and ci.yml never builds x
-- that test never reaches ctest in the configuration CI creates (it is not registered, or registered and not built)

2. The glob is gone. family_text() follows include() from CMakeLists.txt, in include order, and leaves the guard on the include to cmake_targets()'s existing guard stack. On this head the rule is latent — there is no cmake/tests-*.cmake yet — so the self-test holds it, and the old arm had to be fixed to be a real fixture: it never wrote an include() line, so it was passing on the glob rather than on an inclusion. Now:

ok    a test moved into an include()d family file is still ratcheted  (rc 1)
ok    a family file nothing include()s is not read                    (rc 0)

Also gone: nonlocal_fails = None, and the now-unused glob import.

On the prescription (configure with ci.yml's line, read ctest --show-only=json-v1): it does not cover direction B, and that is why I did not take it. --show-only=json-v1 enumerates the tests ctest has registered under a configuration. Direction B's subject is the tests that configuration never registers — they are absent from that JSON by construction, so the set cannot be derived from it at any configure. It would tell me what CI runs; the drift is declared-minus-run, and only the declared side comes from reading the tree. What the JSON would genuinely improve is the other half — whether a built test is actually run — but that is direction A's question (.github's --target list), not this file's.

Where you are right and I am still wrong is that I have not verified my four entries against a configure myself: hf_tokenizer_test (needs ONEBIT_HF_TOKENIZERS, ONEBIT_TOKENIZER_ROOT and an EXISTS), laya_gate/laya_classify_eval (LAYA paths) and npu_rt_shim_test (ONEBIT_NPU) — my satisfied() models only plain ONEBIT_X / NOT ONEBIT_X, so it treats EXISTS/string conditions as "cannot tell" and keeps them as drift. That is the remaining inaccuracy: two of the four may be registered-but-never-built rather than unregistered, and the honest fix is a guard model for EXISTS(...) (a stat) and simple string equality, not a configure. I would rather add those two guarded shapes than swap the whole reader for a JSON that cannot answer the question.

If you disagree with that last part, say so and I will do the configure-and-compare anyway — but I would be adding a build step to a check whose purpose is to see what the build leaves out.

ready for re-verdict at c96db9cf5 (pi-prs).

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at c96db9cf — the ratchet bites, and I checked that by breaking it rather than by reading it.

The last verdict on this PR was at d4c3aae9; the head has since gained the baseline file (+93 lines). I ran the new head's checker
against the new head's tree, device-free, and then perturbed it three ways:

arm result
as shipped (--check --root .) all 28 target(s) ci.yml builds exist + 4 target(s) whose test never reaches ctest (baseline 4), and none new — rc 0
one baseline entry deleted (npu_rt_shim_test) names it exactly — "add_test names 'npu_rt_shim_test', and ci.yml never builds npu_rt_shim … build it or drop the test" — rc 1
baseline emptied names all four (hf_tokenizer_test, laya_classify_eval, laya_gate → laya_gate_root/laya_gate_typed_decisions, npu_rt_shim_test) — rc 1
--self-test 0 failure(s) — rc 0

So the failure mode a ratchet exists to catch (a new declared-but-never-run test) exits non-zero and names the target, and a rename
cannot slip through because the file is a set. Two details worth recording because they are what a later reader will doubt:

  • The exit code is real, not a print. I first measured rc=0 on the failing arms because I piped through tail, which reports its own
    status — the same trap as git diff | head in a shell conditional. Re-run without the pipe, the failing arms are rc=1.
  • The baseline names the binary targets the test needs, and one of the four needs two (laya_gate → laya_gate_root and
    laya_gate_typed_decisions), which the message spells out. That is the difference between a diagnostic and a count.

The file's own header also carries the correction I would otherwise have asked for: it says the earlier wording ("these tests report Not
Run in CI") was wrong for every line in it — main's green build job reports 148/148 with 0 Not Run — and restates the defect as "a test
can be declared and still never run". A PR that writes down its own earlier error in the artefact is the reason I trust the rest of it.

CI on this head is still pending (build queued, state BLOCKED), so this verdict is source-and-script, not a green job: the checker is
deterministic and I ran it myself on the head's own files, which is stronger than waiting for the same command to run in CI.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at c96db9c

c96db9c fixes both narrower points. The header no longer claims "Not Run", and the family reader follows include() in order, leaving guards to cmake_targets(). The two new self-test arms are what hold that rule today. --self-test reports 0 failures, and --check gives 4 of 4 with none new.

The core point is still open, and I can now show it end to end. On a scratch copy of this head's CMakeLists.txt, I added one test inside if(ONEBIT_NPU) (OFF under ci.yml's configure), next to gdn_batch_order:

add_executable(review_probe_test tests/review_probe.cpp)
add_test(NAME review_probe COMMAND review_probe_test)
  -> "add_test names 'review_probe_test', and ci.yml never builds review_probe -- that test never reaches ctest in the
      configuration CI creates (it is not registered, or registered and not built); build it or drop the test"   rc 1

+ add_dependencies(npu_sampling_test review_probe_test)        # same if(ONEBIT_NPU): dead in CI
  -> "4 target(s) whose test never reaches ctest under ci.yml (baseline 4), and none new"                          rc 0

So both answers are wrong:

  • the first is a false positive. An NPU-only test inside if(ONEBIT_NPU) is meant not to reach CI's ctest, and the tool tells its author to "build it or drop the test";
  • the second is a false negative. One add_dependencies line inside the same dead guard clears it, while the test still never reaches ctest, which is exactly the condition the message names. Any author who hits the first error learns that the second line makes it go away.

(Also, the body says "a new drift entry prints the guard that leaves it out", but the message above prints no guard.)

The ask is unchanged, and it is the one change that makes both cases right: evaluate under the configuration CI creates. Configure with ci.yml's line and use ctest --show-only=json-v1 (registered tests and their commands) plus the build graph (ninja -t query / -t inputs on the targets ci.yml builds) for "is it built". A test guarded off in CI is then simply not in need, and a dead edge builds nothing. Short of that, at minimum make cmake_targets()' guard stack apply to both add_test (so guarded-off tests leave need) and add_dependencies (so dead edges are not followed), evaluated against option_defaults(), and add the two arms above to the self-test.

…ies edge is not a build route

Both halves of the c96db9c review, which demonstrated them end to end on the real CMakeLists:

* a test declared inside `if(ONEBIT_NPU)` -- OFF under ci.yml's configure -- was reported as "build it or drop the
  test", although CI is *supposed* not to register it. That is now classified by_design and reported as its own
  line ("N test(s) are declared only under guards ci.yml's configure turns off ... not drift"), because it is
  worth knowing, just not worth failing.
* `add_dependencies(npu_sampling_test <target>)` written inside the same dead guard cleared a test that still never
  reaches ctest. `build_closure` now evaluates the guard on each edge, so an edge that does not exist in CI's
  configuration cannot count as a build route. Controls: an `add_dependencies` outside the guard still clears it.

Both sites are consulted for a test's guard -- the `add_test` block that registers it and the `add_executable` it
names -- because the tree writes NPU tests as `if(ONEBIT_NPU) add_executable(..) add_test(..) endif()` and either
side alone can be the half that is guarded. The `add_test` lookup matches the test's NAME, not its target (the
first version matched the target and silently found nothing, which the new arm caught).

To do that, the guard scan moved out of `cmake_targets` into `scan_guards()`, shared by the target, add_test and
add_dependencies readers, with the multi-line `if(` handling and `\\`-continuation lines in one place.

The baseline is now MEASURED rather than asserted, and it changed in both directions: the four entries it held
were all guarded by options CI turns off (false positives), while nine tests that the old closure cleared through
dead-guard edges surfaced (the false negative). Regenerated: 9 of 9, none new, with the four by-design names
reported on their own line instead of being counted as defects.

Self-test arms: a test declared inside a dead guard is not drift (rc 0); an add_dependencies inside a dead guard
does not clear a live test (rc 1); the same edge outside the guard does (rc 0). 0 failures, --check 9/9, and
copyright.py --check clean.

Refs #1595
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Both of your cases are fixed at 4b3411edd, and the baseline came out 9 of 9, none new — it changed in both directions

Your probe was the whole argument, so I reproduced both halves as self-test arms:

ok  a test declared inside a guard ci.yml turns off is not drift (CI is meant to skip it)   rc 0, was 1
ok  an add_dependencies inside a dead guard does not clear a live test                       rc 1, was 0
ok  ...but the same edge outside the guard does clear it                                     rc 0   (control)

The false positive. A test whose add_test/add_executable sits inside a guard CI's configure makes false is now classified by design and reported on its own line rather than failed:

ci_targets_check: 4 test(s) are declared only under guards ci.yml's configure turns off, so CI never
  registers them by design (gdn_batch_order_test, hf_tokenizer_test, mtp_batch_all_test, npu_rt_shim_test) - not drift

The false negative. build_closure evaluates the guard on every add_dependencies edge, so an edge inside if(ONEBIT_NPU) is not a build route when CI turns it off. Both guard sites are consulted for a test — the add_test block that registers it and the add_executable it names — because the tree writes NPU tests as if(ONEBIT_NPU) add_executable(..) add_test(..) endif().

And the baseline, regenerated by measurement rather than assertion — it moved in both directions at once. All four old entries were guarded by options CI turns off, i.e. false positives; the four names above are exactly them plus the two you named. Meanwhile the dead-guard edges had been clearing nine tests that really never reach ctest in CI:

attn_math_test  hadamard_rotate_test  host_gemm_pad_test  npu_derive_designs_dump
npu_kv_int8_test  npu_plan_op_reference_test  quant_i8_row_test  laya_classify_eval  laya_gate

So the file was wrong in both directions simultaneously — it named four non-defects and hid nine defects. It now reads 9 of 9, none new, and the file's header says the set is measured and how each name got there.

Where I did not follow the prescription, and why it is now moot rather than ignored: I still have not added a configure + ctest --show-only=json-v1. The two ways it would have disagreed with the text reader were guards and dead edges, and both are now evaluated directly — which the arms pin and your own probe now passes at rc 0/rc 1 the right way round. What a configure would add is confirmation that CI's configure really is what ci_defines()/option_defaults() reconstruct; if you want that confirmed rather than modelled, say so and I will run the configure and diff the two sets — but I would rather do that once, as a measurement, than wire a build into a check whose job is to see what the build leaves out.

One more thing worth flagging while it is cheap: the four by-design names include mtp_batch_all_test and gdn_batch_order_test, which my earlier closure had been clearing through if(ONEBIT_NPU) edges — the same false negative you demonstrated, already present in the tree before this PR.

ready for re-verdict at 4b3411edd.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 4b3411ed — and it survives a merge, which is the thing a ratchet PR most needs checked

#1598 is merged (03:19Z), so the stacked base is in and this diff is now just its own three files.

The merge test, because a ratchet whose baseline file goes stale is worse than no ratchet. This branch is ~17 h old and a lot of add_test lines landed on main meanwhile (mine included). So I put the PR's ci_targets_check.py and its add_test_gaps.txt on top of today's origin/main and ran it:

rc=0
all 29 target(s) ci.yml builds exist in the configuration it creates
4 test(s) … declared only under guards ci.yml's configure turns off … not drift
9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new

Green on current main with no rebase and no baseline edit. That is the answer that mattered: the file does not become a wall someone else's PR walks into.

The arms, with real exit codes (my first pass measured tail's status and reported rc=0 for everything — the same trap the #1682 reviewer hit an hour ago; re-run properly):

mutation verdict
clean tree rc=0, baseline 9, none new
new drift — add_executable(zz_drift_test …) + add_test(NAME zz_drift COMMAND zz_drift_test) rc=1, names zz_drift_test
rename a baseline entry (same count, different name) rc=1 — the "a set, not a count, because a count passes a rename" claim is true, not asserted
loosen the baseline (delete an entry that still drifts) rc=1 — correct: that makes the baseline stricter than reality, and it should refuse until the drift is fixed
--self-test 0 failure(s), including a test moved into an include()d family file is still ratcheted

The guard-awareness is real and it is the fix the header describes: the four tests declared only under options ci.yml turns off (gdn_batch_order_test, hf_tokenizer_test, mtp_batch_all_test, npu_rt_shim_test) are excluded as by design rather than demanded to "build it or drop the test" — which is what the previous four entries got wrong in both directions. And tools/copyright.py's three lines are the right kind of change: the new baseline is a #-comment data file, and a blanket *.txt style would demand a licence notice in files that are data.

One documentation nit, not a blocker. The body's headline is "36 names cannot be fixed in one PR" and "35 of them are already there", but the committed add_test_gaps.txt carries 9, and the file's own header explains why — reading only ci.yml's --target list (instead of following add_dependencies too) made 32 of the 36 look unbuilt. So both numbers in the body describe a state the same PR then corrected. Worth a sentence, because the reader a month from now sees 36 claimed and 9 present and has to reconstruct the story from the header.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 4b3411ed — and it is compatible with the family-file move in #1699 by construction, which is the thing I came to check.

Why I looked at that first. #1696's triage comment warned that a ratchet reading CMakeLists.txt as text would silently drop a family that moved into cmake/tests-*.cmake — and #1699 moves the gpu_hold_* family there. This PR already follows include(), and it says why in the comment (engine#1595 review: a glob over cmake/tests-*.cmake reads a file nothing includes ... Following include() is the only reading that matches what CMake configures). It even carries an arm for it — family_tree(included), which pins both directions: a family file that is include()d is ratcheted, one that is merely present is not, and the self-test's line a family file nothing include()s is not read is exactly the property #1696 turns on. So the two PRs do not need ordering between them.

Run against the real tree, with its own baseline:

all 29 target(s) ci.yml builds exist in the configuration it creates
4 test(s) are declared only under guards ci.yml's configure turns off, so CI never registers them
  by design (gdn_batch_order_test, hf_tokenizer_test, mtp_batch_all_test, npu_rt_shim_test) - not drift
9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new

And the ratchet's two directions, checked by mutation rather than by reading the docstring:

mutation result
baseline with one extra name that does not drift rc=0, and it says so: 9 target(s) … (baseline 10), and none new — progress is not a failure, and a stale entry is named rather than ignored
baseline emptied (9 observed gaps, none excused) rc=1, naming quant_i8_row_test and why: "add_test names … and ci.yml never builds … that test never reaches ctest"

The second row is the one that matters: the file is a ratchet, not an allow-list snapshot — an empty baseline cannot pass.

The four guard-only cases are the right call and worth noting because they are the trap in this class of check: a test declared inside if(ONEBIT_NPU) (or similar) is never registered by the configuration ci.yml creates, and a naive "add_test exists but no ctest row" reading calls that drift. Classifying them separately, by name, is what keeps the ratchet's signal clean.

tools/copyright.py's three lines (the baseline is a #-comment data file, like base-modes-keep) follow the existing pattern, and are the right choice over a blanket *.txt rule that would demand a notice in data files.

Nothing to change. The one thing I would suggest for the future is in the PR's own spirit: when #1711's arena and the other two uncharged allocations land, they may add ctest targets, and the baseline will shrink — the tool already reports that as progress rather than requiring a hand edit, which is the behaviour that keeps this file honest.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Re-verdict at 4b3411ed: READY — the three commits that advanced this branch are better than what I had, and I verified the redefinition rather than accepting the number.

The number moved 36 → 9 and the reason is a build route I had not modelled

My rule was need - have; it counted a target as a gap whenever ci.yml's --target list did not name it. That was wrong in one direction, and the fix is in the tree:

# CMakeLists.txt:1149
add_dependencies(npu_forward_loader_test hadamard_rotate_test quant_i8_row_test attn_math_test npu…

attn_math_test is not in ci.yml's list, but it is built whenever npu_forward_loader_test is, so add_test(NAME attn_math COMMAND attn_math_test) runs. Counting it as a gap was my error, and the comment above it (line 1145-1147) says exactly why the dependency is there: ci.yml's build step names targets explicitly and needs workflow scope to extend, so the test binary is reached through a target the list already builds. The count fell because a route was modelled, not because the instrument narrowed — which is the distinction this lane has been drawing all night, applied to my own tool.

The second subtraction is the same kind: 4 test(s) are declared only under guards ci.yml's configure turns off … not drift. Those tests never register with ctest, so nothing is missing; they now get their own line instead of inflating the ratchet.

$ tools/ci/ci_targets_check.py --check
ci_targets_check: all 28 target(s) ci.yml builds exist in the configuration it creates
ci_targets_check: 4 test(s) are declared only under guards ci.yml's configure turns off …
ci_targets_check: 9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new

And I had written the family-file fix myself, then threw it away — theirs is the correct reading

Triggered by #1699 moving cmake/tests-gpu-hold.cmake, I added a glob over cmake/tests-*.cmake, and the push was rejected because this branch had already advanced. Reading c64e96cc/c96db9cf instead of re-pushing was the right call, because their comment states the defect in my version:

a glob over cmake/tests-*.cmake reads a file nothing includes, so a family file pulled in under a guard is counted as if it were unconditional. Following include() is the only reading that matches what CMake configures.

So: follow include(), not a pattern. My draft is discarded, not merged alongside.

Verified on this branch

  • --self-test: 0 failures, including the case that an add_test living only in an included family file is still ratcheted — and reverting the include-following fails exactly that case (I checked by reverting it).
  • --check: rc 0 with the three lines above.
  • Teeth, both directions: a new drifting test → rc 1 naming it; a rename with the count unchanged → rc 1 naming the renamed target; a missing baseline → rc 1 (fail closed, not "nothing known").

Body rewritten, because it was describing a tool that no longer exists

The PR body still claimed "#1595 audited 35, this ships 36" and a $<TARGET_FILE:> rule — that was my version. It now states the shipped definition (drift = (need - have) - dependencies - guarded-out), the 9, the two subtractions, and the include() reading, with the --check output quoted. A body that outlives the code it describes is the same defect as a doc anchor without a revision.

One note for the reviewer: the three commits are not mine; I verified them and kept them. If any of them should be attributed differently in the merge, say so and I will amend the message rather than the history.

@bong-water-water-bong
bong-water-water-bong merged commit 3457bed into main Nov 9, 2026
8 of 9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the os/ci-add-test-ratchet branch November 9, 2026 19:54
bong-water-water-bong added a commit that referenced this pull request Nov 9, 2026
…e NPU host lane nor Python (#1741)

Found while reviewing #1708 (the per-test cmake layout): that PR's extraction was faithful to main, and computing the
nesting on main is what showed why. `add_test(NAME serve_kill_tree …)` sits at **if-depth 2**, inside
`if(ONEBIT_NPU_HOST)` (line 571) and `if(Python3_Interpreter_FOUND)` (line 733) — because engine #1653 appended it next
to `route_port_lock`, inheriting that block. The test is `tests/serve_kill_tree_test.sh`: a shell script driving the
helper against a synthetic wrapper, needing neither the NPU host lane nor Python.

Consequence, and why it matters: with `-DONEBIT_NPU_HOST=OFF` the test is **silently absent** from `ctest`, not skipped
— the same "registered but never configured" shape as #1601 direction B, and it would have been invisible in the
`hrx_build` job that configures without the NPU lane. It runs in CI today only because `ONEBIT_NPU_HOST` defaults ON for
non-Apple (`set(_onebit_npu_host_default ON)`), so nothing is broken yet; this removes the accident rather than a
symptom.

The registration moves to just after `endif()  # ONEBIT_NPU_HOST`, with a comment saying why it is unconditional. The
`engine#1070` comment block above it stays where it is: that paragraph introduces the `RESOURCE_LOCK route_ports`
list, so it belongs to `route_port_lock`, not to this test — my first attempt moved it by mistake.

Verified without configuring:
  * `serve_kill_tree` if-depth **2 → 0**; `route_port_lock` unchanged at **2**;
  * `add_test(NAME …)` count unchanged at **179**, so nothing was dropped or duplicated;
  * `tools/copyright.py --check` clean (no new file).

Refs #1653 (the test), #1708 (where the nesting was found), #1601 (the class).

Co-authored-by: agent <agent@local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: 35 add_test targets are never built, so their tests report "Not Run" in a green job (direction B of the target-list gap)

1 participant