Repository navigation
fix(engine): reuse the first class-data fetch for the first test of a class row - #7195
Conversation
… class row The test builder fetches each class data row once before it evaluates the method data sources, then fetched it again for every test. With SharedType.None the first fetch created an extra instance per class row that no test held, so the object tracker never disposed it. The first test built from a class row now takes the objects of that first fetch instead of calling the factory again, and a row skipped as incompatible with a generic method hands its class objects to the next test. Both the batch and streaming build paths do this, so source-generated and reflection modes behave the same. Fixes #7157
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9eb88bb6c
ℹ️ 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".
[Medium impact] The PR appears safe to merge; the previous initialization bug is fixed, with no new blocking issues found. SummaryThe PR reuses the first class-data fetch where possible and tracks temporary-instance fixtures without passing them to runnable tests.
|
…nce that uses it An instance method data source needs a temporary instance of the test class. The builder creates it from the class row's first fetch and disposes it once the method rows are read. The first test took those same objects, so a class that disposed its injected fixture closed the first test's fixture before the test ran. When a temporary instance is created, it now owns the first fetch, and every test fetches its own row, as before #7157. Adds a regression test in which the test class disposes its fixture and reads it through an InstanceMethodDataSource.
An instance method data source needs a temporary instance of the test class, built from the class row's first fetch. Since the previous commit no test takes that fetch, so its objects were disposed only if the test class disposed its own constructor arguments. The fetch is now lent to the temporary instance. The next test built from the class row, whether it passes, fails or is skipped, tracks those objects without receiving them, so the object tracker disposes them when that test completes. TestDetails.FailedDataRowObjects is renamed to UnreceivedDataRowObjects, since it now also holds these objects. Adds a regression test with a test class that does not dispose its fixture.
…y instance used Objects a test tracks without receiving them (UnreceivedDataRowObjects) were added to TrackedObjects, and execution initializes every tracked object. A fixture lent to the temporary instance for an instance method data source was therefore initialized after that instance had disposed it. Discovery now records the objects reachable only from unreceived rows, and execution skips them while disposal still counts them.
|
Review dispositions for this round:
|
|
Review of #7195 I reviewed the diff statically. I did not build the PR or run its tests. The fix targets the right cause. The first class-row fetch is now held in UnclaimedClassRow, and the first test built from the row takes it instead of calling the factory again. That removes the untracked extra instance. The batch and streaming paths use the same holder, so source-generated and reflection modes behave the same. The new Issue7157 repro covers both modes. I checked that LendToTemporaryInstance runs once per class row. Calling it twice would drop the lent objects, but it doesn't happen in either path. The DisposalOnlyTrackedObjects skip in ObjectLifecycleService also stops execution from initializing objects the temporary instance may already have disposed. That is the main risk, and it is handled. Suggestions (non-blocking):
Overall this is a solid, well-documented fix. |
) * fix(engine): use UnreceivedDataRowObjects in skipped-test tracking #7195 renamed TestDetails.FailedDataRowObjects to UnreceivedDataRowObjects, and #7191 merged afterwards with a use of the old name, so main no longer compiles (CS1061 in TestArgumentRegistrationService). The CloudShop Example Tests workflow fails at its Build step on every main push since then. * ci: give the macOS AOT test-app publish a 60-minute module timeout Since #7131 the publish builds into its own artifacts tree and overlaps the test modules. On the 3-core macOS runner it now takes 24-27 minutes when it passes, and the default 30-minute ModularPipelines timeout killed it on six .NET runs on 2026-10-20, failing those PRs' macOS jobs.
Summary
Before: for each class data row,
TestBuildercalled the class data factory once by itself, before it evaluated the method data sources, and then once more for every test. WithSharedType.None, the first call created an extra instance per class row. No test held that instance, so it was never tracked or disposed. In the issue's repro, two tests created four instances and only the two the tests used were disposed.After: each class row creates one instance per test. Each test holds its instance, and the instance is disposed with the test. The extra instance's constructor and
IAsyncDiscoveryInitializerno longer run.Cause
The objects from the first fetch were used only to evaluate the method data sources (
classInstanceArgumentsand the instance for instance-data sources). After that, every test fetched the row again. Only the instances from those later fetches reachedTestData.ClassData, so only they were tracked. The streaming path (BuildTestsStreamingAsync/BuildSingleTestAsync) had the same pattern.Fix
UnclaimedClassRowholder keeps the objects of the first fetch. The first test built from the row takes them instead of calling the factory again. Later tests fetch as before, so eachSharedType.Nonetest still gets its own instance.SkipIfEmptytest of a method data source already holdsclassData, so building it clears the holder.This builds on #7150 (failed rows). Disposal for skipped tests and rows is tracked separately in #7156 and is not changed here.
Known remaining edge case: if no test is built from a class row (all method data sources yield nothing and none uses
SkipIfEmpty), the first fetch still has no owner, as before. It cannot be disposed safely at build time, because a shared or keyed instance may still be used by tests that are not registered yet.Tests
tests/TUnit.TestProject/Bugs/_7157: twoSharedType.Noneclasses, one with two tests and one with two[Arguments]rows run twice with[Repeat(1)]. An observer checks that the number of instances created equals the number of tests and that all of them are disposed. Before the fix it failed withExpected to be 2 but found 4in both source-gen and--reflectionmodes. After the fix it passes in both modes.tests/TUnit.Engine.Tests/Issue7157Testswraps that run.Issue7123Testsstill passes.TUnit.Engine.Tests(net10.0): 468 passed, 264 skipped (AOT/OS-gated), 1 failed. The failure isFSharp.Test, from a local SDK issue: with the 11 RC SDK,TUnit.Assertions.FSharpbinds FSharp.Core 11 while the test project ships 10.1. It is unrelated to this change, and I filed it as TUnit.Assertions.FSharp picks up the building SDK's FSharp.Core, so F# tests fail to load FSharp.Core 11 on an 11 RC SDK #7193.TUnit.UnitTests(net10.0): 608 passed, 4 skipped.Fixes #7157
Review follow-ups (73d135d)
InstanceMethodDataSourceDisposingFixtureTests.Issue7157Testsnow expects 9 tests.Eventssubscribed during a row fetch never reach the test. This meansDependencyInjectionDataSourceAttributescopes are never disposed. It also happens onmain, so it is filed separately as DependencyInjectionDataSourceAttribute scopes are never disposed: builder-context events never reach the test #7205.Summary by CodeRabbit
Bug Fixes
Tests