fix: assign TestDetails before TestContext is published to ClassHookContext.Tests#6182
Conversation
…ontext.Tests ContextProvider.CreateTestContext published the TestContext via classContext.AddTest() while TestDetails was still null, leaving a window where AfterEvery(Class) hooks iterating ClassHookContext.Tests could observe a partially-built context. For dynamic tests registered at runtime into a shared ClassHookContext this raced with sibling test construction, causing a flaky NRE in hooks dereferencing test.Metadata.TestDetails. All call sites already built TestDetails before calling CreateTestContext, so plumb it through as a parameter and assign it before AddTest. Removes the post-hoc assignment at all six engine call sites. Fixes #6180
There was a problem hiding this comment.
Code Review
This is a clean and correct fix for the race condition described in #6180. The root-cause diagnosis is accurate and the approach chosen is the right one.
What the fix gets right
Atomic publication pattern: Moving assignment inside (before ) is the correct architectural choice. The alternative — filtering to hide partially-built contexts — would mask the symptom while leaving the broken invariant in place, and would be harder to reason about across the codebase. Making the context fully-formed before it becomes observable eliminates the entire class of partial-state bugs.
Call-site cleanup is consistent: All six call sites in TestBuilder and TestBuilderPipeline have the post-hoc context.Metadata.TestDetails = testDetails lines removed. The PR diff looks complete — no leftover assignments.
Test pins the invariant, not the timing: The regression test in ContextProviderTests.cs correctly validates the structural guarantee (a context visible in ClassHookContext.Tests always has TestDetails set) rather than trying to reproduce the non-deterministic race. That's the right approach.
Dual-mode coverage: Since ContextProvider lives in TUnit.Core and is shared, the fix applies to both source-gen and reflection paths without needing separate changes to the generated code paths.
One observation worth flagging
This is a breaking change to a public interface. IContextProvider.CreateTestContext is part of the public API surface (as confirmed by the four PublicAPI snapshot updates). Any external code implementing IContextProvider will fail to compile after this change. The PR description is thorough in other respects but doesn't call this out explicitly. If there are known external consumers of this interface, a changelog entry or migration note would be appropriate.
The API snapshots are correctly updated for all four TFMs (net8.0, net9.0, net10.0, net4.7), so the project's own API-gate tests will pass — this is just about downstream consumers.
Minor: parameter placement follows convention
Inserting testDetails before cancellationToken is correct — CancellationToken should always be last by .NET convention. Good.
Verdict
Approve. The fix eliminates the race by construction rather than masking it, the regression test is well-scoped, and the change is consistent across all call sites. The only thing worth a follow-up note (not a blocker) is documenting the breaking API change for downstream implementors of IContextProvider.
There was a problem hiding this comment.
Code Review
This is a clean and correct fix for the race condition described in #6180. The root-cause diagnosis is accurate and the approach chosen is the right one.
What the fix gets right
Atomic publication pattern: Moving TestDetails assignment inside CreateTestContext (before AddTest) is the correct architectural choice. The alternative — filtering Tests to hide partially-built contexts — would mask the symptom while leaving the broken invariant in place, and would be harder to reason about across the codebase. Making the context fully-formed before it becomes observable eliminates the entire class of partial-state bugs.
Call-site cleanup is consistent: All six call sites in TestBuilder and TestBuilderPipeline have the post-hoc context.Metadata.TestDetails = testDetails lines removed. The PR diff looks complete — no leftover assignments.
Test pins the invariant, not the timing: The regression test in ContextProviderTests.cs correctly validates the structural guarantee (a context visible in ClassHookContext.Tests always has TestDetails set) rather than trying to reproduce the non-deterministic race. That is the right approach.
Dual-mode coverage: Since ContextProvider lives in TUnit.Core and is shared, the fix applies to both source-gen and reflection paths without needing separate changes to the generated code paths.
One observation worth flagging
This is a breaking change to a public interface. IContextProvider.CreateTestContext is part of the public API surface (as confirmed by the four PublicAPI snapshot updates). Any external code implementing IContextProvider will fail to compile after this change. The PR description is thorough in other respects but does not call this out explicitly. If there are known external consumers of this interface, a changelog entry or migration note would be appropriate.
The API snapshots are correctly updated for all four TFMs (net8.0, net9.0, net10.0, net4.7), so the project own API-gate tests will pass — this is just about downstream consumers.
Minor: parameter placement follows convention
Inserting testDetails before cancellationToken is correct — CancellationToken should always be last by .NET convention. Good.
Verdict
Approve. The fix eliminates the race by construction rather than masking it, the regression test is well-scoped, and the change is consistent across all call sites. The only thing worth a follow-up (not a blocker) is documenting the breaking API change for downstream implementors of IContextProvider.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 1 medium |
🟢 Metrics 5 complexity
Metric Results Complexity 5
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.
testName was never used by the TestContext ctor and is redundant with testDetails.TestName (now passed before publication). Removed from the interface, implementation, all six call sites, and test stubs.
There was a problem hiding this comment.
Follow-up Review (updated commits ce74860 → 70c23d4)
Picking up from my previous review — two commits landed after it:
4f69cd46 — refactor: drop unused testName parameter
This is a good cleanup. The testName parameter in the original CreateTestContext signature was vestigial once testDetails.TestName was being passed into TestContext from inside the method. Removing the explicit parameter closes the potential for a caller accidentally passing a name that differed from testDetails.TestName, which would have been a subtle bug. The change is mechanically applied at all six call sites and in the test stub in SessionActivityLifecycleTests. All four TFM PublicAPI snapshots are updated correctly.
70c23d4f — chore: remove accidentally committed diff.txt
No code impact; clean housekeeping.
Previous observations resolved
My earlier note about the breaking public API change stands as written — the signature change is intentional and the snapshots are correctly updated. No new concerns arise from the follow-up refactor.
Final verdict
The PR is in good shape. The fix is correct, the follow-up refactor makes the API cleaner and removes a latent inconsistency risk, and the regression test pins the invariant. Approve.
These types are only consumed within TUnit.Engine and test assemblies, all of which already have InternalsVisibleTo access to TUnit.Core. Making them internal removes the context-building API from the public surface, so signature changes like the TestDetails parameter added in this PR are no longer breaking changes for downstream consumers. PublicAPI snapshots updated to drop both types.
There was a problem hiding this comment.
Code Review
This is a correct and well-executed fix. The existing Claude reviews have already covered the main points (atomic publication invariant, complete call-site cleanup, test pinning the structural guarantee rather than the non-deterministic race, dual-mode coverage). This review focuses on what hasn't yet been called out.
Residual testName parameter in TestContext constructor
The TestContext constructor still accepts string testName as its first parameter, but the constructor body does not store it anywhere — it is immediately discarded. Since this PR moved the only meaningful use of that value to testDetails.TestName inside ContextProvider.CreateTestContext, the constructor parameter is now dead weight.
// TestContext constructor accepts testName but never assigns it
public TestContext(string testName, IServiceProvider serviceProvider, ...)
{
_testBuilderContext = testBuilderContext;
CancellationToken = cancellationToken;
// testName is not stored
...
}The one call to that constructor now passes testDetails.TestName just to satisfy the parameter. This suggests two cleaner options:
- Accept
TestDetailsdirectly in the constructor and drop thetestNameparam entirely - Drop
testNamefrom the constructor signature and derive it fromTestDetailsat the point whereDisplayNameis needed
Option 1 would be the stronger fix — it closes the last path by which a TestContext can be constructed without a TestDetails (the TestNodeLocationTests local helper bypasses ContextProvider and must still post-hoc assign context.TestDetails = ...). This would make the invariant compiler-enforced rather than convention-enforced.
TestDetails { get; set; } = null! is still mutable after publication
The property is internal TestDetails TestDetails { get; set; } = null!. The PR correctly ensures it is set before AddTest, but set remains accessible to all internal callers. The TestNodeLocationTests helper demonstrates this — it bypasses ContextProvider and assigns the property post-construction.
This isn't a regression (it existed before), but the PR's stated goal was to make partial-state unobservable by construction. Changing the setter to init (or making the constructor accept TestDetails) would make that guarantee enforced by the type system rather than by code review convention. Worth a follow-up issue.
IContextProvider / ContextProvider visibility change — good
The public → internal change is correct and desirable. These types had no reason to be public surface; making them internal closes off a footgun where external code could supply a custom IContextProvider that violated the publication invariant. The PublicAPI snapshots correctly reflect the removal across all four TFMs.
Test completeness
The regression test in ContextProviderTests.cs correctly covers the single-threaded invariant. The concurrent case is not covered, which the PR description acknowledges is by design since it's non-deterministic — this is the right call.
One small note: the test calls GetOrCreateClassContext after CreateTestContext to fetch the class context. Capturing it before calling CreateTestContext and then asserting the returned context appears in that pre-fetched context's Tests would make the "published before return" ordering more explicit. Not a correctness issue.
Verdict
Approve. The fix correctly closes the race by ensuring TestDetails is assigned before AddTest publishes the context. The items above (testName dead parameter, mutable setter) are pre-existing issues that this PR has made more visible but did not introduce — worth a follow-up rather than blocking this merge.
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>
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.49.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 Commits viewable in [compare view](thomhurst/TUnit@v1.49.0...v1.51.0). </details> Updated [TUnit.AspNetCore](https://github.com/thomhurst/TUnit) from 1.49.0 to 1.51.0. <details> <summary>Release notes</summary> _Sourced from [TUnit.AspNetCore'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 Commits viewable in [compare view](thomhurst/TUnit@v1.49.0...v1.51.0). </details> 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.Core](https://github.com/thomhurst/TUnit) from 1.48.6 to 1.51.0. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core'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>
Fixes #6180
Problem
ContextProvider.CreateTestContextcalledclassContext.AddTest(testContext)whileTestContext.TestDetailswas stillnull!— it was only assigned afterwards by the caller.AfterEvery(Class)hooks iteratingClassHookContext.Testscould therefore observe a partially-built context. For dynamic tests (AddDynamicTest) registered at runtime into aClassHookContextshared byType, this raced with sibling test construction → flaky NRE ontest.Metadata.TestDetails.TestName.Fix
Every call site already constructed the full
TestDetailsbefore callingCreateTestContext, so this just plumbs it through as a parameter and assigns it beforeAddTestpublishes the context. The partially-initialized published state is removed entirely (rather than masked by filteringTests). SharedContextProvider/builder path → covers both source-gen and reflection modes.IContextProvider.CreateTestContext/ContextProvider.CreateTestContext: newTestDetails testDetailsparameter, assigned beforeclassContext.AddTest(...)TestBuilder×2,TestBuilderPipeline×4): passtestDetails, post-hoc assignment removedContextProviderTestspins the invariant: a context visible viaClassHookContext.Testsalways hasTestDetailssetVerification
TUnit.UnitTests(net10.0): 219/219 passed, incl. new regression testTUnit.PublicAPICore snapshots: 4/4 passed after accepting the diffTUnit.TestProject--treenode-filter "/*/*DynamicTests/*/*": 29/29 passed in source-gen and--reflectionmodeTUnit.TestProject--treenode-filter "/*/*/AfterEveryClassTests/*": passedNote: the original race is timing-dependent and not deterministically reproducible; the regression test pins the publication invariant that closes it.