Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Flow TestContext.Properties through Assembly/Class lifecycle - #8386

Merged
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties
May 20, 2026
Merged

Flow TestContext.Properties through Assembly/Class lifecycle#8386
Amaury Levé (Evangelink) merged 1 commit into
mainfrom
dev/amauryleve/flow-testcontext-properties

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Summary

Flows TestContext.Properties written during [AssemblyInitialize] and [ClassInitialize] down through the rest of the test lifecycle. Fixes#5986.

Today, every call to PlatformServiceProvider.GetTestContext builds a brand-new TestContextImplementation with its own private dictionary copied from the per-test seed. So a context.Properties[X] = ... written inside AssemblyInitialize is stored in a context that's thrown away when the method returns - subsequent tests, class-init, and cleanups never see it. (And on tests #2+ the assembly-init context is never even passed in because of the cached-result short-circuit.) The MSTest docs say this scenario should work; this PR makes it work.

Behavior after this change

  • Properties set in [AssemblyInitialize] are visible in [ClassInitialize], the test class ctor, [TestInitialize], the test method, [TestCleanup], [ClassCleanup] and [AssemblyCleanup] of every test in the assembly.
  • Properties set in [ClassInitialize] are visible in tests, [TestInitialize], [TestCleanup] and [ClassCleanup] of that class. They override any conflicting assembly-init value within that class's scope.
  • [AssemblyCleanup] deliberately does not see [ClassInitialize] properties (assembly-scoped, picking any one class would be arbitrary).
  • Per-test writes to Properties still don't propagate to sibling tests (no behavioral regression).

Design

After each init body completes successfully, capture a shallow snapshot of the live property bag onto the corresponding TestAssemblyInfo / TestClassInfo, then merge those snapshots into all subsequent contexts:

CaptureWhereWhen
TestAssemblyInfo.PostAssemblyInitPropertiesinside _assemblyInfoExecuteSyncSemaphoreafter AssemblyInitialize body returns
TestClassInfo.PostClassInitPropertiesinside _testClassExecuteSyncSemaphoreafter RunClassInitializeAsync returns (includes base-chain writes)
MergeSnapshot used
class-init contextPostAssemblyInitProperties
test-execution contextPostAssemblyInitProperties + PostClassInitProperties (merged in place before TestMethodRunner.ExecuteAsync)
class-cleanup context (gated on isLastTestInClass)PostAssemblyInitProperties + PostClassInitProperties
assembly-cleanup contextPostAssemblyInitProperties
ClassCleanupManager.ForceCleanup fallback contextssame as above

Snapshots exclude the per-context labels (FullyQualifiedTestClassName, TestName) and MergeProperties refuses to overwrite them, so per-test identity stays intact. Snapshots are shallow (reference-type values are aliased across all flowed contexts) - documented on the new XML doc-comments.

Files

Source (5)

  • TestContextImplementation.cs - new internal CaptureLifecycleProperties() + MergeProperties(); defensive switch from _properties.Add to indexer assignment for the label keys so a seeded bag never throws.
  • TestAssemblyInfo.cs - new PostAssemblyInitProperties capture point.
  • TestClassInfo.cs - new PostClassInitProperties capture point.
  • UnitTestRunner.cs - 4 merge sites + perf tweak (class-cleanup merge moved inside the isLastTestInClass guard).
  • ClassCleanupManager.cs - merges in the fallback path.

Tests (4)

  • TestContextImplementationTests - 7 new tests covering MergeProperties (skip labels, null-tolerant, overwrite semantics), CaptureLifecycleProperties (snapshot independence, shallow/aliasing), and the defensive ctor change.
  • TestAssemblyInfoTests - 4 new tests (capture on success, label exclusion, null when no init method, null on failure).
  • TestClassInfoTests - 4 new tests (capture on success, null when no init method, null on failure, base+derived chain).
  • New TestContextPropertyFlowTests acceptance suite (its own asset, runs on net462/net8.0/net10.0) covering: AssemblyInit→tests, ClassInit→tests, ClassInit override of AssemblyInit value, ClassCleanup observes both, AssemblyCleanup observes AssemblyInit only, cross-class isolation, no per-test leakage, [DataRow] shared bag.

API surface

None. Public API unchanged; all new types/properties are internal.

Verification

  • build.cmd -pack -c Release -> 0 warnings, 0 errors.
  • 804 / 804MSTestAdapter.PlatformServices.UnitTests pass on net9.0.
  • 3 / 3 new acceptance test runs pass (one per TFM).
  • 10 / 10 existing TestContextTests acceptance tests still pass.
  • Reviewed twice with the expert-reviewer agent; all actionable findings addressed.

