diff --git a/global.json b/global.json index f874223fd2..a71a01938e 100644 --- a/global.json +++ b/global.json @@ -1,39 +1 @@ -{ - "tools": { - "dotnet": "10.0.100-preview.7.25359.101", - "runtimes": { - "dotnet": [ - "3.1.32", - "6.0.36", - "7.0.20", - "8.0.14", - "9.0.3" - ], - "dotnet/x86": [ - "3.1.32", - "6.0.36", - "9.0.3" - ], - "aspnetcore": [ - "9.0.3" - ] - }, - "vs": { - "version": "17.8.0" - } - }, - "sdk": { - "version": "10.0.100-preview.7.25359.101", - "paths": [ - ".dotnet", - "$host$" - ], - "errorMessage": "The .NET SDK could not be found, please run ./build.cmd on Windows or ./build.sh on Linux and macOS.", - "allowPrerelease": true, - "rollForward": "latestFeature" - }, - "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25358.3", - "MSBuild.Sdk.Extras": "3.0.44" - } -} +{"sdk":{"version":"8.0.117","rollForward":"latestMinor"}} diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/FrameworkConditionAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/FrameworkConditionAttribute.cs new file mode 100644 index 0000000000..c1c102f893 --- /dev/null +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/FrameworkConditionAttribute.cs @@ -0,0 +1,119 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// This attribute is used to conditionally control whether a test class or a test method will run or be ignored based on the .NET framework being used. +/// +/// +/// This attribute isn't inherited. Applying it to a base class will not affect derived classes. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +public sealed class FrameworkConditionAttribute : ConditionBaseAttribute +{ + private readonly Frameworks _frameworks; + + /// + /// Initializes a new instance of the class. + /// + /// Decides whether the frameworks will be included or excluded. + /// The .NET frameworks that this test includes/excludes. + public FrameworkConditionAttribute(ConditionMode mode, Frameworks frameworks) + : base(mode) + { + _frameworks = frameworks; + IgnoreMessage = mode == ConditionMode.Include + ? $"Test is only supported on {frameworks}" + : $"Test is not supported on {frameworks}"; + } + + /// + /// Initializes a new instance of the class. + /// + /// The .NET frameworks that this test supports. + public FrameworkConditionAttribute(Frameworks frameworks) + : this(ConditionMode.Include, frameworks) + { + } + + /// + /// Gets a value indicating whether the test method or test class should run. + /// + public override bool ShouldRun => IsCurrentFrameworkSupported(); + + /// + /// Gets the ignore message (in case returns ). + /// + public override string? IgnoreMessage { get; } + + /// + /// Gets the group name for this attribute. + /// + public override string GroupName => nameof(FrameworkConditionAttribute); + + private bool IsCurrentFrameworkSupported() + { + Frameworks currentFramework = GetCurrentFramework(); + return (_frameworks & currentFramework) != 0; + } + + private static Frameworks GetCurrentFramework() + { + string frameworkDescription = RuntimeInformation.FrameworkDescription; + + // Check for UWP first as it may also have .NET Core or .NET in its description + if (IsRunningOnUwp()) + { + return Frameworks.Uwp; + } + + // Check for .NET Framework + if (frameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase)) + { + return Frameworks.NetFramework; + } + + // Check for .NET Core + if (frameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase)) + { + return Frameworks.NetCore; + } + + // Check for .NET 5+ (includes .NET 5, 6, 7, 8, 9, etc.) + if (frameworkDescription.StartsWith(".NET ", StringComparison.OrdinalIgnoreCase)) + { + return Frameworks.Net; + } + + // Default to .NET for unknown cases + return Frameworks.Net; + } + + private static bool IsRunningOnUwp() + { + try + { + // Try to access Windows.ApplicationModel.Package.Current + // This is only available in UWP applications + var packageType = Type.GetType("Windows.ApplicationModel.Package, Windows.Runtime"); + if (packageType is not null) + { + var currentProperty = packageType.GetProperty("Current"); + if (currentProperty is not null) + { + var current = currentProperty.GetValue(null); + return current is not null; + } + } + } + catch + { + // If we can't access the UWP APIs, we're not running on UWP + } + + return false; + } +} \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/Frameworks.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/Frameworks.cs new file mode 100644 index 0000000000..f08c2ac8e4 --- /dev/null +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/Frameworks.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.TestTools.UnitTesting; + +/// +/// An enum that is used with to control which .NET frameworks a test method or test class supports or doesn't support. +/// +[Flags] +public enum Frameworks +{ + /// + /// Represents .NET Framework (the full framework on Windows). + /// + NetFramework = 1 << 0, + + /// + /// Represents .NET Core 1.x, 2.x, and 3.x. + /// + NetCore = 1 << 1, + + /// + /// Represents .NET 5 and later versions (unified platform). + /// + Net = 1 << 2, + + /// + /// Represents Universal Windows Platform (UWP). + /// + Uwp = 1 << 3, +} \ No newline at end of file diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index 766c80085b..e03c17c73b 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -41,3 +41,14 @@ static Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert.Equals(obje static Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert.ReferenceEquals(object? objA, object? objB) -> bool static Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.Equals(object? objA, object? objB) -> bool static Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.ReferenceEquals(object? objA, object? objB) -> bool +Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks +Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks.Net = 4 -> Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks +Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks.NetCore = 2 -> Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks +Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks.NetFramework = 1 -> Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks +Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks.Uwp = 8 -> Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks +Microsoft.VisualStudio.TestTools.UnitTesting.FrameworkConditionAttribute +Microsoft.VisualStudio.TestTools.UnitTesting.FrameworkConditionAttribute.FrameworkConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks frameworks) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.FrameworkConditionAttribute.FrameworkConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.Frameworks frameworks) -> void +override Microsoft.VisualStudio.TestTools.UnitTesting.FrameworkConditionAttribute.GroupName.get -> string! +override Microsoft.VisualStudio.TestTools.UnitTesting.FrameworkConditionAttribute.IgnoreMessage.get -> string? +override Microsoft.VisualStudio.TestTools.UnitTesting.FrameworkConditionAttribute.ShouldRun.get -> bool diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/FrameworkConditionAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/FrameworkConditionAttributeTests.cs new file mode 100644 index 0000000000..ec9e6e3d52 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/FrameworkConditionAttributeTests.cs @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; + +using TestFramework.ForTestingMSTest; + +namespace UnitTestFramework.Tests; + +/// +/// Tests for class FrameworkConditionAttribute. +/// +public class FrameworkConditionAttributeTests : TestContainer +{ + public void Constructor_SetsCorrectMode() + { + // Arrange & Act + var includeAttribute = new FrameworkConditionAttribute(ConditionMode.Include, Frameworks.NetFramework); + var excludeAttribute = new FrameworkConditionAttribute(ConditionMode.Exclude, Frameworks.NetCore); + + // Assert + includeAttribute.Mode.Should().Be(ConditionMode.Include); + excludeAttribute.Mode.Should().Be(ConditionMode.Exclude); + } + + public void Constructor_WithFrameworkOnly_DefaultsToIncludeMode() + { + // Arrange & Act + var attribute = new FrameworkConditionAttribute(Frameworks.Net); + + // Assert + attribute.Mode.Should().Be(ConditionMode.Include); + } + + public void GroupName_ReturnsCorrectValue() + { + // Arrange + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, Frameworks.Net); + + // Act & Assert + attribute.GroupName.Should().Be(nameof(FrameworkConditionAttribute)); + } + + public void IgnoreMessage_IncludeMode_ReturnsCorrectMessage() + { + // Arrange + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, Frameworks.NetFramework); + + // Act & Assert + attribute.IgnoreMessage.Should().Be("Test is only supported on NetFramework"); + } + + public void IgnoreMessage_ExcludeMode_ReturnsCorrectMessage() + { + // Arrange + var attribute = new FrameworkConditionAttribute(ConditionMode.Exclude, Frameworks.NetCore); + + // Act & Assert + attribute.IgnoreMessage.Should().Be("Test is not supported on NetCore"); + } + + public void IgnoreMessage_MultipleFrameworks_ReturnsCorrectMessage() + { + // Arrange + var frameworks = Frameworks.NetFramework | Frameworks.NetCore; + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, frameworks); + + // Act & Assert + attribute.IgnoreMessage.Should().Be("Test is only supported on NetFramework, NetCore"); + } + + public void IgnoreMessage_UwpFramework_ReturnsCorrectMessage() + { + // Arrange + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, Frameworks.Uwp); + + // Act & Assert + attribute.IgnoreMessage.Should().Be("Test is only supported on Uwp"); + } + + public void IgnoreMessage_ExcludeUwpFramework_ReturnsCorrectMessage() + { + // Arrange + var attribute = new FrameworkConditionAttribute(ConditionMode.Exclude, Frameworks.Uwp); + + // Act & Assert + attribute.IgnoreMessage.Should().Be("Test is not supported on Uwp"); + } + + public void ShouldRun_IncludeMode_CurrentFrameworkMatches_ReturnsTrue() + { + // Arrange + var currentFramework = GetCurrentFrameworkEnum(); + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, currentFramework); + + // Act & Assert + attribute.ShouldRun.Should().BeTrue(); + } + + public void ShouldRun_ExcludeMode_CurrentFrameworkMatches_ReturnsTrue() + { + // Arrange + var currentFramework = GetCurrentFrameworkEnum(); + var attribute = new FrameworkConditionAttribute(ConditionMode.Exclude, currentFramework); + + // Act & Assert + // ShouldRun returns true when the condition is detected (current framework matches) + // The framework handles include/exclude logic separately + attribute.ShouldRun.Should().BeTrue(); + } + + public void ShouldRun_IncludeMode_CurrentFrameworkDoesNotMatch_ReturnsFalse() + { + // Arrange + var currentFramework = GetCurrentFrameworkEnum(); + var differentFramework = GetDifferentFramework(currentFramework); + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, differentFramework); + + // Act & Assert + attribute.ShouldRun.Should().BeFalse(); + } + + public void ShouldRun_ExcludeMode_CurrentFrameworkDoesNotMatch_ReturnsFalse() + { + // Arrange + var currentFramework = GetCurrentFrameworkEnum(); + var differentFramework = GetDifferentFramework(currentFramework); + var attribute = new FrameworkConditionAttribute(ConditionMode.Exclude, differentFramework); + + // Act & Assert + // ShouldRun returns false when the condition is NOT detected + attribute.ShouldRun.Should().BeFalse(); + } + + public void ShouldRun_Net_OnCurrentDotNet_ReturnsTrue() + { + // Arrange + var attribute = new FrameworkConditionAttribute(Frameworks.Net); + + // Act & Assert + // This test verifies that .NET 5+ apps match the Net flag + if (Environment.Version.Major >= 5 && !System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase)) + { + attribute.ShouldRun.Should().BeTrue(); + } + } + + public void ShouldRun_MultipleFrameworks_IncludesCurrent_ReturnsTrue() + { + // Arrange + var currentFramework = GetCurrentFrameworkEnum(); + var multipleFrameworks = currentFramework | Frameworks.NetFramework; // Include current plus another + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, multipleFrameworks); + + // Act & Assert + attribute.ShouldRun.Should().BeTrue(); + } + + public void ShouldRun_MultipleFrameworks_ExcludesCurrent_ReturnsFalse() + { + // Arrange + var currentFramework = GetCurrentFrameworkEnum(); + var differentFramework = GetDifferentFramework(currentFramework); + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, differentFramework); + + // Act & Assert + attribute.ShouldRun.Should().BeFalse(); + } + + public void ShouldRun_UwpFramework_OnNonUwp_ReturnsFalse() + { + // Arrange + var attribute = new FrameworkConditionAttribute(Frameworks.Uwp); + + // Act & Assert + // This test assumes we're not running on UWP in the test environment + if (!IsRunningOnUwp()) + { + attribute.ShouldRun.Should().BeFalse(); + } + } + + public void ShouldRun_ExcludeUwpFramework_OnNonUwp_ReturnsTrue() + { + // Arrange + var attribute = new FrameworkConditionAttribute(ConditionMode.Exclude, Frameworks.Uwp); + + // Act & Assert + // This test assumes we're not running on UWP in the test environment + if (!IsRunningOnUwp()) + { + attribute.ShouldRun.Should().BeFalse(); + } + } + + public void ShouldRun_MultipleFrameworksIncludingUwp_IncludesCurrent_ReturnsTrue() + { + // Arrange + var currentFramework = GetCurrentFrameworkEnum(); + var multipleFrameworks = currentFramework | Frameworks.Uwp; // Include current plus UWP + var attribute = new FrameworkConditionAttribute(ConditionMode.Include, multipleFrameworks); + + // Act & Assert + attribute.ShouldRun.Should().BeTrue(); + } + + private static Frameworks GetCurrentFrameworkEnum() + { + // Check for UWP first + if (IsRunningOnUwp()) + { + return Frameworks.Uwp; + } + + string frameworkDescription = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; + + if (frameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase)) + { + return Frameworks.NetFramework; + } + + if (frameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase)) + { + return Frameworks.NetCore; + } + + // .NET 5+ + return Frameworks.Net; + } + + private static bool IsRunningOnUwp() + { + try + { + // Try to access Windows.ApplicationModel.Package.Current + // This is only available in UWP applications + var packageType = Type.GetType("Windows.ApplicationModel.Package, Windows.Runtime"); + if (packageType is not null) + { + var currentProperty = packageType.GetProperty("Current"); + if (currentProperty is not null) + { + var current = currentProperty.GetValue(null); + return current is not null; + } + } + } + catch + { + // If we can't access the UWP APIs, we're not running on UWP + } + + return false; + } + + private static Frameworks GetDifferentFramework(Frameworks current) + { + // Return a framework that's different from the current one + if ((current & Frameworks.NetFramework) != 0) + { + return Frameworks.NetCore; + } + if ((current & Frameworks.NetCore) != 0) + { + return Frameworks.NetFramework; + } + if ((current & Frameworks.Net) != 0) + { + return Frameworks.NetFramework; + } + if ((current & Frameworks.Uwp) != 0) + { + return Frameworks.NetFramework; + } + return Frameworks.NetCore; + } +} \ No newline at end of file