diff --git a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj index b4792d9cad..23655fc099 100644 --- a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj +++ b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj @@ -82,6 +82,12 @@ that reference is suppressed above, so they are referenced explicitly here. --> + + + 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/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/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/TestSourceHandler.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs index a2ce548535..ae97ba6ab2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs @@ -110,7 +110,7 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) try { string? referenceAssemblyName = referenceAssembly.Name; - byte[] referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken(); + byte[]? referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken(); // ReflectionOnlyLoadFrom loads from the specified path only (no probing) and does not // execute any code from the loaded assembly. @@ -139,8 +139,20 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) } } - private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right) + 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; 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/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/Services/DesktopTestSourceTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceTests.cs index 461efbd2dd..2ea65db70a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceTests.cs @@ -29,5 +29,20 @@ public class DesktopTestSourceTests : TestContainer public void IsAssemblyReferencedShouldReturnTrueIfAnAssemblyIsReferencedInSource() => _testSource.IsAssemblyReferenced(typeof(Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute).Assembly.GetName(), Assembly.GetExecutingAssembly().Location).Should().BeTrue(); public void IsAssemblyReferencedShouldReturnFalseIfAnAssemblyIsNotReferencedInSource() => _testSource.IsAssemblyReferenced(new AssemblyName("foobar"), Assembly.GetExecutingAssembly().Location).Should().BeFalse(); + + // The source references the strong-named MSTest.TestFramework assembly. Passing an assembly name with the + // same simple name but no public key token (as an unsigned build would have) must not be considered a match: + // the public key tokens differ (signed vs. unsigned), so IsAssemblyReferenced returns false. This guards the + // signed-vs-unsigned branch of the public-key-token comparison. + public void IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenIsMissing() => _testSource.IsAssemblyReferenced(new AssemblyName("MSTest.TestFramework"), Assembly.GetExecutingAssembly().Location).Should().BeFalse(); + + // Same simple name but a different (non-null) public key token must also not match: the tokens are compared + // byte-by-byte and differ. This guards the differing-token branch of the public-key-token comparison. + public void IsAssemblyReferencedShouldReturnFalseIfPublicKeyTokenDiffers() + { + var assemblyName = new AssemblyName("MSTest.TestFramework"); + assemblyName.SetPublicKeyToken([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + _testSource.IsAssemblyReferenced(assemblyName, Assembly.GetExecutingAssembly().Location).Should().BeFalse(); + } } #endif