Discover conformance runner tests instead of naming them (#572) - #594
Conversation
Sensitive Change Detection (shadow mode)This PR modifies control-plane files:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7946b0c40d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR closes a build-level false-green: the conformance-runner-tests Makefile recipe and .github/workflows/test.yml each selected the Python and Ruby runner suites by naming a single file (test_delay_gaps.py / delay_gaps_test.rb), so two other suites (test_replay_runner.py, replay_runner_test.rb) that matched neither name were executed by nothing. The fix makes every recipe discover its suites, splits the target per language so CI calls the make target instead of respelling commands, and adds a fail-closed reachability guard (scripts/check-runner-test-reachability) wired into make check and the spec-gates CI job. It is the first of a three-PR stack (#553, #573 follow).
I verified: no stale references to the renamed conformance-swift-runner-tests target remain repo-wide; the guard's content markers and discovery globs match the actual runner trees; the awk recipe extractor deliberately excludes the surrounding comment blocks (so the enumeration ban won't false-positive on the #572 comments that mention the old filenames); and both previously-unrun suites (unittest-based Python, Minitest-based Ruby) are now genuinely collected by pytest -q and the *_test.rb glob loop.
Changes:
- Split
conformance-runner-testsintoconformance-runner-tests-{go,python,ruby,kotlin,swift}(all discovery-based; renamed the old Swift target into the family) and pointed each CI language job at its make target viaworking-directory: .. - Added
scripts/check-runner-test-reachability— a two-tooth guard (enumeration ban + discoverability, both fail-closed) with a--self-test, wired intomake checkandspec-gates. - Moved the Python interpreter pin from
--pythonto the CI-sideUV_PYTHONenv so the make target stays version-agnostic.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
scripts/check-runner-test-reachability |
New fail-closed guard asserting every runner test file is reachable from discovery and no recipe/workflow names a test file; includes a self-test. |
Makefile |
Splits the aggregate runner-test target into per-language discovery-based targets, renames the Swift target, and wires check-runner-test-reachability into .PHONY, check, and help. |
.github/workflows/test.yml |
Language jobs now invoke make conformance-runner-tests-<lang> (Python pin via UV_PYTHON); spec-gates runs the reachability check and its self-test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
7946b0c to
63e434f
Compare
63e434f to
55fadde
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 55fadde6d1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
55fadde to
3e4a812
Compare
3e4a812 to
50d7402
Compare
50d7402 to
84adba2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 84adba2a7f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
84adba2 to
5224891
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f72bd9623
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…couldn't see
Two more, and both are the check looking past the thing it exists to find.
build/ was pruned globally. `build` sat in PRUNE alongside .venv and
node_modules, so find never handed conformance/runner/python/build/ to the
Python arm — the arm whose norecursedirs rule exists to report exactly that
directory. A guaranteed-failing test there was collected by pytest, reported by
this check, and run by neither: the check exited 0. A global prune that hides a
file from the arm meant to classify it is a blind spot, not a prune. Gradle's
tree is unaffected (the Kotlin arm scans src/, Gradle writes to build/ beside
it), SwiftPM's .build stays pruned, and every build/ under a scan root today is
inside node_modules, pruned before find descends.
Disabled steps counted as invocations. A step carrying `if: ${{ false }}` is
skipped by Actions while its `run: make conformance-runner-tests-go` stays in
the file, so the invocation check reported the Go suite as invoked. Parking a
suite behind a disabled step is the natural way to reopen #572.
`drop_disabled_workflow_blocks` removes statically-disabled steps and jobs
before any invocation grep runs, and the bound is deliberate: only a LITERALLY
false condition disables. This workflow runs the Ruby runner tests under
`if: matrix.ruby == '3.3'`, so a rule that treated any `if:` as disqualifying
would fail the very workflow the check protects.
Self-tests 22-25. 22/23/24 fail against the state they were reported on; 25 is
the bound, and it plants a condition at BOTH step and job level — an earlier
draft carried only the step one, and a mutant broadening the job predicate
passed the whole suite.
…iscovery * origin/main: deps(ts): bump the npm-dependencies group in /typescript with 2 updates (#609) deps(ruby): bump simplecov in /ruby in the bundler-dependencies group (#607) deps(kotlin): bump the ktor group in /kotlin with 5 updates (#608) Unbreak doc-constants-check: grant SPEC.md's two as-of pin citations (#605)
|
Follow-up to my earlier note about Nothing else changed: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2b378d5320
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…t self-locating Ruby's arm had the bug the Python arm was already fixed for. `require "minitest/autorun"` counted as a test declaration, so a conventional test_helper.rb — required BY the suites rather than discovered, matching no `*_test.rb` glob — failed the build while declaring nothing runnable. It now keys on declarations: a `Minitest::Test`/`Spec` subclass, a `def test_`, or a spec-style `describe`/`it`. Both real Ruby suites match on two of those, so the counts are unchanged. The converse stays loud, and for a different reason than pytest's: minitest/autorun runs every test class LOADED into the process, so whether a declaration in a helper executes depends on some *_test.rb requiring the file — an edge this check cannot see and will not assume. A helper that declares tests is still reported. `vitest run` is not self-locating. Vitest's root is the process cwd, and the test-typescript job declares `defaults: run: working-directory: typescript`, so the identical command with the step's override dropped re-runs the SDK's own suite and collects none of the six runner files this check counts — while the text of the command is unchanged. Matching the command without its directory asserted that a string appears in a workflow, not that these tests run. vitest_invocation_dirs resolves each vitest step's effective working directory (step-level `working-directory:`, else the job default), and one of them must be conformance/runner/typescript. Self-tests 26-29, each loud case paired with its quiet one; 26 and 28 fail against the state they were reported on.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f560484790
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… its matrix Invoking a target is not running the tests. The per-language loop established only that CI calls `make conformance-runner-tests-<lang>`; the target itself is one `@true` away from running nothing, with the workflow byte-identical. The TypeScript wrapper was already followed for exactly this reason, and leaving the other five trusted by name was an asymmetry rather than a decision. Each recipe must now contain the command that runs its suite — `go test`, `pytest`, `bundle exec ruby`, `:conformance:test`, `swift test` — kept as one table so "what runs the tests" stays written down instead of inferred. It moved next to the enumeration ban, since it is a fact about the Makefile: a tree with no workflow file skipped it where it first sat, which self-test 32 caught. A matrix condition is now read against its matrix. `if: matrix.ruby == '3.3'` stops running the moment '3.3' leaves `matrix.ruby` — an ordinary consequence of moving supported runtimes on — and the step survives untouched, so nothing in that diff says a suite stopped running. The step filter now also drops a step whose pinned value the enclosing job's `strategy:` block does not offer, and one pinned in a job with no matrix at all. The bound stays narrow and is still tested: expressions are not evaluated, `!=` is not reasoned about, and a pin the matrix DOES offer still counts. Self-test 25 now carries a matrix that offers its value and asserts it stays counted. Self-tests 30, 31 and 32; all three fail against the state they were reported on, while 25 stays quiet in both.
|
Main's #576/#597 added two runner tests and wired them up by name — the Resolved by keeping this branch's side in all four hunks — the enumeration is Every count went up by one without anybody adding a name anywhere, which is the Also of note: this round's work happened in a second worktree. The lane worktree |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4f93478d0c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
4f93478 to
b4ccca1
Compare
b4ccca1 to
4f93478
Compare
The third and last instance of one bug. A support module doing
`import type { Mock } from 'vitest'`, imported BY the suites, matches no
`.test.`/`.spec.` glob, so keying the TypeScript arm on the framework import
failed the build over a file that declares nothing runnable — the same defect
already fixed for conftest.py and test_helper.rb, in the one arm I had not
carried the rule to.
The marker is now `describe`/`it`/`test`/`suite`/`bench`, including the suffixed
and tagged-template forms (`it.each([…])(`, `test.skip(`, ``it.each`…` ``),
because a declaration written any of those ways is still runnable and missing
one is the silent direction. All seven committed suites match it, between 3 and
19 declarations each; the three helper modules match none.
That completes an invariant worth naming: every arm keys on a DECLARATION —
`func TestX`, a `def test`, a `Minitest::Test`, a `describe`/`it`, an `@Test`,
an XCTestCase — and none on the presence of a framework import. Support modules
are reachable by construction in every language, and this check no longer has an
opinion about them.
Self-test 33, the TypeScript twin of 14 and 28; case 17 is its loud pair and
still passes. Fails against the marker it was reported on.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac1f990b39
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| grep -qF -- "$target_tool" <<<"$target_recipe" \ | ||
| || hollow_targets+=("conformance-runner-tests-$target runs no '$target_tool'") |
There was a problem hiding this comment.
Require an executable test-runner command
Fresh evidence beyond the prior @true self-test is that replacing the Go recipe with @echo go test leaves this checker exiting 0 with all 12 checks passing. This predicate searches the complete recipe text, so an echo, comment, or other non-executed occurrence of the tool name certifies a hollow target even though CI runs no tests; validate an executable runner command rather than a substring.
Useful? React with 👍 / 👎.
| missing_targets=() | ||
| for target in go python ruby kotlin swift; do | ||
| invoked_by_workflow "make[[:space:]]+conformance-runner-tests-$target" \ | ||
| || missing_targets+=("conformance-runner-tests-$target") |
There was a problem hiding this comment.
Verify the Swift target runs on macOS
Fresh evidence beyond the earlier workflow-invocation fixes is that changing the inspected test-swift job from macos-15 to ubuntu-latest still produces Runner-test reachability clean (12 checks passed). In that environment conformance-runner-tests-swift takes its ifndef IS_MACOS branch and successfully prints SKIP, so merely finding this command does not establish that any Swift runner tests execute; associate the invocation with a macOS job or otherwise validate that the target's platform gate is satisfied.
Useful? React with 👍 / 👎.
Closes #572. First of a three-PR stack on conformance-harness false greens: this one, then #553 (whose fix edits the two files this PR makes runnable), then #573.
The defect
conformance-runner-testsselected the Python and Ruby runner suites by naming one file each —test_delay_gaps.pyanddelay_gaps_test.rb— and.github/workflows/test.ymlnamed the same two a second time. Two suites in the tree matched neither name and were therefore executed by nothing:conformance/runner/python/test_replay_runner.pyconformance/runner/ruby/replay_runner_test.rbGo's line was
go test ./..., so itsreplay_runner_test.goran incidentally; Kotlin, Swift and vitest auto-discover. Only the two enumerated languages had the hole, andgit grep replay_runner_test -- Makefile .github scriptsreturned nothing, so nothing else picked them up either.This is the exact defect
conformance-runner-testswas created to close — assertion code no target exercises — reproduced one level up in the build.The fix
Every recipe discovers, recursively. Python runs bare
pytest -q(collecting nothing is exit 5, so an emptied suite fails rather than green-passes); Ruby runs afind-driven*_test.rbloop (pruningvendor/and.bundle/) that aborts when discovery matches nothing. Go, Kotlin and Swift already discovered and are unchanged in behavior.Split per language.
conformance-runner-tests-{go,python,ruby,kotlin,swift}, and CI calls those targets instead of respelling the commands — one definition of what "run the runner tests" means.conformance-swift-runner-testsis renamed into that family. The Python interpreter pin moves from--pythonto CI-sideUV_PYTHON, uv's env spelling of the same flag.A reachability guard,
scripts/check-runner-test-reachability, wired intomake checkand thespec-gatesjob. Two teeth, because the gap has two ways back in:*_test.rb) are not themselves reported.Both fail closed: a language whose content scan finds zero test files is a failure, since a detector that matches nothing cannot report anything unreachable.
Reachability is name AND placement, and Ruby is why
This is the review finding that reshaped the PR (thread). An earlier revision modelled reachability as the name alone: recursive
find, then fnmatch the basename against the discovery glob. Correct for Go (go test ./...), pytest and vitest, all of which recurse. Ruby's discovery is not a toolchain's — it was hand-rolled asfor f in *_test.rb, a glob that expands inconformance/runner/rubyand nowhere below it. The check and the recipe disagreed, and the check was the permissive one.Demonstrated with one added file,
conformance/runner/ruby/nested/probe_test.rb, whose only test isassert false. The pre-fix script is byte-identical across all three pushed heads of this stack (sha106011498844e), as is the pre-fix Ruby recipe (sha1a06f51258dd8over its indented body):Three files counted, two opened. A guaranteed-failing file, certified reachable by the gate and executed by nothing — #572's own defect, inside #572's gate.
Fixed on both sides, because either alone leaves a way back in:
recursive(only the name decides) ortoplevel(a correctly-named file one directory down is reported). Ruby's scope is derived from the Makefile recipe, not assumed:ruby_discovery_scopejoins the recipe's backslash continuations and reads the discovery construct back out. An unrecognized shape reportsunknownand fails. So reverting the recipe to a top-level glob re-arms the placement tooth rather than silently reopening the gap.Same probe file, both halves in place:
Both blocks above are abbreviated: the first shows only the Ruby line of nine, the second only the failing file of the recipe's four (make stops there). The probe's own minitest output is verbatim. Note
REAL_EXIT=2: throughmake, a recipe failure is 2, not the runner binary's own 1.The content detector must not miss a valid test form either
Second review finding (thread). A file the content detector never recognizes is never considered, and that failure is silent in the dangerous direction: the arm's
foundcount stays nonzero thanks to the other files, so it printsok.The Go marker was
^func (Test|Benchmark|Fuzz|Example)[[:upper:]]— demanding a suffix, and demanding it start with a letter. Go's rule (go help test) is that Xxx must not start with a lowercase letter, andExampletakes no suffix at all. Sofunc Example(),func Test(andfunc Test_helper(were invisible. Now[^a-z], that rule spelled directly.Red proof
This PR's check against pristine
origin/main(5efc52f09) — the enumeration it removes. Verbatim:Main's Ruby recipe names an individual test file, so
ruby_discovery_scopecannot read a discovery construct out of it and reportsunknown— correct, and fail-closed. A revision of this PR judged that scope before scanning, so the Ruby arm returned early, never populated the basename list, and the enumeration ban below it had nodelay_gaps_test.rbto report — it namedtest_delay_gaps.pyalone. The run still failed, so nothing passed silently, but it named half the enumeration against the exact state it exists to reject. The content scan now runs first and the scope is judged after: a check that fires, fires completely.The Python detector had the same hole, and its fix has a second edge
Third review finding (thread). The Python marker was
unittest\.TestCase|^import pytest|^from unittest|^def test_. A class-based pytest suite imports nothing and indents its test method:No
unittest, noimport pytest, and^def test_is anchored at column 1 — so that file was invisible. Saved asstranded.pyit is collected by nothing, and the arm still printedok. Self-test case 5, against the old marker:The obvious fix is wrong in the other direction. Matching the class name (
^class Test) fails the real repo:conformance/runner/python/runner.pydeclaresTestCase,TestResult,TestRunnerandTestTrackeras its own plumbing.Verbatim, no elision — and note the second failure, which is the worse one. Once
runner.pyis treated as a test file its basename enters the enumeration ban's list, and.github/workflows/test.ymllegitimately namesrunner.py(it is the conformance runner's entrypoint). So a name-based detector does not just report one false positive; it cascades into tooth (1) and accuses the workflow of enumerating a test file it does not have.So the detector keys on the indented test method (
^[[:space:]]+def test) — the thing pytest actually collects — not on the class name. Verified against the real tree: the widened marker matches exactlytest_delay_gaps.py,test_replay_runner.py,test_request_count.py, and no other.pyunderconformance/runner/python.TypeScript's marker gets the same sweep for a smaller reason: it was the literal
from "vitest", sofrom 'vitest'— same import, same discovery — did not match. Now quote-agnostic.Two more "recursive means the toolchain's rule, not all"
Seventh and eighth findings (pytest norecursedirs, YAML comments).
pytest's
norecursedirsdefault —pytest --helpgives it as*.egg .* _darcs build CVS dist node_modules venv {arch}— means a correctly-named test underdist/orvenv/is matched by the glob and collected by nothing. Confirmed against the real tree withconformance/runner/python/dist/test_stranded.pypresent (a bareassert False):pytest -qstill reported 29 passed and the check still reportedok: Python — 4 test file(s). Python now declares that ignore pattern the way Go declares its own.And the per-language target loop grepped the workflow verbatim, so a step commented out wholesale still matched —
# - run: make conformance-runner-tests-goreads as an invocation to a raw grep while CI no longer runs it. Both invocation checks now read the comment-stripped file. The enumeration ban above deliberately keeps scanning the raw file: a test filename left in a comment selects nothing, but it is a stale reference worth surfacing rather than hiding.Self-test cases 12 and 13, against the previous revision, verbatim:
ok: Python — 2 test file(s)andok: … invokes all five conformance-runner-tests-* targetsare the two false greens. (The inner FAILs are the synthetic trees' absent language directories, which every self-test case has by design.)TypeScript could have left CI without this check noticing
Sixth finding (thread), and the first against tooth (1) rather than the detectors.
Tooth (1) verified that the workflow invokes every
conformance-runner-tests-*target. TypeScript has no such target — its runner tests and its conformance suite are one vitest invocation, which CI spellsnpx vitest rundirectly. So the loop could not speak for it, and the seven TypeScript test files this check counts could vanish from CI while all nine checks reported green.Three-way proof.
npx vitest runon line 299 of the workflow replaced withnpx echo STEP-REMOVED, nothing else changed:The middle one is the reason for the
sed 's/#.*//'. The workflow explains its own choice ofnpx vitest runovernpm testin a comment three lines above the step, so a verbatim grep matched the prose and passed with the step deleted. Match what CI would run, not what it says about it. That bug was in the first version of this check, and its own red proof is what caught it.Self-test case 11 pins it with a synthetic workflow that invokes the five make targets and nothing else.
@Testis not JUnit Jupiter's only test annotationFifth detector finding (thread).
kotlin/conformance/build.gradle.ktscarriestestImplementation(libs.junit.jupiter)anduseJUnitPlatform(), so@ParameterizedTest,@RepeatedTest,@TestFactoryand@TestTemplateall declare runnable tests. The Kotlin detector matched the literal@Testand saw none of them — a@ParameterizedTeststranded undersrc/main/kotlincompiles into the product, runs nowhere, andDelayGapsTestkeepsfoundnonzero so the arm printsok.Self-test case 10, against the literal-
@Testmarker, verbatim:ok: Kotlin — 1 test file(s)whileStrandedTest.ktsat undetected in the main source set. (The inner FAILs are the synthetic tree's absent language directories, which every self-test case has by design.)"recursive" does not mean "every subdirectory"
Fourth detector finding (thread). The Go arm was declared
recursiveongo test ./...'s authority — true, but not the whole rule. Pergo help packages:So
conformance/runner/go/_ignored/probe_test.gois matched by the glob, reached byfind, counted as reachable — and compiled by nothing. Confirmed against the real tree with that file present:One package listed, four files counted. Each glob language now also declares the locations its toolchain skips, and a test-bearing file in one is reported; Go's pattern is Go's own rule spelled out, the other three declare none.
Self-test case 9, against the previous revision (ignore pattern disabled), verbatim:
ok: Go — 2 test file(s)is the false green — both counted, one never compiled. (The inner FAILs are the synthetic tree's absent language directories, which every self-test case has by design.)Swift: "under Tests/" is not the same as "compiled"
Two more of the same class (sibling target, parameterized attribute), on the arm where discovery is a manifest rather than a convention.
conformance/runner/swift/Package.swiftdeclares its sole test target withpath: "Tests/ConformanceSupportTests". SwiftPM compiles that directory and nothing else — soTests/ReplayTests/ReplayTests.swift, a plain sibling, is compiled by nothing while looking entirely at home. The check usedTests/as the test root, counted the sibling as reachable, and printedok.The test root is now derived from the manifest (
swift_test_target_path), the same way Ruby's scope is derived from the Makefile, and an unreadable manifest fails rather than defaulting. Gradle is the same story one directory up: its Kotlin test source set issrc/test/kotlin, notsrc/test.The scan root widened to match. Looking only under
Sources/for stranded tests could never have found a stranded test underTests/; the whole module is scanned now, and anything test-bearing outside the compiled test directory is reported.Second, the attribute regex demanded whitespace-or-EOL after
@Test, so every parameterized form — Swift Testing's@Test("display name")and@Test(arguments:), JUnit 4's@Test(expected = ...)— was invisible. It now accepts(too.Both are pinned as self-test cases 7 and 8. Against the previous roots and marker, verbatim:
Case 7's
ok: Swift — 2 test file(s), all under conformance/runner/swift/Testsis the false green: two files counted, one compiled by nothing. Case 8'sok: Swift — 1 test file(s)is the other: the@Test("a display name")underSources/was never seen. (The inner FAILs are the synthetic trees' absent language directories, which every self-test case has by design.)A gate that aborts is a gate that did not run
Two array expansions were unguarded under
set -u. bash 3.2 — still/bin/bashon macOS — treats"${empty[@]}"as an unbound variable and kills the script, so on a machine whoseenv bashresolves there, the check would die partway through instead of reporting.Reproduced against the previous revision of this file, on a synthetic tree with a toplevel Ruby recipe and no nested files — the exact shape a repo has the moment someone reverts the recipe to a top-level glob, which is the case the placement tooth exists to catch:
Nothing elided — the script died there, so the Ruby arm and every tooth after it never ran. The same tree under this revision reports
ok: Ruby — 1 test file(s), all matched by *_test.rb (toplevel discovery)and goes on to run the rest.Both expansions are now count-guarded, matching the
-gt 0guard the others already had. An empty basename list is additionally reported rather than silently iterated: it means every tooth-(2) arm bailed out, which is worth saying out loud. Verified under bash 5.3.9 and/bin/bash3.2.57, and shellcheck-clean.Self-test
--self-test, wired intospec-gates, now carries thirteen cases. Three exist to keep the others honest — a detector can be wrong by missing a real test, and equally wrong by flagging something that is not one:replay_runner_spec.rb, matched by no glob, is reported.nested/probe_test.rbunder a top-level Ruby recipe is reported. The committed form of the false green above.examples.goholdingfunc Example()is reported, beside a Go file that keeps the arm'sfoundcount nonzero.stranded.py, importing nothing, is reported.Test*-named plumbing is not reported. Without case 6, case 5 would also pass under a name-based detector that fails the real repo overrunner.py.Tests/" is not "compiled".@Test("name")underSources/is reported._test.gounder_ignored/is reported —recursivemeans "walks subdirectories", not "walks every subdirectory".@ParameterizedTestundersrc/main/kotlinis reported.dist/is reported — pytest'snorecursedirsdefault.Case 4 against the old character class, verbatim:
ok: Go — 1 test file(s)is the point — green whileexamples.gosat undetected beside it. ([...]elides the synthetic tree's absent-language-directory FAILs, which every self-test case has by design.)Verification (real exit codes, measured on this commit)
That Python 17 is the target's scope — the whole
conformance/runner/pythondirectory's collection — not any one file. Broken out withpytest -q --collect-only:test_delay_gaps.py12,test_replay_runner.py5. The 5 intest_replay_runner.pyare the tests this PR makes reachable; quoting 17 as "the newly-reachable tests" would overstate it by 12.Both previously-unrun suites pass under discovery, so this adds coverage rather than exposing drift. Swift ran (39 tests executed, not the macOS SKIP line).
Local figures — cite the CI job's own numbers where they differ.