Fixes#5986

Properties written to TestContext.Properties in [AssemblyInitialize] now
flow to every [ClassInitialize], test method, [ClassCleanup] and
[AssemblyCleanup]. Properties written in [ClassInitialize] flow to test
methods and [ClassCleanup] of that class.
Implementation:
- TestContextImplementation: new internal CaptureLifecycleProperties()
and MergeProperties(); defensive switch from Add to indexer assignment
for the per-context label keys.
- TestAssemblyInfo.PostAssemblyInitProperties: snapshot captured inside
the existing _assemblyInfoExecuteSyncSemaphore after AssemblyInit
body completes successfully.
- TestClassInfo.PostClassInitProperties: snapshot captured inside the
existing _testClassExecuteSyncSemaphore after ClassInit completes
(includes base-chain class-init writes via InheritanceBehavior).
- UnitTestRunner.RunSingleTestAsync: merges snapshots into class-init,
test-execution, class-cleanup and assembly-cleanup contexts. The
class-cleanup merge is gated on isLastTestInClass to avoid wasted
copies on every test.
- ClassCleanupManager.ForceCleanup: same merges on the fallback contexts.
Per-context labels (FullyQualifiedTestClassName, TestName) are excluded
from snapshots and preserved on merge so per-test identity stays intact.
Snapshots are shallow (reference-type values are aliased across all
flowed contexts) - documented in the new XML doc-comments.
Class-init properties are intentionally NOT flowed to AssemblyCleanup
because AssemblyCleanup is assembly-scoped and picking one class would
be arbitrary.
Tests:
- 7 unit tests for MergeProperties/CaptureLifecycleProperties.
- 4 unit tests for TestAssemblyInfo snapshot capture.
- 4 unit tests for TestClassInfo snapshot capture (incl. base+derived chain).
- New TestContextPropertyFlowTests acceptance suite covering AssemblyInit
to tests, ClassInit to tests, override precedence, cross-class
isolation, AssemblyCleanup excluding class-init props, no leakage
between sibling tests, and [DataRow] shared bag.
No public API changes.
Fixes#5986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 20, 2026 12:53

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Enables TestContext.Properties values written during [AssemblyInitialize] and [ClassInitialize] to flow through subsequent MSTest lifecycle phases by snapshotting the property bag after init and merging those snapshots into later contexts.

Changes:

  • Added internal snapshot/merge helpers to TestContextImplementation and adjusted label seeding to be overwrite-safe.
  • Captured post-init property snapshots on TestAssemblyInfo / TestClassInfo and merged them at key lifecycle points (class init, test execution, cleanups).
  • Added unit + acceptance coverage for merge/snapshot semantics and end-to-end lifecycle visibility.
Show a summary per file
FileDescription
src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.csAdds MergeProperties/CaptureLifecycleProperties and makes label seeding overwrite-safe.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.csCaptures a post-assembly-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.csCaptures a post-class-init properties snapshot.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.csMerges captured snapshots into class-init, test execution, and cleanup contexts.
src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.csMerges snapshots in ForceCleanup fallback cleanup contexts.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.csAdds unit tests for merge/snapshot behavior and label seeding change.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.csAdds unit tests for post-assembly-init snapshot capture.
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.csAdds unit tests for post-class-init snapshot capture (including base/derived chain).
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestContextPropertyFlowTests.csAdds acceptance suite to validate end-to-end lifecycle property flow across TFMs.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 18

Comment on lines +405 to +406
public void MergePropertiesShouldAddNewKeysIntoThePropertyBag()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +420 to +421
public void MergePropertiesShouldOverwriteExistingKeys()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +430 to +431
public void MergePropertiesShouldIgnoreNull()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +440 to +441
public void MergePropertiesShouldNotOverwritePerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +458 to +459
public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextLabels()
{

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties["AnotherKey"].Should().Be(42);
}

public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAssemblyInitProperties()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

_testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
}

public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullOnFailure()

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: MSTestAdapter.PlatformServices.UnitTests uses the internal TestContainer base from TestFramework.ForTestingMSTest rather than vanilla MSTest. In that framework any public parameterless method is treated as a test method (see the 800+ existing tests in this file that also have no [TestMethod] attribute). No change needed.

Tracked via the follow-up PR #8396 alongside the other review items.

