diff --git a/src/Adapter/MSTest.TestAdapter/AdapterTestProperties.cs b/src/Adapter/MSTest.TestAdapter/AdapterTestProperties.cs new file mode 100644 index 0000000000..eaa2668c8d --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/AdapterTestProperties.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +/// +/// The VSTest object-model registrations used by the adapter to carry MSTest +/// discovery/execution metadata (and host test-case-management values) on VSTest test cases and results. +/// These live in the adapter layer because they depend on the VSTest object model; the platform services +/// engine describes the same information with its own neutral types. +/// +internal static class AdapterTestProperties +{ + #region Test Property registration + internal static readonly TestProperty WorkItemIdsProperty = TestProperty.Register("WorkItemIds", WorkItemIdsLabel, typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestClassNameProperty = TestProperty.Register("MSTestDiscoverer.TestClassName", TestClassNameLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + +#pragma warning disable CS0618 // Type or member is obsolete + internal static readonly TestProperty TestCategoryProperty = TestProperty.Register("MSTestDiscoverer.TestCategory", TestCategoryLabel, typeof(string[]), TestPropertyAttributes.Hidden | TestPropertyAttributes.Trait, typeof(TestCase)); +#pragma warning restore CS0618 // Type or member is obsolete + + internal static readonly TestProperty PriorityProperty = TestProperty.Register("MSTestDiscoverer.Priority", PriorityLabel, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + +#if !WINDOWS_UWP && !WIN_UI + internal static readonly TestProperty DeploymentItemsProperty = TestProperty.Register("MSTestDiscoverer.DeploymentItems", DeploymentItemsLabel, typeof(KeyValuePair[]), TestPropertyAttributes.Hidden, typeof(TestCase)); +#endif + + internal static readonly TestProperty DoNotParallelizeProperty = TestProperty.Register("MSTestDiscoverer.DoNotParallelize", DoNotParallelizeLabel, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty ExecutionIdProperty = TestProperty.Register("ExecutionId", ExecutionIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); + + internal static readonly TestProperty ParentExecIdProperty = TestProperty.Register("ParentExecId", ParentExecIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); + + internal static readonly TestProperty TestRunIdProperty = TestProperty.Register(TestRunId, TestRunId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestPlanIdProperty = TestProperty.Register(TestPlanId, TestPlanId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestCaseIdProperty = TestProperty.Register(TestCaseId, TestCaseId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestPointIdProperty = TestProperty.Register(TestPointId, TestPointId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestConfigurationIdProperty = TestProperty.Register(TestConfigurationId, TestConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestConfigurationNameProperty = TestProperty.Register(TestConfigurationName, TestConfigurationName, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty IsInLabEnvironmentProperty = TestProperty.Register(IsInLabEnvironment, IsInLabEnvironment, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildConfigurationIdProperty = TestProperty.Register(BuildConfigurationId, BuildConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildDirectoryProperty = TestProperty.Register(BuildDirectory, BuildDirectory, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildFlavorProperty = TestProperty.Register(BuildFlavor, BuildFlavor, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildNumberProperty = TestProperty.Register(BuildNumber, BuildNumber, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildPlatformProperty = TestProperty.Register(BuildPlatform, BuildPlatform, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildUriProperty = TestProperty.Register(BuildUri, BuildUri, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TfsServerCollectionUrlProperty = TestProperty.Register(TfsServerCollectionUrl, TfsServerCollectionUrl, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TfsTeamProjectProperty = TestProperty.Register(TfsTeamProject, TfsTeamProject, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty ParameterTypesProperty = TestProperty.Register("MSTest.ParameterTypes", "ParameterTypes", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestDynamicDataTypeProperty = TestProperty.Register("MSTest.DynamicDataType", "DynamicDataType", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestCaseIndexProperty = TestProperty.Register("MSTest.TestCaseIndex", "TestCaseIndex", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestDynamicDataProperty = TestProperty.Register("MSTest.DynamicData", "DynamicData", typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestDataSourceIgnoreMessageProperty = TestProperty.Register("MSTest.TestDataSourceIgnoreMessageProperty", "TestDataSourceIgnoreMessageProperty", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty UnfoldingStrategy = TestProperty.Register("MSTestDiscoverer.UnfoldingStrategy", "UnfoldingStrategy", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + #endregion + + #region Private Constants + + /// + /// These are the Test properties used by the adapter, which essentially correspond + /// to attributes on tests, and may be available in command line/TeamBuild to filter tests. + /// These Property names should not be localized. + /// + private const string TestClassNameLabel = "ClassName"; + private const string TestCategoryLabel = "TestCategory"; + private const string PriorityLabel = "Priority"; +#if !WINDOWS_UWP && !WIN_UI + private const string DeploymentItemsLabel = "DeploymentItems"; +#endif + private const string DoNotParallelizeLabel = "DoNotParallelize"; + private const string ExecutionIdLabel = "ExecutionId"; + private const string ParentExecIdLabel = "ParentExecId"; + private const string WorkItemIdsLabel = "WorkItemIds"; + + private const string TestRunId = "__Tfs_TestRunId__"; + private const string TestPlanId = "__Tfs_TestPlanId__"; + private const string TestCaseId = "__Tfs_TestCaseId__"; + private const string TestPointId = "__Tfs_TestPointId__"; + private const string TestConfigurationId = "__Tfs_TestConfigurationId__"; + private const string TestConfigurationName = "__Tfs_TestConfigurationName__"; + private const string IsInLabEnvironment = "__Tfs_IsInLabEnvironment__"; + private const string BuildConfigurationId = "__Tfs_BuildConfigurationId__"; + private const string BuildDirectory = "__Tfs_BuildDirectory__"; + private const string BuildFlavor = "__Tfs_BuildFlavor__"; + private const string BuildNumber = "__Tfs_BuildNumber__"; + private const string BuildPlatform = "__Tfs_BuildPlatform__"; + private const string BuildUri = "__Tfs_BuildUri__"; + private const string TfsServerCollectionUrl = "__Tfs_TfsServerCollectionUrl__"; + private const string TfsTeamProject = "__Tfs_TeamProject__"; + + #endregion +} diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TcmTestPropertiesProvider.cs b/src/Adapter/MSTest.TestAdapter/Execution/TcmTestPropertiesProvider.cs new file mode 100644 index 0000000000..93e2892098 --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/Execution/TcmTestPropertiesProvider.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using TestPlatformObjectModel = Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; + +/// +/// Reads and parses the test-case-management (TCM) properties supplied by a host on a test case, in order to +/// surface them to the running test through TestContext. +/// +/// +/// This is a translation point between the VSTest test-case object model and the neutral execution context bag +/// consumed by the platform services engine. It runs at the adapter boundary (when a host hands the adapter +/// concrete test cases to run) and produces a string-keyed dictionary so the engine never depends on the VSTest +/// TestProperty object model. It is expected to move entirely into the adapter layer once the platform +/// services no longer reference the VSTest object model. +/// +internal static class TcmTestPropertiesProvider +{ + /// + /// Gets the host execution context properties from a test case, keyed by the host property identifier. + /// + /// Test case. + /// The properties, or when the test case is null or carries no test case id. + public static IReadOnlyDictionary? GetTcmProperties(TestPlatformObjectModel.TestCase? testCase) + { + // Return empty properties when testCase is null or when test case id is zero. + if (testCase == null || + testCase.GetPropertyValue(AdapterTestProperties.TestCaseIdProperty, default) == 0) + { + return null; + } + + var tcmProperties = new Dictionary(capacity: 15) + { + // Step 1: Add common properties. + [AdapterTestProperties.TestRunIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestRunIdProperty, default), + [AdapterTestProperties.TestPlanIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestPlanIdProperty, default), + [AdapterTestProperties.BuildConfigurationIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildConfigurationIdProperty, default), + [AdapterTestProperties.BuildDirectoryProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildDirectoryProperty, default), + [AdapterTestProperties.BuildFlavorProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildFlavorProperty, default), + [AdapterTestProperties.BuildNumberProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildNumberProperty, default), + [AdapterTestProperties.BuildPlatformProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildPlatformProperty, default), + [AdapterTestProperties.BuildUriProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildUriProperty, default), + [AdapterTestProperties.TfsServerCollectionUrlProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TfsServerCollectionUrlProperty, default), + [AdapterTestProperties.TfsTeamProjectProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TfsTeamProjectProperty, default), + [AdapterTestProperties.IsInLabEnvironmentProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.IsInLabEnvironmentProperty, default), + + // Step 2: Add test case specific properties. + [AdapterTestProperties.TestCaseIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestCaseIdProperty, default), + [AdapterTestProperties.TestConfigurationIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestConfigurationIdProperty, default), + [AdapterTestProperties.TestConfigurationNameProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestConfigurationNameProperty, default), + [AdapterTestProperties.TestPointIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestPointIdProperty, default), + }; + + return tcmProperties; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs similarity index 86% rename from src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs rename to src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs index 4c69d171ee..b3c757cfad 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -89,53 +88,53 @@ internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase } string? managedTypeName = testCase.GetManagedType(); - string? legacyTestClassName = testCase.GetPropertyValue(EngineConstants.TestClassNameProperty) as string; + string? legacyTestClassName = testCase.GetPropertyValue(AdapterTestProperties.TestClassNameProperty) as string; string? testClassName = testCase.GetClassNameWhenFullyQualifiedNameStartsWith(managedTypeName) ?? testCase.GetClassNameWhenFullyQualifiedNameStartsWith(legacyTestClassName) ?? managedTypeName ?? legacyTestClassName; string name = testCase.GetTestName(testClassName); - var testMethod = new TestMethod(testCase.GetManagedMethod(), testCase.GetHierarchy(), name, testClassName!, source, testCase.DisplayName, testCase.GetPropertyValue(EngineConstants.ParameterTypesProperty, null)); + var testMethod = new TestMethod(testCase.GetManagedMethod(), testCase.GetHierarchy(), name, testClassName!, source, testCase.DisplayName, testCase.GetPropertyValue(AdapterTestProperties.ParameterTypesProperty, null)); - var dataType = (DynamicDataType)testCase.GetPropertyValue(EngineConstants.TestDynamicDataTypeProperty, (int)DynamicDataType.None); + var dataType = (DynamicDataType)testCase.GetPropertyValue(AdapterTestProperties.TestDynamicDataTypeProperty, (int)DynamicDataType.None); if (dataType != DynamicDataType.None) { - string[]? data = testCase.GetPropertyValue(EngineConstants.TestDynamicDataProperty, null); + string[]? data = testCase.GetPropertyValue(AdapterTestProperties.TestDynamicDataProperty, null); testMethod.DataType = dataType; testMethod.SerializedData = data; - testMethod.TestCaseIndex = testCase.GetPropertyValue(EngineConstants.TestCaseIndexProperty, 0); - testMethod.TestDataSourceIgnoreMessage = testCase.GetPropertyValue(EngineConstants.TestDataSourceIgnoreMessageProperty) as string; + testMethod.TestCaseIndex = testCase.GetPropertyValue(AdapterTestProperties.TestCaseIndexProperty, 0); + testMethod.TestDataSourceIgnoreMessage = testCase.GetPropertyValue(AdapterTestProperties.TestDataSourceIgnoreMessageProperty) as string; } var testElement = new UnitTestElement(testMethod) { - TestCategory = testCase.GetPropertyValue(EngineConstants.TestCategoryProperty) as string[], - Priority = testCase.GetPropertyValue(EngineConstants.PriorityProperty) as int?, - UnfoldingStrategy = (TestDataSourceUnfoldingStrategy)testCase.GetPropertyValue(EngineConstants.UnfoldingStrategy, (int)TestDataSourceUnfoldingStrategy.Auto), + TestCategory = testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty) as string[], + Priority = testCase.GetPropertyValue(AdapterTestProperties.PriorityProperty) as int?, + UnfoldingStrategy = (TestDataSourceUnfoldingStrategy)testCase.GetPropertyValue(AdapterTestProperties.UnfoldingStrategy, (int)TestDataSourceUnfoldingStrategy.Auto), }; if (testCase.Traits.Any()) { - testElement.Traits = [.. testCase.Traits]; + testElement.Traits = [.. testCase.Traits.Select(t => new TestTrait(t.Name, t.Value))]; } - string[]? workItemIds = testCase.GetPropertyValue(EngineConstants.WorkItemIdsProperty, null); + string[]? workItemIds = testCase.GetPropertyValue(AdapterTestProperties.WorkItemIdsProperty, null); if (workItemIds is { Length: > 0 }) { testElement.WorkItemIds = workItemIds; } #if !WINDOWS_UWP && !WIN_UI - KeyValuePair[]? deploymentItems = testCase.GetPropertyValue[]>(EngineConstants.DeploymentItemsProperty, null); + KeyValuePair[]? deploymentItems = testCase.GetPropertyValue[]>(AdapterTestProperties.DeploymentItemsProperty, null); if (deploymentItems is { Length: > 0 }) { testElement.DeploymentItems = deploymentItems; } #endif - testElement.DoNotParallelize = testCase.GetPropertyValue(EngineConstants.DoNotParallelizeProperty, false); + testElement.DoNotParallelize = testCase.GetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, false); return testElement; } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestResultExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs similarity index 93% rename from src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestResultExtensions.cs rename to src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs index 0eac8b99a2..834b522ee8 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestResultExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; using VSTestAttachmentSet = Microsoft.VisualStudio.TestPlatform.ObjectModel.AttachmentSet; @@ -48,8 +49,8 @@ internal static VSTestTestResult ToTestResult(this TestResult frameworkTestResul ComputerName = computerName, }; - testResult.SetPropertyValue(EngineConstants.ExecutionIdProperty, frameworkTestResult.ExecutionId); - testResult.SetPropertyValue(EngineConstants.ParentExecIdProperty, frameworkTestResult.ParentExecId); + testResult.SetPropertyValue(AdapterTestProperties.ExecutionIdProperty, frameworkTestResult.ExecutionId); + testResult.SetPropertyValue(AdapterTestProperties.ParentExecIdProperty, frameworkTestResult.ParentExecId); if (!StringEx.IsNullOrEmpty(frameworkTestResult.LogOutput)) { diff --git a/src/Adapter/MSTest.TestAdapter/Extensions/UnitTestElementExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/UnitTestElementExtensions.cs new file mode 100644 index 0000000000..c6a82a8870 --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/Extensions/UnitTestElementExtensions.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; + +/// +/// Materializes a VSTest from a platform-agnostic . This is the +/// adapter-boundary translation from the neutral execution model to the VSTest object model; the platform services +/// engine never performs it and only threads the resulting test case (as an opaque handle) to the recorder and the +/// deployment boundary. +/// +internal static class UnitTestElementExtensions +{ + private static readonly byte[] OpenParen = [40, 0]; // Encoding.Unicode.GetBytes("("); + private static readonly byte[] CloseParen = [41, 0]; // Encoding.Unicode.GetBytes(")"); + private static readonly byte[] OpenBracket = [91, 0]; // Encoding.Unicode.GetBytes("["); + private static readonly byte[] CloseBracket = [93, 0]; // Encoding.Unicode.GetBytes("]"); + + /// + /// Returns the host test case for this test, reusing the host-provided one when present and otherwise + /// materializing a single VSTest test case on demand and caching it (so deployment, test-start and every + /// reported result share one instance, matching the historical "one test case per discovered test"). + /// + internal static TestCase GetOrCreateHostTestCase(this UnitTestElement element) + { + if (element.HostRecordingHandle is not TestCase testCase) + { + testCase = element.ToTestCase(); + element.HostRecordingHandle = testCase; + } + + return testCase; + } + + /// + /// Convert the UnitTestElement instance to an Object Model testCase instance. + /// + /// The unit test element to convert. + /// An instance of . + internal static TestCase ToTestCase(this UnitTestElement element) + { + TestMethod testMethod = element.TestMethod; + + // This causes compatibility problems with older runners. + // string testFullName = this.TestMethod.HasManagedMethodAndTypeProperties + // ? $"{TestMethod.ManagedTypeName}.{TestMethod.ManagedMethodName}" + // : $"{TestMethod.FullClassName}.{TestMethod.Name}"; + string testFullName = $"{testMethod.FullClassName}.{testMethod.Name}"; + + TestCase testCase = new(testFullName, EngineConstants.ExecutorUri, testMethod.AssemblyName) + { + DisplayName = testMethod.DisplayName, + LocalExtensionData = element, + }; + + if (testMethod.HasManagedMethodAndTypeProperties) + { + testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, testMethod.ManagedTypeName); + testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, testMethod.ManagedMethodName); + } + + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, testMethod.FullClassName); + + if (testMethod.ParameterTypes is not null) + { + testCase.SetPropertyValue(AdapterTestProperties.ParameterTypesProperty, testMethod.ParameterTypes); + } + + IReadOnlyCollection? hierarchy = testMethod.Hierarchy; + if (hierarchy is { Count: > 0 }) + { + testCase.SetHierarchy([.. hierarchy]); + } + + // Set only if some test category is present + if (element.TestCategory is { Length: > 0 }) + { + testCase.SetPropertyValue(AdapterTestProperties.TestCategoryProperty, element.TestCategory); + } + + // Set priority if present + if (element.Priority != null) + { + testCase.SetPropertyValue(AdapterTestProperties.PriorityProperty, element.Priority.Value); + } + + if (element.Traits is { Length: > 0 }) + { + foreach (TestTrait trait in element.Traits) + { + testCase.Traits.Add(new Trait(trait.Name, trait.Value)); + } + } + + if (element.WorkItemIds != null) + { + testCase.SetPropertyValue(AdapterTestProperties.WorkItemIdsProperty, element.WorkItemIds); + } + +#if !WINDOWS_UWP && !WIN_UI + // The list of items to deploy before running this test. + if (element.DeploymentItems is { Length: > 0 }) + { + testCase.SetPropertyValue(AdapterTestProperties.DeploymentItemsProperty, element.DeploymentItems); + } +#endif + + // Set the Do not parallelize state if present + if (element.DoNotParallelize) + { + testCase.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, element.DoNotParallelize); + } + + if (element.UnfoldingStrategy != TestDataSourceUnfoldingStrategy.Auto) + { + testCase.SetPropertyValue(AdapterTestProperties.UnfoldingStrategy, (int)element.UnfoldingStrategy); + } + + // Store resolved data if any + if (testMethod.DataType != DynamicDataType.None) + { + testCase.SetPropertyValue(AdapterTestProperties.TestDynamicDataTypeProperty, (int)testMethod.DataType); + testCase.SetPropertyValue(AdapterTestProperties.TestDynamicDataProperty, testMethod.SerializedData); + testCase.SetPropertyValue(AdapterTestProperties.TestCaseIndexProperty, testMethod.TestCaseIndex); + // VSTest serialization doesn't handle null so instead don't set the property so that it's deserialized as null + if (testMethod.TestDataSourceIgnoreMessage is not null) + { + testCase.SetPropertyValue(AdapterTestProperties.TestDataSourceIgnoreMessageProperty, testMethod.TestDataSourceIgnoreMessage); + } + } + + testCase.LineNumber = element.DeclaringLineNumber ?? -1; + testCase.CodeFilePath = element.DeclaringFilePath; + + testCase.Id = GenerateSerializedDataStrategyTestId(element, testFullName); + + return testCase; + } + + private static Guid GenerateSerializedDataStrategyTestId(UnitTestElement element, string testFullName) + { + TestMethod testMethod = element.TestMethod; + + // Below comment is copied over from Test Platform. + // If source is a file name then just use the filename for the identifier since the file might have moved between + // discovery and execution (in appx mode for example). This is not elegant because the Source contents should be + // a black box to the framework. + // For example in the database adapter case this is not a file path. + // As discussed with team, we found no scenario for netcore, & fullclr where the Source is not present where ID + // is generated, which means we would always use FileName to generate ID. In cases where somehow Source Path + // contained garbage character the API Path.GetFileName() we are simply returning original input. + // For UWP where source during discovery, & during execution can be on different machine, in such case we should + // always use Path.GetFileName(). + string fileNameOrFilePath = testMethod.AssemblyName; + try + { + fileNameOrFilePath = Path.GetFileName(fileNameOrFilePath); + } + catch (ArgumentException) + { + // In case path contains invalid characters. + } + + byte hashVersion = 1; // Increment when changing the hashing algorithm. + var hash = new TestFx.Hashing.XxHash128(); + hash.Append(Encoding.Unicode.GetBytes(fileNameOrFilePath)); + hash.Append(Encoding.Unicode.GetBytes(testFullName)); + if (testMethod.ParameterTypes is not null) + { + hash.Append(OpenParen); + hash.Append(Encoding.Unicode.GetBytes(testMethod.ParameterTypes)); + hash.Append(CloseParen); + } + + if (testMethod.DataType != DynamicDataType.None) + { + hash.Append(OpenBracket); + hash.Append(Encoding.Unicode.GetBytes(testMethod.TestCaseIndex.ToString(CultureInfo.InvariantCulture))); + hash.Append(CloseBracket); + } + + byte[] hashBytes = hash.GetCurrentHash(); + + return VersionedGuidFromHash(hashBytes, hashVersion); + } + + internal /* for testing */ static Guid VersionedGuidFromHash(byte[] hashBytes, byte hashVersion) + { + int firstByte = 0; + int versionByte = 6; + + // We set first 4 bits to 0001, which is our version. Increase this when we change the hashing algorithm. + // + // Note: The logic below is operating on int32, because bitwise operators are not defined for byte in C#. But casting + // does not affect endianness, so we can safely assume the byte data are at the end of the int. + // We also don't care to specify the whole expansion of the int in the bit masks, which effectively ignores all the data in the int + // before the last byte data, but that is okay, they will always be zero. + hashBytes[firstByte] = (byte)((hashBytes[firstByte] & 0b0000_1111) | (hashVersion << 4)); + // We set first 4bits 7th byte to 8 to indicate that this is a version 8 UUID. https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-8 + hashBytes[versionByte] = (byte)((hashBytes[versionByte] & 0b0000_1111) | 0b1000_0000); + + // We set the first 2 bits of nineth byte to 0b10. In the guid this is stored as byte, so we don't care about endiannes. + int variantByte = 8; + hashBytes[variantByte] = (byte)((hashBytes[variantByte] & 0b0011_1111) | 0b1000_0000); + + Guid guid; + + // On .NET Framework we cannot specify the endianness of the Guid because the new Guid(hashBytes, bigEndian: true); api is not available. + // Instead we construct the int and short values manually, doing what the constructor would do, to put the bytes in big-endian order. + guid = new Guid( + (hashBytes[0] << 24) | (hashBytes[1] << 16) | (hashBytes[2] << 8) | hashBytes[3], + (short)((hashBytes[4] << 8) | hashBytes[5]), + (short)((hashBytes[6] << 8) | hashBytes[7]), + hashBytes[8], hashBytes[9], hashBytes[10], hashBytes[11], hashBytes[12], hashBytes[13], hashBytes[14], hashBytes[15]); + +#if DEBUG && NET9_0_OR_GREATER + // Version property is only available on .NET 9 and later. + Debug.Assert(guid.Version == 8, $"Expected Guid version to be 8, but it was {guid.Version}"); + // The field represents the 4 bit value, but according to the specification only the first 2 bits are used for UUID v8. + // So we shift 2 bits to the right to get the actual variant value. + int variant = guid.Variant >> 2; + Debug.Assert(variant == 2, $"Expected Guid variant to be 2, but it was {variant}"); +#endif + return guid; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/UnitTestOutcomeHelper.cs b/src/Adapter/MSTest.TestAdapter/Helpers/UnitTestOutcomeHelper.cs similarity index 100% rename from src/Adapter/MSTestAdapter.PlatformServices/Helpers/UnitTestOutcomeHelper.cs rename to src/Adapter/MSTest.TestAdapter/Helpers/UnitTestOutcomeHelper.cs diff --git a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj index 20616c2118..b4792d9cad 100644 --- a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj +++ b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj @@ -193,6 +193,7 @@ + diff --git a/src/Adapter/MSTest.TestAdapter/Services/AdapterMessageLoggerExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/AdapterMessageLoggerExtensions.cs new file mode 100644 index 0000000000..b8d4ca1b42 --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/Services/AdapterMessageLoggerExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +/// +/// Bridges a VSTest to the platform-agnostic . +/// +/// +/// This is the single translation point between the VSTest object model and the platform services +/// message-logging abstraction. It is expected to move entirely into the adapter layer once the +/// execution pipeline no longer flows VSTest handles through the platform services (see the tracking +/// issue linked in the pull request that removes the VSTest object model from platform services). +/// +internal static class AdapterMessageLoggerExtensions +{ + /// + /// Wraps a VSTest as an . + /// + /// The host message logger to wrap. + /// A platform-agnostic logger that forwards to . + internal static IAdapterMessageLogger ToAdapterMessageLogger(this IMessageLogger messageLogger) + => new HostMessageLogger(messageLogger ?? throw new ArgumentNullException(nameof(messageLogger))); + + private sealed class HostMessageLogger : IAdapterMessageLogger + { + private readonly IMessageLogger _messageLogger; + + public HostMessageLogger(IMessageLogger messageLogger) + => _messageLogger = messageLogger; + + public void SendMessage(MessageLevel level, string message) + => _messageLogger.SendMessage(level.ToTestMessageLevel(), message); + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/MessageLevel.cs b/src/Adapter/MSTest.TestAdapter/Services/MessageLevel.cs similarity index 100% rename from src/Adapter/MSTestAdapter.PlatformServices/Services/MessageLevel.cs rename to src/Adapter/MSTest.TestAdapter/Services/MessageLevel.cs diff --git a/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs new file mode 100644 index 0000000000..317159ed4a --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +/// +/// Bridges a VSTest to the platform-agnostic . +/// +/// +/// This is the single translation point between the neutral execution model ( and +/// the framework ) and the VSTest result object model (TestResult, +/// TestOutcome, attachments, ...). It lives in the adapter layer; the platform services engine reports +/// through the neutral only. The element's host test case is resolved (and +/// materialized/cached on demand for internally discovered tests) via UnitTestElementExtensions.GetOrCreateHostTestCase +/// so recorded results preserve host-injected data (test-case-management / data-collector properties) with full fidelity. +/// +internal static class TestResultRecorderExtensions +{ + /// + /// Wraps a VSTest as an . + /// + /// The host recorder to wrap. + /// The computer name stamped on reported results. + /// The current MSTest settings used to map framework outcomes to host outcomes. + /// A platform-agnostic recorder that forwards to . + public static ITestResultRecorder ToTestResultRecorder(this ITestExecutionRecorder testExecutionRecorder, string computerName, MSTestSettings settings) + => new HostTestResultRecorder(testExecutionRecorder, computerName, settings); + + private sealed class HostTestResultRecorder : ITestResultRecorder + { + private readonly ITestExecutionRecorder _testExecutionRecorder; + private readonly string _computerName; + private readonly MSTestSettings _settings; + + public HostTestResultRecorder(ITestExecutionRecorder testExecutionRecorder, string computerName, MSTestSettings settings) + { + _testExecutionRecorder = testExecutionRecorder; + _computerName = computerName; + _settings = settings; + } + + public void RecordStart(UnitTestElement testElement) + => _testExecutionRecorder.RecordStart(testElement.GetOrCreateHostTestCase()); + + public void RecordEmptyResult(UnitTestElement testElement) + => _testExecutionRecorder.RecordEnd(testElement.GetOrCreateHostTestCase(), TestOutcome.None); + + public bool RecordResult(UnitTestElement testElement, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime) + { + TestCase testCase = testElement.GetOrCreateHostTestCase(); + var testResult = unitTestResult.ToTestResult(testCase, startTime, endTime, _computerName, _settings); + + _testExecutionRecorder.RecordEnd(testCase, testResult.Outcome); + + bool isFailed = testResult.Outcome == TestOutcome.Failed; + if (isFailed && PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) + { + PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor:Test {0} failed. ErrorMessage:{1}, ErrorStackTrace:{2}.", testResult.TestCase.FullyQualifiedName, testResult.ErrorMessage, testResult.ErrorStackTrace); + } + + try + { + if (testResult.Outcome != TestOutcome.NotFound + || !RuntimeContext.IsHotReloadEnabled) + { + _testExecutionRecorder.RecordResult(testResult); + } + } + catch (TestCanceledException) + { + // Ignore this exception + } + + // A failure is reported only once RecordResult has completed (a swallowed TestCanceledException + // still counts as completed). This mirrors the original inline flow where the failure was + // observed as part of reporting the result. + return isFailed; + } + } +} diff --git a/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs new file mode 100644 index 0000000000..79ac2f6772 --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +/// +/// Bridges a VSTest to the platform-agnostic +/// . +/// +/// +/// This is the single translation point between the neutral model and the +/// VSTest discovery object model (TestCase, ITestCaseDiscoverySink). It materializes a +/// VSTest TestCase for each discovered element via UnitTestElementExtensions.ToTestCase. It is +/// expected to move entirely into the adapter layer once discovery no longer flows VSTest discovery sinks +/// through the platform services (see the tracking issue linked in the pull request that removes the VSTest +/// object model from platform services). +/// +internal static class UnitTestElementSinkExtensions +{ + /// + /// Wraps a VSTest as an . + /// + /// The host discovery sink to wrap. + /// A platform-agnostic sink that forwards to . + internal static IUnitTestElementSink ToUnitTestElementSink(this ITestCaseDiscoverySink discoverySink) + => new HostDiscoverySink(discoverySink ?? throw new ArgumentNullException(nameof(discoverySink))); + + private sealed class HostDiscoverySink : IUnitTestElementSink + { + private readonly ITestCaseDiscoverySink _discoverySink; + + public HostDiscoverySink(ITestCaseDiscoverySink discoverySink) + => _discoverySink = discoverySink; + + public void SendTestElement(UnitTestElement testElement) + => _discoverySink.SendTestCase(testElement.ToTestCase()); + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs b/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs similarity index 61% rename from src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs rename to src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs index 481220ad0c..4a6d42ea3c 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs +++ b/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -18,12 +19,12 @@ internal sealed class TestMethodFilter internal TestMethodFilter() => _supportedProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { - [EngineConstants.TestCategoryProperty.Label] = EngineConstants.TestCategoryProperty, - [EngineConstants.PriorityProperty.Label] = EngineConstants.PriorityProperty, + [AdapterTestProperties.TestCategoryProperty.Label] = AdapterTestProperties.TestCategoryProperty, + [AdapterTestProperties.PriorityProperty.Label] = AdapterTestProperties.PriorityProperty, [TestCaseProperties.FullyQualifiedName.Label] = TestCaseProperties.FullyQualifiedName, [TestCaseProperties.DisplayName.Label] = TestCaseProperties.DisplayName, [TestCaseProperties.Id.Label] = TestCaseProperties.Id, - [EngineConstants.TestClassNameProperty.Label] = EngineConstants.TestClassNameProperty, + [AdapterTestProperties.TestClassNameProperty.Label] = AdapterTestProperties.TestClassNameProperty, }; /// @@ -33,7 +34,7 @@ internal sealed class TestMethodFilter /// Handler to report test messages/start/end and results. /// Indicates that the filter is unsupported/has an error. /// A filter expression. - internal ITestCaseFilterExpression? GetFilterExpression(IDiscoveryContext? context, IMessageLogger logger, out bool filterHasError) + internal ITestCaseFilterExpression? GetFilterExpression(IDiscoveryContext? context, IAdapterMessageLogger logger, out bool filterHasError) { filterHasError = false; if (context == null) @@ -51,12 +52,30 @@ internal sealed class TestMethodFilter catch (TestPlatformFormatException ex) { filterHasError = true; - logger.SendMessage(TestMessageLevel.Error, ex.Message); + logger.SendMessage(MessageLevel.Error, ex.Message); } return filter; } + /// + /// Builds a platform-agnostic for the properties supported by the adapter. + /// + /// The current context of the run. + /// Handler to report test messages/start/end and results. + /// Indicates that the filter is unsupported/has an error. + /// + /// A filter that evaluates instances, or when no + /// filter applies (in which case every element is included). + /// + internal ITestElementFilter? GetTestElementFilter(IDiscoveryContext? context, IAdapterMessageLogger logger, out bool filterHasError) + { + ITestCaseFilterExpression? filterExpression = GetFilterExpression(context, logger, out filterHasError); + return filterExpression is null + ? null + : new TestElementFilter(this, filterExpression); + } + /// /// Provides TestProperty for property name 'propertyName' as used in filter. /// @@ -118,7 +137,7 @@ internal TestProperty PropertyProvider(string propertyName) /// The logger to log exception messages too. /// Filter expression. [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "GetTestCaseFilter is part of the VSTest discovery contract on the concrete DiscoveryContext type; the runtime guarantees the method exists on supported hosts.")] - private ITestCaseFilterExpression? GetTestCaseFilterFromDiscoveryContext(IDiscoveryContext context, IMessageLogger logger) + private ITestCaseFilterExpression? GetTestCaseFilterFromDiscoveryContext(IDiscoveryContext context, IAdapterMessageLogger logger) { try { @@ -134,9 +153,49 @@ internal TestProperty PropertyProvider(string propertyName) } catch (Exception ex) { - logger.SendMessage(TestMessageLevel.Warning, ex.Message); + logger.SendMessage(MessageLevel.Warning, ex.Message); } return null; } + + /// + /// Platform-agnostic filter that evaluates a against a VSTest + /// . This is the single point where the neutral element model is + /// translated to the VSTest test case in order to reuse the host's filter matching. + /// + private sealed class TestElementFilter : ITestElementFilter + { + private readonly TestMethodFilter _testMethodFilter; + private readonly ITestCaseFilterExpression _filterExpression; + + public TestElementFilter(TestMethodFilter testMethodFilter, ITestCaseFilterExpression filterExpression) + { + _testMethodFilter = testMethodFilter; + _filterExpression = filterExpression; + } + + public bool Matches(UnitTestElement testElement) + { + var testCase = testElement.ToTestCase(); + return _filterExpression.MatchTestCase(testCase, propertyName => _testMethodFilter.PropertyValueProvider(testCase, propertyName)); + } + } +} + +/// +/// Adapter-boundary that closes over the VSTest discovery/run context +/// and produces the neutral via . Created at the +/// boundary and passed into the platform services engine/discoverer so the filter (and any parse-error report) +/// is built at the exact point the engine needs it, preserving the original per-source timing. +/// +internal sealed class TestElementFilterProvider : ITestElementFilterProvider +{ + private readonly TestMethodFilter _testMethodFilter = new(); + private readonly IDiscoveryContext? _context; + + public TestElementFilterProvider(IDiscoveryContext? context) => _context = context; + + public ITestElementFilter? GetTestElementFilter(IAdapterMessageLogger logger, out bool filterHasError) + => _testMethodFilter.GetTestElementFilter(_context, logger, out filterHasError); } diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs index a98e00c83a..ebe26f397c 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs @@ -84,9 +84,10 @@ internal async Task DiscoverTestsAsync(IEnumerable sources, IDiscoveryCo try { - if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, logger, configuration, _testSourceHandler)) + IAdapterMessageLogger adapterLogger = logger.ToAdapterMessageLogger(); + if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext?.RunSettings?.SettingsXml, adapterLogger, configuration, _testSourceHandler)) { - new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, logger, discoverySink, discoveryContext, isMTP); + new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext?.RunSettings?.SettingsXml, new TestElementFilterProvider(discoveryContext), isMTP); } } finally diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs index 7518f455a7..f1fd235958 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs @@ -2,8 +2,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.VSTestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; @@ -139,12 +142,26 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run try { - if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext, frameworkHandle, configuration, new TestSourceHandler())) + if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger(), configuration, new TestSourceHandler())) { return; } - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(tests, runContext, frameworkHandle, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + // Convert the host test cases into the neutral model at this boundary so the execution engine runs + // on UnitTestElement end-to-end. Each element keeps its originating test case as an opaque handle + // (for full-fidelity result recording and deployment) plus the host execution-context (TCM) + // properties surfaced through TestContext. + UnitTestElement[] testElements = [.. tests.Select(ToUnitTestElement)]; + + // Translate the VSTest recorder into the platform-agnostic result recorder at this boundary; the + // execution engine reports results through this neutral recorder and never constructs VSTest results. + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + + // Extract the neutral run inputs (test-run directory + run settings XML) from the host run context at + // this boundary so the execution engine no longer depends on the VSTest run context. + var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); + + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, deploymentContext, frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { @@ -152,6 +169,14 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run } } + private static UnitTestElement ToUnitTestElement(TestCase testCase) + { + UnitTestElement testElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); + testElement.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + testElement.HostRecordingHandle = testCase; + return testElement; + } + internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? runContext, IFrameworkHandle? frameworkHandle, IConfiguration? configuration, bool isMTP) { if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) @@ -183,13 +208,22 @@ internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? run try { TestSourceHandler testSourceHandler = new(); - if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext, frameworkHandle, configuration, testSourceHandler)) + if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger(), configuration, testSourceHandler)) { return; } sources = testSourceHandler.GetTestSources(sources); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + + // Translate the VSTest recorder into the platform-agnostic result recorder at this boundary; the + // execution engine reports results through this neutral recorder and never constructs VSTest results. + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + + // Extract the neutral run inputs (test-run directory + run settings XML) from the host run context at + // this boundary so the execution engine no longer depends on the VSTest run context. + var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); + + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, deploymentContext, frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.cs b/src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.cs new file mode 100644 index 0000000000..4bb4f09f3a --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +/// +/// Apartment state parsed from the ExecutionThreadApartmentState run settings and the +/// mstest:execution:executionApartmentState configuration value. This is the adapter's own neutral +/// replacement for the former VSTest PlatformAbstractions.PlatformApartmentState enum. +/// +internal enum ApartmentStateSetting +{ + // The member order is load-bearing: Enum.TryParse maps numeric-string run settings values ("0"/"1") + // by number, so MTA must stay 0 and STA must stay 1 to preserve parse compatibility with the former + // VSTest PlatformAbstractions.PlatformApartmentState enum. Do not reorder. + MTA, + STA, +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs new file mode 100644 index 0000000000..e6c35de10c --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; + +/// +/// Platform-agnostic inputs the execution pipeline needs from the running test host: the test-run/results +/// directory and the run settings XML. This lets the platform services execution and deployment layers run +/// without taking a dependency on a specific test platform's run-context object model (for example the VSTest +/// IRunContext / IRunSettings types). It is populated at the adapter boundary. +/// +internal sealed class DeploymentContext +{ + public DeploymentContext(string? testRunDirectory, string? runSettingsXml) + { + TestRunDirectory = testRunDirectory; + RunSettingsXml = runSettingsXml; + } + + /// + /// Gets the host-provided test-run directory (the root under which results/deployment directories are + /// created), or to fall back to a temp directory. + /// + public string? TestRunDirectory { get; } + + /// + /// Gets the run settings XML supplied by the host, or when none was provided. + /// + public string? RunSettingsXml { get; } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs index 16e83134c8..454373159d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; @@ -23,7 +22,7 @@ internal sealed class AssemblyEnumeratorWrapper /// Gets test elements from an assembly. /// /// A collection of test elements. - internal static ICollection? GetTests(string? assemblyFileName, IRunSettings? runSettings, ITestSourceHandler testSourceHandler, bool isMTP, out List warnings) + internal static ICollection? GetTests(string? assemblyFileName, string? settingsXml, ITestSourceHandler testSourceHandler, bool isMTP, out List warnings) { warnings = []; @@ -48,7 +47,7 @@ internal sealed class AssemblyEnumeratorWrapper try { // Load the assembly in isolation if required. - AssemblyEnumerationResult result = GetTestsInIsolation(fullFilePath, runSettings, isMTP); + AssemblyEnumerationResult result = GetTestsInIsolation(fullFilePath, settingsXml, isMTP); warnings.AddRange(result.Warnings); return result.TestElements; } @@ -86,9 +85,9 @@ internal sealed class AssemblyEnumeratorWrapper } } - private static AssemblyEnumerationResult GetTestsInIsolation(string fullFilePath, IRunSettings? runSettings, bool isMTP) + private static AssemblyEnumerationResult GetTestsInIsolation(string fullFilePath, string? settingsXml, bool isMTP) { - using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(fullFilePath, runSettings); + using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(fullFilePath, settingsXml); // Create an instance of a type defined in adapter so that adapter gets loaded in the child app domain var assemblyEnumerator = (AssemblyEnumerator)isolationHost.CreateInstanceForType(typeof(AssemblyEnumerator), [MSTestSettings.CurrentSettings])!; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs index fad95daf23..1ae8056ed7 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs @@ -4,8 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -13,13 +11,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; [SuppressMessage("Performance", "CA1852: Seal internal types", Justification = "Overrides required for testability")] internal class UnitTestDiscoverer { - private readonly TestMethodFilter _testMethodFilter; - - internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) - { - _testMethodFilter = new TestMethodFilter(); - _testSource = testSourceHandler; - } + internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) => _testSource = testSourceHandler; /// /// Discovers the tests available from the provided sources. @@ -27,18 +19,20 @@ internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) /// The sources. /// The logger. /// The discovery Sink. - /// The discovery context. + /// The run settings XML, or when none was provided. + /// Provider for the test filter, or for no filter. /// Flag set to true when the platform running discovery is MTP. internal void DiscoverTests( IEnumerable sources, - IMessageLogger logger, - ITestCaseDiscoverySink discoverySink, - IDiscoveryContext discoveryContext, + IAdapterMessageLogger logger, + IUnitTestElementSink discoverySink, + string? settingsXml, + ITestElementFilterProvider? filterProvider, bool isMTP) { foreach (string source in sources) { - DiscoverTestsInSource(source, logger, discoverySink, discoveryContext, isMTP); + DiscoverTestsInSource(source, logger, discoverySink, settingsXml, filterProvider, isMTP); } } @@ -48,16 +42,18 @@ internal void DiscoverTests( /// The source. /// The logger. /// The discovery Sink. - /// The discovery context. + /// The run settings XML, or when none was provided. + /// Provider for the test filter, or for no filter. /// Flag set to true when the platform running discovery is MTP. internal virtual void DiscoverTestsInSource( string source, - IMessageLogger logger, - ITestCaseDiscoverySink discoverySink, - IDiscoveryContext? discoveryContext, + IAdapterMessageLogger logger, + IUnitTestElementSink discoverySink, + string? settingsXml, + ITestElementFilterProvider? filterProvider, bool isMTP) { - ICollection? testElements = AssemblyEnumeratorWrapper.GetTests(source, discoveryContext?.RunSettings, _testSource, isMTP, out List warnings); + ICollection? testElements = AssemblyEnumeratorWrapper.GetTests(source, settingsXml, _testSource, isMTP, out List warnings); if (MSTestSettings.CurrentSettings.TreatDiscoveryWarningsAsErrors) { @@ -86,7 +82,7 @@ internal virtual void DiscoverTestsInSource( } string message = string.Format(CultureInfo.CurrentCulture, Resource.DiscoveryWarning, source, warning); - logger.SendMessage(TestMessageLevel.Warning, message); + logger.SendMessage(MessageLevel.Warning, message); } } @@ -104,15 +100,16 @@ internal virtual void DiscoverTestsInSource( source); } - SendTestCases(testElements, discoverySink, discoveryContext, logger); + SendTestCases(testElements, discoverySink, filterProvider, logger); } private readonly ITestSourceHandler _testSource; - internal void SendTestCases(IEnumerable testElements, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, IMessageLogger logger) + internal static void SendTestCases(IEnumerable testElements, IUnitTestElementSink discoverySink, ITestElementFilterProvider? filterProvider, IAdapterMessageLogger logger) { - // Get filter expression and skip discovery in case filter expression has parsing error. - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, logger, out bool filterHasError); + // Get filter and skip discovery in case filter expression has parsing error. + bool filterHasError = false; + ITestElementFilter? filter = filterProvider?.GetTestElementFilter(logger, out filterHasError); if (filterHasError) { return; @@ -120,15 +117,13 @@ internal void SendTestCases(IEnumerable testElements, ITestCase foreach (UnitTestElement testElement in testElements) { - var testCase = testElement.ToTestCase(); - - // Filter tests based on test case filters - if (filterExpression != null && !filterExpression.MatchTestCase(testCase, p => _testMethodFilter.PropertyValueProvider(testCase, p))) + // Filter tests based on test case filters. + if (filter is not null && !filter.Matches(testElement)) { continue; } - discoverySink.SendTestCase(testCase); + discoverySink.SendTestElement(testElement); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs b/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs index 1fefb90f68..3b3a2f90ed 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; - namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; internal static class EngineConstants @@ -55,107 +53,6 @@ internal static class EngineConstants /// internal const string ClassCleanupFixtureTrait = "ClassCleanup"; - #region Test Property registration - internal static readonly TestProperty WorkItemIdsProperty = TestProperty.Register("WorkItemIds", WorkItemIdsLabel, typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestClassNameProperty = TestProperty.Register("MSTestDiscoverer.TestClassName", TestClassNameLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - -#pragma warning disable CS0618 // Type or member is obsolete - internal static readonly TestProperty TestCategoryProperty = TestProperty.Register("MSTestDiscoverer.TestCategory", TestCategoryLabel, typeof(string[]), TestPropertyAttributes.Hidden | TestPropertyAttributes.Trait, typeof(TestCase)); -#pragma warning restore CS0618 // Type or member is obsolete - - internal static readonly TestProperty PriorityProperty = TestProperty.Register("MSTestDiscoverer.Priority", PriorityLabel, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - -#if !WINDOWS_UWP && !WIN_UI - internal static readonly TestProperty DeploymentItemsProperty = TestProperty.Register("MSTestDiscoverer.DeploymentItems", DeploymentItemsLabel, typeof(KeyValuePair[]), TestPropertyAttributes.Hidden, typeof(TestCase)); -#endif - - internal static readonly TestProperty DoNotParallelizeProperty = TestProperty.Register("MSTestDiscoverer.DoNotParallelize", DoNotParallelizeLabel, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty ExecutionIdProperty = TestProperty.Register("ExecutionId", ExecutionIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); - - internal static readonly TestProperty ParentExecIdProperty = TestProperty.Register("ParentExecId", ParentExecIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); - - internal static readonly TestProperty TestRunIdProperty = TestProperty.Register(TestRunId, TestRunId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestPlanIdProperty = TestProperty.Register(TestPlanId, TestPlanId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestCaseIdProperty = TestProperty.Register(TestCaseId, TestCaseId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestPointIdProperty = TestProperty.Register(TestPointId, TestPointId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestConfigurationIdProperty = TestProperty.Register(TestConfigurationId, TestConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestConfigurationNameProperty = TestProperty.Register(TestConfigurationName, TestConfigurationName, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty IsInLabEnvironmentProperty = TestProperty.Register(IsInLabEnvironment, IsInLabEnvironment, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildConfigurationIdProperty = TestProperty.Register(BuildConfigurationId, BuildConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildDirectoryProperty = TestProperty.Register(BuildDirectory, BuildDirectory, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildFlavorProperty = TestProperty.Register(BuildFlavor, BuildFlavor, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildNumberProperty = TestProperty.Register(BuildNumber, BuildNumber, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildPlatformProperty = TestProperty.Register(BuildPlatform, BuildPlatform, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildUriProperty = TestProperty.Register(BuildUri, BuildUri, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TfsServerCollectionUrlProperty = TestProperty.Register(TfsServerCollectionUrl, TfsServerCollectionUrl, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TfsTeamProjectProperty = TestProperty.Register(TfsTeamProject, TfsTeamProject, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty ParameterTypesProperty = TestProperty.Register("MSTest.ParameterTypes", "ParameterTypes", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestDynamicDataTypeProperty = TestProperty.Register("MSTest.DynamicDataType", "DynamicDataType", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestCaseIndexProperty = TestProperty.Register("MSTest.TestCaseIndex", "TestCaseIndex", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestDynamicDataProperty = TestProperty.Register("MSTest.DynamicData", "DynamicData", typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestDataSourceIgnoreMessageProperty = TestProperty.Register("MSTest.TestDataSourceIgnoreMessageProperty", "TestDataSourceIgnoreMessageProperty", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty UnfoldingStrategy = TestProperty.Register("MSTestDiscoverer.UnfoldingStrategy", "UnfoldingStrategy", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - #endregion - - #region Private Constants - - /// - /// These are the Test properties used by the adapter, which essentially correspond - /// to attributes on tests, and may be available in command line/TeamBuild to filter tests. - /// These Property names should not be localized. - /// - private const string TestClassNameLabel = "ClassName"; - private const string TestCategoryLabel = "TestCategory"; - private const string PriorityLabel = "Priority"; -#if !WINDOWS_UWP && !WIN_UI - private const string DeploymentItemsLabel = "DeploymentItems"; -#endif - private const string DoNotParallelizeLabel = "DoNotParallelize"; - private const string ExecutionIdLabel = "ExecutionId"; - private const string ParentExecIdLabel = "ParentExecId"; - private const string WorkItemIdsLabel = "WorkItemIds"; - - private const string TestRunId = "__Tfs_TestRunId__"; - private const string TestPlanId = "__Tfs_TestPlanId__"; - private const string TestCaseId = "__Tfs_TestCaseId__"; - private const string TestPointId = "__Tfs_TestPointId__"; - private const string TestConfigurationId = "__Tfs_TestConfigurationId__"; - private const string TestConfigurationName = "__Tfs_TestConfigurationName__"; - private const string IsInLabEnvironment = "__Tfs_IsInLabEnvironment__"; - private const string BuildConfigurationId = "__Tfs_BuildConfigurationId__"; - private const string BuildDirectory = "__Tfs_BuildDirectory__"; - private const string BuildFlavor = "__Tfs_BuildFlavor__"; - private const string BuildNumber = "__Tfs_BuildNumber__"; - private const string BuildPlatform = "__Tfs_BuildPlatform__"; - private const string BuildUri = "__Tfs_BuildUri__"; - private const string TfsServerCollectionUrl = "__Tfs_TfsServerCollectionUrl__"; - private const string TfsTeamProject = "__Tfs_TeamProject__"; - - #endregion - /// /// Uri of the MSTest executor. /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs index a41268c299..16081a0f4b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs @@ -3,7 +3,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using AdapterApplicationStateGuard = Microsoft.VisualStudio.TestPlatform.MSTestAdapter.ApplicationStateGuard; @@ -53,7 +53,7 @@ public void MarkClassComplete(string fullClassName) } } - internal static void ForceCleanup(TypeCache typeCache, IDictionary sourceLevelParameters, IMessageLogger logger) + internal static void ForceCleanup(TypeCache typeCache, IDictionary sourceLevelParameters, IAdapterMessageLogger logger) { IEnumerable classInfoCache = typeCache.ClassInfoListWithExecutableCleanupMethods; foreach (TestClassInfo classInfo in classInfoCache) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs deleted file mode 100644 index 92d8534c6b..0000000000 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; - -using TestPlatformObjectModel = Microsoft.VisualStudio.TestPlatform.ObjectModel; - -namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; - -/// -/// Reads and parses the TcmTestProperties in order to populate them in TestRunParameters. -/// -internal static class TcmTestPropertiesProvider -{ - /// - /// Gets tcm properties from test case. - /// - /// Test case. - /// Tcm properties. - public static IDictionary? GetTcmProperties(TestPlatformObjectModel.TestCase? testCase) - { - // Return empty properties when testCase is null or when test case id is zero. - if (testCase == null || - testCase.GetPropertyValue(EngineConstants.TestCaseIdProperty, default) == 0) - { - return null; - } - - var tcmProperties = new Dictionary(capacity: 15) - { - // Step 1: Add common properties. - [EngineConstants.TestRunIdProperty] = testCase.GetPropertyValue(EngineConstants.TestRunIdProperty, default), - [EngineConstants.TestPlanIdProperty] = testCase.GetPropertyValue(EngineConstants.TestPlanIdProperty, default), - [EngineConstants.BuildConfigurationIdProperty] = testCase.GetPropertyValue(EngineConstants.BuildConfigurationIdProperty, default), - [EngineConstants.BuildDirectoryProperty] = testCase.GetPropertyValue(EngineConstants.BuildDirectoryProperty, default), - [EngineConstants.BuildFlavorProperty] = testCase.GetPropertyValue(EngineConstants.BuildFlavorProperty, default), - [EngineConstants.BuildNumberProperty] = testCase.GetPropertyValue(EngineConstants.BuildNumberProperty, default), - [EngineConstants.BuildPlatformProperty] = testCase.GetPropertyValue(EngineConstants.BuildPlatformProperty, default), - [EngineConstants.BuildUriProperty] = testCase.GetPropertyValue(EngineConstants.BuildUriProperty, default), - [EngineConstants.TfsServerCollectionUrlProperty] = testCase.GetPropertyValue(EngineConstants.TfsServerCollectionUrlProperty, default), - [EngineConstants.TfsTeamProjectProperty] = testCase.GetPropertyValue(EngineConstants.TfsTeamProjectProperty, default), - [EngineConstants.IsInLabEnvironmentProperty] = testCase.GetPropertyValue(EngineConstants.IsInLabEnvironmentProperty, default), - - // Step 2: Add test case specific properties. - [EngineConstants.TestCaseIdProperty] = testCase.GetPropertyValue(EngineConstants.TestCaseIdProperty, default), - [EngineConstants.TestConfigurationIdProperty] = testCase.GetPropertyValue(EngineConstants.TestConfigurationIdProperty, default), - [EngineConstants.TestConfigurationNameProperty] = testCase.GetPropertyValue(EngineConstants.TestConfigurationNameProperty, default), - [EngineConstants.TestPointIdProperty] = testCase.GetPropertyValue(EngineConstants.TestPointIdProperty, default), - }; - - return tcmProperties; - } -} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs index 8ae3f43e28..d76e64a597 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs @@ -1,30 +1,29 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// -/// The test case discovery sink. +/// The discovery sink used internally by execution to collect the tests discovered from a source. /// -internal sealed class TestCaseDiscoverySink : ITestCaseDiscoverySink +/// +/// It collects the neutral model directly, so the execution pipeline can flow +/// discovered tests without round-tripping them through a VSTest TestCase. +/// +internal sealed class TestCaseDiscoverySink : IUnitTestElementSink { /// - /// Gets the tests. + /// Gets the discovered tests. /// - public ICollection Tests { get; } = []; + public ICollection TestElements { get; } = []; /// - /// Sends the test case. + /// Collects the discovered test element. /// - /// The discovered test. - public void SendTestCase(TestCase? discoveredTest) - { - if (discoveredTest != null) - { - Tests.Add(discoveredTest); - } - } + /// The discovered test element. + public void SendTestElement(UnitTestElement testElement) + => TestElements.Add(testElement); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 90d4be897c..9f7b4ffdd3 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -1,14 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; @@ -21,16 +18,21 @@ internal partial class TestExecutionManager /// Execute the parameter tests. /// /// Tests to execute. - /// The run context. - /// Handle to record test start/end/results. + /// Host-provided test-run directory and run settings XML. + /// Logger used to report test messages back to the host. + /// Recorder used to report test results back to the host. + /// Provider for the test filter, or for no filter. /// Indicates if deployment is done. - internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) + internal virtual async Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { - InitializeRandomTestOrder(frameworkHandle); + _testResultRecorder = testResultRecorder; + _testElementFilterProvider = filterProvider; + + InitializeRandomTestOrder(messageLogger); var testsBySource = (from test in tests - group test by test.Source into testGroup - select new { Source = testGroup.Key, Tests = (IEnumerable)testGroup }).ToArray(); + group test by test.TestMethod.AssemblyName into testGroup + select new { Source = testGroup.Key, Tests = (IEnumerable)testGroup }).ToArray(); if (_testOrderRandom is { } random) { @@ -40,7 +42,7 @@ group test by test.Source into testGroup foreach (var group in testsBySource) { _testRunCancellationToken?.ThrowIfCancellationRequested(); - await ExecuteTestsInSourceAsync(group.Tests, runContext, frameworkHandle, group.Source, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsInSourceAsync(group.Tests, deploymentContext, messageLogger, group.Source, isDeploymentDone).ConfigureAwait(false); } } @@ -48,11 +50,11 @@ group test by test.Source into testGroup /// Execute the parameter tests present in parameter source. /// /// Tests to execute. - /// The run context. - /// Handle to record test start/end/results. + /// Host-provided test-run directory and run settings XML. + /// Logger used to report test messages back to the host. /// The test container for the tests. /// Indicates if deployment is done. - private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, string source, bool isDeploymentDone) + private async Task ExecuteTestsInSourceAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, string source, bool isDeploymentDone) { DebugEx.Assert(!StringEx.IsNullOrEmpty(source), "Source cannot be empty"); @@ -63,7 +65,9 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo } #endif - using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, runContext?.RunSettings); + IAdapterMessageLogger adapterMessageLogger = messageLogger; + + using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, deploymentContext.RunSettingsXml); bool usesAppDomains = isolationHost is TestSourceHost { UsesAppDomain: true }; if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) @@ -72,7 +76,8 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo } // Default test set is filtered tests based on user provided filter criteria - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, frameworkHandle, out bool filterHasError); + bool filterHasError = false; + ITestElementFilter? filter = _testElementFilterProvider?.GetTestElementFilter(adapterMessageLogger, out filterHasError); if (filterHasError) { // Bail out without processing everything else below. @@ -112,13 +117,13 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo int parallelWorkers = sourceSettings.Workers; ExecutionScope parallelScope = sourceSettings.Scope; - TestCase[] testsToRun = [.. tests.Where(t => MatchTestFilter(filterExpression, t, _testMethodFilter))]; + UnitTestElement[] testsToRun = [.. tests.Where(t => MatchTestFilter(filter, t, source))]; if (_testOrderRandom is { } sourceRandom) { Shuffle(sourceRandom, testsToRun); } - UnitTestElement[] unitTestElements = [.. testsToRun.Select(e => e.ToUnitTestElementWithUpdatedSource(source))]; + UnitTestElement[] unitTestElements = [.. testsToRun.Select(e => e.WithUpdatedSource(source))]; // Create an instance of a type defined in adapter so that adapter gets loaded in the child app domain var testRunner = (UnitTestRunner)isolationHost.CreateInstanceForType( typeof(UnitTestRunner), @@ -142,17 +147,17 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo if (!MSTestSettings.CurrentSettings.DisableParallelization && sourceSettings.CanParallelizeAssembly && parallelWorkers > 0) { // Parallelization is enabled. Let's do further classification for sets. - frameworkHandle.SendMessage( - TestMessageLevel.Informational, + adapterMessageLogger.SendMessage( + MessageLevel.Informational, string.Format(CultureInfo.CurrentCulture, Resource.TestParallelizationBanner, source, parallelWorkers, parallelScope)); // Create test sets for execution, we can execute them in parallel based on parallel settings // Parallel and not parallel sets. // Single-pass partition: enumerate testsToRun once via GroupBy (lazy GroupBy evaluated twice // — once per FirstOrDefault — caused 2 full passes over testsToRun). - IEnumerable? parallelizableTestSet = null; - IEnumerable? nonParallelizableTestSet = null; - foreach (IGrouping group in testsToRun.GroupBy(t => t.GetPropertyValue(EngineConstants.DoNotParallelizeProperty, false))) + IEnumerable? parallelizableTestSet = null; + IEnumerable? nonParallelizableTestSet = null; + foreach (IGrouping group in testsToRun.GroupBy(t => t.DoNotParallelize)) { if (group.Key) { @@ -166,7 +171,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo if (parallelizableTestSet != null) { - ConcurrentQueue>? queue = null; + ConcurrentQueue>? queue = null; // Chunk the sets into further groups based on parallel level switch (parallelScope) @@ -174,13 +179,13 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo case ExecutionScope.MethodLevel: if (_testOrderRandom is { } methodRandom) { - IEnumerable[] methodChunks = [.. parallelizableTestSet.Select(t => (IEnumerable)[t])]; + IEnumerable[] methodChunks = [.. parallelizableTestSet.Select(t => (IEnumerable)[t])]; Shuffle(methodRandom, methodChunks); - queue = new ConcurrentQueue>(methodChunks); + queue = new ConcurrentQueue>(methodChunks); } else { - queue = new ConcurrentQueue>(parallelizableTestSet.Select(t => new[] { t })); + queue = new ConcurrentQueue>(parallelizableTestSet.Select(t => new[] { t })); } break; @@ -188,18 +193,18 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo case ExecutionScope.ClassLevel: if (_testOrderRandom is { } classRandom) { - IEnumerable[] classChunks = + IEnumerable[] classChunks = [ .. parallelizableTestSet - .GroupBy(t => t.GetPropertyValue(EngineConstants.TestClassNameProperty) as string) - .Select(g => (IEnumerable)g.ToArray()), + .GroupBy(t => t.TestMethod.FullClassName) + .Select(g => (IEnumerable)g.ToArray()), ]; Shuffle(classRandom, classChunks); - queue = new ConcurrentQueue>(classChunks); + queue = new ConcurrentQueue>(classChunks); } else { - queue = new ConcurrentQueue>(parallelizableTestSet.GroupBy(t => t.GetPropertyValue(EngineConstants.TestClassNameProperty) as string)); + queue = new ConcurrentQueue>(parallelizableTestSet.GroupBy(t => t.TestMethod.FullClassName)); } break; @@ -219,9 +224,9 @@ .. parallelizableTestSet { _testRunCancellationToken?.ThrowIfCancellationRequested(); - if (queue.TryDequeue(out IEnumerable? testSet)) + if (queue.TryDequeue(out IEnumerable? testSet)) { - await ExecuteTestsWithTestRunnerAsync(testSet, frameworkHandle, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); + await ExecuteTestsWithTestRunnerAsync(testSet, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } } } @@ -248,7 +253,7 @@ .. parallelizableTestSet PlatformServiceProvider.Instance.AdapterTraceLogger.Error("Error occurred while executing tests in parallel{0}{1}", Environment.NewLine, exceptionToString); } - frameworkHandle.SendMessage(TestMessageLevel.Error, exceptionToString); + adapterMessageLogger.SendMessage(MessageLevel.Error, exceptionToString); throw; } } @@ -256,17 +261,17 @@ .. parallelizableTestSet // Queue the non parallel set if (nonParallelizableTestSet != null) { - await ExecuteTestsWithTestRunnerAsync(nonParallelizableTestSet, frameworkHandle, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); + await ExecuteTestsWithTestRunnerAsync(nonParallelizableTestSet, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } } else { - await ExecuteTestsWithTestRunnerAsync(testsToRun, frameworkHandle, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); + await ExecuteTestsWithTestRunnerAsync(testsToRun, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } if (PlatformServiceProvider.Instance.IsGracefulStopRequested) { - testRunner.ForceCleanup(sourceLevelParameters!, new RemotingMessageLogger(frameworkHandle)); + testRunner.ForceCleanup(sourceLevelParameters!, new RemotingMessageLogger(adapterMessageLogger)); } if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs index dd43d20b47..67de43cfe2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs @@ -1,27 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; internal partial class TestExecutionManager { internal void SendTestResults( - TestCase test, + UnitTestElement test, TestTools.UnitTesting.TestResult[] unitTestResults, DateTimeOffset startTime, DateTimeOffset endTime, - ITestExecutionRecorder testExecutionRecorder) + ITestResultRecorder testResultRecorder) { if (unitTestResults.Length == 0) { - testExecutionRecorder.RecordEnd(test, TestOutcome.None); + testResultRecorder.RecordEmptyResult(test); return; } @@ -29,46 +25,21 @@ internal void SendTestResults( { _testRunCancellationToken?.ThrowIfCancellationRequested(); - var testResult = unitTestResult.ToTestResult( - test, - startTime, - endTime, - _environment.MachineName, - MSTestSettings.CurrentSettings); - - testExecutionRecorder.RecordEnd(test, testResult.Outcome); - - if (testResult.Outcome == TestOutcome.Failed) - { - if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) - { - PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor:Test {0} failed. ErrorMessage:{1}, ErrorStackTrace:{2}.", testResult.TestCase.FullyQualifiedName, testResult.ErrorMessage, testResult.ErrorStackTrace); - } - #if !WINDOWS_UWP && !WIN_UI - _hasAnyTestFailed = true; -#endif - } - - try - { - if (testResult.Outcome != TestOutcome.NotFound - || !RuntimeContext.IsHotReloadEnabled) - { - testExecutionRecorder.RecordResult(testResult); - } - } - catch (TestCanceledException) + if (testResultRecorder.RecordResult(test, unitTestResult, startTime, endTime)) { - // Ignore this exception + _hasAnyTestFailed = true; } +#else + testResultRecorder.RecordResult(test, unitTestResult, startTime, endTime); +#endif } } - private static bool MatchTestFilter(ITestCaseFilterExpression? filterExpression, TestCase test, TestMethodFilter testMethodFilter) + private static bool MatchTestFilter(ITestElementFilter? filter, UnitTestElement test, string source) { - if (filterExpression != null - && !filterExpression.MatchTestCase(test, p => testMethodFilter.PropertyValueProvider(test, p))) + if (filter is not null + && !filter.Matches(test.WithUpdatedSource(source))) { // Skip test if not fitting filter criteria. return false; @@ -78,24 +49,27 @@ private static bool MatchTestFilter(ITestCaseFilterExpression? filterExpression, } private async Task ExecuteTestsWithTestRunnerAsync( - IEnumerable tests, - ITestExecutionRecorder testExecutionRecorder, + IEnumerable tests, + IAdapterMessageLogger adapterMessageLogger, string source, IDictionary sourceLevelParameters, UnitTestRunner testRunner, bool usesAppDomains) { - IEnumerable orderedTests = MSTestSettings.CurrentSettings.OrderTestsByNameInClass && !MSTestSettings.CurrentSettings.RandomizeTestOrder - ? tests.OrderBy(t => t.GetManagedType()).ThenBy(t => t.GetManagedMethod()) + // Ordering keys mirror the historical VSTest ManagedType/ManagedMethod test-case properties, which are + // only populated when the test method carries managed method metadata (see UnitTestElement.ToTestCase). + IEnumerable orderedTests = MSTestSettings.CurrentSettings.OrderTestsByNameInClass && !MSTestSettings.CurrentSettings.RandomizeTestOrder + ? tests.OrderBy(t => t.TestMethod.HasManagedMethodAndTypeProperties ? t.TestMethod.ManagedTypeName : null) + .ThenBy(t => t.TestMethod.HasManagedMethodAndTypeProperties ? t.TestMethod.ManagedMethodName : null) : tests; - // If testRunner is in a different AppDomain, we cannot pass the testExecutionRecorder directly. + // If testRunner is in a different AppDomain, we cannot pass the message logger directly. // Instead, we pass a proxy (remoting object) that is marshallable by ref. - IMessageLogger remotingMessageLogger = usesAppDomains - ? new RemotingMessageLogger(testExecutionRecorder) - : testExecutionRecorder; + IAdapterMessageLogger remotingMessageLogger = usesAppDomains + ? new RemotingMessageLogger(adapterMessageLogger) + : adapterMessageLogger; - foreach (TestCase currentTest in orderedTests) + foreach (UnitTestElement currentTest in orderedTests) { _testRunCancellationToken?.ThrowIfCancellationRequested(); if (PlatformServiceProvider.Instance.IsGracefulStopRequested) @@ -103,9 +77,11 @@ private async Task ExecuteTestsWithTestRunnerAsync( break; } - UnitTestElement unitTestElement = currentTest.ToUnitTestElementWithUpdatedSource(source); + UnitTestElement unitTestElement = currentTest.WithUpdatedSource(source); - testExecutionRecorder.RecordStart(currentTest); + // Report through the neutral recorder using the element itself; the adapter-side recorder resolves + // the host test case (preserving host-injected TCM / data-collector properties) with full fidelity. + _testResultRecorder.RecordStart(currentTest); DateTimeOffset startTime = DateTimeOffset.Now; @@ -115,7 +91,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( } // Run single test passing test context properties to it. - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(currentTest); + IReadOnlyDictionary? tcmProperties = currentTest.ExecutionContextProperties; Dictionary testContextProperties = GetTestContextProperties(tcmProperties, sourceLevelParameters, unitTestElement); TestTools.UnitTesting.TestResult[] unitTestResult; @@ -143,7 +119,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( DateTimeOffset endTime = DateTimeOffset.Now; - SendTestResults(currentTest, unitTestResult, startTime, endTime, testExecutionRecorder); + SendTestResults(currentTest, unitTestResult, startTime, endTime, _testResultRecorder); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs index 7db20bc62a..e465af8e72 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs @@ -3,9 +3,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -20,7 +18,7 @@ internal partial class TestExecutionManager /// The unit test element to get properties from. /// Test context properties. private static Dictionary GetTestContextProperties( - IDictionary? tcmProperties, + IReadOnlyDictionary? tcmProperties, IDictionary sourceLevelParameters, UnitTestElement unitTestElement) { @@ -38,9 +36,9 @@ internal partial class TestExecutionManager // Add tcm properties. if (tcmProperties is not null) { - foreach (KeyValuePair kvp in tcmProperties) + foreach (KeyValuePair kvp in tcmProperties) { - testContextProperties[kvp.Key.Id] = kvp.Value; + testContextProperties[kvp.Key] = kvp.Value; } } @@ -52,7 +50,7 @@ internal partial class TestExecutionManager if (unitTestElement.Traits is { Length: > 0 }) { - foreach (Trait trait in unitTestElement.Traits) + foreach (TestTrait trait in unitTestElement.Traits) { ValidateAndAssignTestProperty(testContextProperties, trait.Name, trait.Value); } @@ -99,16 +97,16 @@ private static void ValidateAndAssignTestProperty( } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle errors in user specified run parameters")] - private void CacheSessionParameters(IRunContext? runContext, ITestExecutionRecorder testExecutionRecorder) + private void CacheSessionParameters(string? settingsXml, IAdapterMessageLogger messageLogger) { - if (StringEx.IsNullOrEmpty(runContext?.RunSettings?.SettingsXml)) + if (StringEx.IsNullOrEmpty(settingsXml)) { return; } try { - Dictionary? testRunParameters = RunSettingsUtilities.GetTestRunParameters(runContext.RunSettings.SettingsXml); + Dictionary? testRunParameters = RunSettingsUtilities.GetTestRunParameters(settingsXml); if (testRunParameters != null) { // Clear sessionParameters to prevent key collisions of test run parameters in case @@ -122,7 +120,7 @@ private void CacheSessionParameters(IRunContext? runContext, ITestExecutionRecor } catch (Exception ex) { - testExecutionRecorder.SendMessage(TestMessageLevel.Error, ex.Message); + messageLogger.SendMessage(MessageLevel.Error, ex.Message); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs index c21f1add17..d23abdff7b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs @@ -1,28 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; internal partial class TestExecutionManager { + /// + /// A message logger that can be marshaled into the isolation (child app-domain) host so log messages + /// raised while running a test cross back to the parent-domain . + /// Marshaling is by reference (the wrapped logger stays in the parent domain), so no VSTest object-model + /// type is required in the child domain. + /// private sealed class RemotingMessageLogger : #if NETFRAMEWORK MarshalByRefObject, #endif - IMessageLogger + IAdapterMessageLogger { - private readonly IMessageLogger _realMessageLogger; + private readonly IAdapterMessageLogger _realMessageLogger; - public RemotingMessageLogger(IMessageLogger messageLogger) + public RemotingMessageLogger(IAdapterMessageLogger messageLogger) => _realMessageLogger = messageLogger; - public void SendMessage(TestMessageLevel testMessageLevel, string message) - => _realMessageLogger.SendMessage(testMessageLevel, message); + public void SendMessage(MessageLevel level, string message) + => _realMessageLogger.SendMessage(level, message); } - private void InitializeRandomTestOrder(IMessageLogger frameworkHandle) + private void InitializeRandomTestOrder(IAdapterMessageLogger messageLogger) { if (!MSTestSettings.CurrentSettings.RandomizeTestOrder) { @@ -36,8 +43,8 @@ private void InitializeRandomTestOrder(IMessageLogger frameworkHandle) int seed = MSTestSettings.CurrentSettings.RandomTestOrderSeed ?? Guid.NewGuid().GetHashCode(); _testOrderRandom = new Random(seed); - frameworkHandle.SendMessage( - TestMessageLevel.Informational, + messageLogger.SendMessage( + MessageLevel.Informational, string.Format(CultureInfo.CurrentCulture, Resource.RandomTestOrderBanner, seed)); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index 83e42fe0c8..d8858cdaea 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -21,7 +20,6 @@ internal partial class TestExecutionManager /// Dictionary for test run parameters. /// private readonly IDictionary _sessionParameters; - private readonly IEnvironment _environment; private readonly Func, Task> _taskFactory; /// @@ -35,19 +33,23 @@ internal partial class TestExecutionManager /// private Random? _testOrderRandom; + /// + /// Recorder used to report test start/end/results back to the host. Supplied at the adapter boundary and set + /// once per run in before any source is processed. + /// + private ITestResultRecorder _testResultRecorder = null!; + /// /// Initializes a new instance of the class. /// public TestExecutionManager() - : this(EnvironmentWrapper.Instance) + : this(null) { } - internal TestExecutionManager(IEnvironment environment, Func, Task>? taskFactory = null) + internal TestExecutionManager(Func, Task>? taskFactory = null) { - _testMethodFilter = new TestMethodFilter(); _sessionParameters = new Dictionary(); - _environment = environment; _taskFactory = taskFactory ?? DefaultFactoryAsync; } @@ -67,9 +69,11 @@ private static Task DefaultFactoryAsync(Func taskGetter) } /// - /// Gets or sets method filter for filtering tests. + /// Provider for the test filter, supplied at the adapter boundary and set once per run in + /// so the filter is built at the same point (and with the same per-source + /// timing) as before. /// - private readonly TestMethodFilter _testMethodFilter; + private ITestElementFilterProvider? _testElementFilterProvider; #if !WINDOWS_UWP && !WIN_UI /// @@ -82,30 +86,32 @@ private static Task DefaultFactoryAsync(Func taskGetter) /// Runs the tests. /// /// Tests to be run. - /// Context to use when executing the tests. - /// Handle to the framework to record results and to do framework operations. + /// Host-provided test-run directory and run settings XML. + /// Logger used to report test messages back to the host. + /// Recorder used to report test results back to the host. + /// Provider for the test filter, or for no filter. /// Test run cancellation token. - internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, TestRunCancellationToken runCancellationToken) + internal async Task RunTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, TestRunCancellationToken runCancellationToken) { DebugEx.Assert(tests != null, "tests"); - DebugEx.Assert(runContext != null, "runContext"); - DebugEx.Assert(frameworkHandle != null, "frameworkHandle"); + DebugEx.Assert(deploymentContext != null, "deploymentContext"); + DebugEx.Assert(messageLogger != null, "messageLogger"); DebugEx.Assert(runCancellationToken != null, "runCancellationToken"); _testRunCancellationToken = runCancellationToken; PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, runContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, deploymentContext, messageLogger); #else const bool isDeploymentDone = false; #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext, frameworkHandle); + CacheSessionParameters(deploymentContext.RunSettingsXml, messageLogger); // Execute the tests - await ExecuteTestsAsync(tests, runContext, frameworkHandle, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, deploymentContext, messageLogger, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -115,41 +121,41 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runC #endif } - internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) + internal async Task RunTestsAsync(IEnumerable sources, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) { _testRunCancellationToken = cancellationToken; PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; var discoverySink = new TestCaseDiscoverySink(); - var tests = new List(); + var tests = new List(); + + IAdapterMessageLogger logger = messageLogger; // deploy everything first. foreach (string source in sources) { _testRunCancellationToken?.ThrowIfCancellationRequested(); - var logger = (IMessageLogger)frameworkHandle; - // discover the tests - GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, runContext, isMTP); - tests.AddRange(discoverySink.Tests); + GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, deploymentContext.RunSettingsXml, filterProvider, isMTP); + tests.AddRange(discoverySink.TestElements); // Clear discoverSinksTests so that it just stores test for one source at one point of time - discoverySink.Tests.Clear(); + discoverySink.TestElements.Clear(); } #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, runContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, deploymentContext, messageLogger); #else const bool isDeploymentDone = false; #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext, frameworkHandle); + CacheSessionParameters(deploymentContext.RunSettingsXml, messageLogger); // Run tests. - await ExecuteTestsAsync(tests, runContext, frameworkHandle, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, deploymentContext, messageLogger, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -159,5 +165,14 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC #endif } +#if !WINDOWS_UWP && !WIN_UI + /// + /// Runs deployment for the given tests via the platform-agnostic deployment service using the neutral + /// supplied by the adapter boundary. + /// + private static bool Deploy(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger) + => PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, deploymentContext, messageLogger); +#endif + internal virtual UnitTestDiscoverer GetUnitTestDiscoverer(ITestSourceHandler testSourceHandler) => new(testSourceHandler); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs index 5999529378..90e7c4698b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -39,7 +38,7 @@ public static TestRunInfo CreateFrom(IReadOnlyList testsToRun) private static PlannedTest ToPlannedTest(UnitTestElement element) { - Trait[]? traits = element.Traits; + TestTrait[]? traits = element.Traits; KeyValuePair[] testProperties; if (traits is { Length: > 0 }) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs index 8537205e91..396541bad2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs @@ -5,7 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -14,7 +13,7 @@ internal sealed partial class UnitTestRunner { // Task cannot cross app domains. // For now, TestExecutionManager will call this sync method which is hacky. - internal TestResult[] RunSingleTest(UnitTestElement unitTestElement, IDictionary testContextProperties, IMessageLogger messageLogger) + internal TestResult[] RunSingleTest(UnitTestElement unitTestElement, IDictionary testContextProperties, IAdapterMessageLogger messageLogger) => RunSingleTestAsync(unitTestElement, testContextProperties, messageLogger).GetAwaiter().GetResult(); /// @@ -24,7 +23,7 @@ internal TestResult[] RunSingleTest(UnitTestElement unitTestElement, IDictionary /// The test context properties. /// The message logger. /// The . - internal async Task RunSingleTestAsync(UnitTestElement unitTestElement, IDictionary testContextProperties, IMessageLogger messageLogger) + internal async Task RunSingleTestAsync(UnitTestElement unitTestElement, IDictionary testContextProperties, IAdapterMessageLogger messageLogger) { if (unitTestElement is null) { @@ -288,5 +287,5 @@ private static bool IsTestMethodRunnable( return true; } - internal void ForceCleanup(IDictionary sourceLevelParameters, IMessageLogger logger) => ClassCleanupManager.ForceCleanup(_typeCache, sourceLevelParameters, logger); + internal void ForceCleanup(IDictionary sourceLevelParameters, IAdapterMessageLogger logger) => ClassCleanupManager.ForceCleanup(_typeCache, sourceLevelParameters, logger); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs index 8b0ba506c4..89f2a214eb 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs @@ -6,7 +6,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; // The programmatic test-filter types (ITestFilter, TestFilterContext, TestFilterResult, @@ -150,7 +149,7 @@ private static TestFilterContext CreateFilterContext(UnitTestElement element) private async Task FinishFilteredOutTestAsync( TestMethod testMethod, IDictionary testContextProperties, - IMessageLogger messageLogger, + IAdapterMessageLogger messageLogger, TestResult[] filterResult, ITestContext testContextForTestExecution) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs index 593c6b73f5..20b5ef982d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs @@ -3,8 +3,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -20,7 +19,7 @@ internal static class MSTestDiscovererHelpers internal static bool AreValidSources(IEnumerable sources, ITestSourceHandler testSourceHandler) => sources.Any(source => testSourceHandler.ValidSourceExtensions.Any(extension => string.Equals(Path.GetExtension(source), extension, StringComparison.OrdinalIgnoreCase))); - internal static bool InitializeDiscovery(IEnumerable sources, IDiscoveryContext? discoveryContext, IMessageLogger messageLogger, IConfiguration? configuration, ITestSourceHandler testSourceHandler) + internal static bool InitializeDiscovery(IEnumerable sources, string? settingsXml, IAdapterMessageLogger messageLogger, IConfiguration? configuration, ITestSourceHandler testSourceHandler) { if (!AreValidSources(sources, testSourceHandler)) { @@ -30,12 +29,12 @@ internal static bool InitializeDiscovery(IEnumerable sources, IDiscovery // Populate the runsettings. try { - MSTestSettings.PopulateSettings(discoveryContext, messageLogger, configuration); + MSTestSettings.PopulateSettings(settingsXml, messageLogger, configuration); return true; } catch (AdapterSettingsException ex) { - messageLogger.SendMessage(TestMessageLevel.Error, ex.Message); + messageLogger.SendMessage(MessageLevel.Error, ex.Message); return false; } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs index cd6c40f80c..518667da1d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs @@ -5,9 +5,9 @@ using System.Security; #endif +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; @@ -198,7 +198,7 @@ internal static bool IsDoNotParallelizeSet(Assembly assembly) /// /// The member to inspect. /// List of traits. - internal static Trait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider) + internal static TestTrait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider) => PlatformServiceProvider.Instance.ReflectionOperations.GetTestPropertiesAsTraits(testPropertyProvider); /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs index 721f5afd7a..b85063c118 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; @@ -62,7 +62,7 @@ public static string[] GetTestCategories(this IReflectionOperations reflectionOp /// The reflection operations to use. /// The member to inspect. /// List of traits. - public static Trait[] GetTestPropertiesAsTraits(this IReflectionOperations reflectionOperations, MethodInfo testPropertyProvider) + public static TestTrait[] GetTestPropertiesAsTraits(this IReflectionOperations reflectionOperations, MethodInfo testPropertyProvider) { Attribute[] attributesFromMethod = reflectionOperations.GetCustomAttributesCached(testPropertyProvider); Attribute[] attributesFromClass = testPropertyProvider.ReflectedType is { } testClass ? reflectionOperations.GetCustomAttributesCached(testClass) : []; @@ -89,13 +89,13 @@ public static Trait[] GetTestPropertiesAsTraits(this IReflectionOperations refle return []; } - var traits = new Trait[countTestPropertyAttribute]; + var traits = new TestTrait[countTestPropertyAttribute]; int index = 0; foreach (Attribute attribute in attributesFromMethod) { if (attribute is TestPropertyAttribute testProperty) { - traits[index++] = new Trait(testProperty.Name, testProperty.Value); + traits[index++] = new TestTrait(testProperty.Name, testProperty.Value); } } @@ -103,7 +103,7 @@ public static Trait[] GetTestPropertiesAsTraits(this IReflectionOperations refle { if (attribute is TestPropertyAttribute testProperty) { - traits[index++] = new Trait(testProperty.Name, testProperty.Value); + traits[index++] = new TestTrait(testProperty.Name, testProperty.Value); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs index a8d30dc0de..9d52227cd5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; @@ -18,6 +17,11 @@ internal static class RunSettingsUtilities IgnoreWhitespace = true, }; + // The runsettings node names, historically taken from the VSTest object model's Constants type. + internal const string RunConfigurationSettingsName = "RunConfiguration"; + + internal const string TestRunParametersName = "TestRunParameters"; + /// /// Gets the set of user defined test run parameters from settings xml as key value pairs. /// @@ -25,23 +29,23 @@ internal static class RunSettingsUtilities /// The test run parameters. /// If there is no test run parameters section defined in the settingsxml a blank dictionary is returned. internal static Dictionary? GetTestRunParameters(string? settingsXml) - => GetNodeValue(settingsXml, Constants.TestRunParametersName, TestRunParameters.FromXml); + => GetNodeValue(settingsXml, TestRunParametersName, TestRunParameters.FromXml); /// /// Throws if the node has an attribute. /// /// The reader. - /// Thrown if the node has an attribute. + /// Thrown if the node has an attribute. internal static void ThrowOnHasAttributes(XmlReader reader) { if (reader.HasAttributes) { reader.MoveToNextAttribute(); - throw new SettingsException( + throw new InvalidRunSettingsException( string.Format( CultureInfo.CurrentCulture, Resource.InvalidSettingsXmlAttribute, - TestPlatform.ObjectModel.Constants.RunConfigurationSettingsName, + RunConfigurationSettingsName, reader.Name)); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs index 9a324be172..c891b114d4 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; @@ -47,11 +47,11 @@ internal static Dictionary FromXml(XmlReader reader) break; default: - throw new SettingsException( + throw new InvalidRunSettingsException( string.Format( CultureInfo.CurrentCulture, Resource.InvalidSettingsXmlElement, - Constants.TestRunParametersName, + RunSettingsUtilities.TestRunParametersName, reader.Name)); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs new file mode 100644 index 0000000000..7daf921451 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; + +/// +/// Minimal, platform-agnostic navigation helpers for reading runsettings XML. +/// These replace the equivalent helpers from the VSTest object model so the platform services layer does not +/// depend on it; the navigation semantics are unchanged. +/// +internal static class XmlReaderUtilities +{ + private const string RunSettingsRootNodeName = "RunSettings"; + + /// + /// Advances the reader to the root element and verifies it is the <RunSettings> node. + /// + /// The reader positioned before the root element. + /// Thrown when the root element is not <RunSettings>. + internal static void ReadToRootNode(XmlReader reader) + { + reader.ReadToNextElement(); + + // Verify that it is a "RunSettings" node. + if (reader.Name != RunSettingsRootNodeName) + { + throw new InvalidRunSettingsException($"Could not find '{RunSettingsRootNodeName}' node in the runsettings XML. Found '<{reader.Name}>' instead."); + } + } + + /// + /// Reads until the next element node (or end of document). + /// + /// The reader. + internal static void ReadToNextElement(this XmlReader reader) + { + while (!reader.EOF && reader.Read() && reader.NodeType != XmlNodeType.Element) + { + } + } + + /// + /// Skips the current subtree and positions the reader on the next element node. + /// + /// The reader. + internal static void SkipToNextElement(this XmlReader reader) + { + reader.Skip(); + + if (reader.NodeType != XmlNodeType.Element) + { + reader.ReadToNextElement(); + } + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs index 25761fb0a8..8537dd59e7 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs @@ -3,7 +3,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; @@ -67,7 +66,7 @@ internal interface IPlatformServiceProvider /// /// The source. /// - /// + /// /// The run Settings for the session. /// /// @@ -75,7 +74,7 @@ internal interface IPlatformServiceProvider /// ITestSourceHost CreateTestSourceHost( string source, - TestPlatform.ObjectModel.Adapter.IRunSettings? runSettings); + string? settingsXml); /// /// Gets the TestContext object for a platform. @@ -97,5 +96,5 @@ ITestSourceHost CreateTestSourceHost( /// /// This was required for compatibility reasons since the TestContext object that the V1 adapter had for desktop is not .Net Core compliant. /// - ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger messageLogger, UTF.UnitTestOutcome outcome); + ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger messageLogger, UTF.UnitTestOutcome outcome); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IAdapterMessageLogger.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IAdapterMessageLogger.cs new file mode 100644 index 0000000000..fd894a8ad9 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IAdapterMessageLogger.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic sink for diagnostic messages (informational / warning / error) that the adapter +/// surfaces to whichever test host is running the tests. +/// +/// +/// This abstraction lets the platform services layer report messages without taking a dependency on a +/// specific test platform's object model. The mapping to a concrete host logger (for example the VSTest +/// IMessageLogger) is provided by the adapter layer. +/// +internal interface IAdapterMessageLogger +{ + /// + /// Reports a diagnostic message to the running test host. + /// + /// The severity of the message. + /// The message text. + void SendMessage(MessageLevel level, string message); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs index 1aabea8f1c..a3215764fb 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs @@ -3,8 +3,8 @@ #if !WINDOWS_UWP && !WIN_UI -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; @@ -14,13 +14,13 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Int internal interface ITestDeployment { /// - /// Deploy deployment items for the specified test cases. + /// Deploy deployment items for the specified tests. /// - /// The test cases. - /// The run context. - /// The framework handle. + /// The tests. + /// The host deployment inputs (test-run directory, run settings XML). + /// The logger used to surface deployment warnings. /// True if deployment is done. - bool Deploy(IEnumerable testCases, IRunContext? runContext, IFrameworkHandle frameworkHandle); + bool Deploy(IEnumerable testElements, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger); /// /// Gets the set of deployment items on a method and its corresponding class. diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilter.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilter.cs new file mode 100644 index 0000000000..357f8762e4 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilter.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic predicate that decides whether a discovered or executed +/// should be included in the current test run, based on the user-provided test-case filter. +/// +/// +/// This abstraction lets the platform services discovery and execution pipelines apply filtering over the +/// neutral model without taking a dependency on a specific test platform's +/// filter object model (for example the VSTest ITestCaseFilterExpression, TestProperty and +/// IRunContext.GetTestCaseFilter types). The concrete filter is produced at the platform boundary by a +/// wrapper that parses the host's filter (currently TestMethodFilter, which wraps the VSTest filter +/// expression), and is expected to move fully out of the platform services layer in a later phase. +/// A filter means "no filter" and every element is included. +/// +internal interface ITestElementFilter +{ + /// + /// Determines whether the given matches the filter and should be included. + /// + /// The test element to evaluate. + /// if the element matches the filter; otherwise, . + bool Matches(UnitTestElement testElement); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilterProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilterProvider.cs new file mode 100644 index 0000000000..202b395383 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilterProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic factory that produces the in effect for the current +/// run or discovery. +/// +/// +/// This abstraction lets the platform services discovery and execution pipelines obtain the active filter +/// without taking a dependency on a specific test platform's filter object model (for example the VSTest +/// ITestCaseFilterExpression parsed from an IRunContext / IDiscoveryContext). The concrete +/// provider is created at the adapter boundary, closing over the host filter context; the engine invokes it at +/// the exact point it needs the filter so parse-error reporting keeps the same timing and per-source semantics. +/// +internal interface ITestElementFilterProvider +{ + /// + /// Builds the for the current run/discovery. + /// + /// Logger used to report a filter parsing error. + /// + /// Set to when the filter is unsupported or failed to parse; in that case the caller + /// should report no tests for the affected source. + /// + /// + /// The filter to apply, or when no filter applies (every element is included). + /// + ITestElementFilter? GetTestElementFilter(IAdapterMessageLogger logger, out bool filterHasError); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs new file mode 100644 index 0000000000..7887c7df51 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic sink for reporting the lifecycle and results of test execution to whichever +/// test host is running the tests. +/// +/// +/// This abstraction lets the platform services execution engine report test start/end and results using only +/// the neutral and framework models, without +/// taking a dependency on a specific test platform's result object model (for example the VSTest +/// TestResult, TestOutcome and attachment types). The concrete recorder — which builds the host +/// result representation — is provided at the adapter boundary (see the VSTest implementation in +/// TestResultRecorderExtensions in the adapter layer). +/// +internal interface ITestResultRecorder +{ + /// + /// Signals that execution of the given test has started. + /// + /// The test whose execution is starting. + void RecordStart(UnitTestElement testElement); + + /// + /// Signals that execution of the given test ended without producing any result. + /// + /// The test whose execution ended. + void RecordEmptyResult(UnitTestElement testElement); + + /// + /// Reports a single framework for the given test to the test host. + /// + /// The test the result belongs to. + /// The framework result produced by executing the test. + /// The time at which the test started executing. + /// The time at which the test finished executing. + /// if the result was reported as a failure; otherwise, . + bool RecordResult(UnitTestElement testElement, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IUnitTestElementSink.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IUnitTestElementSink.cs new file mode 100644 index 0000000000..7f52cf48f8 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IUnitTestElementSink.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic sink that receives the tests discovered by the adapter and hands them to the +/// discovery consumer (a test host during discovery, or the execution pipeline during a run). +/// +/// +/// This abstraction lets the platform services discovery pipeline emit the neutral +/// model without taking a dependency on a specific test platform's +/// discovery object model (for example the VSTest TestCase and ITestCaseDiscoverySink +/// types). The concrete sink is produced at the platform boundary by a wrapper over the host's +/// discovery sink (currently UnitTestElementSinkExtensions, which wraps the VSTest +/// ITestCaseDiscoverySink and materializes a TestCase for each element), and is expected +/// to move fully out of the platform services layer in a later phase. +/// +internal interface IUnitTestElementSink +{ + /// + /// Reports a discovered to the running test host. + /// + /// The discovered test element. + void SendTestElement(UnitTestElement testElement); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj b/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj index 6a9e8820b1..97813d16d1 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj @@ -36,7 +36,6 @@ - + + + + diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs index 6768b0b290..ee399d9ab5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs @@ -10,9 +10,8 @@ #endif using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using MessageLevel = Microsoft.VisualStudio.TestTools.UnitTesting.MessageLevel; using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -20,34 +19,34 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; internal sealed partial class MSTestSettings { /// - /// Populate adapter settings from the context. + /// Populate adapter settings from the run settings XML. /// - /// The discovery context. + /// The run settings XML, or when none was provided. /// The logger for messages. /// The configuration. - internal static void PopulateSettings(IDiscoveryContext? context, IMessageLogger? logger, IConfiguration? configuration) + internal static void PopulateSettings(string? settingsXml, IAdapterMessageLogger? logger, IConfiguration? configuration) { #if !WINDOWS_UWP if (configuration?["mstest"] is not null - && context?.RunSettings is not null - && RunSettingsFileHasMSTestSettings(context.RunSettings.SettingsXml)) + && settingsXml is not null + && RunSettingsFileHasMSTestSettings(settingsXml)) { throw new InvalidOperationException(Resource.DuplicateConfigurationError); } #endif var settings = new MSTestSettings(); - var runConfigurationSettings = RunConfigurationSettings.GetSettings(context?.RunSettings?.SettingsXml); + var runConfigurationSettings = RunConfigurationSettings.GetSettings(settingsXml); #if !WINDOWS_UWP - if (!StringEx.IsNullOrEmpty(context?.RunSettings?.SettingsXml) && configuration?["mstest"] is null) + if (!StringEx.IsNullOrEmpty(settingsXml) && configuration?["mstest"] is null) #else - if (!StringEx.IsNullOrEmpty(context?.RunSettings?.SettingsXml)) + if (!StringEx.IsNullOrEmpty(settingsXml)) #endif { - MSTestSettings? aliasSettings = GetSettings(context.RunSettings.SettingsXml, SettingsNameAlias, logger); - settings = aliasSettings ?? GetSettings(context.RunSettings.SettingsXml, SettingsName, logger) ?? new MSTestSettings(); - SetGlobalSettings(context.RunSettings.SettingsXml, settings, logger); + MSTestSettings? aliasSettings = GetSettings(settingsXml, SettingsNameAlias, logger); + settings = aliasSettings ?? GetSettings(settingsXml, SettingsName, logger) ?? new MSTestSettings(); + SetGlobalSettings(settingsXml, settings, logger); } #if !WINDOWS_UWP else if (configuration?["mstest"] is not null) @@ -62,7 +61,7 @@ internal static void PopulateSettings(IDiscoveryContext? context, IMessageLogger if (settings.RandomizeTestOrder && settings.OrderTestsByNameInClass) { - logger?.SendMessage(TestMessageLevel.Warning, Resource.RandomTestOrderAndOrderTestsByNameInClassConflict); + logger?.SendMessage(MessageLevel.Warning, Resource.RandomTestOrderAndOrderTestsByNameInClassConflict); } // Track configuration source for telemetry. @@ -71,14 +70,14 @@ internal static void PopulateSettings(IDiscoveryContext? context, IMessageLogger { telemetry.ConfigurationSource = configuration?["mstest"] is not null ? "testconfig.json" - : !StringEx.IsNullOrEmpty(context?.RunSettings?.SettingsXml) + : !StringEx.IsNullOrEmpty(settingsXml) ? "runsettings" : "none"; } #endif } - private static void SetGlobalSettings(string runsettingsXml, MSTestSettings settings, IMessageLogger? logger) + private static void SetGlobalSettings(string runsettingsXml, MSTestSettings settings, IAdapterMessageLogger? logger) { XElement? runConfigElement = XDocument.Parse(runsettingsXml).Element("RunSettings")?.Element("RunConfiguration"); if (runConfigElement == null) @@ -93,12 +92,12 @@ private static void SetGlobalSettings(string runsettingsXml, MSTestSettings sett } else if (disableParallelizationString is not null) { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, disableParallelizationString, "DisableParallelization")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, disableParallelizationString, "DisableParallelization")); } } #if !WINDOWS_UWP - private static void ParseBooleanSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseBooleanSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -111,11 +110,11 @@ private static void ParseBooleanSetting(IConfiguration configuration, string key } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); } } - private static void ParseDebuggerLaunchModeSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseDebuggerLaunchModeSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -128,11 +127,11 @@ private static void ParseDebuggerLaunchModeSetting(IConfiguration configuration, } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); } } - private static void ParseTimeoutSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseTimeoutSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -149,11 +148,11 @@ private static void ParseTimeoutSetting(IConfiguration configuration, string key } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, value, key)); } } - private static void ParseSignedIntegerSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseSignedIntegerSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -166,7 +165,7 @@ private static void ParseSignedIntegerSetting(IConfiguration configuration, stri } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); } } @@ -176,7 +175,7 @@ private static void ParseSignedIntegerSetting(IConfiguration configuration, stri /// Configuration to load the settings from. /// The logger for messages. /// The MSTest settings. - internal static void SetSettingsFromConfig(IConfiguration configuration, IMessageLogger? logger, MSTestSettings settings) + internal static void SetSettingsFromConfig(IConfiguration configuration, IAdapterMessageLogger? logger, MSTestSettings settings) { // 'orderTestsByNameInClass' has moved under 'execution:' for consistency with the other execution settings. // Prefer the new 'mstest:execution:orderTestsByNameInClass' key. Only fall back to the deprecated flat @@ -188,7 +187,7 @@ internal static void SetSettingsFromConfig(IConfiguration configuration, IMessag } else if (configuration["mstest:orderTestsByNameInClass"] is not null) { - logger?.SendMessage(TestMessageLevel.Warning, Resource.DeprecatedFlatOrderTestsByNameInClassKey); + logger?.SendMessage(MessageLevel.Warning, Resource.DeprecatedFlatOrderTestsByNameInClassKey); ParseBooleanSetting(configuration, "orderTestsByNameInClass", logger, value => settings.OrderTestsByNameInClass = value); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs index 73dd94d50b..84d021f440 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using DebuggerLaunchMode = Microsoft.VisualStudio.TestTools.UnitTesting.DebuggerLaunchMode; +using MessageLevel = Microsoft.VisualStudio.TestTools.UnitTesting.MessageLevel; using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -44,7 +45,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) } using var stringReader = new StringReader(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); XmlReaderUtilities.ReadToRootNode(reader); reader.ReadToNextElement(); @@ -60,7 +61,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) } #endif - internal static MSTestSettings? GetSettings(string? runSettingsXml, string settingName, IMessageLogger? logger) + internal static MSTestSettings? GetSettings(string? runSettingsXml, string settingName, IAdapterMessageLogger? logger) { if (StringEx.IsNullOrWhiteSpace(runSettingsXml)) { @@ -68,7 +69,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) } using var stringReader = new StringReader(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); XmlReaderUtilities.ReadToRootNode(reader); reader.ReadToNextElement(); @@ -81,7 +82,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) return !reader.EOF ? ToSettings(reader.ReadSubtree(), logger) : null; } - private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logger) + private static MSTestSettings ToSettings(XmlReader reader, IAdapterMessageLogger? logger) { if (reader is null) { @@ -108,7 +109,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, captureTraceOutput, "CaptureTraceOutput")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, captureTraceOutput, "CaptureTraceOutput")); } break; @@ -120,7 +121,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapInconclusiveToFailed, "MapInconclusiveToFailed")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapInconclusiveToFailed, "MapInconclusiveToFailed")); } break; @@ -132,7 +133,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapNotRunnableToFailed, "MapNotRunnableToFailed")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapNotRunnableToFailed, "MapNotRunnableToFailed")); } break; @@ -144,7 +145,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, treatDiscoveryWarningsAsErrors, "TreatDiscoveryWarningsAsErrors")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, treatDiscoveryWarningsAsErrors, "TreatDiscoveryWarningsAsErrors")); } break; @@ -166,7 +167,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, considerEmptyDataSourceAsInconclusive, "ConsiderEmptyDataSourceAsInconclusive")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, considerEmptyDataSourceAsInconclusive, "ConsiderEmptyDataSourceAsInconclusive")); } break; @@ -193,7 +194,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, cooperativeCancellationTimeout, "CooperativeCancellationTimeout")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, cooperativeCancellationTimeout, "CooperativeCancellationTimeout")); } break; @@ -205,7 +206,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, orderTestsByNameInClass, "OrderTestsByNameInClass")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, orderTestsByNameInClass, "OrderTestsByNameInClass")); } break; @@ -217,7 +218,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomizeTestOrder, "RandomizeTestOrder")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomizeTestOrder, "RandomizeTestOrder")); } break; @@ -229,7 +230,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomTestOrderSeed, "RandomTestOrderSeed")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomTestOrderSeed, "RandomTestOrderSeed")); } break; @@ -241,7 +242,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, launchDebuggerOnAssertionFailure, "LaunchDebuggerOnAssertionFailure")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, launchDebuggerOnAssertionFailure, "LaunchDebuggerOnAssertionFailure")); } break; @@ -256,7 +257,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge return settings; } - private static void ParseTimeoutSetting(string rawValue, string settingName, IMessageLogger? logger, Action setSetting) + private static void ParseTimeoutSetting(string rawValue, string settingName, IAdapterMessageLogger? logger, Action setSetting) { if (int.TryParse(rawValue, out int result) && result > 0) { @@ -264,7 +265,7 @@ private static void ParseTimeoutSetting(string rawValue, string settingName, IMe } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, rawValue, settingName)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, rawValue, settingName)); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/InvalidRunSettingsException.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/InvalidRunSettingsException.cs new file mode 100644 index 0000000000..3e9a421915 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/InvalidRunSettingsException.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +/// +/// Thrown when the runsettings XML is structurally invalid (for example a bad attribute, an unexpected element, +/// or a wrong root node). This is the platform-agnostic replacement for the settings exception the platform +/// services layer historically surfaced from the VSTest object model. It is intentionally distinct from +/// : is caught by the discovery +/// initialization path (reported and treated as "no tests"), whereas a structural runsettings error propagates +/// to the host, preserving the original behavior. +/// +internal sealed class InvalidRunSettingsException : Exception +{ + internal InvalidRunSettingsException(string? message) + : base(message) + { + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs index f108598c65..a95cc80dcd 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs @@ -135,6 +135,11 @@ private static string GetManagedTypeName(string fullClassName) internal TestMethod Clone() => (TestMethod)MemberwiseClone(); + // NOTE: This method has a latent bug (it assigns AssemblyName/MethodInfo on `this` instead of on the + // returned clone), tracked by https://github.com/microsoft/testfx/issues/9573. It is intentionally left + // untouched here so the existing ToTestCase / test-case filter bridge callers keep their exact current + // behavior; the execution engine uses the correct CloneWithSource below instead. The two are unified once + // #9573 is addressed. internal TestMethod CloneWithUpdatedSource(string source) { var clone = (TestMethod)MemberwiseClone(); @@ -142,4 +147,15 @@ internal TestMethod CloneWithUpdatedSource(string source) MethodInfo = null; return clone; } + + // Correct counterpart of CloneWithUpdatedSource: assigns the updated source and clears the cached + // MethodInfo on the returned clone (leaving `this` untouched). Used by the execution engine's source + // resolution. See https://github.com/microsoft/testfx/issues/9573. + internal TestMethod CloneWithSource(string source) + { + var clone = (TestMethod)MemberwiseClone(); + clone.AssemblyName = source; + clone.MethodInfo = null; + return clone; + } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestTrait.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestTrait.cs new file mode 100644 index 0000000000..44c4799091 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestTrait.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +/// +/// A platform-agnostic name/value trait associated with a test (for example the values produced by +/// [TestProperty] attributes). This mirrors the shape historically carried by the test platform's +/// trait type but does not depend on any specific test platform's object model, so the platform services +/// layer can describe test traits without referencing that object model. +/// +#if NETFRAMEWORK +// A (which carries these) is serialized across app domains on .NET Framework. +[Serializable] +#endif +internal readonly struct TestTrait +{ + public TestTrait(string name, string value) + { + Name = name; + Value = value; + } + + /// + /// Gets the trait name. + /// + public string Name { get; } + + /// + /// Gets the trait value. + /// + public string Value { get; } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs index fb20da9858..8549fcc16e 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; @@ -17,11 +14,6 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; [DebuggerDisplay("{TestMethod.DisplayName} ({TestMethod.ManagedTypeName})")] internal sealed class UnitTestElement { - private static readonly byte[] OpenParen = [40, 0]; // Encoding.Unicode.GetBytes("("); - private static readonly byte[] CloseParen = [41, 0]; // Encoding.Unicode.GetBytes(")"); - private static readonly byte[] OpenBracket = [91, 0]; // Encoding.Unicode.GetBytes("["); - private static readonly byte[] CloseBracket = [93, 0]; // Encoding.Unicode.GetBytes("]"); - /// /// Initializes a new instance of the class. /// @@ -53,7 +45,7 @@ public UnitTestElement(TestMethod testMethod) /// /// Gets or sets the traits for test method. /// - public Trait[]? Traits { get; set; } + public TestTrait[]? Traits { get; set; } /// /// Gets or sets the priority of the test method, if any. @@ -81,6 +73,35 @@ public UnitTestElement(TestMethod testMethod) /// internal string[]? WorkItemIds { get; set; } + /// + /// Gets or sets host-provided execution context properties (for example the values historically read + /// from a test-case-management host: run id, plan id, build configuration, test point id, ...) that are + /// surfaced to the running test through TestContext. Keyed by the host property identifier so the + /// platform services layer does not depend on a specific test platform's property object model. This is + /// only populated when a host supplies such values (for internally discovered tests it stays null). + /// +#if NETFRAMEWORK + // Consumed on the source-processing side before execution; the isolation host copy never reads it. + [field: NonSerialized] +#endif + internal IReadOnlyDictionary? ExecutionContextProperties { get; set; } + + /// + /// Gets or sets the host's test case for this test, used to report its lifecycle and results back to the + /// host with full fidelity (preserving any host-injected data — such as test-case-management or + /// data-collector properties — that the neutral model does not otherwise carry) and to describe it to the + /// (still VSTest-based) deployment service. For tests handed to the adapter by a host it is that original + /// test case; for tests discovered internally by the platform services it is until + /// materialized on demand by the adapter's UnitTestElementExtensions.GetOrCreateHostTestCase. The execution engine treats it as an + /// opaque handle — it reads nothing VSTest-specific off it and only threads it into the result recorder and + /// the deployment boundary. + /// +#if NETFRAMEWORK + // Result recording and deployment happen on the source-processing side; the isolation host copy never reads it. + [field: NonSerialized] +#endif + internal object? HostRecordingHandle { get; set; } + internal UnitTestElement Clone() { var clone = (UnitTestElement)MemberwiseClone(); @@ -88,6 +109,11 @@ internal UnitTestElement Clone() return clone; } + // Legacy source-updating clone used only by the ToTestCase / test-case filter bridge + // (TestCaseExtensions.ToUnitTestElementWithUpdatedSource). It delegates to the buggy + // TestMethod.CloneWithUpdatedSource and is retained to preserve that path's exact current behavior; the + // execution engine uses WithUpdatedSource / CloneWithSource instead. Tracked by + // https://github.com/microsoft/testfx/issues/9573. internal UnitTestElement CloneWithUpdatedSource(string source) { var clone = (UnitTestElement)MemberwiseClone(); @@ -95,190 +121,24 @@ internal UnitTestElement CloneWithUpdatedSource(string source) return clone; } - /// - /// Convert the UnitTestElement instance to an Object Model testCase instance. - /// - /// An instance of . - internal TestCase ToTestCase() - { - // This causes compatibility problems with older runners. - // string testFullName = this.TestMethod.HasManagedMethodAndTypeProperties - // ? $"{TestMethod.ManagedTypeName}.{TestMethod.ManagedMethodName}" - // : $"{TestMethod.FullClassName}.{TestMethod.Name}"; - string testFullName = $"{TestMethod.FullClassName}.{TestMethod.Name}"; - - TestCase testCase = new(testFullName, EngineConstants.ExecutorUri, TestMethod.AssemblyName) - { - DisplayName = TestMethod.DisplayName, - LocalExtensionData = this, - }; - - if (TestMethod.HasManagedMethodAndTypeProperties) - { - testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, TestMethod.ManagedTypeName); - testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, TestMethod.ManagedMethodName); - } - - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, TestMethod.FullClassName); - - if (TestMethod.ParameterTypes is not null) - { - testCase.SetPropertyValue(EngineConstants.ParameterTypesProperty, TestMethod.ParameterTypes); - } - - IReadOnlyCollection? hierarchy = TestMethod.Hierarchy; - if (hierarchy is { Count: > 0 }) - { - testCase.SetHierarchy([.. hierarchy]); - } - - // Set only if some test category is present - if (TestCategory is { Length: > 0 }) - { - testCase.SetPropertyValue(EngineConstants.TestCategoryProperty, TestCategory); - } - - // Set priority if present - if (Priority != null) - { - testCase.SetPropertyValue(EngineConstants.PriorityProperty, Priority.Value); - } - - if (Traits is { Length: > 0 }) - { - testCase.Traits.AddRange(Traits); - } - - if (WorkItemIds != null) - { - testCase.SetPropertyValue(EngineConstants.WorkItemIdsProperty, WorkItemIds); - } - -#if !WINDOWS_UWP && !WIN_UI - // The list of items to deploy before running this test. - if (DeploymentItems is { Length: > 0 }) - { - testCase.SetPropertyValue(EngineConstants.DeploymentItemsProperty, DeploymentItems); - } -#endif - - // Set the Do not parallelize state if present - if (DoNotParallelize) - { - testCase.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, DoNotParallelize); - } - - if (UnfoldingStrategy != TestDataSourceUnfoldingStrategy.Auto) - { - testCase.SetPropertyValue(EngineConstants.UnfoldingStrategy, (int)UnfoldingStrategy); - } - - // Store resolved data if any - if (TestMethod.DataType != DynamicDataType.None) - { - testCase.SetPropertyValue(EngineConstants.TestDynamicDataTypeProperty, (int)TestMethod.DataType); - testCase.SetPropertyValue(EngineConstants.TestDynamicDataProperty, TestMethod.SerializedData); - testCase.SetPropertyValue(EngineConstants.TestCaseIndexProperty, TestMethod.TestCaseIndex); - // VSTest serialization doesn't handle null so instead don't set the property so that it's deserialized as null - if (TestMethod.TestDataSourceIgnoreMessage is not null) - { - testCase.SetPropertyValue(EngineConstants.TestDataSourceIgnoreMessageProperty, TestMethod.TestDataSourceIgnoreMessage); - } - } - - testCase.LineNumber = DeclaringLineNumber ?? -1; - testCase.CodeFilePath = DeclaringFilePath; - - SetTestCaseId(testCase, testFullName); - - return testCase; - } - - private void SetTestCaseId(TestCase testCase, string testFullName) - => testCase.Id = GenerateSerializedDataStrategyTestId(testFullName); - - private Guid GenerateSerializedDataStrategyTestId(string testFullName) + // Correct source-updating clone: the returned clone (only) targets the new source. Used by the execution + // engine via WithUpdatedSource. See https://github.com/microsoft/testfx/issues/9573. + internal UnitTestElement CloneWithSource(string source) { - // Below comment is copied over from Test Platform. - // If source is a file name then just use the filename for the identifier since the file might have moved between - // discovery and execution (in appx mode for example). This is not elegant because the Source contents should be - // a black box to the framework. - // For example in the database adapter case this is not a file path. - // As discussed with team, we found no scenario for netcore, & fullclr where the Source is not present where ID - // is generated, which means we would always use FileName to generate ID. In cases where somehow Source Path - // contained garbage character the API Path.GetFileName() we are simply returning original input. - // For UWP where source during discovery, & during execution can be on different machine, in such case we should - // always use Path.GetFileName(). - string fileNameOrFilePath = TestMethod.AssemblyName; - try - { - fileNameOrFilePath = Path.GetFileName(fileNameOrFilePath); - } - catch (ArgumentException) - { - // In case path contains invalid characters. - } - - byte hashVersion = 1; // Increment when changing the hashing algorithm. - var hash = new TestFx.Hashing.XxHash128(); - hash.Append(Encoding.Unicode.GetBytes(fileNameOrFilePath)); - hash.Append(Encoding.Unicode.GetBytes(testFullName)); - if (TestMethod.ParameterTypes is not null) - { - hash.Append(OpenParen); - hash.Append(Encoding.Unicode.GetBytes(TestMethod.ParameterTypes)); - hash.Append(CloseParen); - } - - if (TestMethod.DataType != DynamicDataType.None) - { - hash.Append(OpenBracket); - hash.Append(Encoding.Unicode.GetBytes(TestMethod.TestCaseIndex.ToString(CultureInfo.InvariantCulture))); - hash.Append(CloseBracket); - } - - byte[] hashBytes = hash.GetCurrentHash(); - - return VersionedGuidFromHash(hashBytes, hashVersion); + var clone = (UnitTestElement)MemberwiseClone(); + clone.TestMethod = TestMethod.CloneWithSource(source); + return clone; } - internal /* for testing */ static Guid VersionedGuidFromHash(byte[] hashBytes, byte hashVersion) - { - int firstByte = 0; - int versionByte = 6; - - // We set first 4 bits to 0001, which is our version. Increase this when we change the hashing algorithm. - // - // Note: The logic below is operating on int32, because bitwise operators are not defined for byte in C#. But casting - // does not affect endianness, so we can safely assume the byte data are at the end of the int. - // We also don't care to specify the whole expansion of the int in the bit masks, which effectively ignores all the data in the int - // before the last byte data, but that is okay, they will always be zero. - hashBytes[firstByte] = (byte)((hashBytes[firstByte] & 0b0000_1111) | (hashVersion << 4)); - // We set first 4bits 7th byte to 8 to indicate that this is a version 8 UUID. https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-8 - hashBytes[versionByte] = (byte)((hashBytes[versionByte] & 0b0000_1111) | 0b1000_0000); - - // We set the first 2 bits of nineth byte to 0b10. In the guid this is stored as byte, so we don't care about endiannes. - int variantByte = 8; - hashBytes[variantByte] = (byte)((hashBytes[variantByte] & 0b0011_1111) | 0b1000_0000); - - Guid guid; - - // On .NET Framework we cannot specify the endianness of the Guid because the new Guid(hashBytes, bigEndian: true); api is not available. - // Instead we construct the int and short values manually, doing what the constructor would do, to put the bytes in big-endian order. - guid = new Guid( - (hashBytes[0] << 24) | (hashBytes[1] << 16) | (hashBytes[2] << 8) | hashBytes[3], - (short)((hashBytes[4] << 8) | hashBytes[5]), - (short)((hashBytes[6] << 8) | hashBytes[7]), - hashBytes[8], hashBytes[9], hashBytes[10], hashBytes[11], hashBytes[12], hashBytes[13], hashBytes[14], hashBytes[15]); - -#if DEBUG && NET9_0_OR_GREATER - // Version property is only available on .NET 9 and later. - Debug.Assert(guid.Version == 8, $"Expected Guid version to be 8, but it was {guid.Version}"); - // The field represents the 4 bit value, but according to the specification only the first 2 bits are used for UUID v8. - // So we shift 2 bits to the right to get the actual variant value. - int variant = guid.Variant >> 2; - Debug.Assert(variant == 2, $"Expected Guid variant to be 2, but it was {variant}"); -#endif - return guid; - } + /// + /// Returns this element when it already targets , otherwise a clone whose test + /// method points at . This mirrors the source resolution the adapter previously + /// performed while converting a host test case, so a deployed source (relocated to the deployment + /// directory) is honored without reloading the assembly from its original location (see + /// https://github.com/microsoft/testfx/issues/6713). + /// + /// The (possibly deployment-relocated) source of the test. + /// An element whose is . + internal UnitTestElement WithUpdatedSource(string source) + => TestMethod.AssemblyName == source ? this : CloneWithSource(source); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs index 81c975c823..aa460a8231 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; @@ -130,7 +129,7 @@ internal static IPlatformServiceProvider Instance /// /// The source. /// - /// + /// /// The run Settings for the session. /// /// @@ -138,9 +137,9 @@ internal static IPlatformServiceProvider Instance /// public ITestSourceHost CreateTestSourceHost( string source, - TestPlatform.ObjectModel.Adapter.IRunSettings? runSettings) + string? settingsXml) { - var testSourceHost = new TestSourceHost(source, runSettings); + var testSourceHost = new TestSourceHost(source, settingsXml); testSourceHost.SetupHost(); return testSourceHost; @@ -166,7 +165,7 @@ public ITestSourceHost CreateTestSourceHost( /// /// This was required for compatibility reasons since the TestContext object that the V1 adapter had for desktop is not .Net Core compliant. /// - public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger messageLogger, UTF.UnitTestOutcome outcome) + public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger messageLogger, UTF.UnitTestOutcome outcome) { var testContextImplementation = new TestContextImplementation(testMethod, testClassFullName, properties, messageLogger, TestRunCancellationToken); testContextImplementation.SetOutcome(outcome); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs b/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs index 5723c35a44..8423ab05b6 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs @@ -5,7 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; #endif -using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -96,13 +95,13 @@ private static RunConfigurationSettings ToSettings(XmlReader reader) { case "EXECUTIONTHREADAPARTMENTSTATE": { - if (Enum.TryParse(reader.ReadInnerXml(), out PlatformApartmentState platformApartmentState)) + if (Enum.TryParse(reader.ReadInnerXml(), out ApartmentStateSetting apartmentStateSetting)) { - settings.ExecutionApartmentState = platformApartmentState switch + settings.ExecutionApartmentState = apartmentStateSetting switch { - PlatformApartmentState.STA => ApartmentState.STA, - PlatformApartmentState.MTA => ApartmentState.MTA, - _ => throw new NotSupportedException($"Platform apartment state '{platformApartmentState}' is not supported."), + ApartmentStateSetting.STA => ApartmentState.STA, + ApartmentStateSetting.MTA => ApartmentState.MTA, + _ => throw new NotSupportedException($"Platform apartment state '{apartmentStateSetting}' is not supported."), }; } @@ -131,13 +130,13 @@ internal static RunConfigurationSettings SetRunConfigurationSettingsFromConfig(I // } // } string? apartmentStateValue = configuration["mstest:execution:executionApartmentState"]; - if (Enum.TryParse(apartmentStateValue, out PlatformApartmentState platformApartmentState)) + if (Enum.TryParse(apartmentStateValue, out ApartmentStateSetting apartmentStateSetting)) { - settings.ExecutionApartmentState = platformApartmentState switch + settings.ExecutionApartmentState = apartmentStateSetting switch { - PlatformApartmentState.STA => ApartmentState.STA, - PlatformApartmentState.MTA => ApartmentState.MTA, - _ => throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, platformApartmentState, "execution:executionApartmentState")), + ApartmentStateSetting.STA => ApartmentState.STA, + ApartmentStateSetting.MTA => ApartmentState.MTA, + _ => throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, apartmentStateSetting, "execution:executionApartmentState")), }; } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs index d844431a45..c10ea5169b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs @@ -4,9 +4,9 @@ #if !WINDOWS_UWP using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -206,7 +206,7 @@ public static bool IsAppDomainCreationDisabled(string? settingsXml) if (!StringEx.IsNullOrEmpty(settingsXml)) { StringReader stringReader = new(settingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); var xmlDoc = new XmlDocument() { XmlResolver = null }; xmlDoc.Load(reader); @@ -384,7 +384,7 @@ private void ReadAssemblyResolutionPath(XmlReader reader) else { string message = string.Format(CultureInfo.CurrentCulture, Resource.InvalidSettingsXmlElement, reader.Name, "AssemblyResolution"); - throw new SettingsException(message); + throw new InvalidRunSettingsException(message); } // Move to the next element under tag AssemblyResolution diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs index 5efc98a041..9cfb5f78d4 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs @@ -10,7 +10,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using ITestMethod = Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel.ITestMethod; @@ -71,7 +70,7 @@ public override string ToString() #else private readonly object _propertiesLock = new(); #endif - private readonly IMessageLogger? _messageLogger; + private readonly IAdapterMessageLogger? _messageLogger; private readonly TestRunCancellationToken? _testRunCancellationToken; private CancellationTokenRegistration? _cancellationTokenRegistration; @@ -113,7 +112,7 @@ public override string ToString() /// Properties/configuration passed in. /// The message logger to use. /// The global test run cancellation token. - internal TestContextImplementation(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger? messageLogger, TestRunCancellationToken? testRunCancellationToken) + internal TestContextImplementation(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger? messageLogger, TestRunCancellationToken? testRunCancellationToken) { // testMethod can be null when running ForceCleanup (done when reaching --maximum-failed-tests. DebugEx.Assert(properties != null, "properties is not null"); @@ -428,7 +427,7 @@ public void SetDisplayName(string? displayName) /// public override void DisplayMessage(MessageLevel messageLevel, string message) - => _messageLogger?.SendMessage(messageLevel.ToTestMessageLevel(), message); + => _messageLogger?.SendMessage(messageLevel, message); #endregion /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs index 11a4cd49cb..1591ad3481 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs @@ -4,14 +4,10 @@ #if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -#if NETFRAMEWORK -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -#endif using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -104,13 +100,13 @@ public void Cleanup() /// /// Deploy files related to the list of tests specified. /// - /// The tests. - /// The run context. - /// The framework handle. + /// The tests. + /// The host deployment inputs. + /// The logger used to surface deployment warnings. /// Return true if deployment is done. - public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFrameworkHandle frameworkHandle) + public bool Deploy(IEnumerable testElements, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger) { - DebugEx.Assert(testCases != null, "tests"); + DebugEx.Assert(testElements != null, "tests"); // Reset runDirectories before doing deployment, so that older values of runDirectories is not picked // even if test host is kept alive. @@ -118,7 +114,7 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr _adapterSettings = MSTestSettingsProvider.Settings; bool canDeploy = CanDeploy(); - bool hasDeploymentItems = testCases.Any(DeploymentItemUtility.HasDeploymentItems); + bool hasDeploymentItems = testElements.Any(DeploymentItemUtility.HasDeploymentItems); // deployment directories should not be created in this case,simply return if (!canDeploy && hasDeploymentItems) @@ -127,8 +123,8 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr } #if NETFRAMEWORK - string? firstTestSource = testCases.FirstOrDefault()?.Source; - RunDirectories = _deploymentUtility.CreateDeploymentDirectories(runContext, firstTestSource); + string? firstTestSource = testElements.FirstOrDefault()?.TestMethod.AssemblyName; + RunDirectories = _deploymentUtility.CreateDeploymentDirectories(deploymentContext, firstTestSource); // Deployment directories are created but deployment will not happen. // This is added just to keep consistency with MSTest v1 behavior. @@ -143,8 +139,8 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr return false; } - string? firstTestSource = testCases.FirstOrDefault()?.Source; - RunDirectories = _deploymentUtility.CreateDeploymentDirectories(runContext, firstTestSource); + string? firstTestSource = testElements.FirstOrDefault()?.TestMethod.AssemblyName; + RunDirectories = _deploymentUtility.CreateDeploymentDirectories(deploymentContext, firstTestSource); #endif // Object model currently does not have support for SuspendCodeCoverage. We can remove this once support is added @@ -153,15 +149,15 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr #endif { // Group the tests by source - var testsBySource = from test in testCases - group test by test.Source into testGroup + var testsBySource = from test in testElements + group test by test.TestMethod.AssemblyName into testGroup select new { Source = testGroup.Key, Tests = testGroup }; TestRunDirectories runDirectories = RunDirectories; foreach (var group in testsBySource) { // do the deployment - _deploymentUtility.Deploy(@group.Tests, @group.Source, runContext, frameworkHandle, RunDirectories); + _deploymentUtility.Deploy(@group.Tests, @group.Source, deploymentContext, messageLogger, RunDirectories); } // Update the runDirectories diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs index 4255641119..ae97ba6ab2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs @@ -5,9 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.AppContainer; #endif using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -#if NETFRAMEWORK -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -#endif namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -79,7 +76,7 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) { #if NETFRAMEWORK // This loads the dll in a different app domain. We can optimize this to load in the current domain since this code could be run in a new app domain anyway. - bool? utfReference = AssemblyHelper.DoesReferencesAssembly(source, assemblyName); + bool? utfReference = DoesSourceReferenceAssembly(source, assemblyName); // If no reference to UTF don't run discovery. Take conservative approach. If not able to find proceed with discovery. return !utfReference.HasValue || utfReference.Value; @@ -94,6 +91,85 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) #endif } +#if NETFRAMEWORK + /// + /// Checks whether the source assembly directly references the given assembly. + /// Only the assembly simple name and public key token are matched; version is ignored. + /// Returns if the reference could not be determined. + /// + /// The path to the source assembly to inspect. + /// The assembly to look for in the source's references. + /// if referenced, if not, if undeterminable. + private static bool? DoesSourceReferenceAssembly(string source, AssemblyName referenceAssembly) + { + if (string.IsNullOrEmpty(source) || referenceAssembly is null) + { + return null; + } + + try + { + string? referenceAssemblyName = referenceAssembly.Name; + byte[]? referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken(); + + // ReflectionOnlyLoadFrom loads from the specified path only (no probing) and does not + // execute any code from the loaded assembly. + var assembly = Assembly.ReflectionOnlyLoadFrom(source); + + foreach (AssemblyName referencedAssembly in assembly.GetReferencedAssemblies()) + { + // Match without version: only the simple name and public key token. + if (!string.Equals(referencedAssembly.Name, referenceAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (ArePublicKeyTokensEqual(referencedAssembly.GetPublicKeyToken(), referenceAssemblyPublicKeyToken)) + { + return true; + } + } + + return false; + } + catch + { + // Return null if we are not able to check. + return null; + } + } + + private static bool ArePublicKeyTokensEqual(byte[]? left, byte[]? right) + { + // GetPublicKeyToken() returns null (or an empty array) for unsigned assemblies. + // Treat two unsigned tokens as equal, and a signed vs. unsigned pair as unequal. + if (left is null || left.Length == 0) + { + return right is null || right.Length == 0; + } + + if (right is null || right.Length == 0) + { + return false; + } + + if (left.Length != right.Length) + { + return false; + } + + for (int i = 0; i < left.Length; ++i) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } +#endif + /// /// Gets the set of sources (dll's/exe's) that contain tests. If a source is a package (appx), return the file (dll/exe) that contains tests from it. /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs index 7ff1be7380..933d8d3185 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if !WINDOWS_UWP @@ -8,13 +8,6 @@ #if NETFRAMEWORK using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; #endif -#if NETFRAMEWORK -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -#endif -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -#if NETFRAMEWORK || (NET && !WINDOWS_UWP) -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -#endif #if !WINDOWS_UWP using Microsoft.VisualStudio.TestTools.UnitTesting; #endif @@ -30,6 +23,13 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; #pragma warning disable CA1852 // Seal internal types - needs to be non-sealed because it's mocked in tests. internal class TestSourceHost : ITestSourceHost { +#if NETFRAMEWORK || (NET && !WINDOWS_UWP) + private const string ObjectModelAssemblyName = "Microsoft.VisualStudio.TestPlatform.ObjectModel"; +#endif +#if NETFRAMEWORK + private const string CoreUtilitiesAssemblyName = "Microsoft.TestPlatform.CoreUtilities"; +#endif + #if !WINDOWS_UWP #pragma warning disable IDE0052 // Remove unread private members private readonly string _sourceFileName; @@ -66,10 +66,10 @@ internal class TestSourceHost : ITestSourceHost /// Initializes a new instance of the class. /// /// The source file name. - /// The run-settings provided for this session. - public TestSourceHost(string sourceFileName, IRunSettings? runSettings) + /// The run-settings XML provided for this session. + public TestSourceHost(string sourceFileName, string? settingsXml) #if NETFRAMEWORK - : this(sourceFileName, runSettings, new AppDomainWrapper()) + : this(sourceFileName, settingsXml, new AppDomainWrapper()) #endif { #if !WINDOWS_UWP && !NETFRAMEWORK @@ -81,7 +81,7 @@ public TestSourceHost(string sourceFileName, IRunSettings? runSettings) } #if NETFRAMEWORK - internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDomain appDomain) + internal TestSourceHost(string sourceFileName, string? settingsXml, IAppDomain appDomain) { _sourceFileName = sourceFileName; _appDomain = appDomain; @@ -90,7 +90,7 @@ internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDo SetContext(sourceFileName); // Set isAppDomainCreationDisabled flag - _isAppDomainCreationDisabled = runSettings != null && MSTestAdapterSettings.IsAppDomainCreationDisabled(runSettings.SettingsXml); + _isAppDomainCreationDisabled = settingsXml is not null && MSTestAdapterSettings.IsAppDomainCreationDisabled(settingsXml); } /// @@ -179,7 +179,7 @@ public void SetupHost() // For unknown reasons, with MSTest 3.4+ we start to see infinite cycles of assembly resolution of this dll in the new app // domain. In older versions, this was not the case, and the callback was allowing to fully lookup and load the dll before // triggering the next resolution. - AppDomain.Load(typeof(EqtTrace).Assembly.GetName()); + AppDomain.Load(GetLoadedAssembly(CoreUtilitiesAssemblyName).GetName()); // Add an assembly resolver in the child app-domain... Type assemblyResolverType = typeof(AssemblyResolver); @@ -352,6 +352,17 @@ internal string GetTargetFrameworkVersionString(string sourceFileName) #endif #if NETFRAMEWORK || (NET && !WINDOWS_UWP) + /// + /// Resolves a loaded test-platform assembly by simple name, so this file needs no compile-time reference to + /// the VSTest object model. These assemblies are loaded by the adapter/host before the source host runs, so + /// the resolved identity matches what a direct type reference would have produced. + /// + /// The simple assembly name. + /// The loaded assembly. + private static Assembly GetLoadedAssembly(string simpleName) + => AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => string.Equals(a.GetName().Name, simpleName, StringComparison.Ordinal)) + ?? Assembly.Load(simpleName); + /// /// Gets the probing paths to load the test assembly dependencies. /// @@ -403,7 +414,7 @@ internal virtual List GetResolutionPaths(string sourceFileName, bool isP resolutionPaths.Add(adapterDirectory); } - string? testPlatformDirectory = Path.GetDirectoryName(AssemblyFileLocator.TryGetLocation(typeof(AssemblyHelper).Assembly)); + string? testPlatformDirectory = Path.GetDirectoryName(AssemblyFileLocator.TryGetLocation(GetLoadedAssembly(ObjectModelAssemblyName))); if (!string.IsNullOrEmpty(testPlatformDirectory) && !resolutionPaths.Contains(testPlatformDirectory)) { // Adding TestPlatform folder to resolution paths diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs index a5751f5031..f3d0b2afbc 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if NETFRAMEWORK @@ -6,7 +6,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; @@ -18,9 +17,22 @@ internal static class AppDomainUtilities { private const string ObjectModelVersionBuiltAgainst = "11.0.0.0"; + private const string ObjectModelAssemblyName = "Microsoft.VisualStudio.TestPlatform.ObjectModel"; + private static readonly Version DefaultVersion = new(); private static readonly Version Version45 = new("4.5"); + /// + /// Resolves the loaded VSTest object-model assembly by simple name, so this AppDomain-wiring code does not + /// need a compile-time reference to it. By the time these methods run (test source host setup during + /// discovery/execution) the adapter has already loaded the object model into the current (parent) domain, + /// so its identity — including any binding redirect in effect — matches what a direct type reference resolved to. + /// + /// The object-model assembly. + private static Assembly GetObjectModelAssembly() + => AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => string.Equals(a.GetName().Name, ObjectModelAssemblyName, StringComparison.Ordinal)) + ?? Assembly.Load(ObjectModelAssemblyName); + /// /// Gets or sets the Xml Utilities instance. /// @@ -90,7 +102,7 @@ internal static string GetTargetFrameworkVersionString(string testSourcePath) var resolutionPaths = new List { - Path.GetDirectoryName(typeof(TestCase).Assembly.Location), + Path.GetDirectoryName(GetObjectModelAssembly().Location), Path.GetDirectoryName(testSourcePath), }; @@ -157,10 +169,10 @@ internal static void SetConfigurationFile(AppDomainSetup appDomainSetup, string? try { // Add redirection of the built 11.0 Object Model assembly to the current version if that is not 11.0 - string currentVersionOfObjectModel = typeof(TestCase).Assembly.GetName().Version.ToString(); + string currentVersionOfObjectModel = GetObjectModelAssembly().GetName().Version.ToString(); if (!string.Equals(currentVersionOfObjectModel, ObjectModelVersionBuiltAgainst, StringComparison.Ordinal)) { - AssemblyName assemblyName = typeof(TestCase).Assembly.GetName(); + AssemblyName assemblyName = GetObjectModelAssembly().GetName(); byte[] configurationBytes = XmlUtilities.AddAssemblyRedirection( testSourceConfigFile, diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs index 50546a66e2..c371e13baa 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs @@ -3,9 +3,9 @@ #if !WINDOWS_UWP && !WIN_UI +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; @@ -105,21 +105,17 @@ internal static bool IsValidDeploymentItem([NotNullWhen(true)] string? sourcePat /// /// Returns whether there are any deployment items defined on the test. /// - /// The test Case. + /// The test. /// True if has deployment items. - internal static bool HasDeploymentItems(TestCase testCase) - { - KeyValuePair[]? deploymentItems = GetDeploymentItems(testCase); - - return deploymentItems is { Length: > 0 }; - } + internal static bool HasDeploymentItems(UnitTestElement testElement) + => testElement.DeploymentItems is { Length: > 0 }; - internal static IList GetDeploymentItems(IEnumerable tests) + internal static IList GetDeploymentItems(IEnumerable tests) { List allDeploymentItems = []; - foreach (TestCase test in tests) + foreach (UnitTestElement test in tests) { - KeyValuePair[]? items = GetDeploymentItems(test); + KeyValuePair[]? items = test.DeploymentItems; if (items == null || items.Length == 0) { continue; @@ -220,14 +216,6 @@ private static List GetDeploymentItems(IEnumerable - /// Returns the deployment items defined on the test. - /// - /// The test Case. - /// The . - private static KeyValuePair[]? GetDeploymentItems(TestCase testCase) => testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty) as - KeyValuePair[]; - private static KeyValuePair[]? ToKeyValuePairs(IEnumerable deploymentItemList) { if (!deploymentItemList.Any()) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs index be6335ba69..47713d16af 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs @@ -4,13 +4,11 @@ #if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; - -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; @@ -43,31 +41,31 @@ public DeploymentUtilityBase(DeploymentItemUtility deploymentItemUtility, Assemb protected AssemblyUtility AssemblyUtility { get; set; } - public bool Deploy(IEnumerable tests, string source, IRunContext? runContext, ITestExecutionRecorder testExecutionRecorder, TestRunDirectories runDirectories) + public bool Deploy(IEnumerable tests, string source, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, TestRunDirectories runDirectories) { IList deploymentItems = DeploymentItemUtility.GetDeploymentItems(tests); // we just deploy source if there are no deployment items for current source but there are deployment items for other sources - return Deploy(source, runContext, testExecutionRecorder, deploymentItems, runDirectories); + return Deploy(source, deploymentContext, messageLogger, deploymentItems, runDirectories); } /// /// Create deployment directories. /// - /// The run context. + /// The host deployment inputs. /// /// The path to the test assembly of the first test case. In most cases, all /// test cases belong to the same assembly, but not guaranteed. We are using the path from /// the first test case as a "best effort" implementation. DeploymentItem isn't correctly designed and should be deprecated in future. /// /// TestRunDirectories instance. - public TestRunDirectories CreateDeploymentDirectories(IRunContext? runContext, string? firstTestSource) + public TestRunDirectories CreateDeploymentDirectories(DeploymentContext deploymentContext, string? firstTestSource) { - string resultsDirectory = GetTestResultsDirectory(runContext); + string resultsDirectory = GetTestResultsDirectory(deploymentContext); string rootDeploymentDirectory = GetRootDeploymentDirectory(resultsDirectory); #if NETFRAMEWORK - bool isAppDomainCreationDisabled = runContext?.RunSettings != null && MSTestAdapterSettings.IsAppDomainCreationDisabled(runContext.RunSettings.SettingsXml); + bool isAppDomainCreationDisabled = deploymentContext.RunSettingsXml is not null && MSTestAdapterSettings.IsAppDomainCreationDisabled(deploymentContext.RunSettingsXml); #else // AppDomains are only supported in .NET Framework. const bool isAppDomainCreationDisabled = true; @@ -94,10 +92,10 @@ public TestRunDirectories CreateDeploymentDirectories(IRunContext? runContext, s /// /// Get the parent test results directory where deployment will be done. /// - /// The run context. + /// The host deployment inputs. /// The test results directory. - public static string GetTestResultsDirectory(IRunContext? runContext) => !StringEx.IsNullOrEmpty(runContext?.TestRunDirectory) - ? runContext.TestRunDirectory + public static string GetTestResultsDirectory(DeploymentContext deploymentContext) => !StringEx.IsNullOrEmpty(deploymentContext.TestRunDirectory) + ? deploymentContext.TestRunDirectory : Path.GetFullPath(Path.Combine(Path.GetTempPath(), TestRunDirectories.DefaultDeploymentRootDirectory)); /// @@ -397,20 +395,20 @@ protected static bool IsOutputDirectoryValid(DeploymentItem deploymentItem, stri /// /// Log the parameter warnings on the parameter logger. /// - /// Execution recorder. + /// Message logger. /// Warnings. - private static void LogWarnings(ITestExecutionRecorder testExecutionRecorder, IEnumerable warnings) + private static void LogWarnings(IAdapterMessageLogger messageLogger, IEnumerable warnings) { - DebugEx.Assert(testExecutionRecorder != null, "Logger should not be null"); + DebugEx.Assert(messageLogger != null, "Logger should not be null"); // log the warnings foreach (string warning in warnings) { - testExecutionRecorder.SendMessage(TestMessageLevel.Warning, warning); + messageLogger.SendMessage(MessageLevel.Warning, warning); } } - private bool Deploy(string source, IRunContext? runContext, ITestExecutionRecorder testExecutionRecorder, IList deploymentItems, TestRunDirectories runDirectories) + private bool Deploy(string source, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, IList deploymentItems, TestRunDirectories runDirectories) { if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) { @@ -427,10 +425,10 @@ private bool Deploy(string source, IRunContext? runContext, ITestExecutionRecord PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor: Using deployment directory {0} for source {1}.", runDirectories.OutDirectory, source); } - IEnumerable warnings = Deploy([.. deploymentItems], source, runDirectories.OutDirectory, GetTestResultsDirectory(runContext)); + IEnumerable warnings = Deploy([.. deploymentItems], source, runDirectories.OutDirectory, GetTestResultsDirectory(deploymentContext)); // Log warnings - LogWarnings(testExecutionRecorder, warnings); + LogWarnings(messageLogger, warnings); return deploymentItems is { Count: > 0 }; } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/SuspendCodeCoverage.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/SuspendCodeCoverage.cs new file mode 100644 index 0000000000..4a8dc95a16 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/SuspendCodeCoverage.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; + +/// +/// Suspends dynamic code-coverage instrumentation of the modules that are loaded while this object is alive +/// (between construction and disposal) by setting the well-known collector environment variable. The previous +/// value of the environment variable is restored on dispose. +/// +internal sealed class SuspendCodeCoverage : IDisposable +{ + private const string SuspendCodeCoverageEnvVarName = "__VANGUARD_SUSPEND_INSTRUMENT__"; + private const string SuspendCodeCoverageEnvVarTrueValue = "TRUE"; + + private readonly string? _previousEnvironmentValue; + + private bool _isDisposed; + + public SuspendCodeCoverage() + { + _previousEnvironmentValue = Environment.GetEnvironmentVariable(SuspendCodeCoverageEnvVarName, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(SuspendCodeCoverageEnvVarName, SuspendCodeCoverageEnvVarTrueValue, EnvironmentVariableTarget.Process); + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + Environment.SetEnvironmentVariable(SuspendCodeCoverageEnvVarName, _previousEnvironmentValue, EnvironmentVariableTarget.Process); + _isDisposed = true; + } +} +#endif diff --git a/src/TestFramework/TestFramework/TestFramework.csproj b/src/TestFramework/TestFramework/TestFramework.csproj index f7e4b961f1..759d85cc85 100644 --- a/src/TestFramework/TestFramework/TestFramework.csproj +++ b/src/TestFramework/TestFramework/TestFramework.csproj @@ -58,6 +58,7 @@ + diff --git a/test/IntegrationTests/MSTest.IntegrationTests/TestCaseFilteringTests.cs b/test/IntegrationTests/MSTest.IntegrationTests/TestCaseFilteringTests.cs new file mode 100644 index 0000000000..11d9b30662 --- /dev/null +++ b/test/IntegrationTests/MSTest.IntegrationTests/TestCaseFilteringTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using Microsoft.MSTestV2.CLIAutomation; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +using TestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult; + +namespace MSTest.IntegrationTests; + +/// +/// Regression guard for the platform-agnostic filtering refactor (neutral ITestElementFilter). +/// Filtering is now expressed over the neutral UnitTestElement: discovery matches +/// element.ToTestCase(), and execution matches +/// incomingTestCase.ToUnitTestElementWithUpdatedSource(source).ToTestCase(). These tests lock the +/// end-to-end discover-then-run behavior for FullyQualifiedName and Id filters — the two +/// properties most sensitive to the TestCaseUnitTestElement round-trip that the refactor +/// re-routes — so a future change cannot silently break --filter. +/// +[TestClass] +public class TestCaseFilteringTests : CLITestBase +{ + private const string TestAsset = "DiscoverInternalsProject"; + + // A non-parameterized test with a stable, filter-safe fully qualified name (no filter operator chars). + private const string TargetDisplayName = "TopLevelInternalClass_TestMethod1"; + + [TestMethod] + public void FilterByFullyQualifiedNameSelectsExactlyTheMatchingTestDuringDiscovery() + { + string assemblyPath = GetAssetFullPath(TestAsset); + TestCase target = GetTargetTestCase(assemblyPath); + + ImmutableArray filtered = DiscoverTests(assemblyPath, $"FullyQualifiedName={target.FullyQualifiedName}"); + + Assert.HasCount(1, filtered); + Assert.AreEqual(target.FullyQualifiedName, filtered[0].FullyQualifiedName); + Assert.AreEqual(target.Id, filtered[0].Id); + } + + [TestMethod] + public void FilterByIdSelectsExactlyTheMatchingTestDuringDiscovery() + { + string assemblyPath = GetAssetFullPath(TestAsset); + TestCase target = GetTargetTestCase(assemblyPath); + + ImmutableArray filtered = DiscoverTests(assemblyPath, $"Id={target.Id}"); + + Assert.HasCount(1, filtered); + Assert.AreEqual(target.FullyQualifiedName, filtered[0].FullyQualifiedName); + Assert.AreEqual(target.Id, filtered[0].Id); + } + + [TestMethod] + public async Task FilterByFullyQualifiedNameSelectsExactlyTheMatchingTestDuringExecution() + { + string assemblyPath = GetAssetFullPath(TestAsset); + ImmutableArray allTests = DiscoverTests(assemblyPath); + TestCase target = allTests.First(t => t.DisplayName == TargetDisplayName); + + SimulateCrossProcessTestCases(allTests); + + ImmutableArray results = await RunTestsAsync(allTests, $"FullyQualifiedName={target.FullyQualifiedName}"); + + // HasCount(1) is the load-bearing assertion: it fails if the filter is ignored (all tests run) or if + // the FQN no longer matches after reconstruction (no test runs). The outcome check proves the target + // was actually executed via the reconstructed element, not merely selected. + Assert.HasCount(1, results); + Assert.AreEqual(target.FullyQualifiedName, results[0].TestCase.FullyQualifiedName); + Assert.AreEqual(TestOutcome.Passed, results[0].Outcome); + } + + [TestMethod] + public async Task FilterByIdSelectsExactlyTheMatchingTestDuringExecution() + { + string assemblyPath = GetAssetFullPath(TestAsset); + ImmutableArray allTests = DiscoverTests(assemblyPath); + Guid targetId = allTests.First(t => t.DisplayName == TargetDisplayName).Id; + + SimulateCrossProcessTestCases(allTests); + + ImmutableArray results = await RunTestsAsync(allTests, $"Id={targetId}"); + + // HasCount(1) is the load-bearing assertion: filtering by Id after the TestCase -> UnitTestElement -> + // TestCase reconstruction must still recompute the same Id and select exactly the target. The outcome + // check proves the target was actually executed via the reconstructed element. + Assert.HasCount(1, results); + Assert.AreEqual(targetId, results[0].TestCase.Id); + Assert.AreEqual(TestOutcome.Passed, results[0].Outcome); + } + + private static TestCase GetTargetTestCase(string assemblyPath) + => DiscoverTests(assemblyPath).First(t => t.DisplayName == TargetDisplayName); + + // Clears LocalExtensionData so the execution pipeline rebuilds the UnitTestElement (and its regenerated + // TestCase/Id) from the test-case properties, mirroring the out-of-proc VSTest path where deserialized + // test cases carry no LocalExtensionData. This is the round-trip the filtering refactor must preserve. + private static void SimulateCrossProcessTestCases(ImmutableArray testCases) + { + foreach (TestCase testCase in testCases) + { + testCase.LocalExtensionData = null; + } + } +} diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index c1d2b1fad9..40326955f6 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -7,11 +7,15 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using ITestResultRecorder = Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ITestResultRecorder; using TestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult; namespace Microsoft.MSTestV2.CLIAutomation; @@ -27,7 +31,7 @@ internal static ImmutableArray DiscoverTests(string assemblyPath, stri string runSettingsXml = GetRunSettingsXml(string.Empty); var context = new InternalDiscoveryContext(runSettingsXml, testCaseFilter); - unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger, sink, context, false); + unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink.ToUnitTestElementSink(), runSettingsXml, new TestElementFilterProvider(context), false); return sink.DiscoveredTests; } @@ -37,10 +41,36 @@ internal static async Task> RunTestsAsync(IEnumerable var testExecutionManager = new TestExecutionManager(); var frameworkHandle = new InternalFrameworkHandle(); - await testExecutionManager.ExecuteTestsAsync(testCases, null, frameworkHandle, false); + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(null, null), frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, filterProvider: null, false); return frameworkHandle.GetFlattenedTestResults(); } + internal static async Task> RunTestsAsync(IEnumerable testCases, string? testCaseFilter) + { + var testExecutionManager = new TestExecutionManager(); + var frameworkHandle = new InternalFrameworkHandle(); + + string runSettingsXml = GetRunSettingsXml(string.Empty); + var runContext = new InternalRunContext(runSettingsXml, testCaseFilter); + + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(runContext.TestRunDirectory, runContext.RunSettings?.SettingsXml), frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), false); + return frameworkHandle.GetFlattenedTestResults(); + } + + // Mirrors the adapter's execution boundary (see MSTestExecutor): each host test case becomes a neutral + // UnitTestElement carrying its execution-context (TCM) properties and the originating test case as an + // opaque recording handle, so results are recorded against the exact same TestCase instances. + private static IEnumerable ToUnitTestElements(IEnumerable testCases) + => testCases.Select(static testCase => + { + UnitTestElement element = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); + element.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + element.HostRecordingHandle = testCase; + return element; + }); + #region Helper classes private class InternalLogger : IMessageLogger { @@ -73,15 +103,46 @@ public InternalDiscoveryContext(string runSettings, string? testCaseFilter) public IRunSettings? RunSettings { get; } public ITestCaseFilterExpression? GetTestCaseFilter(IEnumerable supportedProperties, Func propertyProvider) => _filter; + } - private class InternalRunSettings : IRunSettings - { - public InternalRunSettings(string runSettings) => SettingsXml = runSettings; + private sealed class InternalRunContext : IRunContext + { + private readonly ITestCaseFilterExpression? _filter; - public string SettingsXml { get; } + public InternalRunContext(string runSettings, string? testCaseFilter) + { + RunSettings = new InternalRunSettings(runSettings); - public ISettingsProvider? GetSettings(string? settingsName) => throw new NotImplementedException(); + if (testCaseFilter != null) + { + _filter = TestCaseFilterFactory.ParseTestFilter(testCaseFilter); + } } + + public IRunSettings? RunSettings { get; } + + public bool KeepAlive => false; + + public bool InIsolation => false; + + public bool IsDataCollectionEnabled => false; + + public bool IsBeingDebugged => false; + + public string? TestRunDirectory => null; + + public string? SolutionDirectory => null; + + public ITestCaseFilterExpression? GetTestCaseFilter(IEnumerable? supportedProperties, Func propertyProvider) => _filter; + } + + private sealed class InternalRunSettings : IRunSettings + { + public InternalRunSettings(string runSettings) => SettingsXml = runSettings; + + public string SettingsXml { get; } + + public ISettingsProvider? GetSettings(string? settingsName) => throw new NotImplementedException(); } private sealed class InternalFrameworkHandle : IFrameworkHandle diff --git a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs index f052814199..aafd28b3a1 100644 --- a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs +++ b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs @@ -5,11 +5,8 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -using Moq; - using TestFramework.ForTestingMSTest; namespace PlatformServices.Desktop.ComponentTests; @@ -37,9 +34,10 @@ public void ParentDomainShouldHonorSearchDirectoriesSpecifiedInRunsettings() """; + LoadMSTestSettings(runSettingsXml); _testSourceHost = new TestSourceHost( GetTestAssemblyPath("DesktopTestProjectx86Debug"), - GetMockedIRunSettings(runSettingsXml).Object); + runSettingsXml); _testSourceHost.SetupHost(); // Loading SampleProjectForAssemblyResolution.dll should not throw. @@ -67,9 +65,10 @@ public void ChildDomainResolutionPathsShouldHaveSearchDirectoriesSpecifiedInRuns """; + LoadMSTestSettings(runSettingsXml); _testSourceHost = new TestSourceHost( GetTestAssemblyPath("DesktopTestProjectx86Debug"), - GetMockedIRunSettings(runSettingsXml).Object); + runSettingsXml); _testSourceHost.SetupHost(); var asm = Assembly.LoadFrom(sampleProjectPath); @@ -125,17 +124,12 @@ private static string GetTestAssemblyPath(string assetName) return testAssetPath; } - private static Mock GetMockedIRunSettings(string runSettingsXml) + private static void LoadMSTestSettings(string runSettingsXml) { - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - StringReader stringReader = new(runSettingsXml); var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); MSTestSettingsProvider mstestSettingsProvider = new(); reader.ReadToFollowing("MSTestV2"); mstestSettingsProvider.Load(reader); - - return mockRunSettings; } } diff --git a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj index de991b9157..0f8dac1571 100644 --- a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj +++ b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj @@ -15,6 +15,9 @@ + + diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs index 2766c1f9da..6fd99105f0 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.ObjectModel; @@ -9,6 +9,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -64,7 +65,7 @@ public void ConstructorShouldPopulateSettings() actualReader.ReadInnerXml(); }); var mockMessageLogger = new Mock(); - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, mockMessageLogger.Object); + var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, mockMessageLogger.Object.ToAdapterMessageLogger()); adapterSettings.Should().NotBeNull(); // Constructor has the side effect of populating the passed settings to MSTestSettings.CurrentSettings diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs index 73c2af6afc..a45bce3a57 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs @@ -8,6 +8,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -120,7 +121,7 @@ public void GetTestsShouldReturnBaseTestMethodsFromAnotherAssemblyByDefault() mockRunContext.Setup(dc => dc.RunSettings).Returns(mockRunSettings.Object); mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(mockRunContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true); TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DummyDerivedTestClass), Assembly.GetExecutingAssembly().FullName!); @@ -150,7 +151,7 @@ public void GetTestsShouldReturnBaseTestMethodsFromAnotherAssemblyByConfiguratio mockRunContext.Setup(dc => dc.RunSettings).Returns(mockRunSettings.Object); mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(mockRunContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true); TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DummyDerivedTestClass), Assembly.GetExecutingAssembly().FullName!); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs index 1cb7bcd452..361eb07e69 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs @@ -80,7 +80,7 @@ public void DiscoverTestsShouldThrowOnFileNotFound() .Returns(false); } - Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, sources[0])); } @@ -93,7 +93,7 @@ public void DiscoverTestsInSourceShouldThrowOnFileNotFound() _testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.DoesFileExist(Source)) .Returns(false); - Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, Source)); } @@ -122,10 +122,10 @@ public void DiscoverTestsInSourceShouldSendBackTestCasesDiscovered() """); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.AtLeastOnce); @@ -157,10 +157,10 @@ public void DiscoverTestsInSourceShouldThrowWhenTreatDiscoveryWarningsAsErrorsIs """; _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(settingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Assert action.Should().Throw() @@ -195,11 +195,11 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError """); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act & Assert // Should not throw an exception - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Verify warning message was sent to logger (not error) _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.AtLeastOnce); @@ -209,7 +209,7 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError public void SendTestCasesShouldNotSendAnyTestCasesIfThereAreNoTestElements() { // There is a null check for testElements in the code flow before this function call. So not adding a unit test for that. - _unitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object); + UnitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(_mockDiscoveryContext.Object), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.Never); @@ -221,7 +221,7 @@ public void SendTestCasesShouldSendAllTestCaseData() var test2 = new UnitTestElement(CreateTestMethod("M2", "C", "A", displayName: null)); var testElements = new List { test1, test2 }; - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(_mockDiscoveryContext.Object), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -240,7 +240,7 @@ public void SendTestCasesShouldSendFilteredTestCasesIfValidFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -259,7 +259,7 @@ public void SendTestCasesShouldSendAllTestCasesIfNullFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -278,7 +278,7 @@ public void SendTestCasesShouldSendAllTestCasesIfGetTestCaseFilterNotPresent() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -297,7 +297,7 @@ public void SendTestCasesShouldNotSendAnyTestCasesIfFilterError() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Never); @@ -325,15 +325,16 @@ public DummyNavigationData(string fileName, int minLineNumber, int maxLineNumber { internal override void DiscoverTestsInSource( string source, - IMessageLogger logger, - ITestCaseDiscoverySink discoverySink, - IDiscoveryContext? discoveryContext, + IAdapterMessageLogger logger, + IUnitTestElementSink discoverySink, + string? settingsXml, + ITestElementFilterProvider? filterProvider, bool isMTP) { - var testCase1 = new TestCase("A", new Uri("executor://testExecutor"), source); - var testCase2 = new TestCase("B", new Uri("executor://testExecutor"), source); - discoverySink.SendTestCase(testCase1); - discoverySink.SendTestCase(testCase2); + var testElement1 = new UnitTestElement(new TestMethod("A", "C", source, displayName: null)); + var testElement2 = new UnitTestElement(new TestMethod("B", "C", source, displayName: null)); + discoverySink.SendTestElement(testElement1); + discoverySink.SendTestElement(testElement2); } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs index 64e70e9b80..f0b4f62f51 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs @@ -1,8 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -15,26 +16,26 @@ public class TcmTestPropertiesProviderTests : TestContainer { private readonly TestProperty[] _tcmKnownProperties = [ - EngineConstants.TestRunIdProperty, - EngineConstants.TestPlanIdProperty, - EngineConstants.BuildConfigurationIdProperty, - EngineConstants.BuildDirectoryProperty, - EngineConstants.BuildFlavorProperty, - EngineConstants.BuildNumberProperty, - EngineConstants.BuildPlatformProperty, - EngineConstants.BuildUriProperty, - EngineConstants.TfsServerCollectionUrlProperty, - EngineConstants.TfsTeamProjectProperty, - EngineConstants.IsInLabEnvironmentProperty, - EngineConstants.TestCaseIdProperty, - EngineConstants.TestConfigurationIdProperty, - EngineConstants.TestConfigurationNameProperty, - EngineConstants.TestPointIdProperty, + AdapterTestProperties.TestRunIdProperty, + AdapterTestProperties.TestPlanIdProperty, + AdapterTestProperties.BuildConfigurationIdProperty, + AdapterTestProperties.BuildDirectoryProperty, + AdapterTestProperties.BuildFlavorProperty, + AdapterTestProperties.BuildNumberProperty, + AdapterTestProperties.BuildPlatformProperty, + AdapterTestProperties.BuildUriProperty, + AdapterTestProperties.TfsServerCollectionUrlProperty, + AdapterTestProperties.TfsTeamProjectProperty, + AdapterTestProperties.IsInLabEnvironmentProperty, + AdapterTestProperties.TestCaseIdProperty, + AdapterTestProperties.TestConfigurationIdProperty, + AdapterTestProperties.TestConfigurationNameProperty, + AdapterTestProperties.TestPointIdProperty, ]; public void GetTcmPropertiesShouldReturnEmptyDictionaryIfTestCaseIsNull() { - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(null); + IReadOnlyDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(null); tcmProperties.Should().BeNull(); } @@ -61,7 +62,7 @@ public void GetTcmPropertiesShouldReturnEmptyDictionaryIfTestCaseIdIsZero() ]; SetTestCaseProperties(testCase, propertiesValue); - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + IReadOnlyDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); tcmProperties.Should().BeNull(); } @@ -88,7 +89,7 @@ public void GetTcmPropertiesShouldGetAllPropertiesFromTestCase() ]; SetTestCaseProperties(testCase, propertiesValue); - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + IReadOnlyDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); VerifyTcmProperties(tcmProperties, testCase); } @@ -116,7 +117,7 @@ public void GetTcmPropertiesShouldCopyMultiplePropertiesCorrectlyFromTestCase() 345 ]; SetTestCaseProperties(testCase1, propertiesValue1); - IDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); + IReadOnlyDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); VerifyTcmProperties(tcmProperties1, testCase1); // Verify 2nd call. @@ -140,7 +141,7 @@ public void GetTcmPropertiesShouldCopyMultiplePropertiesCorrectlyFromTestCase() 346 ]; SetTestCaseProperties(testCase2, propertiesValue2); - IDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); + IReadOnlyDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); VerifyTcmProperties(tcmProperties2, testCase2); } @@ -167,7 +168,7 @@ public void GetTcmPropertiesShouldHandleDuplicateTestsProperlyFromTestCase() 345 ]; SetTestCaseProperties(testCase1, propertiesValue1); - IDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); + IReadOnlyDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); VerifyTcmProperties(tcmProperties1, testCase1); // Verify 2nd call. @@ -191,7 +192,7 @@ public void GetTcmPropertiesShouldHandleDuplicateTestsProperlyFromTestCase() 346 ]; SetTestCaseProperties(testCase2, propertiesValue2); - IDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); + IReadOnlyDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); VerifyTcmProperties(tcmProperties2, testCase2); // Verify 3rd call. @@ -215,7 +216,7 @@ public void GetTcmPropertiesShouldHandleDuplicateTestsProperlyFromTestCase() 347 ]; SetTestCaseProperties(testCase3, propertiesValue3); - IDictionary? tcmProperties3 = TcmTestPropertiesProvider.GetTcmProperties(testCase3); + IReadOnlyDictionary? tcmProperties3 = TcmTestPropertiesProvider.GetTcmProperties(testCase3); VerifyTcmProperties(tcmProperties3, testCase3); } @@ -232,12 +233,12 @@ private void SetTestCaseProperties(TestCase testCase, object[] propertiesValue) } } - private void VerifyTcmProperties(IDictionary? tcmProperties, TestCase testCase) + private void VerifyTcmProperties(IReadOnlyDictionary? tcmProperties, TestCase testCase) { tcmProperties.Should().NotBeNull(); foreach (TestProperty property in _tcmKnownProperties) { - testCase.GetPropertyValue(property)!.Equals(tcmProperties[property]).Should().BeTrue(); + testCase.GetPropertyValue(property)!.Equals(tcmProperties[property.Id]).Should().BeTrue(); } } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs index 8eb4ab7011..07f916bd40 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs @@ -4,7 +4,7 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using TestFramework.ForTestingMSTest; @@ -18,25 +18,31 @@ public class TestCaseDiscoverySinkTests : TestContainer public void TestCaseDiscoverySinkConstructorShouldInitializeTests() { - _testCaseDiscoverySink.Tests.Should().NotBeNull(); - _testCaseDiscoverySink.Tests.Count.Should().Be(0); + _testCaseDiscoverySink.TestElements.Should().NotBeNull(); + _testCaseDiscoverySink.TestElements.Count.Should().Be(0); } - public void SendTestCaseShouldNotAddTestIfTestCaseIsNull() + public void SendTestElementShouldAddTheTestElement() { - _testCaseDiscoverySink.SendTestCase(null); + var testElement = new UnitTestElement(new TestMethod("M", "C", "A", displayName: null)); - _testCaseDiscoverySink.Tests.Should().NotBeNull(); - _testCaseDiscoverySink.Tests.Count.Should().Be(0); + _testCaseDiscoverySink.SendTestElement(testElement); + + _testCaseDiscoverySink.TestElements.Should().NotBeNull(); + _testCaseDiscoverySink.TestElements.Count.Should().Be(1); + _testCaseDiscoverySink.TestElements.ToArray()[0].Should().BeSameAs(testElement); } - public void SendTestCaseShouldAddTheTestCaseToTests() + public void SendTestElementShouldAddEachTestElementInOrder() { - TestCase tc = new("TAttribute", new Uri("executor://TestExecutorUri"), "A"); - _testCaseDiscoverySink.SendTestCase(tc); + var testElement1 = new UnitTestElement(new TestMethod("M1", "C", "A", displayName: null)); + var testElement2 = new UnitTestElement(new TestMethod("M2", "C", "A", displayName: null)); + + _testCaseDiscoverySink.SendTestElement(testElement1); + _testCaseDiscoverySink.SendTestElement(testElement2); - _testCaseDiscoverySink.Tests.Should().NotBeNull(); - _testCaseDiscoverySink.Tests.Count.Should().Be(1); - _testCaseDiscoverySink.Tests.ToArray()[0].Should().Be(tc); + _testCaseDiscoverySink.TestElements.Count.Should().Be(2); + _testCaseDiscoverySink.TestElements.ToArray()[0].Should().BeSameAs(testElement1); + _testCaseDiscoverySink.TestElements.ToArray()[1].Should().BeSameAs(testElement2); } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 18cfc4734a..464ea6f415 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -5,8 +5,10 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Discovery; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; @@ -33,27 +35,41 @@ public class TestExecutionManagerTests : TestContainer private readonly TestProperty[] _tcmKnownProperties = [ - EngineConstants.TestRunIdProperty, - EngineConstants.TestPlanIdProperty, - EngineConstants.BuildConfigurationIdProperty, - EngineConstants.BuildDirectoryProperty, - EngineConstants.BuildFlavorProperty, - EngineConstants.BuildNumberProperty, - EngineConstants.BuildPlatformProperty, - EngineConstants.BuildUriProperty, - EngineConstants.TfsServerCollectionUrlProperty, - EngineConstants.TfsTeamProjectProperty, - EngineConstants.IsInLabEnvironmentProperty, - EngineConstants.TestCaseIdProperty, - EngineConstants.TestConfigurationIdProperty, - EngineConstants.TestConfigurationNameProperty, - EngineConstants.TestPointIdProperty, + AdapterTestProperties.TestRunIdProperty, + AdapterTestProperties.TestPlanIdProperty, + AdapterTestProperties.BuildConfigurationIdProperty, + AdapterTestProperties.BuildDirectoryProperty, + AdapterTestProperties.BuildFlavorProperty, + AdapterTestProperties.BuildNumberProperty, + AdapterTestProperties.BuildPlatformProperty, + AdapterTestProperties.BuildUriProperty, + AdapterTestProperties.TfsServerCollectionUrlProperty, + AdapterTestProperties.TfsTeamProjectProperty, + AdapterTestProperties.IsInLabEnvironmentProperty, + AdapterTestProperties.TestCaseIdProperty, + AdapterTestProperties.TestConfigurationIdProperty, + AdapterTestProperties.TestConfigurationNameProperty, + AdapterTestProperties.TestPointIdProperty, ]; private TestableRunContextTestExecutionTests _runContext; private List _callers = []; private int _enqueuedParallelTestsCount; + /// + /// Builds the VSTest-backed result recorder the execution engine now receives at its boundary, recording into + /// exactly as the adapter does in production. + /// + private ITestResultRecorder TestResultRecorder + => _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings); + + /// + /// Builds the neutral run inputs (test-run directory + run settings XML) the execution engine now receives at + /// its boundary, extracted from exactly as the adapter does in production. + /// + private DeploymentContext CurrentDeploymentContext + => new(_runContext.TestRunDirectory, _runContext.RunSettings?.SettingsXml); + public TestExecutionManagerTests() { _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(_ => true)); @@ -63,7 +79,6 @@ public TestExecutionManagerTests() _mockTestSourceHandler = new Mock(); _testExecutionManager = new TestExecutionManager( - EnvironmentWrapper.Instance, task => { _enqueuedParallelTestsCount++; @@ -92,7 +107,7 @@ public async Task RunTestsForTestWithFilterErrorShouldSendZeroResults() // Causing the FilterExpressionError _runContext = new TestableRunContextTestExecutionTests(() => throw new TestPlatformFormatException()); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // No Results _frameworkHandle.TestCaseStartList.Count.Should().Be(0); @@ -108,7 +123,7 @@ public async Task RunTestsForTestWithFilterShouldSendResultsForFilteredTests() _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(p => p.DisplayName == "PassingTest")); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // FailingTest should be skipped because it does not match the filter criteria. List expectedTestCaseStartList = ["PassingTest"]; @@ -125,7 +140,7 @@ public void SendTestResults_WhenUnitTestResultsIsEmpty_RecordsEndWithoutResult() TestCase testCase = GetTestCase(typeof(DummyTestClass), "PassingTest"); Microsoft.VisualStudio.TestTools.UnitTesting.TestResult[] unitTestResults = []; - _testExecutionManager.SendTestResults(testCase, unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle); + _testExecutionManager.SendTestResults(ToUnitTestElement(testCase), unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings)); _frameworkHandle.TestCaseEndList.Should().Equal("PassingTest:None"); _frameworkHandle.ResultsList.Should().BeEmpty(); @@ -136,7 +151,7 @@ public async Task RunTestsForIgnoredTestShouldSendResultsMarkingIgnoredTestsAsSk TestCase testCase = GetTestCase(typeof(DummyTestClass), "IgnoredTest"); TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); _frameworkHandle.TestCaseStartList[0].Should().Be("IgnoredTest"); _frameworkHandle.TestCaseEndList[0].Should().Be("IgnoredTest:Skipped"); @@ -149,7 +164,7 @@ public async Task RunTestsForASingleTestShouldSendSingleResult() TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); List expectedTestCaseStartList = ["PassingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed"]; @@ -166,7 +181,7 @@ public async Task RunTestsForMultipleTestShouldSendMultipleResults() TestCase failingTestCase = GetTestCase(typeof(DummyTestClass), "FailingTest"); TestCase[] tests = [testCase, failingTestCase]; - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); List expectedTestCaseStartList = ["PassingTest", "FailingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed", "FailingTest:Failed"]; @@ -186,7 +201,7 @@ public async Task RunTestsForCancellationTokenCanceledSetToTrueShouldSendZeroRes // Cancel the test run _cancellationToken.Cancel(); - Func func = () => _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); await func.Should().ThrowAsync(); // No Results @@ -204,13 +219,13 @@ public async Task RunTestsForTestShouldDeployBeforeExecution() // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.MockTestDeployment.Setup( - td => td.Deploy(tests, _runContext, _frameworkHandle)).Callback(() => SetCaller("Deploy")); + td => td.Deploy(It.IsAny>(), It.IsAny(), It.IsAny())).Callback(() => SetCaller("Deploy")); await _testExecutionManager.RunTestsAsync( - tests, - _runContext, - _frameworkHandle, - new TestRunCancellationToken()); + ToUnitTestElements(tests), + CurrentDeploymentContext, + _frameworkHandle.ToAdapterMessageLogger(), + TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _callers[0].Should().Be("Deploy", "Deploy should be called before execution."); _callers[1].Should().Be("LoadAssembly", "Deploy should be called before execution."); @@ -230,7 +245,7 @@ public async Task RunTestsForTestShouldCleanupAfterExecution() td => td.Cleanup()).Callback(() => SetCaller("Cleanup")); #endif - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _callers[0].Should().Be("LoadAssembly", "Cleanup should be called after execution."); @@ -247,7 +262,7 @@ public async Task RunTestsForTestShouldNotCleanupOnTestFailure() TestCase[] tests = [testCase, failingTestCase]; TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockTestDeployment.Verify(td => td.Cleanup(), Times.Never); } @@ -262,11 +277,11 @@ public async Task RunTestsForTestShouldLoadSourceFromDeploymentDirectoryIfDeploy // Setup mocks. testablePlatformService.MockTestDeployment.Setup( - td => td.Deploy(tests, _runContext, _frameworkHandle)).Returns(true); + td => td.Deploy(It.IsAny>(), It.IsAny(), It.IsAny())).Returns(true); testablePlatformService.MockTestDeployment.Setup(td => td.GetDeploymentDirectory()) .Returns(@"C:\temp"); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockFileOperations.Verify( fo => fo.LoadAssembly(It.Is(s => s.StartsWith("C:\\temp"))), @@ -292,7 +307,7 @@ public async Task RunTestsForTestShouldPassInTestRunParametersInformationAsPrope """); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -314,7 +329,7 @@ public async Task RunTestsForTestShouldPassInTcmPropertiesAsPropertiesToTheTest( """); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); VerifyTcmProperties(DummyTestClass.TestContextProperties, testCase); } @@ -327,7 +342,7 @@ public async Task RunTestsForTestShouldPassInDeploymentInformationAsPropertiesTo // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockSettingsProvider.Verify(sp => sp.GetProperties(It.IsAny()), Times.Once); } @@ -351,7 +366,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger First Run - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Update runsettings to have different values for similar keys _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -368,7 +383,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger another Run - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); "http://updatedLocalHost".Equals(DummyTestClass.TestContextProperties!["webAppUrl"]).Should().BeTrue(); } @@ -382,7 +397,7 @@ private async Task RunTestsForSourceShouldRunTestsInASource() { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); _frameworkHandle.TestCaseStartList.Contains("PassingTest").Should().BeTrue(); _frameworkHandle.TestCaseEndList.Contains("PassingTest:Passed").Should().BeTrue(); @@ -404,7 +419,7 @@ private async Task RunTestsForSourceShouldPassInTestRunParametersInformationAsPr """); - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -415,7 +430,7 @@ private async Task RunTestsForSourceShouldPassInDeploymentInformationAsPropertie { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties.Should().NotBeNull(); } @@ -426,10 +441,10 @@ public async Task RunTestsForMultipleSourcesShouldRunEachTestJustOnce() var sources = new List { Assembly.GetExecutingAssembly().Location, Assembly.GetExecutingAssembly().Location }; TestableTestExecutionManager testableTestExecutionManager = new() { - ExecuteTestsWrapper = (tests, runContext, frameworkHandle, isDeploymentDone) => testsCount += tests.Count(), + ExecuteTestsWrapper = (tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone) => testsCount += tests.Count(), }; - await testableTestExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await testableTestExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); testsCount.Should().Be(4); } @@ -461,8 +476,8 @@ public async Task RunTestsForTestShouldRunTestsInParallelWhenEnabledInRunsetting try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -498,8 +513,8 @@ public async Task RunTestsForTestShouldRunTestsByMethodLevelWhenSpecified() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -535,8 +550,8 @@ public async Task RunTestsForTestShouldRunTestsWithSpecifiedNumberOfWorkers() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -570,7 +585,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -599,7 +614,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -629,7 +644,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -658,7 +673,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -675,8 +690,8 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() TestCase testCase3 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod3"); TestCase testCase4 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod4"); - testCase3.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, true); - testCase4.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, true); + testCase3.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, true); + testCase4.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, true); TestCase[] tests = [testCase1, testCase2, testCase3, testCase4]; _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -696,8 +711,8 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); DummyTestClassWithDoNotParallelizeMethods.ParallelizableTestsThreadIds.Count.Should().BeOneOf(1, 2); @@ -730,7 +745,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -763,7 +778,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -785,7 +800,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState TestCase testCase3 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod3"); TestCase testCase4 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod4"); - testCase4.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, true); + testCase4.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, true); TestCase[] tests = [testCase1, testCase2, testCase3, testCase4]; _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -802,8 +817,8 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.Count.Should().Be(1); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.ToArray()[0].Should().Be(Thread.CurrentThread.GetApartmentState()); @@ -842,15 +857,15 @@ static TestCase[] BuildTests() => """); - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); var firstHandle = new TestableFrameworkHandle(); - var firstManager = new TestExecutionManager(EnvironmentWrapper.Instance, task => task()); - await firstManager.RunTestsAsync(BuildTests(), _runContext, firstHandle, new TestRunCancellationToken()); + var firstManager = new TestExecutionManager(task => task()); + await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, firstHandle.ToAdapterMessageLogger(), firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); var secondHandle = new TestableFrameworkHandle(); - var secondManager = new TestExecutionManager(EnvironmentWrapper.Instance, task => task()); - await secondManager.RunTestsAsync(BuildTests(), _runContext, secondHandle, new TestRunCancellationToken()); + var secondManager = new TestExecutionManager(task => task()); + await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, secondHandle.ToAdapterMessageLogger(), secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Same seed must produce the same order across separate runs. firstHandle.TestCaseStartList.Should().Equal(secondHandle.TestCaseStartList); @@ -885,9 +900,9 @@ public async Task RunTestsWithoutRandomizeTestOrderShouldPreserveInputOrder() """); - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _frameworkHandle.TestCaseStartList.Should().Equal("TestA", "TestB", "TestC"); } @@ -904,6 +919,21 @@ private static TestCase GetTestCase(Type typeOfClass, string testName) return element.ToTestCase(); } + // Mirrors the conversion the adapter performs at the execution boundary (see MSTestExecutor): each host + // test case becomes a neutral UnitTestElement carrying the host execution-context (TCM) properties and the + // originating test case as an opaque recording handle, so recorded results are reported against the exact + // same TestCase instances the tests build (keeping the framework-handle Verify assertions meaningful). + private static UnitTestElement[] ToUnitTestElements(params TestCase[] tests) + => [.. tests.Select(ToUnitTestElement)]; + + private static UnitTestElement ToUnitTestElement(TestCase testCase) + { + UnitTestElement element = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); + element.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + element.HostRecordingHandle = testCase; + return element; + } + private TestablePlatformServiceProvider SetupTestablePlatformService() { var testablePlatformService = new TestablePlatformServiceProvider(); @@ -1248,11 +1278,11 @@ internal sealed class TestableTestCaseFilterExpression : ITestCaseFilterExpressi internal class TestableTestExecutionManager : TestExecutionManager { - internal Action, IRunContext?, IFrameworkHandle, bool> ExecuteTestsWrapper { get; set; } = null!; + internal Action, DeploymentContext, IAdapterMessageLogger, ITestResultRecorder, bool> ExecuteTestsWrapper { get; set; } = null!; - internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) + internal override Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { - ExecuteTestsWrapper?.Invoke(tests, runContext, frameworkHandle, isDeploymentDone); + ExecuteTestsWrapper?.Invoke(tests, deploymentContext, messageLogger, testResultRecorder, isDeploymentDone); return Task.CompletedTask; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs index 990bc3ddb1..4ac15c0473 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs @@ -1,10 +1,12 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -89,7 +91,7 @@ public void PropertyProviderValueForValidTestAndSupportedPropertyNameReturnsValu public void GetFilterExpressionForNullRunContextReturnsNull() { TestableTestExecutionRecorder recorder = new(); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(null, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(null, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeFalse(); @@ -100,7 +102,7 @@ public void GetFilterExpressionForValidRunContextReturnsValidTestCaseFilterExpre TestableTestExecutionRecorder recorder = new(); var dummyFilterExpression = new TestableTestCaseFilterExpression(); TestableRunContext runContext = new(() => dummyFilterExpression); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().Be(dummyFilterExpression); filterHasError.Should().BeFalse(); @@ -114,7 +116,7 @@ public void GetFilterExpressionForDiscoveryContextWithGetTestCaseFilterReturnsVa TestableTestExecutionRecorder recorder = new(); var dummyFilterExpression = new TestableTestCaseFilterExpression(); TestableDiscoveryContextWithGetTestCaseFilter discoveryContext = new(() => dummyFilterExpression); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().Be(dummyFilterExpression); filterHasError.Should().BeFalse(); @@ -127,7 +129,7 @@ public void GetFilterExpressionForDiscoveryContextWithoutGetTestCaseFilterReturn { TestableTestExecutionRecorder recorder = new(); TestableDiscoveryContextWithoutGetTestCaseFilter discoveryContext = new(); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeFalse(); @@ -137,7 +139,7 @@ public void GetFilterExpressionForRunContextGetTestCaseFilterThrowingExceptionRe { TestableTestExecutionRecorder recorder = new(); TestableRunContext runContext = new(() => throw new TestPlatformFormatException("DummyException")); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeTrue(); @@ -152,7 +154,7 @@ public void GetFilterExpressionForDiscoveryContextWithGetTestCaseFilterThrowingE { TestableTestExecutionRecorder recorder = new(); TestableDiscoveryContextWithGetTestCaseFilter discoveryContext = new(() => throw new TestPlatformFormatException("DummyException")); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeTrue(); @@ -160,6 +162,55 @@ public void GetFilterExpressionForDiscoveryContextWithGetTestCaseFilterThrowingE recorder.TestMessageLevel.Should().Be(TestMessageLevel.Error); } + public void GetTestElementFilterForNullContextReturnsNull() + { + TestableTestExecutionRecorder recorder = new(); + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(null, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().BeNull(); + filterHasError.Should().BeFalse(); + } + + public void GetTestElementFilterForContextWithoutFilterReturnsNull() + { + TestableTestExecutionRecorder recorder = new(); + TestableDiscoveryContextWithoutGetTestCaseFilter discoveryContext = new(); + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().BeNull(); + filterHasError.Should().BeFalse(); + } + + public void GetTestElementFilterMatchesElementsUsingUnderlyingFilterExpression() + { + TestableTestExecutionRecorder recorder = new(); + MatchingTestCaseFilterExpression matchingFilterExpression = new(testCase => testCase.DisplayName == "M1"); + TestableRunContext runContext = new(() => matchingFilterExpression); + + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().NotBeNull(); + filterHasError.Should().BeFalse(); + + var matching = new UnitTestElement(new TestMethod("M1", "C", "A", displayName: "M1")); + var nonMatching = new UnitTestElement(new TestMethod("M2", "C", "A", displayName: "M2")); + + filter!.Matches(matching).Should().BeTrue(); + filter.Matches(nonMatching).Should().BeFalse(); + } + + public void GetTestElementFilterForFilterErrorReturnsNullWithFilterHasErrorTrue() + { + TestableTestExecutionRecorder recorder = new(); + TestableRunContext runContext = new(() => throw new TestPlatformFormatException("DummyException")); + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().BeNull(); + filterHasError.Should().BeTrue(); + recorder.Message.Should().Be("DummyException"); + recorder.TestMessageLevel.Should().Be(TestMessageLevel.Error); + } + [DummyTestClass] internal class DummyTestClassWithTestMethods { @@ -232,5 +283,16 @@ private sealed class TestableTestCaseFilterExpression : ITestCaseFilterExpressio public bool MatchTestCase(TestCase testCase, Func propertyValueProvider) => throw new NotImplementedException(); } + private sealed class MatchingTestCaseFilterExpression : ITestCaseFilterExpression + { + private readonly Func _matchTestCase; + + public MatchingTestCaseFilterExpression(Func matchTestCase) => _matchTestCase = matchTestCase; + + public string TestCaseFilterValue => null!; + + public bool MatchTestCase(TestCase testCase, Func propertyValueProvider) => _matchTestCase(testCase); + } + private class DummyTestClassAttribute : TestClassAttribute; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs index e5cb2d9162..3395bd01e3 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs @@ -39,7 +39,7 @@ protected override void Dispose(bool disposing) public void GetTestMethodInfoShouldAddPropertiesFromContainingClassCorrectly() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassBase).GetMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived))!)]; + TestTrait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassBase).GetMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived))!)]; traits.Length.Should().Be(3); traits[0].Name.Should().Be("TestMethodKeyFromBase"); traits[0].Value.Should().Be("TestMethodValueFromBase"); @@ -51,7 +51,7 @@ public void GetTestMethodInfoShouldAddPropertiesFromContainingClassCorrectly() public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsTestMethod() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived))!)]; + TestTrait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived))!)]; traits.Length.Should().Be(6); traits[0].Name.Should().Be("DerivedMethod1Key"); traits[0].Value.Should().Be("DerivedMethod1Value"); @@ -69,7 +69,7 @@ public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClasse public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsNotTestMethod() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase))!)]; + TestTrait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase))!)]; traits.Length.Should().Be(6); traits[0].Name.Should().Be("DerivedMethod2Key"); traits[0].Value.Should().Be("DerivedMethod2Value"); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs index e787279850..2dbfe026bf 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs @@ -54,7 +54,7 @@ public void CreateFromPopulatesCategoriesAndProperties() var element = new UnitTestElement(testMethod) { TestCategory = ["Smoke", "Compatibility"], - Traits = [new Trait("Owner", "alice"), new Trait("Owner", "bob"), new Trait("Priority", "1")], + Traits = [new TestTrait("Owner", "alice"), new TestTrait("Owner", "bob"), new TestTrait("Priority", "1")], }; var info = TestRunInfo.CreateFrom([element]); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs index 97ec776f20..91615b5c93 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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; @@ -7,6 +7,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -817,7 +818,7 @@ public void GetTestMethodInfoWhenTimeoutAttributeNotSetShouldReturnTestMethodInf """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); @@ -841,7 +842,7 @@ public void GetTestMethodInfoWhenTimeoutAttributeSetShouldReturnTimeoutBasedOnAt """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); @@ -870,7 +871,7 @@ public void GetTestMethodInfoForInvalidGlobalTimeoutShouldReturnTestMethodInfoWi """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs index bfa71fc029..cb9b662fb9 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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; @@ -7,6 +7,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -65,7 +66,7 @@ public void ConstructorShouldPopulateSettings() actualReader.ReadInnerXml(); }); - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); adapterSettings.Should().NotBeNull(); var assemblyEnumerator = new UnitTestRunner(adapterSettings, []); @@ -449,7 +450,7 @@ private MSTestSettings GetSettingsWithDebugTrace(bool captureDebugTraceValue) actualReader.ReadInnerXml(); }); - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); return settings; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs index 9aa147eb9e..b515c22a03 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs @@ -3,6 +3,7 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -22,9 +23,9 @@ public void ToUnitTestElementShouldReturnUnitTestElementWithFieldsSet() }; string[] testCategories = ["DummyCategory"]; - testCase.SetPropertyValue(EngineConstants.PriorityProperty, 2); - testCase.SetPropertyValue(EngineConstants.TestCategoryProperty, testCategories); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName"); + testCase.SetPropertyValue(AdapterTestProperties.PriorityProperty, 2); + testCase.SetPropertyValue(AdapterTestProperties.TestCategoryProperty, testCategories); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "DummyClassName"); UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); @@ -37,7 +38,7 @@ public void ToUnitTestElementShouldReturnUnitTestElementWithFieldsSet() public void ToUnitTestElementForTestCaseWithNoPropertiesShouldReturnUnitTestElementWithDefaultFields() { TestCase testCase = new("DummyClass.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName"); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "DummyClassName"); UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); @@ -49,7 +50,7 @@ public void ToUnitTestElementForTestCaseWithNoPropertiesShouldReturnUnitTestElem public void ToUnitTestElementShouldAddDeclaringClassNameToTestElementWhenAvailable() { TestCase testCase = new("DummyClass.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName"); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "DummyClassName"); UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); @@ -59,7 +60,7 @@ public void ToUnitTestElementShouldAddDeclaringClassNameToTestElementWhenAvailab public void ToUnitTestElementShouldPreferManagedTypeOverTestClassNameWhenAvailable() { TestCase testCase = new("SemanticClassName.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "SyntacticClassName"); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "SyntacticClassName"); testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, "SemanticClassName"); testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, "DummyMethod"); @@ -74,7 +75,7 @@ public void ToUnitTestElementShouldParseLegacyClosedGenericFullyQualifiedNameWhe Type closedType = typeof(DummyGenericTestClass); string methodName = nameof(DummyGenericTestClass<>.GenericTestMethod); TestCase testCase = new($"{closedType.FullName}.{methodName}", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, closedType.FullName); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, closedType.FullName); testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, typeof(DummyGenericTestClass<>).FullName); testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, methodName); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs index 8eb46d4545..cd07a3efac 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs @@ -3,6 +3,7 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -153,8 +154,8 @@ public void ToUnitTestResultsForTestResultShouldSetParentInfo() var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new()); - ((Guid)convertedResult.GetPropertyValue(EngineConstants.ExecutionIdProperty)!).Should().Be(executionId); - ((Guid)convertedResult.GetPropertyValue(EngineConstants.ParentExecIdProperty)!).Should().Be(parentExecId); + ((Guid)convertedResult.GetPropertyValue(AdapterTestProperties.ExecutionIdProperty)!).Should().Be(executionId); + ((Guid)convertedResult.GetPropertyValue(AdapterTestProperties.ParentExecIdProperty)!).Should().Be(parentExecId); } public void ToUnitTestResultsShouldHaveResultsFileProvidedToTestResult() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs index e2301915a4..b09f10bfd0 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs @@ -4,7 +4,7 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using TestFramework.ForTestingMSTest; @@ -131,7 +131,7 @@ public void GetTestRunParametersThrowsWhenTRPNodeHasAttributes() """; - new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); + new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); } public void GetTestRunParametersThrowsWhenTRPNodeHasNonParameterTypeChildNodes() @@ -152,7 +152,7 @@ public void GetTestRunParametersThrowsWhenTRPNodeHasNonParameterTypeChildNodes() """; - new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); + new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); } public void GetTestRunParametersIgnoresMalformedKeyValues() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs index e256a8db07..27ba637848 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -28,7 +29,7 @@ public UnitTestOutcomeHelperTests() """; var mockMessageLogger = new Mock(); - _adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, mockMessageLogger.Object)!; + _adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, mockMessageLogger.Object.ToAdapterMessageLogger())!; } public void UniTestHelperToTestOutcomeForUnitTestOutcomePassedShouldReturnTestOutcomePassed() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj index f2fa7a0cdd..b265512c2a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj @@ -25,6 +25,7 @@ + diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs index 7f3a0cae03..fb598c3b15 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; @@ -57,7 +58,7 @@ public void MapInconclusiveToFailedIsByDefaultFalseWhenNotSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapInconclusiveToFailed.Should().BeFalse(); } @@ -72,7 +73,7 @@ public void MapNotRunnableToFailedIsByDefaultTrueWhenNotSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapNotRunnableToFailed.Should().BeTrue(); } @@ -88,7 +89,7 @@ public void MapInconclusiveToFailedShouldBeConsumedFromRunSettingsWhenSpecified( """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapInconclusiveToFailed.Should().BeTrue(); } @@ -116,7 +117,7 @@ public void RunSettings_WithInvalidValues_GettingAWarningForEachInvalidSetting() """; - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Exactly(13)); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'CooperativeCancellationTimeout', setting will be ignored."), Times.Once); @@ -146,7 +147,7 @@ public void RunSettings_WithZeroTimeout_GettingAWarningAndTimeoutIsNotSet() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(0); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '0' for runsettings entry 'TestTimeout'. The timeout must be a strictly positive integer (in milliseconds); a value of 0 or less is not allowed. Omit the entry to use the default (no timeout). The setting will be ignored."), Times.Once); @@ -163,7 +164,7 @@ public void MapNotRunnableToFailedShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapNotRunnableToFailed.Should().BeTrue(); } @@ -178,7 +179,7 @@ public void CaptureDebugTracesShouldBeTrueByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.CaptureDebugTraces.Should().BeTrue(); } @@ -194,7 +195,7 @@ public void CaptureDebugTracesShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.CaptureDebugTraces.Should().BeFalse(); } @@ -210,7 +211,7 @@ public void TestTimeoutShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(4000); } @@ -225,7 +226,7 @@ public void TestTimeoutShouldBeSetToZeroIfNotSpecifiedInRunSettings() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(0); } @@ -240,7 +241,7 @@ public void TreatDiscoveryWarningsAsErrorsShouldBeTrueByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TreatDiscoveryWarningsAsErrors.Should().BeTrue(); } @@ -256,7 +257,7 @@ public void TreatDiscoveryWarningsAsErrorsShouldBeConsumedFromRunSettingsWhenSpe """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TreatDiscoveryWarningsAsErrors.Should().BeTrue(); } @@ -271,7 +272,7 @@ public void ParallelizationSettingsShouldNotBeSetByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.HasValue.Should().BeFalse(); adapterSettings.ParallelizationScope.HasValue.Should().BeFalse(); @@ -287,7 +288,7 @@ public void RandomizeTestOrderShouldBeFalseByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomizeTestOrder.Should().BeFalse(); adapterSettings.RandomTestOrderSeed.Should().BeNull(); @@ -305,7 +306,7 @@ public void RandomizeTestOrderShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomizeTestOrder.Should().BeTrue(); adapterSettings.RandomTestOrderSeed.Should().Be(42); @@ -322,7 +323,7 @@ public void RandomTestOrderSeedShouldAcceptNegativeValues() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomTestOrderSeed.Should().Be(-7); } @@ -338,7 +339,7 @@ public void RandomizeTestOrderShouldWarnWhenValueIsInvalid() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomizeTestOrder.Should().BeFalse(); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value 'notabool' for runsettings entry 'RandomizeTestOrder', setting will be ignored."), Times.Once); @@ -355,7 +356,7 @@ public void RandomTestOrderSeedShouldWarnWhenValueIsInvalid() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomTestOrderSeed.Should().BeNull(); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value 'notanint' for runsettings entry 'RandomTestOrderSeed', setting will be ignored."), Times.Once); @@ -375,7 +376,7 @@ public void PopulateSettingsShouldWarnWhenRandomizeTestOrderAndOrderTestsByNameI _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.RandomizeTestOrder.Should().BeTrue(); MSTestSettings.CurrentSettings.OrderTestsByNameInClass.Should().BeTrue(); @@ -399,7 +400,7 @@ public void PopulateSettingsShouldNotWarnWhenOnlyRandomizeTestOrderIsEnabled() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.RandomizeTestOrder.Should().BeTrue(); MSTestSettings.CurrentSettings.OrderTestsByNameInClass.Should().BeFalse(); @@ -419,7 +420,7 @@ public void GetSettingsShouldThrowIfParallelizationWorkersIsNotInt() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("Invalid value 'GoneFishing' specified for 'Workers'. The value should be a non-negative integer."); @@ -438,7 +439,7 @@ public void GetSettingsShouldThrowIfParallelizationWorkersIsNegative() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("Invalid value '-1' specified for 'Workers'. The value should be a non-negative integer."); } @@ -456,7 +457,7 @@ public void ParallelizationWorkersShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(2); } @@ -474,7 +475,7 @@ public void ParallelizationWorkersShouldBeSetToProcessorCountWhenSetToZero() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(Environment.ProcessorCount); } @@ -491,7 +492,7 @@ public void ParallelizationSettingsShouldBeSetToDefaultsWhenNotSet() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(Environment.ProcessorCount); adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.ClassLevel); @@ -508,7 +509,7 @@ public void ParallelizationSettingsShouldBeSetToDefaultsOnAnEmptyParalleizeSetti """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(Environment.ProcessorCount); adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.ClassLevel); @@ -528,7 +529,7 @@ public void ParallelizationSettingsShouldBeConsumedFromRunSettingsWhenSpecified( """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(127); adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.MethodLevel); @@ -547,7 +548,7 @@ public void GetSettingsShouldThrowIfParallelizationScopeIsNotValid() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("Invalid value 'JustParallelizeWillYou' specified for 'Scope'. Supported scopes are ClassLevel, MethodLevel."); } @@ -565,7 +566,7 @@ public void ParallelizationScopeShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.MethodLevel); } @@ -583,7 +584,7 @@ public void GetSettingsShouldThrowWhenParallelizeHasInvalidElements() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("MSTestAdapter encountered an unexpected element 'Hola' in its settings 'Parallelize'. Remove this element and try again."); } @@ -601,7 +602,7 @@ public void GetSettingsShouldBeAbleToReadAfterParallelizationSettings() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(12); } @@ -621,7 +622,7 @@ public void GetSettingsShouldBeAbleToReadAfterParallelizationSettingsWithData() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(12); adapterSettings.ParallelizationWorkers.Should().Be(127); @@ -640,7 +641,7 @@ public void GetSettingsShouldBeAbleToReadAfterParallelizationSettingsOnEmptyPara """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(12); } @@ -655,7 +656,7 @@ public void DisableParallelizationShouldBeFalseByDefault() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.DisableParallelization.Should().BeFalse(); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Never); @@ -674,7 +675,7 @@ public void DisableParallelizationShouldBeConsumedFromRunSettingsWhenSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.DisableParallelization.Should().BeTrue(); } @@ -692,7 +693,7 @@ public void DisableParallelization_WithInvalidValue_GettingAWarning() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'DisableParallelization', setting will be ignored."), Times.Once); } @@ -718,7 +719,7 @@ public void GetSettingsShouldProbePlatformSpecificSettingsAlso() actualReader.ReadInnerXml(); }); - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); _testablePlatformServiceProvider.MockSettingsProvider.Verify(sp => sp.Load(It.IsAny()), Times.Once); } @@ -744,7 +745,7 @@ public void GetSettingsShouldOnlyPassTheElementSubTreeToPlatformService() observedXml = actualReader.ReadOuterXml(); }); - MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); (expectedRunSettingXml == observedXml).Should().BeTrue(); } @@ -791,7 +792,7 @@ public void GetSettingsShouldBeAbleToReadSettingsAfterThePlatformServiceReadsIts } }); - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger())!; // Assert. dummyPlatformSpecificSetting.Should().BeTrue(); @@ -844,7 +845,7 @@ public void GetSettingsShouldBeAbleToReadSettingsIfThePlatformServiceDoesNotUnde } }); - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger())!; // Assert. dummyPlatformSpecificSetting.Should().BeTrue(); @@ -893,7 +894,7 @@ public void GetSettingsShouldOnlyReadTheAdapterSection() } }); - MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. outOfScopeCall.Should().BeFalse(); @@ -944,7 +945,7 @@ public void GetSettingsShouldWorkIfThereAreCommentsInTheXML() } }); - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger())!; // Assert. dummyPlatformSpecificSetting.Should().BeTrue(); @@ -980,7 +981,7 @@ public void CurrentSettingShouldReturnCachedLoadedSettings() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; MSTestSettings adapterSettings2 = MSTestSettings.CurrentSettings; @@ -1009,7 +1010,7 @@ public void PopulateSettingsShouldFillInSettingsFromSettingsObject() """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); @@ -1021,7 +1022,7 @@ public void PopulateSettingsShouldFillInSettingsFromSettingsObject() public void PopulateSettingsShouldInitializeDefaultAdapterSettingsWhenDiscoveryContextIsNull() { - MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1032,7 +1033,7 @@ public void PopulateSettingsShouldInitializeDefaultAdapterSettingsWhenDiscoveryC public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull() { - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1044,7 +1045,7 @@ public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsXmlIsEmpty() { _mockDiscoveryContext.Setup(md => md.RunSettings!.SettingsXml).Returns(string.Empty); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1066,7 +1067,7 @@ public void PopulateSettingsShouldInitializeSettingsToDefaultIfNotSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1091,7 +1092,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestSection() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1117,7 +1118,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestV2Section() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1146,7 +1147,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestV2OverMSTestV1Sect _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1185,7 +1186,7 @@ public void ConfigJson_WithInvalidValues_GettingAWarningForEachInvalidSetting() .Returns((string key) => configDictionary.TryGetValue(key, out string? value) ? value : null); var settings = new MSTestSettings(); - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Exactly(12)); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'timeout:useCooperativeCancellation', setting will be ignored."), Times.Once); @@ -1216,7 +1217,7 @@ public void ConfigJson_WithZeroTimeout_GettingAWarningAndTimeoutIsNotSet() .Returns((string key) => configDictionary.TryGetValue(key, out string? value) ? value : null); var settings = new MSTestSettings(); - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); settings.TestTimeout.Should().Be(0); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '0' for runsettings entry 'timeout:test'. The timeout must be a strictly positive integer (in milliseconds); a value of 0 or less is not allowed. Omit the entry to use the default (no timeout). The setting will be ignored."), Times.Once); @@ -1255,7 +1256,7 @@ public void ConfigJson_WithValidValues_ValuesAreSetCorrectly() var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1296,7 +1297,7 @@ public void ConfigJson_WithDeprecatedFlatOrderTestsByNameInClassKey_ValueIsSetAn var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1318,7 +1319,7 @@ public void ConfigJson_WithExecutionOrderTestsByNameInClassKey_ValueIsSetAndNoWa var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1342,7 +1343,7 @@ public void ConfigJson_WithBothOrderTestsByNameInClassKeys_NewExecutionKeyTakesP var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1370,7 +1371,7 @@ private void ConfigJson_Parllelism_Enabled_Core(bool parallelismEnabled) var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.DisableParallelization.Should().Be(!parallelismEnabled); @@ -1390,7 +1391,7 @@ public void ConfigJson_WithValidValues_MethodScope() var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.ParallelizationScope.Should().Be(ExecutionScope.MethodLevel); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs index f8cd836e35..8a13019086 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs @@ -3,6 +3,7 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -33,6 +34,44 @@ public void UnitTestElementConstructorShouldThrowIfTestMethodIsNull() #endregion + #region Source resolution / host test case tests + + public void WithUpdatedSourceShouldReturnSameInstanceWhenSourceUnchanged() + => _unitTestElement.WithUpdatedSource("A").Should().BeSameAs(_unitTestElement); + + public void WithUpdatedSourceShouldReturnCloneWithNewSourceAndLeaveOriginalUnchanged() + { + UnitTestElement clone = _unitTestElement.WithUpdatedSource("B"); + + clone.Should().NotBeSameAs(_unitTestElement); + clone.TestMethod.AssemblyName.Should().Be("B"); + // The original element (and its test method) must not be mutated by the clone. + _unitTestElement.TestMethod.AssemblyName.Should().Be("A"); + clone.TestMethod.Should().NotBeSameAs(_testMethod); + } + + public void GetOrCreateHostTestCaseShouldReturnHostHandleWhenPresent() + { + var hostTestCase = new TestCase("C.M", EngineConstants.ExecutorUri, "A"); + _unitTestElement.HostRecordingHandle = hostTestCase; + + _unitTestElement.GetOrCreateHostTestCase().Should().BeSameAs(hostTestCase); + } + + public void GetOrCreateHostTestCaseShouldMaterializeAndCacheWhenNoHandle() + { + _unitTestElement.HostRecordingHandle.Should().BeNull(); + + TestCase materialized = _unitTestElement.GetOrCreateHostTestCase(); + + materialized.Should().NotBeNull(); + // Subsequent calls (deployment, test-start, each reported result) reuse the same instance. + _unitTestElement.GetOrCreateHostTestCase().Should().BeSameAs(materialized); + _unitTestElement.HostRecordingHandle.Should().BeSameAs(materialized); + } + + #endregion + #region ToTestCase tests public void ToTestCaseShouldSetFullyQualifiedName() @@ -75,7 +114,7 @@ public void ToTestCaseShouldSetTestClassNameProperty() { var testCase = _unitTestElement.ToTestCase(); - (testCase.GetPropertyValue(EngineConstants.TestClassNameProperty) as string).Should().Be("C"); + (testCase.GetPropertyValue(AdapterTestProperties.TestClassNameProperty) as string).Should().Be("C"); } public void ToTestCaseShouldUseFullClassNameAsManagedTypeName() @@ -91,17 +130,17 @@ public void ToTestCaseShouldSetTestCategoryIfPresent() _unitTestElement.TestCategory = null; var testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.TestCategoryProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty).Should().BeNull(); _unitTestElement.TestCategory = []; testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.TestCategoryProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty).Should().BeNull(); _unitTestElement.TestCategory = ["TC"]; testCase = _unitTestElement.ToTestCase(); - new string[] { "TC" }.SequenceEqual((string[])testCase.GetPropertyValue(EngineConstants.TestCategoryProperty)!).Should().BeTrue(); + new string[] { "TC" }.SequenceEqual((string[])testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty)!).Should().BeTrue(); } public void ToTestCaseShouldSetPriorityIfPresent() @@ -109,12 +148,12 @@ public void ToTestCaseShouldSetPriorityIfPresent() _unitTestElement.Priority = null; var testCase = _unitTestElement.ToTestCase(); - ((int)testCase.GetPropertyValue(EngineConstants.PriorityProperty)!).Should().Be(0); + ((int)testCase.GetPropertyValue(AdapterTestProperties.PriorityProperty)!).Should().Be(0); _unitTestElement.Priority = 1; testCase = _unitTestElement.ToTestCase(); - ((int)testCase.GetPropertyValue(EngineConstants.PriorityProperty)!).Should().Be(1); + ((int)testCase.GetPropertyValue(AdapterTestProperties.PriorityProperty)!).Should().Be(1); } public void ToTestCaseShouldSetTraitsIfPresent() @@ -126,7 +165,7 @@ public void ToTestCaseShouldSetTraitsIfPresent() testCase.Traits.Count().Should().Be(0); #pragma warning restore CA1827 // Do not use Count() or LongCount() when Any() can be used - var trait = new Trait("trait", "value"); + var trait = new TestTrait("trait", "value"); _unitTestElement.Traits = [trait]; testCase = _unitTestElement.ToTestCase(); @@ -141,7 +180,7 @@ public void ToTestCaseShouldSetPropertiesIfPresent() var testCase = _unitTestElement.ToTestCase(); - ((string[])testCase.GetPropertyValue(EngineConstants.WorkItemIdsProperty)!).Should().Equal(["2312", "22332"]); + ((string[])testCase.GetPropertyValue(AdapterTestProperties.WorkItemIdsProperty)!).Should().Equal(["2312", "22332"]); } #if !WINDOWS_UWP && !WIN_UI @@ -150,17 +189,17 @@ public void ToTestCaseShouldSetDeploymentItemPropertyIfPresent() _unitTestElement.DeploymentItems = null; var testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.DeploymentItemsProperty).Should().BeNull(); _unitTestElement.DeploymentItems = []; testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.DeploymentItemsProperty).Should().BeNull(); _unitTestElement.DeploymentItems = [new("s", "d")]; testCase = _unitTestElement.ToTestCase(); - _unitTestElement.DeploymentItems.SequenceEqual(testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty) as KeyValuePair[]).Should().BeTrue(); + _unitTestElement.DeploymentItems.SequenceEqual(testCase.GetPropertyValue(AdapterTestProperties.DeploymentItemsProperty) as KeyValuePair[]).Should().BeTrue(); } #endif @@ -188,7 +227,7 @@ public void ToTestCase_WhenStrategyIsData_DoesNotUseDefaultTestCaseId() static Guid GuidFromString(string data) { byte[] hash = TestFx.Hashing.XxHash128.Hash(Encoding.Unicode.GetBytes(data)); - return UnitTestElement.VersionedGuidFromHash(hash, hashVersion: 1); + return UnitTestElementExtensions.VersionedGuidFromHash(hash, hashVersion: 1); } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs index d2fcb3feeb..806e904a6a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -28,7 +29,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomePassedShouldReturnTestOu """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Passed, adapterSettings); resultOutcome.Should().Be(TestOutcome.Passed); @@ -44,7 +45,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeFailedShouldReturnTestOu """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Failed, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -60,7 +61,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeErrorShouldReturnTestOut """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Error, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -77,7 +78,7 @@ public void UnitTestHelperToTestOutcomeForUnitTestOutcomeNotRunnableShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotRunnable, adapterSettings); resultOutcome.Should().Be(TestOutcome.None); @@ -93,7 +94,7 @@ public void UnitTestHelperToTestOutcomeForUnitTestOutcomeNotRunnableShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotRunnable, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -109,7 +110,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeTimeoutShouldReturnTestO """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Timeout, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -125,7 +126,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeIgnoredShouldReturnTestO """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Ignored, adapterSettings); resultOutcome.Should().Be(TestOutcome.Skipped); @@ -141,7 +142,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeInconclusiveShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Inconclusive, adapterSettings); resultOutcome.Should().Be(TestOutcome.Skipped); @@ -158,7 +159,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeInconclusiveShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Inconclusive, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -174,7 +175,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeNotFoundShouldReturnTest """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotFound, adapterSettings); resultOutcome.Should().Be(TestOutcome.NotFound); @@ -190,7 +191,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeInProgressShouldReturnTe """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.InProgress, adapterSettings); resultOutcome.Should().Be(TestOutcome.None); @@ -207,7 +208,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeNotRunnableShouldReturnT """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotRunnable, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.cs new file mode 100644 index 0000000000..16efa478fc --- /dev/null +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.cs @@ -0,0 +1,29 @@ +// 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.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +using TestFramework.ForTestingMSTest; + +namespace MSTestAdapter.PlatformServices.UnitTests; + +public class ObjectModelDecouplingTests : TestContainer +{ + // Guards the platform-agnostic contract: the compiled MSTestAdapter.PlatformServices assembly must not + // reference the VSTest object model (Microsoft.VisualStudio.TestPlatform.ObjectModel) or any other + // Microsoft.*.TestPlatform.* assembly. All VSTest coupling lives in the MSTest.TestAdapter layer above it. + public void PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly() + { + Assembly platformServices = typeof(TestSourceHandler).Assembly; + + IEnumerable testPlatformReferences = platformServices + .GetReferencedAssemblies() + .Select(reference => reference.Name!) + .Where(name => name.IndexOf("TestPlatform", StringComparison.OrdinalIgnoreCase) >= 0); + + testPlatformReferences.Should().BeEmpty( + "PlatformServices must stay platform-agnostic and must not reference the VSTest object model or any Microsoft.*.TestPlatform.* assembly"); + } +} diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs index 9e3d878be2..94e060c056 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs @@ -4,6 +4,7 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -60,7 +61,7 @@ public void ConfigurationSettingsShouldReturnDefaultSettingsIfNotSet() public void PopulateSettingsShouldInitializeDefaultConfigurationSettingsWhenDiscoveryContextIsNull() { - MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -68,7 +69,7 @@ public void PopulateSettingsShouldInitializeDefaultConfigurationSettingsWhenDisc public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull() { - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -77,7 +78,7 @@ public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsXmlIsEmpty() { _mockDiscoveryContext.Setup(md => md.RunSettings!.SettingsXml).Returns(string.Empty); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -96,7 +97,7 @@ public void PopulateSettingsShouldInitializeSettingsToDefaultIfNotSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.Should().NotBeNull(); @@ -119,7 +120,7 @@ public void PopulateSettingsShouldInitializeSettingsFromRunConfigurationSection( _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.Should().NotBeNull(); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs index ad2b3917b3..b154dfc5c1 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs @@ -4,12 +4,11 @@ #if NETFRAMEWORK using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Moq; @@ -45,15 +44,12 @@ public DesktopTestDeploymentTests() public void DeployShouldDeployFilesInASourceAndReturnTrue() { - TestCase testCase = GetTestCase(Assembly.GetExecutingAssembly().Location); + UnitTestElement testCase = GetTestCase(Assembly.GetExecutingAssembly().Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); string? warning; string sourceFile = Assembly.GetExecutingAssembly().GetName().Name + ".exe"; @@ -68,17 +64,14 @@ public void DeployShouldDeployFilesInASourceAndReturnTrue() public void DeployShouldDeployFilesInMultipleSourcesAndReturnTrue() { - TestCase testCase1 = GetTestCase(Assembly.GetExecutingAssembly().Location); + UnitTestElement testCase1 = GetTestCase(Assembly.GetExecutingAssembly().Location); string sourceFile2 = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "a.dll"); - TestCase testCase2 = GetTestCase(sourceFile2); + UnitTestElement testCase2 = GetTestCase(sourceFile2); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase1, testCase2 }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase1, testCase2 }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); string? warning; string sourceFile1 = Assembly.GetExecutingAssembly().GetName().Name + ".exe"; @@ -100,15 +93,12 @@ public void DeployShouldDeployFilesInMultipleSourcesAndReturnTrue() public void DeployShouldCreateDeploymentDirectories() { - TestCase testCase = GetTestCase(typeof(DesktopTestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(DesktopTestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // matched twice because root deployment and out directory are same in net core _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(testRunDirectories.RootDeploymentDirectory), Times.Once); @@ -118,16 +108,16 @@ public void DeployShouldCreateDeploymentDirectories() #region private methods - private TestCase GetTestCase(string source) + private static UnitTestElement GetTestCase(string source) { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), source); + var testCase = new UnitTestElement(new TestMethod("M", "C", source, displayName: null)); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; return testCase; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs index c30b8edf73..e40ca50f4d 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs @@ -8,7 +8,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Moq; @@ -135,10 +134,7 @@ public void SetupHostShouldHaveParentDomainsAppBaseSetToTestSourceLocation() """; string location = typeof(TestSourceHost).Assembly.Location; - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - - TestSourceHost sourceHost = new(location, mockRunSettings.Object); + TestSourceHost sourceHost = new(location, runSettingsXml); try { @@ -189,10 +185,7 @@ public void NoAppDomainShouldGetCreatedWhenDisableAppDomainIsSetToTrue() "; string location = typeof(TestSourceHost).Assembly.Location; - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - - Mock testSourceHost = new(location, mockRunSettings.Object) { CallBase = true }; + Mock testSourceHost = new(location, runSettingsXml) { CallBase = true }; try { @@ -220,10 +213,7 @@ public void AppDomainShouldGetCreatedWhenDisableAppDomainIsSetToFalse() """; string location = typeof(TestSourceHost).Assembly.Location; - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - - Mock testSourceHost = new(location, mockRunSettings.Object) { CallBase = true }; + Mock testSourceHost = new(location, runSettingsXml) { CallBase = true }; try { diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs index eaa1380860..89b89ef3aa 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs @@ -3,10 +3,10 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Moq; @@ -202,7 +202,7 @@ public void ToSettingsShouldNotThrowExceptionWhenRunSettingsXmlUnderTagMSTestV2I """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); MSTestAdapterSettings.ToSettings(reader); @@ -222,12 +222,12 @@ public void ToSettingsShouldThrowExceptionWhenRunSettingsXmlIsWrong() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); void ShouldThrowException() => MSTestAdapterSettings.ToSettings(reader); - new Action(ShouldThrowException).Should().Throw(); + new Action(ShouldThrowException).Should().Throw(); } #endregion @@ -242,7 +242,7 @@ public void DeploymentEnabledIsByDefaultTrueWhenNotSpecified() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeploymentEnabled.Should().BeTrue(); @@ -257,7 +257,7 @@ public void DeploymentEnabledShouldBeConsumedFromRunSettingsWhenSpecified() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeploymentEnabled.Should().BeFalse(); @@ -275,7 +275,7 @@ public void DeployTestSourceDependenciesIsEnabledByDefault() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeployTestSourceDependencies.Should().BeTrue(); @@ -290,7 +290,7 @@ public void DeployTestSourceDependenciesWhenFalse() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeployTestSourceDependencies.Should().BeFalse(); @@ -305,7 +305,7 @@ public void DeployTestSourceDependenciesWhenTrue() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeployTestSourceDependencies.Should().BeTrue(); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs index 568ee31d94..f87ab95723 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs @@ -4,6 +4,7 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; @@ -237,9 +238,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtMethodLevel() { _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("MethodKey", "MethodValue")], MemberTypes.Method); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("MethodKey", "MethodValue") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("MethodKey", "MethodValue") }, options => options.WithStrictOrdering()); } /// @@ -249,9 +250,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtClassLevel() { _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); } /// @@ -262,9 +263,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtAllLevels() _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("MethodKey", "MethodValue")], MemberTypes.Method); _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("MethodKey", "MethodValue"), new Trait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("MethodKey", "MethodValue"), new TestTrait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); } /// @@ -274,9 +275,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeMultipleTestPropertiesAtMethod { _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("Key1", "Value1"), new TestPropertyAttribute("Key2", "Value2")], MemberTypes.Method); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("Key1", "Value1"), new Trait("Key2", "Value2") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("Key1", "Value1"), new TestTrait("Key2", "Value2") }, options => options.WithStrictOrdering()); } /// @@ -286,7 +287,7 @@ public void GetTestPropertiesAsTraitsShouldReturnEmptyWhenNoTestProperties() { _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("SomeCategory")], MemberTypes.Method); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); actual.Should().BeEmpty(); } @@ -577,7 +578,7 @@ public void GetTestPropertiesAsTraitsShouldNotIncludeClassLevelAttributesWhenRef [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(methodWithNullReflectedType.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(methodWithNullReflectedType.Object); actual.Should().BeEmpty(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs index 6062851605..3a6bf32c98 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs @@ -9,8 +9,8 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Moq; @@ -28,7 +28,7 @@ public class TestContextImplementationTests : TestContainer private TestContextImplementation _testContextImplementation = null!; - private TestContextImplementation CreateTestContextImplementation(IMessageLogger? messageLogger = null) + private TestContextImplementation CreateTestContextImplementation(IAdapterMessageLogger? messageLogger = null) => new(_testMethod.Object, null, _properties, messageLogger, null); public void TestContextConstructorShouldInitializeProperties() @@ -346,19 +346,19 @@ public void GetResultFilesShouldReturnListOfAddedResultFiles() public void DisplayMessageShouldForwardToIMessageLogger() { - var messageLoggerMock = new Mock(MockBehavior.Strict); + var messageLoggerMock = new Mock(MockBehavior.Strict); messageLoggerMock - .Setup(l => l.SendMessage(It.IsAny(), It.IsAny())); + .Setup(l => l.SendMessage(It.IsAny(), It.IsAny())); _testContextImplementation = CreateTestContextImplementation(messageLoggerMock.Object); _testContextImplementation.DisplayMessage(MessageLevel.Informational, "InfoMessage"); _testContextImplementation.DisplayMessage(MessageLevel.Warning, "WarningMessage"); _testContextImplementation.DisplayMessage(MessageLevel.Error, "ErrorMessage"); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Informational, "InfoMessage"), Times.Once); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Warning, "WarningMessage"), Times.Once); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Error, "ErrorMessage"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Informational, "InfoMessage"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Warning, "WarningMessage"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Error, "ErrorMessage"), Times.Once); } public void GetAndClearOutput_ShouldReturnContentThenClearBuffer() @@ -399,7 +399,7 @@ public void GetAndClearTrace_ShouldReturnContentThenClearBuffer() public void WritesFromBackgroundThreadShouldNotThrow() { - TestContextImplementation testContextImplementation = CreateTestContextImplementation(new Mock().Object); + TestContextImplementation testContextImplementation = CreateTestContextImplementation(new Mock().Object); var t = new Thread(() => { for (int i = 0; i < 100; i++) @@ -704,12 +704,12 @@ public void CloneForDataDrivenIterationShouldCopyTestRunCount() public void CloneForDataDrivenIterationShouldShareMessageLogger() { - var messageLoggerMock = new Mock(); + var messageLoggerMock = new Mock(); _testContextImplementation = CreateTestContextImplementation(messageLoggerMock.Object); TestContextImplementation clone = _testContextImplementation.CloneForDataDrivenIteration(); clone.DisplayMessage(MessageLevel.Informational, "from-clone"); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Informational, "from-clone"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Informational, "from-clone"), Times.Once); } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs index c88f094732..97f30a2bd2 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs @@ -4,12 +4,11 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Moq; @@ -111,15 +110,12 @@ public void CleanupShouldNotDeleteDirectoriesIfRunSettingsSpecifiesSo() MSTestSettingsProvider mstestSettingsProvider = new(); mstestSettingsProvider.Load(reader); - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); testDeployment.Cleanup(); @@ -128,15 +124,12 @@ public void CleanupShouldNotDeleteDirectoriesIfRunSettingsSpecifiesSo() public void CleanupShouldDeleteRootDeploymentDirectory() { - TestCase testCase = GetTestCase(typeof(DeploymentUtilityTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(DeploymentUtilityTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. testDeployment.Cleanup(); @@ -152,15 +145,12 @@ public void CleanupShouldDeleteRootDeploymentDirectory() public void GetDeploymentDirectoryShouldReturnDeploymentOutputDirectory() { - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. testDeployment.GetDeploymentDirectory().Should().Be(testRunDirectories.OutDirectory); @@ -172,14 +162,14 @@ public void GetDeploymentDirectoryShouldReturnDeploymentOutputDirectory() public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseButHasDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); + UnitTestElement testCase = CreateTestElement("path/to/asm.dll"); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), @@ -194,7 +184,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseButHasDeployme mstestSettingsProvider.Load(reader); // Deployment should not happen - testDeployment.Deploy(new List { testCase }, null, null!).Should().BeFalse(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeFalse(); // Deployment directories should not be created testDeployment.GetDeploymentDirectory().Should().BeNull(); @@ -202,8 +192,8 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseButHasDeployme public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseAndHasNoDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); + UnitTestElement testCase = CreateTestElement("path/to/asm.dll"); + testCase.DeploymentItems = null; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), @@ -217,7 +207,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseAndHasNoDeploy mstestSettingsProvider.Load(reader); // Deployment should not happen - testDeployment.Deploy(new List { testCase }, null, null!).Should().BeFalse(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeFalse(); // Deployment directories should get created on .NET Framework for compat, but not on .NET Core #if NETFRAMEWORK @@ -229,8 +219,8 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseAndHasNoDeploy public void DeployShouldReturnFalseWhenDeploymentEnabledSetToTrueButHasNoDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); + UnitTestElement testCase = CreateTestElement("path/to/asm.dll"); + testCase.DeploymentItems = null; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), @@ -244,7 +234,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToTrueButHasNoDeploym mstestSettingsProvider.Load(reader); // Deployment should not happen - testDeployment.Deploy(new List { testCase }, null, null!).Should().BeFalse(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeFalse(); // Deployment directories should get created on .NET Framework for compat, but not on .NET Core #if NETFRAMEWORK @@ -257,14 +247,14 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToTrueButHasNoDeploym // TODO: This test has to have mocks (tracked by https://github.com/microsoft/testfx/issues/8086). It actually deploys stuff and we cannot assume that all the dependencies get copied over to bin\debug. internal void DeployShouldReturnTrueWhenDeploymentEnabledSetToTrueAndHasDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = CreateTestElement(typeof(TestDeploymentTests).Assembly.Location); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), @@ -278,7 +268,7 @@ internal void DeployShouldReturnTrueWhenDeploymentEnabledSetToTrueAndHasDeployme mstestSettingsProvider.Load(reader); // Deployment should happen - testDeployment.Deploy(new List { testCase }, null, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeTrue(); // Deployment directories should get created testDeployment.GetDeploymentDirectory().Should().NotBeNull(); @@ -309,15 +299,12 @@ public void GetDeploymentInformationShouldReturnAppBaseDirectoryIfRunDirectoryIs public void GetDeploymentInformationShouldReturnRunDirectoryInformationIfSourceIsNull() { // Arrange. - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. IDictionary properties = TestDeployment.GetDeploymentInformation(null); @@ -339,15 +326,12 @@ public void GetDeploymentInformationShouldReturnRunDirectoryInformationIfSourceI public void GetDeploymentInformationShouldReturnRunDirectoryInformationIfSourceIsNotNull() { // Arrange. - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. IDictionary properties = TestDeployment.GetDeploymentInformation(typeof(TestDeploymentTests).Assembly.Location); @@ -383,20 +367,23 @@ private void SetupDeploymentItems(ICustomAttributeProvider attributeProvider, Ke ru => ru.GetAttributes(attributeProvider)).Returns(deploymentItemAttributes); } - private static TestCase GetTestCase(string source) + private static UnitTestElement GetTestCase(string source) { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), source); + UnitTestElement testCase = CreateTestElement(source); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; return testCase; } + private static UnitTestElement CreateTestElement(string source) + => new(new TestMethod("M", "C", source, displayName: null)); + private TestDeployment CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories) { string currentExecutingFolder = Path.GetDirectoryName(typeof(TestDeploymentTests).Assembly.Location)!; diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs index 82126a12c3..baf709a8e7 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Moq; @@ -41,7 +40,12 @@ internal class TestablePlatformServiceProvider : IPlatformServiceProvider public IFileOperations FileOperations => MockFileOperations.Object; - public ITraceLogger AdapterTraceLogger { get => MockTraceLogger.Object; set => throw new NotSupportedException(); } + // The getter always returns the mock so tests can verify trace logging. The setter is a no-op: + // MSTest.TestAdapter's [ModuleInitializer] (MSTestExecutor.SetPlatformLogger) legitimately assigns + // PlatformServiceProvider.Instance.AdapterTraceLogger when that module is first touched (e.g. via the + // ToAdapterMessageLogger bridge), so this double must tolerate the assignment exactly as the real + // PlatformServiceProvider does, rather than throwing. + public ITraceLogger AdapterTraceLogger { get => MockTraceLogger.Object; set => _ = value; } #if !WINDOWS_UWP && !WIN_UI public ITestDeployment TestDeployment => MockTestDeployment.Object; @@ -69,7 +73,7 @@ public IReflectionOperations ReflectionOperations public int GetTestContextCallCount { get; private set; } - public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger messageLogger, UnitTestOutcome outcome) + public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger messageLogger, UnitTestOutcome outcome) { GetTestContextCallCount++; var testContextImpl = new TestContextImplementation(testMethod, testClassFullName, properties, messageLogger, testRunCancellationToken: null); @@ -77,7 +81,7 @@ public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFul return testContextImpl; } - public ITestSourceHost CreateTestSourceHost(string source, TestPlatform.ObjectModel.Adapter.IRunSettings? runSettings) => MockTestSourceHost.Object; + public ITestSourceHost CreateTestSourceHost(string source, string? settingsXml) => MockTestSourceHost.Object; public void SetupMockReflectionOperations() { diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs index c1a11d0c2b..10086d9e2d 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs @@ -4,12 +4,12 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Moq; @@ -19,13 +19,6 @@ namespace MSTestAdapter.PlatformServices.UnitTests.Utilities; public class DeploymentItemUtilityTests : TestContainer { - internal static readonly TestProperty DeploymentItemsProperty = TestProperty.Register( - "MSTestDiscoverer.DeploymentItems", - "DeploymentItems", - typeof(KeyValuePair[]), - TestPropertyAttributes.Hidden, - typeof(TestCase)); - private readonly Mock _mockReflectionOperations; private readonly DeploymentItemUtility _deploymentItemUtility; private readonly ICollection _warnings; @@ -391,22 +384,24 @@ public void IsValidDeploymentItemShouldReturnTrueForAValidDeploymentItem() public void HasDeployItemsShouldReturnFalseForNoDeploymentItems() { - TestCase testCase = new("A.C.M", new Uri("executor://testExecutor"), "A"); - testCase.SetPropertyValue(DeploymentItemsProperty, null); + var testCase = new UnitTestElement(new TestMethod("M", "C", "A", displayName: null)) + { + DeploymentItems = null, + }; DeploymentItemUtility.HasDeploymentItems(testCase).Should().BeFalse(); } public void HasDeployItemsShouldReturnTrueWhenDeploymentItemsArePresent() { - TestCase testCase = new("A.C.M", new Uri("executor://testExecutor"), "A"); + var testCase = new UnitTestElement(new TestMethod("M", "C", "A", displayName: null)); KeyValuePair[] kvpArray = [ new KeyValuePair( _defaultDeploymentItemPath, _defaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; DeploymentItemUtility.HasDeploymentItems(testCase).Should().BeTrue(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs index c66fcba062..4f7d0d9e59 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs @@ -4,14 +4,12 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Moq; @@ -29,8 +27,7 @@ public class DeploymentUtilityTests : TestContainer private readonly Mock _mockReflectionOperations; private readonly Mock _mockFileUtility; private readonly Mock _mockAssemblyUtility; - private readonly Mock _mockRunContext; - private readonly Mock _mockTestExecutionRecorder; + private readonly Mock _mockMessageLogger; private readonly DeploymentUtility _deploymentUtility; @@ -52,16 +49,15 @@ public DeploymentUtilityTests() _mockAssemblyUtility.Object, _mockFileUtility.Object); - _mockRunContext = new Mock(); - _mockTestExecutionRecorder = new Mock(); + _mockMessageLogger = new Mock(); } #region Deploy tests public void DeployShouldReturnFalseWhenNoDeploymentItemsOnTestCase() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), Path.Combine(RootDeploymentDirectory, "asm.dll")); - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); + UnitTestElement testCase = CreateTestElement(Path.Combine(RootDeploymentDirectory, "asm.dll")); + testCase.DeploymentItems = null; var testRunDirectories = new TestRunDirectories(RootDeploymentDirectory, Path.Combine(RootDeploymentDirectory, "asm.dll"), isAppDomainCreationDisabled: true); _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -77,17 +73,17 @@ public void DeployShouldReturnFalseWhenNoDeploymentItemsOnTestCase() #endif _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeFalse(); } #if NETFRAMEWORK public void DeployShouldDeploySourceAndItsConfigFile() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -102,10 +98,10 @@ public void DeployShouldDeploySourceAndItsConfigFile() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -132,7 +128,7 @@ public void DeployShouldDeployDependentFiles() { string dependencyFile = "C:\\temp\\dependency.dll"; - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -147,10 +143,10 @@ public void DeployShouldDeployDependentFiles() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -167,7 +163,7 @@ public void DeployShouldDeployDependentFiles() public void DeployShouldDeploySatelliteAssemblies() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); string assemblyFullPath = Assembly.GetExecutingAssembly().Location; string satelliteFullPath = Path.Combine(Path.GetDirectoryName(assemblyFullPath), "de", "satellite.dll"); @@ -184,10 +180,10 @@ public void DeployShouldDeploySatelliteAssemblies() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -208,7 +204,7 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() string deploymentItemPath = "C:\\temp\\sample.dll"; string deploymentItemOutputDirectory = "..\\..\\out"; - TestCase testCase = GetTestCaseAndTestRunDirectories(deploymentItemPath, deploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(deploymentItemPath, deploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -225,10 +221,10 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -243,10 +239,10 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() Times.Never); // Verify the warning. - _mockTestExecutionRecorder.Verify( + _mockMessageLogger.Verify( ter => ter.SendMessage( - TestMessageLevel.Warning, + MessageLevel.Warning, string.Format( CultureInfo.InvariantCulture, Resource.DeploymentErrorBadDeploymentItem, @@ -257,7 +253,7 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() public void DeployShouldDeployContentsOfADirectoryIfSpecified() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); string content1 = Path.Combine(DefaultDeploymentItemPath, "directoryContents.dll"); var directoryContentFiles = new List { content1 }; @@ -280,10 +276,10 @@ public void DeployShouldDeployContentsOfADirectoryIfSpecified() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -301,7 +297,7 @@ public void DeployShouldDeployContentsOfADirectoryIfSpecified() #if NETFRAMEWORK public void DeployShouldDeployPdbWithSourceIfPdbFileIsPresentInSourceDirectory() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -324,10 +320,10 @@ public void DeployShouldDeployPdbWithSourceIfPdbFileIsPresentInSourceDirectory() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -356,7 +352,7 @@ public void DeployShouldNotDeployPdbFileOfAssemblyIfPdbFileIsNotPresentInAssembl // Path for pdb file of dependent assembly if pdb file is present. string pdbFile = Path.ChangeExtension(dependencyFile, "pdb"); - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -379,10 +375,10 @@ public void DeployShouldNotDeployPdbFileOfAssemblyIfPdbFileIsNotPresentInAssembl // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -411,12 +407,11 @@ public void DeployShouldNotDeployPdbFileOfAssemblyIfPdbFileIsNotPresentInAssembl public void CreateDeploymentDirectoriesShouldCreateDeploymentDirectoryFromRunContext() { // Setup mocks - _mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(TestRunDirectory); _mockFileUtility.Setup(fu => fu.GetNextIterationDirectoryName(TestRunDirectory, It.IsAny())) .Returns(RootDeploymentDirectory); // Act. - _deploymentUtility.CreateDeploymentDirectories(_mockRunContext.Object, null); + _deploymentUtility.CreateDeploymentDirectories(new DeploymentContext(TestRunDirectory, null), null); // Assert. _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(RootDeploymentDirectory), Times.Once); @@ -428,12 +423,11 @@ public void CreateDeploymentDirectoriesShouldCreateDeploymentDirectoryFromRunCon public void CreateDeploymentDirectoriesShouldCreateDefaultDeploymentDirectoryIfTestRunDirectoryIsNull() { // Setup mocks - _mockRunContext.Setup(rc => rc.TestRunDirectory).Returns((string)null!); _mockFileUtility.Setup(fu => fu.GetNextIterationDirectoryName(It.Is(s => s.Contains(TestRunDirectories.DefaultDeploymentRootDirectory)), It.IsAny())) .Returns(RootDeploymentDirectory); // Act. - _deploymentUtility.CreateDeploymentDirectories(_mockRunContext.Object, null); + _deploymentUtility.CreateDeploymentDirectories(new DeploymentContext(null, null), null); // Assert. _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(RootDeploymentDirectory), Times.Once); @@ -449,7 +443,7 @@ public void CreateDeploymentDirectoriesShouldCreateDefaultDeploymentDirectoryIfR .Returns(RootDeploymentDirectory); // Act. - _deploymentUtility.CreateDeploymentDirectories(null, null); + _deploymentUtility.CreateDeploymentDirectories(new DeploymentContext(null, null), null); // Assert. _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(RootDeploymentDirectory), Times.Once); @@ -462,16 +456,16 @@ public void CreateDeploymentDirectoriesShouldCreateDefaultDeploymentDirectoryIfR #region private methods - private static TestCase GetTestCaseAndTestRunDirectories(string deploymentItemPath, string defaultDeploymentItemOutputDirectoryOut, out TestRunDirectories testRunDirectories) + private static UnitTestElement GetTestCaseAndTestRunDirectories(string deploymentItemPath, string defaultDeploymentItemOutputDirectoryOut, out TestRunDirectories testRunDirectories) { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), Assembly.GetExecutingAssembly().Location); + UnitTestElement testCase = CreateTestElement(Assembly.GetExecutingAssembly().Location); KeyValuePair[] kvpArray = [ new KeyValuePair( deploymentItemPath, defaultDeploymentItemOutputDirectoryOut) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; string currentExecutingFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; const bool isAppDomainCreationDisabled = @@ -486,6 +480,9 @@ private static TestCase GetTestCaseAndTestRunDirectories(string deploymentItemPa return testCase; } + private static UnitTestElement CreateTestElement(string source) + => new(new TestMethod("M", "C", source, displayName: null)); + #endregion } #endif