fix: dispose shared fixtures when only a subset of consuming tests runs#6156
Conversation
Shared-object reference counts were incremented at build time for every built test, but only decremented when a test executed. Any test that was built but filtered out post-build (an [Explicit] sibling, a single [Arguments] case selected by an IDE uid filter, or all tests during a discovery-only request) left the count above zero, so PerTestSession / PerClass / PerAssembly / Keyed fixtures never had DisposeAsync called. - Move argument registration (shared-object creation + ref counting) from TestBuilder.BuildTestAsync to the post-filter TestFilterService.RegisterTest, so counts only include tests that will execute - Skip argument registration entirely for discovery-only requests; discovery no longer constructs fixtures as a side effect - Register dynamic tests (AddDynamicTest / CreateTestVariant) explicitly, since they bypass the discovery pipeline registration - Sweep-dispose any still-tracked objects and reset TestDataContainer at run-session end, so a later run request in the same process (IDE server mode) gets fresh fixtures instead of disposed ones Fixes #6151
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 1 minor |
🟢 Metrics 4 complexity
Metric Results Complexity 4
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This is a well-motivated fix with a clear root cause analysis. The architecture change — moving argument registration from build-time to post-filter — is correct, and the TDD approach (red-first test) gives good confidence in the regression coverage. A few observations and concerns follow.
Double-disposal risk for static properties
The most significant concern is in TestSessionCoordinator.ExecuteTests. The session-end sweep calls DisposeAndClearStaticTrackingAsync, which iterates s_trackedObjects and disposes everything still tracked. Static-property objects are added to s_trackedObjects via StaticPropertyHandler.TrackStaticProperties (called from TestScheduler.ScheduleAndExecuteAsync), and they are explicitly disposed by StaticPropertyHandler.DisposeStaticPropertiesAsync later in that same ScheduleAndExecuteAsync call — but DisposeStaticPropertiesAsync disposes directly via _disposer.DisposeAsync, bypassing UntrackObject (i.e. it does not decrement the counter or remove the entry from s_trackedObjects).
That means static-property objects will still be present in s_trackedObjects when the sweep runs in the finally block of ExecuteTests, and DisposeAndClearStaticTrackingAsync will call Disposer.DisposeAsync on them a second time. Whether this causes harm depends entirely on whether user-provided disposable objects are idempotent — which the framework cannot assume.
If the ClearStaticTracking path existed previously and was never called, one might argue this was always a latent issue, but the PR is what introduces the first caller, so it owns the risk. The fix should either:
- Have
DisposeStaticPropertiesAsyncgo throughUntrackObjectinstead of calling_disposer.DisposeAsyncdirectly, so the counter reaches zero and the entry is removed before the sweep runs, or - Skip objects that are already zero-ref-counted (the counter exists but is at 0) in
DisposeAndClearStaticTrackingAsync.
DisposeAndClearStaticTrackingAsync is an instance method on a class with only static state
ObjectTracker.DisposeAndClearStaticTrackingAsync operates entirely on s_trackedObjects and s_asyncCallbackErrors, both of which are static. The method is non-static only because it captures the disposer constructor parameter. This is an existing asymmetry (ClearStaticTracking is already static), but the new method deepens the confusion: callers that hold an ObjectTracker instance see a method that mutates all instances' shared state. A brief comment on why this is intentionally an instance method (or making disposer static if that's feasible) would prevent future confusion.
TestContext.Current = null removal leaves a gap
In the original BuildTestAsync, after calling RegisterTestArgumentsAsync, TestContext.Current was cleared to null. That reset was inside the finally block so it ran regardless of success or failure. The PR removes RegisterTestArgumentsAsync from BuildTestAsync but also removes that TestContext.Current = null reset entirely — there is no longer a finally clause at all. If anything sets TestContext.Current during BuildTestAsync (e.g. the DiscoveredTest assignment at line 76 in the pre-PR code), the context could leak into the next build iteration. Worth verifying that TestContext.Current is reliably reset at the right point on the new code path.
DiscoverTestsFullyStreamingAsync does not call RegisterTestsAsync
TestDiscoveryService.DiscoverTestsFullyStreamingAsync (used by the streaming discovery path) calls InvokePostResolutionEventsInParallelAsync and then yields tests to the caller, but never calls _testFilterService.RegisterTestsAsync(...). DiscoverTests (the non-streaming path) does call it. If DiscoverTestsFullyStreamingAsync is used in any execution scenario (currently it appears to only be used for IDE streaming discovery, isForExecution: false), those tests would not get their argument registration. The PR comment in DiscoverTests line 106 notes isForExecution drives the guard, so this is probably fine today, but it's worth a defensive assertion or comment to make the invariant explicit.
SessionSharedFixture static counters are not reset between runs in the same process
Bug6151FilteredDisposalTests uses SessionSharedFixture.CreatedCount and DisposedCount as static int fields. TestDataContainer.Reset() clears the instance caches so fresh objects are created, but CreatedCount/DisposedCount are user-defined statics that are not reset. If the engine test runs against a long-lived process that has already executed these tests in a previous session, the marker file would contain cumulative counts rather than per-run counts, and the assertion "Created=1;Disposed=1" would fail. This is a testing-infrastructure concern rather than a production bug, but the test's reliability in IDE server mode depends on it.
Minor: RunOptions.EnvironmentVariables mutated via WithEnvironmentVariable
RunOptions is declared as a record with Dictionary<string, string?> EnvironmentVariables { get; init; } = []. WithEnvironmentVariable mutates EnvironmentVariables[key] = value on the existing dictionary rather than returning a new RunOptions with a new dictionary. For a record, this is inconsistent with the With-pattern convention (non-destructive mutation). It works for the current use, but callers who share a RunOptions instance would see side effects. AdditionalArguments has the same pattern in the existing WithArgument — so this is consistent with the existing style — just worth noting if the records are ever used in a more functional style.
What's done well
- The root-cause analysis in the PR description is precise and complete.
DisposeAndClearStaticTrackingAsyncusesTryRemoveto guard against races with lateUntrackObjectcalls — correct.- The
isForExecution: falseskip of argument registration inRegisterTestis the right place and correctly avoids leaking fixture construction into discovery-only requests. - Dynamic-test and test-variant paths now go through
RegisterTestsAsync, closing the registration gap those paths previously had. - The
[After(TestSession)]hook approach for the regression test is clean — inert by default, opt-in via env var. - Public API snapshots are updated for the new
Clear()members.
…arStaticTrackingAsync
Follow-up Review (updated after second commit)The second commit ( Status of issues raised in the previous reviewThe substantive concerns from the earlier review remain open. Summarising for visibility: [Open] Double-disposal of static-property objects [Open] [Open, lower priority] [Non-blocking] The core fix (moving registration to post-filter) is correct and the test coverage is good. The double-disposal concern is the only item I'd want addressed before merging. |
…double-dispose Review feedback on #6156: DisposeStaticPropertiesAsync disposed directly via Disposer, leaving the +1 tracking entry from TrackStaticProperties in s_trackedObjects — the new session-end sweep would then dispose the same object a second time. Untracking instead decrements to zero, disposes once, and removes the entry. Also: reset Bug6151 fixture static counters after writing the marker (per-run counts in long-lived server processes), document why DisposeAndClearStaticTrackingAsync is an instance method, and add a warning comment on the unregistered DiscoverTestsFullyStreamingAsync path.
|
Addressed the review in d4d6cdf: Double-disposal of static properties — confirmed real, fixed. Instance method on static state — documented: the method needs the injected
Static counters not reset between runs — fixed: the marker hook now resets
|
There was a problem hiding this comment.
Final Review — All Outstanding Issues Addressed
All concerns from the two prior review rounds have been resolved in d4d6cdf9b. The PR is ready to merge.
Confirmed addressed
Double-disposal of static-property objects (was the blocking concern)
DisposeStaticPropertiesAsync now calls ObjectTracker.UntrackObjectAsync instead of _disposer.DisposeAsync directly. The new public UntrackObjectAsync method decrements the ref count, disposes at zero, and removes the entry from s_trackedObjects in one operation — exactly the right fix. The session-end sweep will no longer see any entry to dispose a second time. The removal of Disposer from StaticPropertyHandler's constructor is a clean follow-through.
TestContext.Current clearing
Confirmed: only RegisterTestArgumentsAsync set TestContext.Current in the build path, and that call is now removed from BuildTestAsync. No context can leak. The explanation in the PR comment is correct.
Static counters reset between runs
The [After(TestSession)] marker hook now resets CreatedCount/DisposedCount via Interlocked.Exchange after writing the file, so a long-lived server-mode process reports per-session counts correctly.
Streaming discovery path
Defensive comment added to DiscoverTestsFullyStreamingAsync warning that any future execution use must register tests with isForExecution: true first. Good enough given the method has zero callers today.
isForExecution boolean flag
Left as-is and consistent with existing style. Acceptable.
Architecture recap
The fundamental design is sound: registration (shared-object creation + ref counting) moved from build-time to post-filter, with dynamic tests and variants wired into the same post-filter registration path via TestRegistry. The session-end sweep in TestSessionCoordinator handles cancellation/miscount stragglers, and TestDataContainer.Reset() ensures a fresh start for subsequent runs in the same process. The layering is correct and the comments throughout explain the invariants clearly.
Approve.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.48.6 to 1.51.0. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.51.0 <!-- Release notes generated using configuration in .github/release.yml at v1.51.0 --> ## What's Changed ### Other Changes * fix(mocks): WasCalled/WasNeverCalled assertions via [GenerateAssertion] for all ICallVerification types by @thomhurst in thomhurst/TUnit#6176 * fix: create fresh non-shared instances per CombinedDataSources combination by @thomhurst in thomhurst/TUnit#6179 * fix: assign TestDetails before TestContext is published to ClassHookContext.Tests by @thomhurst in thomhurst/TUnit#6182 * fix: resolve inherited instance data source members for MethodDataSource by @thomhurst in thomhurst/TUnit#6178 * feat(mocks): per-element matchers for params array parameters by @thomhurst in thomhurst/TUnit#6181 * fix: invoke inner Func for TestDataRow<Func<T>> data sources (#6161) by @thomhurst in thomhurst/TUnit#6183 ### Dependencies * chore(deps): update _tunitpolyfillversion to 10.8.0 by @thomhurst in thomhurst/TUnit#6167 * chore(deps): update dependency azure.storage.blobs to 12.29.0 by @thomhurst in thomhurst/TUnit#6168 * chore(deps): update aspire by @thomhurst in thomhurst/TUnit#6165 * chore(deps): update dependency cliwrap to 3.10.2 by @thomhurst in thomhurst/TUnit#6166 * chore(deps): update dependency streamjsonrpc to 2.25.25 by @thomhurst in thomhurst/TUnit#6170 * chore(deps): update dependency polyfill to 10.8.0 by @thomhurst in thomhurst/TUnit#6169 * chore(deps): update tunit to 1.5* by @thomhurst in thomhurst/TUnit#6171 * chore(deps): update _tunitpolyfillversion to 10.8.1 by @thomhurst in thomhurst/TUnit#6174 * chore(deps): update dependency polyfill to 10.8.1 by @thomhurst in thomhurst/TUnit#6175 **Full Changelog**: thomhurst/TUnit@v1.50.0...v1.51.0 ## 1.50.0 <!-- Release notes generated using configuration in .github/release.yml at v1.50.0 --> ## What's Changed ### Other Changes * fix(analyzers): decouple code fixers from Rules to prevent MissingFieldException in VS by @thomhurst in thomhurst/TUnit#6158 * Fix mock wrappers for indexers and generic methods by @thomhurst in thomhurst/TUnit#6163 * Add global mock default mode by @thomhurst in thomhurst/TUnit#6164 **Full Changelog**: thomhurst/TUnit@v1.49.0...v1.50.0 ## 1.49.0 <!-- Release notes generated using configuration in .github/release.yml at v1.49.0 --> ## What's Changed ### Other Changes * docs: benchmark page descriptions + promote Benchmarks in sidebar by @thomhurst in thomhurst/TUnit#6143 * feat(mocks): discriminate generic-method mocks by type argument by @thomhurst in thomhurst/TUnit#6153 * fix(source-gen): jagged array data fails to compile (#6150) by @thomhurst in thomhurst/TUnit#6152 * fix: dispose shared fixtures when only a subset of consuming tests runs by @thomhurst in thomhurst/TUnit#6156 ### Dependencies * chore(deps): update tunit to 1.48.6 by @thomhurst in thomhurst/TUnit#6142 * chore(deps): update react to ^19.2.7 by @thomhurst in thomhurst/TUnit#6144 * chore(deps): update aspire to 13.4.0 by @thomhurst in thomhurst/TUnit#6145 * chore(deps): update dependency nunit.analyzers to 4.14.0 by @thomhurst in thomhurst/TUnit#6146 * chore(deps): update dependency polyfill to 10.7.2 by @thomhurst in thomhurst/TUnit#6148 * chore(deps): update dependency polyfill to 10.7.2 by @thomhurst in thomhurst/TUnit#6149 * chore(deps): update dependency dompurify to v3.4.8 by @thomhurst in thomhurst/TUnit#6155 **Full Changelog**: thomhurst/TUnit@v1.48.6...v1.49.0 Commits viewable in [compare view](thomhurst/TUnit@v1.48.6...v1.51.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.45.0 to 1.51.0. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.51.0 <!-- Release notes generated using configuration in .github/release.yml at v1.51.0 --> ## What's Changed ### Other Changes * fix(mocks): WasCalled/WasNeverCalled assertions via [GenerateAssertion] for all ICallVerification types by @thomhurst in thomhurst/TUnit#6176 * fix: create fresh non-shared instances per CombinedDataSources combination by @thomhurst in thomhurst/TUnit#6179 * fix: assign TestDetails before TestContext is published to ClassHookContext.Tests by @thomhurst in thomhurst/TUnit#6182 * fix: resolve inherited instance data source members for MethodDataSource by @thomhurst in thomhurst/TUnit#6178 * feat(mocks): per-element matchers for params array parameters by @thomhurst in thomhurst/TUnit#6181 * fix: invoke inner Func for TestDataRow<Func<T>> data sources (#6161) by @thomhurst in thomhurst/TUnit#6183 ### Dependencies * chore(deps): update _tunitpolyfillversion to 10.8.0 by @thomhurst in thomhurst/TUnit#6167 * chore(deps): update dependency azure.storage.blobs to 12.29.0 by @thomhurst in thomhurst/TUnit#6168 * chore(deps): update aspire by @thomhurst in thomhurst/TUnit#6165 * chore(deps): update dependency cliwrap to 3.10.2 by @thomhurst in thomhurst/TUnit#6166 * chore(deps): update dependency streamjsonrpc to 2.25.25 by @thomhurst in thomhurst/TUnit#6170 * chore(deps): update dependency polyfill to 10.8.0 by @thomhurst in thomhurst/TUnit#6169 * chore(deps): update tunit to 1.5* by @thomhurst in thomhurst/TUnit#6171 * chore(deps): update _tunitpolyfillversion to 10.8.1 by @thomhurst in thomhurst/TUnit#6174 * chore(deps): update dependency polyfill to 10.8.1 by @thomhurst in thomhurst/TUnit#6175 **Full Changelog**: thomhurst/TUnit@v1.50.0...v1.51.0 ## 1.50.0 <!-- Release notes generated using configuration in .github/release.yml at v1.50.0 --> ## What's Changed ### Other Changes * fix(analyzers): decouple code fixers from Rules to prevent MissingFieldException in VS by @thomhurst in thomhurst/TUnit#6158 * Fix mock wrappers for indexers and generic methods by @thomhurst in thomhurst/TUnit#6163 * Add global mock default mode by @thomhurst in thomhurst/TUnit#6164 **Full Changelog**: thomhurst/TUnit@v1.49.0...v1.50.0 ## 1.49.0 <!-- Release notes generated using configuration in .github/release.yml at v1.49.0 --> ## What's Changed ### Other Changes * docs: benchmark page descriptions + promote Benchmarks in sidebar by @thomhurst in thomhurst/TUnit#6143 * feat(mocks): discriminate generic-method mocks by type argument by @thomhurst in thomhurst/TUnit#6153 * fix(source-gen): jagged array data fails to compile (#6150) by @thomhurst in thomhurst/TUnit#6152 * fix: dispose shared fixtures when only a subset of consuming tests runs by @thomhurst in thomhurst/TUnit#6156 ### Dependencies * chore(deps): update tunit to 1.48.6 by @thomhurst in thomhurst/TUnit#6142 * chore(deps): update react to ^19.2.7 by @thomhurst in thomhurst/TUnit#6144 * chore(deps): update aspire to 13.4.0 by @thomhurst in thomhurst/TUnit#6145 * chore(deps): update dependency nunit.analyzers to 4.14.0 by @thomhurst in thomhurst/TUnit#6146 * chore(deps): update dependency polyfill to 10.7.2 by @thomhurst in thomhurst/TUnit#6148 * chore(deps): update dependency polyfill to 10.7.2 by @thomhurst in thomhurst/TUnit#6149 * chore(deps): update dependency dompurify to v3.4.8 by @thomhurst in thomhurst/TUnit#6155 **Full Changelog**: thomhurst/TUnit@v1.48.6...v1.49.0 ## 1.48.6 <!-- Release notes generated using configuration in .github/release.yml at v1.48.6 --> ## What's Changed ### Other Changes * fix(sourcegen): fully-qualify Linq calls in params array binding (#6140) by @thomhurst in thomhurst/TUnit#6141 ### Dependencies * chore(deps): update tunit to 1.48.0 by @thomhurst in thomhurst/TUnit#6135 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6137 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6138 * chore(deps): update verify to 31.19.0 by @thomhurst in thomhurst/TUnit#6139 **Full Changelog**: thomhurst/TUnit@v1.48.0...v1.48.6 ## 1.48.0 <!-- Release notes generated using configuration in .github/release.yml at v1.48.0 --> ## What's Changed ### Other Changes * feat(html-report): baked-in C# syntax highlighting on Source tab by @slang25 in thomhurst/TUnit#6132 * feat(analyzers): suppress VSTHRD200 on test and hook methods by @thomhurst in thomhurst/TUnit#6123 * fix(source-gen): correct source location for cross-project inherited tests by @slang25 in thomhurst/TUnit#6133 * feat(assertions): add WasCalled to tunit mocks assertions by @robertcoltheart in thomhurst/TUnit#6126 * feat(arguments): bind array values to a single array test parameter by @thomhurst in thomhurst/TUnit#6122 * fix: populate retry/flaky attempt history in HTML report (#6119) by @thomhurst in thomhurst/TUnit#6124 ### Dependencies * chore(deps): update tunit to 1.47.0 by @thomhurst in thomhurst/TUnit#6115 * chore(deps): update dependency microsoft.visualstudio.threading.analyzers to 17.14.15 by @thomhurst in thomhurst/TUnit#6134 **Full Changelog**: thomhurst/TUnit@v1.47.0...v1.48.0 ## 1.47.0 <!-- Release notes generated using configuration in .github/release.yml at v1.47.0 --> ## What's Changed ### Other Changes * perf(engine): hoist GetParameters and dict-dedup AfterTestDiscovery hooks by @thomhurst in thomhurst/TUnit#6062 * perf(engine): hoist GetParameters and drop LINQ in reflection discovery by @thomhurst in thomhurst/TUnit#6063 * perf(engine): cache treenode filter path on TestMetadata by @thomhurst in thomhurst/TUnit#6064 * perf: use is T pattern in ReflectionExtensions.HasAttribute fallback (#6060) by @thomhurst in thomhurst/TUnit#6066 * perf: replace OrderBy().ToArray() with Array.Sort in ConstraintKeyScheduler by @thomhurst in thomhurst/TUnit#6067 * perf: pool HashSet in WaitingTestIndex.GetCandidatesForReleasedKeys by @thomhurst in thomhurst/TUnit#6069 * perf: collapse OfType chains in JUnitXmlWriter (#6052) by @thomhurst in thomhurst/TUnit#6070 * perf(engine): avoid closure allocation in AfterHookPairTracker.GetOrCreateAfterAssemblyTask (#6041) by @thomhurst in thomhurst/TUnit#6071 * perf: avoid closure allocation in BeforeHookTaskCache.GetOrCreateBeforeAssemblyTask (#6040) by @thomhurst in thomhurst/TUnit#6073 * perf: use TryAdd in TestDependencyResolver dependency dedupe by @thomhurst in thomhurst/TUnit#6068 * perf: replace LINQ Any with foreach in TestGenericTypeResolver (#6044) by @thomhurst in thomhurst/TUnit#6072 * perf: avoid Cast<object>().FirstOrDefault() iterator alloc in CastHelper (#6029) by @thomhurst in thomhurst/TUnit#6074 * perf(engine): avoid string round-trip when building nested type names (#6049) by @thomhurst in thomhurst/TUnit#6075 * perf(engine): replace Select+ToArray with manual Type[] build (#6043) by @thomhurst in thomhurst/TUnit#6076 * perf(core): replace OfType().FirstOrDefault()/.Any() with foreach in ClassConstructorHelper by @thomhurst in thomhurst/TUnit#6078 * perf(engine): avoid FirstOrDefault iterator alloc in TestGenericTypeResolver by @thomhurst in thomhurst/TUnit#6079 * perf(engine): use SearchValues<char> for reporter filename sanitization by @thomhurst in thomhurst/TUnit#6090 * perf: dedupe TestDataFormatter.FormatArguments with pooled StringBuilder by @thomhurst in thomhurst/TUnit#6088 * perf(engine): use MemoryExtensions.Split for path parsing in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6085 * perf(engine): use CollectionsMarshal.GetValueRefOrAddDefault for dictionary index builds by @thomhurst in thomhurst/TUnit#6086 * perf(engine): replace LINQ Where closure with inline filter in MetadataDependencyExpander BFS by @thomhurst in thomhurst/TUnit#6084 * perf(engine): pool StringBuilder in DisplayNameBuilder.FormatArguments by @thomhurst in thomhurst/TUnit#6082 * Preserve specialized chaining after null assertions by @thomhurst in thomhurst/TUnit#6008 * perf: use EnumerateLines for line splitting in HtmlReportGenerator by @thomhurst in thomhurst/TUnit#6089 * perf: collapse Replace chain in TestNameFormatter.BuildTestId by @thomhurst in thomhurst/TUnit#6083 * perf: use OrdinalIgnoreCase Contains in HtmlReportGenerator span mapping by @thomhurst in thomhurst/TUnit#6093 * perf(assertions): avoid eager interpolated-string alloc in assertion source ctors by @thomhurst in thomhurst/TUnit#6091 * perf: optimize TestNameFormatter argument and bool formatting by @thomhurst in thomhurst/TUnit#6095 * perf: use FrozenSet/FrozenDictionary for read-only static lookups by @thomhurst in thomhurst/TUnit#6099 * perf: avoid GetCustomAttributes() + LINQ chain for per-property attribute scans by @thomhurst in thomhurst/TUnit#6098 * perf(engine): replace magic-string RequiredAttribute match with type check in ConstructorHelper by @thomhurst in thomhurst/TUnit#6087 * perf(core): replace Select+Func factory chain in DataSourceHelpers by @thomhurst in thomhurst/TUnit#6081 * perf: replace LINQ dependency extraction with manual loop by @thomhurst in thomhurst/TUnit#6096 * perf(core): avoid string[] alloc in ArgumentFormatter.FormatArguments by @thomhurst in thomhurst/TUnit#6080 * perf: use [GeneratedRegex] in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6094 * perf: dedupe GetSimpleTypeName into shared TypeNameFormatter by @thomhurst in thomhurst/TUnit#6097 * fix: remove GitVersion MSBuild task, pin local builds to 99.99.99 (#6077) by @thomhurst in thomhurst/TUnit#6101 * HTML Report: source link + code snippet on Source tab (#5993) by @thomhurst in thomhurst/TUnit#6100 * perf(sourcegen): Single-pass attribute classification by @thomhurst in thomhurst/TUnit#6111 * perf(core): eliminate per-test allocations in TestDetails/HookMethod by @thomhurst in thomhurst/TUnit#6109 * perf: hoist char[] alloc in FsCheckPropertyTestExecutor to static SearchValues by @thomhurst in thomhurst/TUnit#6108 * perf(core): de-LINQ data-source expansion by @thomhurst in thomhurst/TUnit#6110 * perf: avoid LINQ chains in TestDependency equality and MethodDataSourceAttribute method matching by @thomhurst in thomhurst/TUnit#6092 * perf(engine): reduce allocations in reflection-mode discovery/execution by @thomhurst in thomhurst/TUnit#6113 * perf(assertions): allocation-free passing path (TUnit.Assertions) by @thomhurst in thomhurst/TUnit#6112 ### Dependencies ... (truncated) ## 1.46.0 <!-- Release notes generated using configuration in .github/release.yml at v1.46.0 --> ## What's Changed ### Other Changes * docs: add Rider VSTest conflict troubleshooting by @smolchanovsky in thomhurst/TUnit#5989 * Populate generated test metadata with full source spans by @Copilot in thomhurst/TUnit#5991 * Add devcontainer configuration by @Copilot in thomhurst/TUnit#5995 * fix: treenode filter pre-filter rejects parenthesised segments (#6026) by @thomhurst in thomhurst/TUnit#6027 * fix(engine): isolate per-session state under MTP server-mode concurrency (#6001) by @thomhurst in thomhurst/TUnit#6025 ### Dependencies * chore(deps): update dependency stackexchange.redis to 2.13.10 by @thomhurst in thomhurst/TUnit#5985 * chore(deps): update tunit to 1.45.29 by @thomhurst in thomhurst/TUnit#5986 * chore(deps): update dependency mockolate to 3.2.1 by @thomhurst in thomhurst/TUnit#5987 * chore(deps): update dependency microsoft.playwright to 1.60.0 by @thomhurst in thomhurst/TUnit#5988 * chore(deps): update dependency messagepack to 3.1.6 by @thomhurst in thomhurst/TUnit#5992 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5998 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5997 * chore(deps): update verify to 31.17.0 by @thomhurst in thomhurst/TUnit#6000 * chore(deps): update verify to 31.18.0 by @thomhurst in thomhurst/TUnit#6013 * chore(deps): update dependency microsoft.net.test.sdk to 18.6.0 by @thomhurst in thomhurst/TUnit#6016 * chore(deps): update dependency dompurify to v3.4.6 by @thomhurst in thomhurst/TUnit#6015 * chore(deps): update dependency dompurify to v3.4.7 by @thomhurst in thomhurst/TUnit#6019 * chore(deps): update dependency npgsql to 10.0.3 by @thomhurst in thomhurst/TUnit#6020 * chore(deps): update dependency stackexchange.redis to 2.13.17 by @thomhurst in thomhurst/TUnit#6021 * chore(deps): update dependency npgsql.entityframeworkcore.postgresql to 10.0.2 by @thomhurst in thomhurst/TUnit#6022 ## New Contributors * @smolchanovsky made their first contribution in thomhurst/TUnit#5989 **Full Changelog**: thomhurst/TUnit@v1.45.29...v1.46.0 ## 1.45.29 <!-- Release notes generated using configuration in .github/release.yml at v1.45.29 --> ## What's Changed ### Other Changes * Fix shared fixture lifetime for reused discovery instances by @thomhurst in thomhurst/TUnit#5983 * Preserve override accessibility in generated mocks by @thomhurst in thomhurst/TUnit#5984 ### Dependencies * chore(deps): update tunit to 1.45.22 by @thomhurst in thomhurst/TUnit#5974 * chore(deps): update dependency messagepack to 3.1.5 by @thomhurst in thomhurst/TUnit#5978 * chore(deps): update aspire to 13.3.5 by @thomhurst in thomhurst/TUnit#5980 **Full Changelog**: thomhurst/TUnit@v1.45.22...v1.45.29 ## 1.45.22 <!-- Release notes generated using configuration in .github/release.yml at v1.45.22 --> ## What's Changed ### Other Changes * Remove ".NET" from Aspire references by @antmdvs in thomhurst/TUnit#5968 * Fix chained mock setup behavior by @thomhurst in thomhurst/TUnit#5973 ### Dependencies * chore(deps): update tunit to 1.45.8 by @thomhurst in thomhurst/TUnit#5958 * chore(deps): update dependency nunit to 4.6.1 by @thomhurst in thomhurst/TUnit#5961 * chore(deps): update dependency testcontainers.postgresql to 4.12.0 by @thomhurst in thomhurst/TUnit#5963 * chore(deps): update dependency testcontainers.redis to 4.12.0 by @thomhurst in thomhurst/TUnit#5965 * chore(deps): update dependency testcontainers.kafka to 4.12.0 by @thomhurst in thomhurst/TUnit#5962 * chore(deps): update aspire to 13.3.4 by @thomhurst in thomhurst/TUnit#5966 * chore(deps): bump webpack-dev-server from 5.2.2 to 5.2.4 in /docs by @dependabot[bot] in thomhurst/TUnit#5964 ## New Contributors * @antmdvs made their first contribution in thomhurst/TUnit#5968 **Full Changelog**: thomhurst/TUnit@v1.45.8...v1.45.22 ## 1.45.8 <!-- Release notes generated using configuration in .github/release.yml at v1.45.8 --> ## What's Changed ### Other Changes * fix(aspire): route CreateHttpClient through IHttpClientFactory by @thomhurst in thomhurst/TUnit#5957 ### Dependencies * chore(deps): update tunit to 1.45.0 by @thomhurst in thomhurst/TUnit#5949 * chore(deps): update dependency dompurify to v3.4.5 by @thomhurst in thomhurst/TUnit#5951 * chore(deps): update dependency microsoft.testing.extensions.codecoverage to 18.7.0 by @thomhurst in thomhurst/TUnit#5953 * chore(deps): update dependency coverlet.collector to 10.0.1 by @thomhurst in thomhurst/TUnit#5952 * chore(deps): update dependency polyfill to 10.6.0 by @thomhurst in thomhurst/TUnit#5955 * chore(deps): update dependency polyfill to 10.6.0 by @thomhurst in thomhurst/TUnit#5954 **Full Changelog**: thomhurst/TUnit@v1.45.0...v1.45.8 Commits viewable in [compare view](thomhurst/TUnit@v1.45.0...v1.51.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Problem
Discussion #6151: a
[ClassDataSource<T>(Shared = SharedType.PerTestSession)]fixture implementingIAsyncDisposablenever getsDisposeAsynccalled when running a single test from Rider (one of two[Arguments]cases).Root cause
Shared-object disposal is pure ref-counting (
ObjectTracker): +1 per test that registers the object, -1 when a test finishes, dispose at 0.The increment happened at build time (
TestBuilder.BuildTestAsync->RegisterTestArgumentsAsync), but filtering happens after building, and decrements only happen for executed tests. Any built-but-not-run test permanently inflated the count, so it never reached zero:[Arguments]case selected by an IDE uid filter (both cases share one metadata entry, so both are always built) - the reported repro[Explicit]sibling matched-then-excluded by a filterFix
RegisterTestArgumentsAsynccall;TestFilterService.RegisterTest(which already performed the same call with identical failure semantics) is now the single registration point, running only for tests that will execute (incl. dependency-expanded ones).AddDynamicTest/CreateTestVariantbypass the discovery pipeline, soTestRegistrynow registers built tests before enqueueing them (this also gives themOnTestRegisteredreceivers, which previously never fired for dynamic tests).TestDataContaineris cleared, so a subsequent run request in the same process (IDE server mode) creates fresh fixtures instead of receiving disposed ones. Runs afterAfter(TestSession)hooks and static-property disposal - existing ordering preserved.ObjectTracker.ClearStaticTrackingpreviously had zero callers; the new sweep wires that lifecycle up properly.Tests (TDD - red first, then green)
TUnit.TestProject/Bugs/6151/Bug6151FilteredDisposalTests.cs- PerTestSession fixture with a normal test +[Explicit]sibling (same built-but-not-run shape as the IDE uid-filter case, reproducible via--treenode-filter). A guarded[After(TestSession)]hook writes created/disposed counts to a marker file (opt-in via env var, inert otherwise).TUnit.Engine.Tests/FilteredSharedFixtureDisposalTests.cs- assertsCreated=1;Disposed=1for the filtered-subset run (failed withDisposed=0before the fix) plus a direct-single-test sanity case.InvokableTestBase.RunOptionsgainedWithEnvironmentVariableto support the marker handshake.Verification
Created=1;Disposed=0), green after - both source-gen and reflection modesTUnit.UnitTests: 218/218TUnit.PublicAPIsnapshots updated for the two newClear()members (ScopedDictionary,ThreadSafeDictionary)Note:
ClassDataSourceTupleTests.Test_ClassDataSource_UnwrapsTuplesfails identically with and without this change (verified via stash) - pre-existing, unrelated.Fixes #6151