// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.TestFramework; using Microsoft.Testing.Platform.Messages; using Microsoft.Testing.Platform.Requests; using Microsoft.Testing.Platform.Telemetry; using Microsoft.Testing.Platform.TestHost; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using TestFramework.ForTestingMSTest; using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests; /// /// Unit tests for and the Microsoft.Testing.Platform-native /// discovery/result seams (, ). /// public sealed class MSTestTestNodeConverterTests : TestContainer { private static UnitTestElement CreateElement(string? managedMethodName = "MyMethod", string fullClassName = "MyNamespace.MyClass", string name = "MyMethod") { var testMethod = new TestMethod(managedMethodName, hierarchyValues: null, name, fullClassName, "MyAssembly.dll", displayName: null, parameterTypes: null); return new UnitTestElement(testMethod); } // --- Discovery node --------------------------------------------------------------------------------------- public void ToDiscoveredTestNode_SetsUidDisplayNameAndDiscoveredState() { UnitTestElement element = CreateElement(); TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); node.Uid.Value.Should().Be(element.GetTestId().ToString()); node.DisplayName.Should().Be("MyMethod"); node.Properties.Any().Should().BeTrue(); node.Properties.Any().Should().BeFalse(); } public void ToDiscoveredTestNode_UsesExplicitDisplayName_WhenProvided() { var testMethod = new TestMethod("MyMethod", hierarchyValues: null, "MyMethod", "MyNamespace.MyClass", "MyAssembly.dll", displayName: "Friendly name", parameterTypes: null); var element = new UnitTestElement(testMethod); TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); node.DisplayName.Should().Be("Friendly name"); } public void ToInProgressTestNode_AddsInProgressState() { TestNode node = MSTestTestNodeConverter.ToInProgressTestNode(CreateElement(), isTrxEnabled: false); node.Properties.Any().Should().BeTrue(); } public void ToInProgressTestNode_AddsTestMethodIdentifier_FromManagedNames() { // In-progress nodes must carry TestMethodIdentifierProperty: it is consumed by // OpenTelemetryResultHandler.NotifyInProgress (test.method/class/namespace/assembly tags) and by the // slow-test reporters via TestNodeIdentity, which otherwise fall back to the display name. TestNode node = MSTestTestNodeConverter.ToInProgressTestNode(CreateElement(), isTrxEnabled: false); node.Properties.Any().Should().BeTrue(); } public void ToDiscoveredTestNode_AddsTestMethodIdentifier_FromManagedNames() { TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(CreateElement(), isTrxEnabled: false); TestMethodIdentifierProperty? identifier = node.Properties.SingleOrDefault(); identifier.Should().NotBeNull(); identifier!.Namespace.Should().Be("MyNamespace"); identifier.TypeName.Should().Be("MyClass"); identifier.MethodName.Should().Be("MyMethod"); identifier.AssemblyFullName.Should().BeEmpty(); identifier.ReturnTypeFullName.Should().BeEmpty(); } public void ToDiscoveredTestNode_DoesNotAddTestMethodIdentifier_WhenNoManagedMethodName() { UnitTestElement element = CreateElement(managedMethodName: null); TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); node.Properties.Any().Should().BeFalse(); } public void ToDiscoveredTestNode_DoesNotAddTestMethodIdentifier_WhenManagedTypeNameIsEmpty() { // ManagedTypeName is derived from FullClassName, so an empty class name leaves no usable type identity. UnitTestElement element = CreateElement(fullClassName: string.Empty); TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); node.Properties.Any().Should().BeFalse(); } public void ToDiscoveredTestNode_AddsFileLocation_WhenDeclaringFileKnown() { UnitTestElement element = CreateElement(); element.DeclaringFilePath = "C:\\src\\MyClass.cs"; element.DeclaringLineNumber = 42; TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); TestFileLocationProperty? location = node.Properties.SingleOrDefault(); location.Should().NotBeNull(); location!.FilePath.Should().Be("C:\\src\\MyClass.cs"); location.LineSpan.Start.Line.Should().Be(42); } public void ToDiscoveredTestNode_DoesNotAddFileLocation_WhenDeclaringFileUnknown() { TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(CreateElement(), isTrxEnabled: false); node.Properties.Any().Should().BeFalse(); } public void ToDiscoveredTestNode_AddsCategoriesAndTraitsAsMetadata() { UnitTestElement element = CreateElement(); element.TestCategory = ["CategoryA", "CategoryB"]; element.Traits = [new TestTrait("Owner", "Alice")]; TestNode node = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); TestMetadataProperty[] metadata = node.Properties.OfType(); metadata.Should().Contain(p => p.Key == "CategoryA" && p.Value == string.Empty); metadata.Should().Contain(p => p.Key == "CategoryB" && p.Value == string.Empty); metadata.Should().Contain(p => p.Key == "Owner" && p.Value == "Alice"); } public void ToDiscoveredTestNode_AddsTrxCategories_OnlyWhenTrxEnabled() { UnitTestElement element = CreateElement(); element.TestCategory = ["CategoryA"]; MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false) .Properties.Any().Should().BeFalse(); MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: true) .Properties.Any().Should().BeTrue(); } public void ToDiscoveredTestNode_AddsTrxWorkItems_OnlyWhenTrxEnabled() { UnitTestElement element = CreateElement(); element.WorkItemIds = ["123", "456"]; MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false) .Properties.Any().Should().BeFalse(); Testing.Extensions.TrxReport.Abstractions.TrxWorkItemsProperty property = MSTestTestNodeConverter .ToDiscoveredTestNode(element, isTrxEnabled: true) .Properties.Single(); property.WorkItemIds.Should().Equal("123", "456"); property.WorkItemIds.Should().NotBeSameAs(element.WorkItemIds); } public void RepeatedConversions_ReuseImmutableBaseProperties_WithoutSharingNodesOrPropertyBags() { UnitTestElement element = CreateElement(); element.DeclaringFilePath = "C:\\src\\MyClass.cs"; element.DeclaringLineNumber = 42; element.TestCategory = ["CategoryA"]; element.Traits = [new TestTrait("Owner", "Alice")]; TestNode discovered = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); TestNode inProgress = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: false); TestNode result = MSTestTestNodeConverter.ToResultTestNode( element, new FrameworkTestResult { Outcome = UnitTestOutcome.Passed }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); discovered.Should().NotBeSameAs(inProgress); inProgress.Should().NotBeSameAs(result); discovered.Properties.Should().NotBeSameAs(inProgress.Properties); inProgress.Properties.Should().NotBeSameAs(result.Properties); inProgress.Uid.Should().BeSameAs(discovered.Uid); result.Uid.Should().BeSameAs(discovered.Uid); inProgress.Properties.Single().Should().BeSameAs(discovered.Properties.Single()); result.Properties.Single().Should().BeSameAs(discovered.Properties.Single()); TestMetadataProperty[] discoveredMetadata = discovered.Properties.OfType(); TestMetadataProperty[] inProgressMetadata = inProgress.Properties.OfType(); TestMetadataProperty[] resultMetadata = result.Properties.OfType(); inProgressMetadata.Should().BeEquivalentTo(discoveredMetadata); resultMetadata.Should().BeEquivalentTo(discoveredMetadata); foreach (TestMetadataProperty metadata in discoveredMetadata) { inProgressMetadata.Single(candidate => candidate.Equals(metadata)).Should().BeSameAs(metadata); resultMetadata.Single(candidate => candidate.Equals(metadata)).Should().BeSameAs(metadata); } inProgress.Properties.Single().Should().BeSameAs(discovered.Properties.Single()); result.Properties.Single().Should().BeSameAs(discovered.Properties.Single()); discovered.Properties.Add(new TestMetadataProperty("MessageOnly", "discovered")); inProgress.Properties.Any().Should().BeTrue(); inProgress.Properties.OfType().Should().NotContain(p => p.Key == "MessageOnly"); } public void TrxCategories_AreCopiedPerNode_AndCannotCrossMutate() { UnitTestElement element = CreateElement(); element.TestCategory = ["CategoryA", "CategoryB"]; element.Traits = [new TestTrait("Owner", "Alice")]; TestNode first = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: true); TestNode second = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: true); Testing.Extensions.TrxReport.Abstractions.TrxCategoriesProperty firstCategories = first.Properties.Single(); Testing.Extensions.TrxReport.Abstractions.TrxCategoriesProperty secondCategories = second.Properties.Single(); firstCategories.Should().NotBeSameAs(secondCategories); firstCategories.Categories.Should().NotBeSameAs(secondCategories.Categories); firstCategories.Categories[0] = "Mutated"; secondCategories.Categories.Should().Equal("CategoryA", "CategoryB"); TestMetadataProperty[] firstMetadata = first.Properties.OfType(); TestMetadataProperty[] secondMetadata = second.Properties.OfType(); secondMetadata.Should().BeEquivalentTo(firstMetadata); foreach (TestMetadataProperty metadata in firstMetadata) { secondMetadata.Single(candidate => candidate.Equals(metadata)).Should().BeSameAs(metadata); } } public void TrxWorkItems_AreCopiedPerNode_AndCannotCrossMutate() { UnitTestElement element = CreateElement(); element.WorkItemIds = ["123", "456"]; TestNode first = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: true); TestNode second = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: true); Testing.Extensions.TrxReport.Abstractions.TrxWorkItemsProperty firstWorkItems = first.Properties.Single(); Testing.Extensions.TrxReport.Abstractions.TrxWorkItemsProperty secondWorkItems = second.Properties.Single(); firstWorkItems.Should().NotBeSameAs(secondWorkItems); firstWorkItems.WorkItemIds.Should().NotBeSameAs(secondWorkItems.WorkItemIds); firstWorkItems.WorkItemIds[0] = "Mutated"; secondWorkItems.WorkItemIds.Should().Equal("123", "456"); } public void ResultDisplayNameOverride_RemainsPerResult_WhileTrxDefinitionUsesTestDefinitionName() { UnitTestElement element = CreateElement(); element.TestMethod.DisplayName = "Test definition"; TestNode first = MSTestTestNodeConverter.ToResultTestNode( element, new FrameworkTestResult { Outcome = UnitTestOutcome.Passed, DisplayName = "Data row 1" }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: true, new MSTestSettings()); TestNode second = MSTestTestNodeConverter.ToResultTestNode( element, new FrameworkTestResult { Outcome = UnitTestOutcome.Passed, DisplayName = "Data row 2" }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: true, new MSTestSettings()); first.DisplayName.Should().Be("Data row 1"); second.DisplayName.Should().Be("Data row 2"); first.Properties.Single().TestDefinitionName.Should().Be("Test definition"); second.Properties.Single().TestDefinitionName.Should().Be("Test definition"); } public void CloneSpecializedAsDataRow_DoesNotReuseParentCachedIdentityOrMetadata() { UnitTestElement element = CreateElement(); element.TestCategory = ["Parent"]; Guid parentId = element.GetTestId(); MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); UnitTestElement dataRow = element.Clone(); dataRow.TestMethod.DisplayName = "Data row"; dataRow.TestMethod.DataType = DynamicDataType.ITestDataSource; dataRow.TestMethod.TestCaseIndex = 1; dataRow.TestCategory = ["Row"]; TestNode dataRowNode = MSTestTestNodeConverter.ToDiscoveredTestNode(dataRow, isTrxEnabled: false); dataRowNode.Uid.Value.Should().NotBe(parentId.ToString()); dataRowNode.DisplayName.Should().Be("Data row"); dataRowNode.Properties.OfType().Should().ContainSingle(p => p.Key == "Row"); dataRowNode.Properties.OfType().Should().NotContain(p => p.Key == "Parent"); } public void SourceUpdatedClone_DoesNotReuseOriginalCachedBaseData() { UnitTestElement element = CreateElement(); TestNode original = MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: false); UnitTestElement relocated = element.CloneWithSource("OtherAssembly.dll"); relocated.DeclaringFilePath = "C:\\deployed\\MyClass.cs"; TestNode updated = MSTestTestNodeConverter.ToDiscoveredTestNode(relocated, isTrxEnabled: false); updated.Uid.Value.Should().NotBe(original.Uid.Value); updated.Properties.Single().FilePath.Should().Be("C:\\deployed\\MyClass.cs"); } public void ConcurrentConversions_AreSafeAndPreserveIndependentMutableProperties() { UnitTestElement element = CreateElement(managedMethodName: "MyMethod(System.String)"); element.TestCategory = ["CategoryA"]; var nodes = new TestNode[128]; Parallel.For(0, nodes.Length, i => nodes[i] = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: true)); nodes.Select(node => node.Uid.Value).Should().OnlyContain(uid => uid == nodes[0].Uid.Value); nodes.Select(node => node.Properties).Distinct().Should().HaveCount(nodes.Length); string[][] parameterTypes = [.. nodes.Select(node => node.Properties.Single().ParameterTypeFullNames)]; parameterTypes.Distinct().Should().HaveCount(nodes.Length); string[][] categories = [.. nodes.Select(node => node.Properties.Single().Categories)]; categories.Distinct().Should().HaveCount(nodes.Length); } public void BaseDataCache_DoesNotKeepElementAlive() { WeakReference elementReference = CreateWeakReferenceToConvertedElement(); for (int i = 0; elementReference.IsAlive && i < 10; i++) { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true); GC.WaitForPendingFinalizers(); } elementReference.IsAlive.Should().BeFalse(); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] private static WeakReference CreateWeakReferenceToConvertedElement() { UnitTestElement element = CreateElement(); MSTestTestNodeConverter.ToDiscoveredTestNode(element, isTrxEnabled: true); return new WeakReference(element); } // --- Result node: outcomes -------------------------------------------------------------------------------- public void ToResultTestNode_MapsPassedOutcome() { TestNode node = ResultNode(UnitTestOutcome.Passed); node.Properties.Any().Should().BeTrue(); } public void ToResultTestNode_MapsFailedOutcome_WithMessageAndStackTrace() { var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Failed, ExceptionMessage = "boom", ExceptionStackTrace = "at Some.Method()" }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); FailedTestNodeStateProperty? failed = node.Properties.SingleOrDefault(); failed.Should().NotBeNull(); failed!.Exception!.Message.Should().Be("boom"); failed.Exception.StackTrace.Should().Be("at Some.Method()"); } public void ToResultTestNode_AddsAssertionFailureProperty_WhenResultCarriesAssertionTexts() { var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Failed, ExceptionMessage = "Assert.AreEqual failed.", ExceptionExpectedText = "5", ExceptionActualText = "2", }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); AssertionFailureProperty? assertionFailure = node.Properties.SingleOrDefault(); assertionFailure.Should().NotBeNull(); assertionFailure!.Expected.Should().Be("5"); assertionFailure.Actual.Should().Be("2"); } public void ToResultTestNode_AddsAssertionFailureProperty_WhenOnlyExpectedTextIsKnown() { // Some assertions (e.g. CollectionAssert.Contains) only have a natural expected value. var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Failed, ExceptionMessage = "CollectionAssert.Contains failed.", ExceptionExpectedText = "\"c\"", }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); AssertionFailureProperty? assertionFailure = node.Properties.SingleOrDefault(); assertionFailure.Should().NotBeNull(); assertionFailure!.Expected.Should().Be("\"c\""); assertionFailure.Actual.Should().BeNull(); } public void ToResultTestNode_DoesNotAddAssertionFailureProperty_WhenFailureIsNotAnAssertion() { var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Failed, ExceptionMessage = "boom" }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); node.Properties.Any().Should().BeFalse(); } public void ToResultTestNode_MapsIgnoredOutcomeToSkipped() { TestNode node = ResultNode(UnitTestOutcome.Ignored); node.Properties.Any().Should().BeTrue(); } public void ToResultTestNode_MapsNotFoundOutcomeToError() { TestNode node = ResultNode(UnitTestOutcome.NotFound); node.Properties.Any().Should().BeTrue(); } public void ToResultTestNode_MapsInconclusiveToSkipped_ByDefault() { // Default MSTestSettings has MapInconclusiveToFailed = false. TestNode node = ResultNode(UnitTestOutcome.Inconclusive); node.Properties.Any().Should().BeTrue(); } // --- Result node: timing, output, attachments ------------------------------------------------------------- public void ToResultTestNode_AddsTimingProperty() { var start = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero); DateTimeOffset end = start.AddSeconds(2); var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Passed, Duration = TimeSpan.FromSeconds(2) }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, start, end, isTrxEnabled: false, new MSTestSettings()); TimingProperty? timing = node.Properties.SingleOrDefault(); timing.Should().NotBeNull(); timing!.GlobalTiming.Duration.Should().Be(TimeSpan.FromSeconds(2)); } public void ToResultTestNode_MapsLogOutputAndLogErrorToStandardStreams() { var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Passed, LogOutput = "hello out", LogError = "hello err", }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); node.Properties.SingleOrDefault()!.StandardOutput.Should().Contain("hello out"); node.Properties.SingleOrDefault()!.StandardError.Should().Contain("hello err"); } public void ToResultTestNode_IncludesDebugTraceAndTestContextBannersInStandardOutput() { var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Passed, DebugTrace = "some trace", TestContextMessages = "some context", }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); string standardOutput = node.Properties.SingleOrDefault()!.StandardOutput; standardOutput.Should().Contain("Debug Trace:").And.Contain("some trace"); standardOutput.Should().Contain("TestContext Messages:").And.Contain("some context"); } // --- Result node: TRX properties -------------------------------------------------------------------------- public void ToResultTestNode_AddsTrxProperties_WhenTrxEnabled() { var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Failed, ExceptionMessage = "boom", ExceptionStackTrace = "at X()" }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: true, new MSTestSettings()); node.Properties.Any().Should().BeTrue(); node.Properties.Any().Should().BeTrue(); Testing.Extensions.TrxReport.Abstractions.TrxFullyQualifiedTypeNameProperty? typeName = node.Properties.SingleOrDefault(); typeName.Should().NotBeNull(); typeName!.FullyQualifiedTypeName.Should().Be("MyNamespace.MyClass"); } public void ToResultTestNode_AddsTestMethodIdentifier_FromManagedNames() { TestNode node = MSTestTestNodeConverter.ToResultTestNode(CreateElement(), new FrameworkTestResult { Outcome = UnitTestOutcome.Passed }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); TestMethodIdentifierProperty? identifier = node.Properties.SingleOrDefault(); identifier.Should().NotBeNull(); identifier!.Namespace.Should().Be("MyNamespace"); identifier.TypeName.Should().Be("MyClass"); identifier.MethodName.Should().Be("MyMethod"); } public void ToResultTestNode_UsesFullClassNameForTrxTypeName_WhenNoManagedMethodName() { // Without a managed method name, no TestMethodIdentifierProperty is added, so the TRX fully-qualified type // name falls back to TestMethod.FullClassName directly (the behavior that replaced TryParseFullyQualifiedType). UnitTestElement element = CreateElement(managedMethodName: null); var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Failed, ExceptionMessage = "boom", ExceptionStackTrace = "at X()" }; TestNode node = MSTestTestNodeConverter.ToResultTestNode(element, result, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: true, new MSTestSettings()); node.Properties.Any().Should().BeFalse(); Testing.Extensions.TrxReport.Abstractions.TrxFullyQualifiedTypeNameProperty? typeName = node.Properties.SingleOrDefault(); typeName.Should().NotBeNull(); typeName!.FullyQualifiedTypeName.Should().Be("MyNamespace.MyClass"); } public void ToResultTestNode_DoesNotAddTrxProperties_WhenTrxDisabled() { TestNode node = ResultNode(UnitTestOutcome.Passed); node.Properties.Any().Should().BeFalse(); node.Properties.Any().Should().BeFalse(); } // --- Base-property caching --------------------------------------------------------------------------------- public void ToResultTestNode_ReusesCachedManagedNameParse_FromInProgressNode() { // The managed-name parse is retained by the element's base descriptor, so the in-progress node and every // result node must still agree on every field of the identifier. UnitTestElement element = CreateElement(); TestMethodIdentifierProperty inProgress = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: false) .Properties.Single(); TestMethodIdentifierProperty result = MSTestTestNodeConverter.ToResultTestNode(element, new FrameworkTestResult { Outcome = UnitTestOutcome.Passed }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()) .Properties.Single(); result.Should().Be(inProgress); // Namespace and TypeName are partial Substring results of ManagedTypeName, so re-running the parse would // hand back fresh string instances. Reference equality is therefore what proves the cached parse was // reused rather than redone - without the cache these assertions fail while the value equality above // still passes. result.Namespace.Should().BeSameAs(inProgress.Namespace); result.TypeName.Should().BeSameAs(inProgress.TypeName); } public void ToResultTestNode_ReusesTestMethodIdentifierInstance_ForParameterlessTestMethod() { // A parameterless identifier is fully immutable (readonly strings plus an empty parameter array, which // cannot be mutated), so the cached descriptor hands back the very same property instance instead of // allocating a new one for every node built from the same test method. UnitTestElement element = CreateElement(); TestMethodIdentifierProperty inProgress = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: false) .Properties.Single(); TestMethodIdentifierProperty result = MSTestTestNodeConverter.ToResultTestNode(element, new FrameworkTestResult { Outcome = UnitTestOutcome.Passed }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()) .Properties.Single(); result.Should().BeSameAs(inProgress); } public void ToResultTestNode_DoesNotReuseTestMethodIdentifierInstance_ForParameterizedTestMethod() { // Parameterized identifiers expose a mutable array, so each node must keep getting its own property. UnitTestElement element = CreateElement(managedMethodName: "MyMethod(System.String)"); TestMethodIdentifierProperty inProgress = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: false) .Properties.Single(); TestMethodIdentifierProperty result = MSTestTestNodeConverter.ToResultTestNode(element, new FrameworkTestResult { Outcome = UnitTestOutcome.Passed }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()) .Properties.Single(); result.Should().NotBeSameAs(inProgress); } public void ToResultTestNode_DoesNotShareParameterTypeArray_WithInProgressNode() { // TestMethodIdentifierProperty exposes ParameterTypeFullNames publicly, so nodes must never alias one // array: a consumer that writes to it would otherwise corrupt every other node of the same test method. UnitTestElement element = CreateElement(managedMethodName: "MyMethod(System.String)"); TestMethodIdentifierProperty inProgress = MSTestTestNodeConverter.ToInProgressTestNode(element, isTrxEnabled: false) .Properties.Single(); TestMethodIdentifierProperty result = MSTestTestNodeConverter.ToResultTestNode(element, new FrameworkTestResult { Outcome = UnitTestOutcome.Passed }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()) .Properties.Single(); inProgress.ParameterTypeFullNames.Should().Equal("System.String"); result.ParameterTypeFullNames.Should().Equal("System.String"); result.ParameterTypeFullNames.Should().NotBeSameAs(inProgress.ParameterTypeFullNames); } public void ToDiscoveredTestNode_DoesNotShareTestMethodIdentifier_AcrossDistinctTestMethods() { // The cache is keyed on the TestMethod instance, so two different test methods must never be conflated. TestMethodIdentifierProperty? first = MSTestTestNodeConverter.ToDiscoveredTestNode(CreateElement(managedMethodName: "MethodA", name: "MethodA"), isTrxEnabled: false) .Properties.SingleOrDefault(); TestMethodIdentifierProperty? second = MSTestTestNodeConverter.ToDiscoveredTestNode(CreateElement(managedMethodName: "MethodB", name: "MethodB"), isTrxEnabled: false) .Properties.SingleOrDefault(); first.Should().NotBeNull(); second.Should().NotBeNull(); second.Should().NotBeSameAs(first); first!.MethodName.Should().Be("MethodA"); second!.MethodName.Should().Be("MethodB"); } // --- GetTestId caching ------------------------------------------------------------------------------------ public void GetTestId_CachesComputedId_AndReturnsSameValueOnSubsequentCalls() { UnitTestElement element = CreateElement(); element.CachedTestNodeUid.Should().BeNull(); Guid first = element.GetTestId(); element.CachedTestNodeUid.Should().Be(first); Guid second = element.GetTestId(); second.Should().Be(first); } public void CloneWithSource_InvalidatesCachedTestId() { UnitTestElement element = CreateElement(); Guid original = element.GetTestId(); element.CachedTestNodeUid.Should().Be(original); UnitTestElement clone = element.CloneWithSource("OtherAssembly.dll"); // The clone must start with a cleared cache and recompute a distinct id for the new source. clone.CachedTestNodeUid.Should().BeNull(); clone.GetTestId().Should().NotBe(original); // The original element keeps its cached id untouched. element.CachedTestNodeUid.Should().Be(original); } public void WithUpdatedSource_InvalidatesCachedTestId_WhenSourceChanges() { UnitTestElement element = CreateElement(); Guid original = element.GetTestId(); UnitTestElement result = element.WithUpdatedSource("OtherAssembly.dll"); result.Should().NotBeSameAs(element); result.CachedTestNodeUid.Should().BeNull(); result.GetTestId().Should().NotBe(original); } public void CloneWithUpdatedSource_InvalidatesCachedTestId() { UnitTestElement element = CreateElement(); element.GetTestId(); element.CachedTestNodeUid.Should().NotBeNull(); UnitTestElement clone = element.CloneWithUpdatedSource("OtherAssembly.dll"); clone.CachedTestNodeUid.Should().BeNull(); } public void Clone_InvalidatesCachedTestId_SoTheCloneCanBeSpecializedAsADataRow() { UnitTestElement element = CreateElement(); Guid original = element.GetTestId(); UnitTestElement clone = element.Clone(); clone.CachedTestNodeUid.Should().BeNull(); element.CachedTestNodeUid.Should().Be(original); } // --- Seams ------------------------------------------------------------------------------------------------ public async Task MtpUnitTestElementSink_PublishesDiscoveredTestNode() { var messageBus = new CapturingMessageBus(); var sessionUid = new SessionUid("session-1"); var sink = new MtpUnitTestElementSink(messageBus, new StubDataProducer(), sessionUid, isTrxEnabled: false); await sink.SendTestElementAsync(CreateElement()); messageBus.Published.Should().ContainSingle(); var message = (TestNodeUpdateMessage)messageBus.Published[0]; message.SessionUid.Value.Should().Be("session-1"); message.TestNode.Properties.Any().Should().BeTrue(); } public async Task MtpTestResultRecorder_RecordStart_PublishesInProgressNode() { var messageBus = new CapturingMessageBus(); var recorder = new MtpTestResultRecorder(messageBus, new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); await recorder.RecordStartAsync(CreateElement()); var message = (TestNodeUpdateMessage)messageBus.Published.Single(); message.TestNode.Properties.Any().Should().BeTrue(); } public async Task MtpTestResultRecorder_RecordEmptyResult_PublishesExecutionCompletedNode() { var messageBus = new CapturingMessageBus(); var recorder = new MtpTestResultRecorder(messageBus, new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); await recorder.RecordEmptyResultAsync(CreateElement()); var message = (TestNodeUpdateMessage)messageBus.Published.Single(); message.TestNode.Properties.Any().Should().BeTrue(); message.TestNode.Properties.Any().Should().BeFalse(); } public async Task MtpTestResultRecorder_RecordEmptyResult_CompletesPublicExecution() { Mock service = new(); Mock activity = new(); service.Setup(s => s.StartTestExecutionActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny())).Returns(activity.Object); using var broker = new TestExecutionActivityBroker(service.Object, PlatformOpenTelemetryOptions.Default); var messageBus = new CapturingMessageBus(); var recorder = new MtpTestResultRecorder( CreateExecuteRequestContext(messageBus, broker), new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); UnitTestElement element = CreateElement(); await recorder.RecordStartAsync(element); await recorder.RecordEmptyResultAsync(element); TestNodeUpdateMessage[] messages = messageBus.Published.Cast().ToArray(); messages.Should().HaveCount(2); TestExecutionActivityProperty startProperty = messages[0].TestNode.Properties.Single(); TestExecutionActivityProperty completionProperty = messages[1].TestNode.Properties.Single(); completionProperty.Reservation.Should().BeSameAs(startProperty.Reservation); completionProperty.IsFinalResult.Should().BeTrue(); messages[1].TestNode.Properties.Any().Should().BeTrue(); element.ExecutionActivityLease.Should().BeNull(); } public async Task MtpTestResultRecorder_RecordResult_PublishesResultNodeAndReturnsFailedFlag() { var messageBus = new CapturingMessageBus(); var recorder = new MtpTestResultRecorder(messageBus, new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); var result = new FrameworkTestResult { Outcome = UnitTestOutcome.Failed, ExceptionMessage = "nope" }; bool isFailed = await recorder.RecordResultAsync(CreateElement(), result, DateTimeOffset.Now, DateTimeOffset.Now); isFailed.Should().BeTrue(); var message = (TestNodeUpdateMessage)messageBus.Published.Single(); message.TestNode.Properties.Any().Should().BeTrue(); } public async Task MtpTestResultRecorder_RecordResult_ReturnsFalse_ForPassedTest() { var messageBus = new CapturingMessageBus(); var recorder = new MtpTestResultRecorder(messageBus, new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); bool isFailed = await recorder.RecordResultAsync(CreateElement(), new FrameworkTestResult { Outcome = UnitTestOutcome.Passed }, DateTimeOffset.Now, DateTimeOffset.Now); isFailed.Should().BeFalse(); } public async Task MtpTestResultRecorder_CarriesOneOpaqueOccurrenceAcrossMultipleResults() { Mock service = new(); Mock activity = new(); service.Setup(s => s.StartTestExecutionActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny())).Returns(activity.Object); using var broker = new TestExecutionActivityBroker(service.Object, PlatformOpenTelemetryOptions.Default); var messageBus = new CapturingMessageBus(); var recorder = new MtpTestResultRecorder( CreateExecuteRequestContext(messageBus, broker), new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); UnitTestElement element = CreateElement(); FrameworkTestResult[] results = [ new() { Outcome = UnitTestOutcome.Passed }, new() { Outcome = UnitTestOutcome.Passed }, ]; await recorder.RecordStartAsync(element); ((Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ITestResultRecorder)recorder) .PrepareResults(element, results); await recorder.RecordResultAsync(element, results[0], DateTimeOffset.Now, DateTimeOffset.Now); messageBus.Published.Should().ContainSingle(); await recorder.RecordResultAsync(element, results[1], DateTimeOffset.Now, DateTimeOffset.Now); TestNodeUpdateMessage[] messages = messageBus.Published.Cast().ToArray(); TestExecutionActivityProperty startProperty = messages[0].TestNode.Properties.Single(); TestExecutionActivityProperty firstResultProperty = messages[1].TestNode.Properties.Single(); TestExecutionActivityProperty finalResultProperty = messages[2].TestNode.Properties.Single(); startProperty.Reservation.Should().BeSameAs(firstResultProperty.Reservation); startProperty.Reservation.Should().BeSameAs(finalResultProperty.Reservation); startProperty.IsFinalResult.Should().BeFalse(); firstResultProperty.IsFinalResult.Should().BeFalse(); finalResultProperty.IsFinalResult.Should().BeTrue(); element.ExecutionActivityLease.Should().BeNull(); } public async Task MtpTestResultRecorder_RecordStart_ActivatesAfterPublishingInProgressNode() { var publishStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var allowPublishToComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); bool publishCompleted = false; var messageBus = new CapturingMessageBus(async () => { publishStarted.SetResult(true); await allowPublishToComplete.Task; publishCompleted = true; }); Mock service = new(); Mock activity = new(); service.Setup(s => s.StartTestExecutionActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny())) .Returns(() => { publishCompleted.Should().BeTrue(); return activity.Object; }); using var broker = new TestExecutionActivityBroker(service.Object, PlatformOpenTelemetryOptions.Default); var recorder = new MtpTestResultRecorder( CreateExecuteRequestContext(messageBus, broker), new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); Task recordStartTask = recorder.RecordStartAsync(CreateElement()); await publishStarted.Task; service.Verify( s => s.StartTestExecutionActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny()), Times.Never); allowPublishToComplete.SetResult(true); await recordStartTask; service.Verify( s => s.StartTestExecutionActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny()), Times.Once); } public async Task MtpTestResultRecorder_RecordStart_DoesNotReserveWhenExecutionLeaseIsUnsupported() { Mock service = new(); using var broker = new TestExecutionActivityBroker(service.Object, PlatformOpenTelemetryOptions.Default); var messageBus = new CapturingMessageBus(); var recorder = new MtpTestResultRecorder( CreateExecuteRequestContext(messageBus, broker), new StubDataProducer(), new SessionUid("s"), isTrxEnabled: false, new MSTestSettings()); UnitTestElement element = CreateElement(); element.SupportsExecutionActivityLease = false; await recorder.RecordStartAsync(element); TestNodeUpdateMessage message = messageBus.Published.Cast().Single(); message.TestNode.Properties.Any().Should().BeFalse(); element.ExecutionActivityLease.Should().BeNull(); service.Verify( s => s.StartTestExecutionActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny()), Times.Never); } private static TestNode ResultNode(UnitTestOutcome outcome) => MSTestTestNodeConverter.ToResultTestNode(CreateElement(), new FrameworkTestResult { Outcome = outcome }, DateTimeOffset.Now, DateTimeOffset.Now, isTrxEnabled: false, new MSTestSettings()); private static ExecuteRequestContext CreateExecuteRequestContext( IMessageBus messageBus, TestExecutionActivityBroker broker) => new( Mock.Of(), messageBus, Mock.Of(), CancellationToken.None, broker); private sealed class CapturingMessageBus(Func? onPublish = null) : IMessageBus { public List Published { get; } = []; public async Task PublishAsync(IDataProducer dataProducer, IData data) { Published.Add(data); if (onPublish is not null) { await onPublish(); } } } private sealed class StubDataProducer : IDataProducer { public Type[] DataTypesProduced { get; } = [typeof(TestNodeUpdateMessage)]; public string Uid => "stub"; public string Version => "1.0.0"; public string DisplayName => "Stub"; public string Description => "Stub data producer for tests."; public Task IsEnabledAsync() => Task.FromResult(true); } }