Comment on lines +333 to +344
var snapshot = new Dictionary<string, object?>(_properties.Count);
foreach (KeyValuePair<string, object?> kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
{
continue;
}

snapshot[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<string, object?>(snapshot);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: CaptureLifecycleProperties now enumerates _properties under a lock so two snapshot calls cannot trip over each other. The doc-comment is explicit that writes via the public TestContext.Properties indexer bypass this lock — a lifecycle method that spawns a background thread which keeps mutating Properties past method return is treated as user error and out of scope, consistent with the long-standing thread-affinity expectation of AssemblyInitialize / ClassInitialize.

Comment on lines +193 to +200
// TODO: PostAssemblyInitProperties is published outside the
// _assemblyInfoExecuteSyncSemaphore via the
// IsAssemblyInitializeExecuted fast path in this method. This
// is consistent with the existing pattern used by
// AssemblyInitializationException and ExecutionContext;
// revisit memory-barrier semantics for all three together
// if it becomes a problem.
PostAssemblyInitProperties = testContextImpl.CaptureLifecycleProperties();

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up PR #8396: PostAssemblyInitProperties (and the matching PostClassInitProperties on TestClassInfo) now use Volatile.Read / Volatile.Write, replacing the temporary TODO left in the merged commit. The publishing thread does the Volatile.Write before the IsAssemblyInitializeExecuted flag flip; consumers Volatile-read the snapshot directly (the call site does not gate on the executed flag), so the snapshot field is the only thing that needs an acquire/release pair to be safely observed on the bypass-the-semaphore fast path.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR correctly implements property-flow from AssemblyInitialize / ClassInitialize into downstream contexts (class init, test execution, class cleanup, assembly cleanup). The core design is sound, the acceptance test covers the critical scenarios (cross-class isolation, override precedence, assembly-cleanup scoping), and the unit tests are well-structured.

Findings

SeverityDimensionFinding
MODERATEThreading & ConcurrencyPostAssemblyInitProperties (and pre-existing ExecutionContext / AssemblyInitializationException) are published without a memory barrier on the fast path that skips the semaphore. Acknowledged via TODO; recommend tracking as a follow-up.
MODERATETest CompletenessClassCleanupManager.ForceCleanup (triggered by --maximum-failed-tests) now merges lifecycle properties, but no test exercises this path to verify property visibility.
MODERATEAlgorithmic CorrectnessMergeProperties uses overwrite semantics, so lifecycle properties silently win over sourceLevelParameters (runsettings) on key collision. This is likely the right priority order, but it should be called out in the doc/tests.

Clean dimensions

Backward compatibility ✅ (all new surface is internal), no init accessors ✅, no PublicAPI.Unshipped.txt required ✅, cross-TFM compatibility ✅, CaptureLifecycleProperties correctly excludes per-context labels ✅, snapshot immutability (ReadOnlyDictionary wrapper) ✅, idempotency of MergeProperties ✅, assembly-cleanup correctly excluded from class-init snapshot ✅.

Generated by Expert Code Review (on open) for issue #8386 · ● 15M

@Evangelink
Amaury Levé (Evangelink) merged commit 689d5e4 into mainMay 20, 2026
34 of 36 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/flow-testcontext-properties branch May 20, 2026 15:28
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Filed follow-up PR #8396 to address the post-merge review feedback. Quick map:

Reviewer / dimensionResolution
copilot-pull-request-reviewer · 16× [TestMethod] missingFalse positives — the file uses the internal TestContainer base; replied inline on each thread.
copilot-pull-request-reviewer · CaptureLifecycleProperties enumeration safetySnapshot enumeration now under a lock on _properties; doc-comment scopes user-thread races as out of scope.
copilot-pull-request-reviewer · PostAssemblyInitProperties safe-publication on the fast pathSnapshot fields now use Volatile.Read / Volatile.Write (replaces the temporary TODO); same treatment applied to PostClassInitProperties.
Amaury Levé (@Evangelink) expert-review · threading TODOClosed by the Volatile change above.
Amaury Levé (@Evangelink) expert-review · no test exercises ClassCleanupManager.ForceCleanupNew TestContextPropertyFlowForceCleanupTests acceptance suite triggers ForceCleanup via --maximum-failed-tests=1 and asserts the snapshot flows into ClassCleanup / AssemblyCleanup (and still excludes ClassInit from AssemblyCleanup).
Amaury Levé (@Evangelink) expert-review · runsettings vs lifecycle precedenceMergeProperties XML doc now explicitly documents the overwrite-wins semantics for keys seeded from runsettings; new MergePropertiesShouldOverrideSeededSourceLevelParameters unit test pins the behavior.

Amaury Levé (Evangelink) added a commit that referenced this pull request May 22, 2026
… flow (#8396)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestContext.Properties across test methods of same class is different instance

2 participants

@Evangelink