diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
index bcfce57967..5efc98a041 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs
@@ -312,7 +312,7 @@ public void AddProperty(string propertyName, string propertyValue)
/// The properties to merge in. May be .
internal void MergeProperties(IReadOnlyDictionary? propertiesToMerge)
{
- if (propertiesToMerge is null)
+ if (propertiesToMerge is null or { Count: 0 })
{
return;
}
@@ -343,6 +343,12 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
/// stored on a TestAssemblyInfo / TestClassInfo and later merged into other
/// contexts via .
///
+ /// Returns when there are no non-label properties to capture
+ /// (the common case when AssemblyInitialize / ClassInitialize do not set
+ /// properties on TestContext). already handles a
+ /// argument as a no-op, so callers need not special-case this.
+ ///
+ ///
/// The snapshot is shallow: keys and value references are copied as-is. Reference-type
/// values stored in the bag (e.g. a mocked file system, a connection pool, a list) are
/// shared across every context the snapshot is later merged into. Mutations of those
@@ -358,15 +364,15 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
/// thread-affinity expectation of AssemblyInitialize / ClassInitialize.
///
///
- /// A read-only snapshot of the current properties.
- internal IReadOnlyDictionary CaptureLifecycleProperties()
+ ///
+ /// A read-only snapshot of the current properties (excluding per-context labels), or
+ /// if there are no such properties to snapshot.
+ ///
+ internal IReadOnlyDictionary? CaptureLifecycleProperties()
{
- Dictionary snapshot;
+ Dictionary? snapshot = null;
lock (_propertiesLock)
{
-#pragma warning disable IDE0028 // Collection initialization can be simplified - capacity hint is intentional.
- snapshot = new Dictionary(_properties.Count);
-#pragma warning restore IDE0028
foreach (KeyValuePair kvp in _properties)
{
if (kvp.Key == FullyQualifiedTestClassNameLabel || kvp.Key == TestNameLabel)
@@ -374,11 +380,14 @@ internal void MergeProperties(IReadOnlyDictionary? propertiesTo
continue;
}
+#pragma warning disable IDE0028 // Collection initialization can be simplified - capacity hint is intentional.
+ snapshot ??= new Dictionary(_properties.Count);
+#pragma warning restore IDE0028
snapshot[kvp.Key] = kvp.Value;
}
}
- return new ReadOnlyDictionary(snapshot);
+ return snapshot is null ? null : new ReadOnlyDictionary(snapshot);
}
///
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.cs
index ebd7b00209..6f881dd76b 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestAssemblyInfoTests.cs
@@ -265,6 +265,22 @@ public async Task RunAssemblyInitializeShouldExcludePerContextLabelsFromPostAsse
_testAssemblyInfo.PostAssemblyInitProperties.Should().ContainKey("UserKey");
}
+ public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitSetsNoProperties()
+ {
+ // AssemblyInitialize exists and succeeds but sets no TestContext.Properties.
+ DummyTestClass.AssemblyInitializeMethodBody = _ => { };
+ _testAssemblyInfo.AssemblyInitializeMethod = typeof(DummyTestClass).GetMethod("AssemblyInitializeMethod")!;
+
+ TestContextImplementation testContext = GetTestContext();
+ TestResult result = await _testAssemblyInfo.RunAssemblyInitializeAsync(testContext);
+
+ result.Outcome.Should().Be(UnitTestOutcome.Passed);
+
+ // CaptureLifecycleProperties returns null when no non-label properties were set,
+ // so MergeProperties can short-circuit without acquiring a lock on each test's context.
+ _testAssemblyInfo.PostAssemblyInitProperties.Should().BeNull();
+ }
+
public async Task RunAssemblyInitializeShouldLeavePostAssemblyInitPropertiesNullWhenAssemblyInitMethodIsNull()
{
_testAssemblyInfo.AssemblyInitializeMethod = null;
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.cs
index 39824780e9..ccfa1a5182 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestClassInfoTests.cs
@@ -448,6 +448,21 @@ public async Task GetResultOrRunClassInitializeAsyncShouldCapturePostClassInitPr
_testClassInfo.PostClassInitProperties.Should().NotContainKey(TestContext.FullyQualifiedTestClassNameLabel);
}
+ public async Task GetResultOrRunClassInitializeAsyncShouldLeavePostClassInitPropertiesNullWhenClassInitSetsNoProperties()
+ {
+ // ClassInitialize exists and succeeds but sets no TestContext.Properties.
+ DummyTestClass.ClassInitializeMethodBody = _ => { };
+ _testClassInfo.ClassInitializeMethod = typeof(DummyTestClass).GetMethod(nameof(DummyTestClass.ClassInitializeMethod));
+
+ TestContextImplementation realTestContext = new(null, _testClassType.FullName, new Dictionary(), null, null);
+ TestResult result = await _testClassInfo.GetResultOrRunClassInitializeAsync(realTestContext, string.Empty, string.Empty, string.Empty, string.Empty);
+
+ result.Outcome.Should().Be(UnitTestOutcome.Passed);
+
+ // CaptureLifecycleProperties returns null when no non-label properties were set.
+ _testClassInfo.PostClassInitProperties.Should().BeNull();
+ }
+
public async Task GetResultOrRunClassInitializeAsyncShouldLeavePostClassInitPropertiesNullWhenClassInitMethodIsNull()
{
_testClassInfo.ClassInitializeMethod = null;
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
index 641197958c..6062851605 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs
@@ -471,6 +471,16 @@ public void MergePropertiesShouldIgnoreNull()
_testContextImplementation.Properties["Key"].Should().Be("Original");
}
+ public void MergePropertiesShouldIgnoreEmptyDictionary()
+ {
+ _testContextImplementation = CreateTestContextImplementation();
+ _testContextImplementation.Properties["Key"] = "Original";
+
+ _testContextImplementation.MergeProperties(new Dictionary());
+
+ _testContextImplementation.Properties["Key"].Should().Be("Original");
+ }
+
public void MergePropertiesShouldNotOverwritePerContextLabels()
{
_testMethod.Setup(tm => tm.FullClassName).Returns("A.C.M");
@@ -497,28 +507,41 @@ public void CaptureLifecyclePropertiesShouldReturnAllPropertiesExceptPerContextL
_testContextImplementation.Properties["UserKey"] = "UserValue";
_testContextImplementation.Properties["AnotherKey"] = 7;
- IReadOnlyDictionary snapshot = _testContextImplementation.CaptureLifecycleProperties();
+ IReadOnlyDictionary? snapshot = _testContextImplementation.CaptureLifecycleProperties();
+ snapshot.Should().NotBeNull();
snapshot.Should().ContainKey("UserKey");
- snapshot["UserKey"].Should().Be("UserValue");
+ snapshot!["UserKey"].Should().Be("UserValue");
snapshot.Should().ContainKey("AnotherKey");
- snapshot["AnotherKey"].Should().Be(7);
+ snapshot!["AnotherKey"].Should().Be(7);
snapshot.Should().NotContainKey("FullyQualifiedTestClassName");
snapshot.Should().NotContainKey("TestName");
}
+ public void CaptureLifecyclePropertiesShouldReturnNullWhenNoNonLabelPropertiesExist()
+ {
+ _testContextImplementation = CreateTestContextImplementation();
+
+ // Context has no properties at all; no labels were seeded because the ITestMethod mock is
+ // unconfigured (FullClassName/Name return null) and testClassFullName is null.
+ IReadOnlyDictionary? snapshot = _testContextImplementation.CaptureLifecycleProperties();
+
+ snapshot.Should().BeNull();
+ }
+
public void CaptureLifecyclePropertiesShouldReturnSnapshotIndependentOfTheLiveBag()
{
_testContextImplementation = CreateTestContextImplementation();
_testContextImplementation.Properties["Key"] = "OriginalValue";
- IReadOnlyDictionary snapshot = _testContextImplementation.CaptureLifecycleProperties();
+ IReadOnlyDictionary? snapshot = _testContextImplementation.CaptureLifecycleProperties();
// Mutating the live bag must not affect the snapshot.
_testContextImplementation.Properties["Key"] = "ChangedValue";
_testContextImplementation.Properties["NewKey"] = "NewValue";
- snapshot["Key"].Should().Be("OriginalValue");
+ snapshot.Should().NotBeNull();
+ snapshot!["Key"].Should().Be("OriginalValue");
snapshot.Should().NotContainKey("NewKey");
}
@@ -528,13 +551,14 @@ public void CaptureLifecyclePropertiesShouldAliasReferenceTypeValues()
var bag = new List { 1 };
_testContextImplementation.Properties["RefKey"] = bag;
- IReadOnlyDictionary snapshot = _testContextImplementation.CaptureLifecycleProperties();
+ IReadOnlyDictionary? snapshot = _testContextImplementation.CaptureLifecycleProperties();
// The snapshot is shallow: the snapshot's value and the live bag share the same instance.
// Mutating the instance must therefore be visible through both. This guards the documented
// contract on CaptureLifecycleProperties from accidentally regressing to a deep copy.
+ snapshot.Should().NotBeNull();
bag.Add(2);
- ((List)snapshot["RefKey"]!).Should().BeEquivalentTo(new[] { 1, 2 });
+ ((List)snapshot!["RefKey"]!).Should().BeEquivalentTo(new[] { 1, 2 });
}
public void CaptureLifecyclePropertiesAndMergePropertiesShouldNotLockOnExposedPropertyBag()