Repository navigation
CI test selection: symbol cap as a share, migration names, replay counts, local-filter, and Guard on repo-walking tests (#5798) - #5801
Merged
Conversation
…local-filter, Guard on repo walkers (#5798)
erikdarlingdata
marked this pull request as ready for review
October 14, 2026 12:52
…lits and cuts its answer (#5798) - Drop the `new X(` exclusion: on 10 corpus heads it took tokens dev's rule gives (CollectorColumn, HostMemory, ...), so the symbol rule named 66 fewer classes than dev. The migration V-number now only adds a token. - local-filter prints 40 classes a filter line, cuts its fallback at the limit, and with --map adds the classes whose text pattern matches a changed file (the web tests that read wwwroot/js). - Pin symbol_cap (30 for 3,023 test files, 25 for 100) and the `new Migration(N,` branch. - McpReadCommandTimeoutTests stays on the shards (49.9 s in the nightly map). - lane-orders: how to use local-filter's output.
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Claude posting for Erik Darling.
Fixes #5798.
Why
Pull requests went red at their first CI run on test classes the lane never ran locally. On dev at d85564e, #5776 had already added the changed-symbol reason to
map-selectand thelocal-classesverb. This PR finishes the issue's remaining parts: the symbol bound is now a share of the tree, migrations count as symbols, the replay measures the rule, a lane gets the filter CI would use, and repo-walking test classes join the guard set.What changes
.github/scripts/ci-select.py). The rule still only adds classes to the file-map selection (a union, never a replacement). Changes:SYMBOL_CAP_SHARE = 0.01, with the oldSYMBOL_CAP = 25as its floor. That is 30 of today's 3,023 test files. A token named in more test files than that is dropped as noisy. Minimum token length stays 4, and only declarations count.private const string V183Sqlandnew Migration(183, ...)both declareV183, which is how the tests name a migration.In-lane fix: a line likeReverted in review round 1 (H1): it took tokens dev gives.new Migration(183, ...)was no longer read as declaringMigration.symbol_classes()can diff a given head commit against its merge base, so the replay can call it, and a newnamed_classes()replaces two copies of the same loop.--replay --map FILE --symbols BASEreports how many classes each corpus row selects before and after the rule. A miss is still counted on the selection without the rule, so the rule cannot hide one. The plain replay is unchanged: 0 misses. Numbers with the 2026-10-14 nightly map (414 rows; the other 317 head commits are not in this clone; 3,826 classes):[FAIL]. My class counts differ from the 9/7/22 in the cost note, which may have counted methods or later runs.local-filter(item 5) is what fixes that.EnumerateFiles/GetFiles/GetFileSystemEntrieswithAllDirectories, from aRepoRoot/RepoFile/ParitySourceroot). It skipped any class that needs a store, skips, or sits in a collection, any file with several classes, and anything taggedCost=Slow. Results:guard-testsjob (windows-latest,-trait Stage=Guard -failSkips, both suites) now runs these classes, and the shards leave them out when they used the guard build.<Class>.cs).<Class>.cs).local-filterverb.python .github/scripts/ci-select.py local-filter [--base origin/dev] [--map test-map.json.gz]prints, per suite, the filter (40 classes a line since review round 1) (lite: -class "*.A" -class "*.B") and the class list on the next (lite classes: A B). The last line is the guard set (guard: -trait Stage=Guard).--mapit gives map-select's own answer. Without a map, or when that answer is FULL or over 300 classes, it gives the part a lane can run: the classes that name a changed symbol, plus the test classes in changed test files. A note on stderr says which.[Fact]/[Theory]are listed (helper classes are left out), and Stage=Guard classes are left to the guard line.dotnet test --filterexpression, but these suites are xunit.v3 MTP executables, wheredotnet testdoes not work. So the filter is printed in the form the executables and CI's shards take (-class/-trait).SymbolExtractor_ReadsTheDeclarationsOfCSharpAndSqlHunks: on a fixture diff with C# and SQL hunks,changed_symbolsreturns exactly CountWidgetSpans, PgMigrations, V184, V184Sql, v_widget_rollup and widget_spans. It skips a call, a parameter and a test-only name. It failed on the old code (Migrationwas taken as a declared name) before the fix above.LocalFilter_PrintsTheClassFilter_ThenTheClassList_ThenTheGuardSet: the class line, the list line and the guard line; a helper class and a Guard class are not listed.Test plan
Lite.Tests.exe -class CiSelectLocalClassesGuardTests, CiSelectReplayGuardTests, CiSelectMapSelectGuardTests, TestMapShadowGuardTests, CrossAppGuardCiGateTests: Total 85, Failed 0.Lite.Tests.exe -trait Stage=Guard -failSkips: Total 1,278, Failed 0, 27.8 s.Darling.Tests.exe -trait Stage=Guard -failSkips(includes every tagged class and GuardStageCensusTests): Total 4,602, Failed 0, 86.5 s. A first run failed GuardStageCensusTests R3 on three tagged classes; I removed their tags (listed above).Darling.Tests.exe -class SlowClassCensusTests, GuardStageWorkflowTests: Total 23, Failed 0.ci-select.py --replay: 0 misses. With--map(2026-10-14 nightly) and--symbols origin/dev: map replay 0 misses, plus the numbers in item 2.Leftovers
Done in review round 1 (I8)..claude/skills/lane-orders/SKILL.mddocumentslocal-classesbut notlocal-filter.Review round 1
Commit d6ea302. One rule above the rest: the selection is a union with dev's and never smaller, and the replay below shows that.
_NEW_EXPRESSIONis gone. On dev, a line that startsnew X(gives the tokenX; the exclusion took that token away, so on 10 corpus heads the symbol rule named 66 fewer classes than dev (lost tokens:Migration26 heads,HostMemory5,NpgsqlConnectionStringBuilder3,CollectorColumn2,CollectorParameter2,PgExtensionDependency,AppLoggerAdapter, ...). It also dropped a real member,new int[] Widgets => _w;._MIGRATIONnow adds the V-number on top of what the other rules take, and the comment says that a rule there may only add tokens. The fixture now expectsMigration(the cap drops it as noisy: 48 test files name it) andWidgets._class_args, aslocal-classesprints it; 700 classes on one line came near Windows' 32,767-character command line (CI: the sharded test steps put every class on one command line, which is at the Windows 32,767-character limit #5100). When it falls back (no map, map FULL, or over 300 classes), the answer is cut atlimit(300), most specific first: a changed test file's classes, then the text-pattern classes (I7), then the classes that name a changed symbol inordered_hitsorder. A note on stderr gives the count it cut from.McpReadCommandTimeoutTestsloses its Stage=Guard tag: 49.882 s in the nightly map, so it stays on the shards with the other slow classes. 47 classes are tagged now, not 48.GuardStageCensusTestspasses with it untagged: its R2 rule does not catch this class.SymbolCap_IsOnePercentOfTheTestFiles_NeverUnder25assertssymbol_capgives 30 for 3,023 test files and 25 for 100. The extractor fixture gainsnew Migration(185, "x", Build()), which pinsV185through thenew Migration(N,branch alone._MIGRATIONcomment says\bV(\d+)Sql\balso fires on a reference (sb.Append(V183Sql)), and that this is wanted. The fixture pins it: that line givesV183._MIGRATION, next to the rule it limits.)--map, whenlocal-filterfalls back, it adds the classes whose text pattern in the map matches a changed file: the "text pattern" reason ofmap_select, now intext_pattern_classes(). It reads the map directly, so it works when the map answer is FULL too, and it is still cut atlimit.map_selectand this function share one matcher (_pattern_hit), so their answers cannot drift apart. Test:LocalFilter_WhenTheMapIsFull_AddsTheTextPatternClasses(awwwroot/js/**class is added, a class whose pattern matches nothing is not)..claude/skills/lane-orders/SKILL.md, Build and test: a five-line bullet onlocal-filter(--base origin/dev,--mapwhen a nightly map is at hand, strip thedarling:/lite:prefix and paste the rest). This replaces the leftover above that left the file alone.Verification (round 1)
python .github/scripts/ci-select.py --replay:replay: 731 rows, 2035 failing classes checked, 1 exempt (class no longer in the tree: 1 distinct), 27 accepted misses (replay-accepted.txt), 0 misses--replay --map <run 38521722522 test-map> --symbols origin/dev:map replay: 731 rows (312 FULL), 2035 failing classes checked, 59 accepted, 0 misses (reported, not failing yet)symbol rule (#5798): 414 rows measured (317 without their head commit here), 3826 classes in the tree, cap 30 test filesselected per row: before median 2682 max 3826, after median 2687.5 max 3826the rule names on its own: median 51 max 700rows not FULL (238): before median 2054.5 max 3017, after median 2060.5 max 3017, added median 1 max 202map_select.lost vs dev: 731 rows, 414 measured (317 without their head commit here), 0 rows select fewer classes than dev (0 classes lost), 26 rows select moreandnamed by the symbol rule alone: 0 rows name fewer classes than dev (0 classes); dev tokens this branch does not take: [].named by the symbol rule alone: 10 rows name fewer classes than dev (66 classes). That matches the review's count. At map level that head lost nothing either (each of those heads was FULL, or its classes were selected for another reason), so the named-set line is the one that proves the fix.CiSelectLocalClassesGuardTests,CiSelectMapSelectGuardTests,CiSelectReplayGuardTests,CrossAppGuardCiGateTests,DarlingReadsTraitGuardTests,TestMapShadowGuardTests,WatermarkPolicyTests:Total: 108, Errors: 0, Failed: 0McpReadCommandTimeoutTests,GuardStageCensusTests,GuardStageWorkflowTests,FleetIdentifierScrubTests,CommandDeadlineScannerAdoptionTests:Total: 75, Errors: 0, Failed: 0.SlowClassCensusTestsalone:Total: 7, Errors: 0, Failed: 0.System.Security.Cryptography.Pkcs9.0.18 via PerformanceMonitor.PlanAnalysis. This PR does not cause them.local-filter --base origin/dev --map <that map>on this branch:CI's map answer runs 1839 classes, over 300then743 classes, over 300: below are the first 300, most specific first. That prints 8darling:filter lines of up to 40 classes each.Round 1 leftovers
--mapis given and awwwroot/jsfile changed (I7). Without a map they are still missed.SECTION: Changed
ENTRY: - CI selects every test class that names a symbol a change defines or edits, repo-walking tests run in the guard set, and a local mode prints the filter CI will use ([#PR]) - The changed-symbol rule now bounds a token by a share of the test files (1%, at least 25) and reads store migration names, the failure replay reports how many classes the rule adds per run, 47 test classes that walk the repository tree carry Stage=Guard so the guard job and a lane's local set run them, and
ci-select.py local-filterprints the per-suite class filter (40 classes a line, at most 300 classes) and the guard filter for a branch.