From 34c92ae8f78a639ef4da6552d42702685304f1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Thu, 28 Nov 2024 17:37:07 +0100 Subject: [PATCH 1/4] Add skipping tests based on the [SupportedOSPlatform] attribute Adding support for `SupportedOSPlatform` is a great because this attributes suppresses the CA1416 code analysis warning. This feature was requested in https://github.com/xunit/xunit/issues/2820 but was not implemented in xUnit.net --- README.md | 19 ++++++++ .../Sdk/SkippableFactTestCase.cs | 6 +++ .../Sdk/SkippableTheoryTestCase.cs | 6 +++ .../Sdk/TestMethodExtensions.cs | 47 +++++++++++++++++++ test/Xunit.SkippableFact.Tests/SampleTests.cs | 43 ++++++++++++++++- 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs diff --git a/README.md b/README.md index ff7cc1b..18b6fd3 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,23 @@ public void TestFunctionalityWhichIsNotSupportedOnSomePlatforms() } ``` +### The [SupportedOSPlatform] attribute + +Since version 1.5, `Xunit.SkippableFact` understands the `SupportedOSPlatform` attribute to skip tests on unsupported platforms. + +```csharp +[SkippableFact, SupportedOSPlatform("Windows")] +public void TestCngKey() +{ + var key = CngKey.Create(CngAlgorithm.Sha256); + Assert.NotNull(key); +} +``` + +Without `[SupportedOSPlatform("Windows")` the [CA1416](CA1416) code analysis warning would trigger: +> This call site is reachable on all platforms. 'CngKey. Create(CngAlgorithm)' is only supported on: 'windows'. + +Adding `[SupportedOSPlatform("Windows")` both suppresses this platform compatibility warning and skips the test when running on Linux or macOS. + [NuPkg]: https://www.nuget.org/packages/Xunit.SkippableFact +[CA1416]: https://learn.microsoft.com/en-gb/dotnet/fundamentals/code-analysis/quality-rules/ca1416 diff --git a/src/Xunit.SkippableFact/Sdk/SkippableFactTestCase.cs b/src/Xunit.SkippableFact/Sdk/SkippableFactTestCase.cs index feb5e23..f81b9f5 100644 --- a/src/Xunit.SkippableFact/Sdk/SkippableFactTestCase.cs +++ b/src/Xunit.SkippableFact/Sdk/SkippableFactTestCase.cs @@ -88,4 +88,10 @@ public override void Deserialize(IXunitSerializationInfo data) base.Deserialize(data); this.SkippingExceptionNames = data.GetValue(nameof(this.SkippingExceptionNames)); } + + /// + protected override string GetSkipReason(IAttributeInfo factAttribute) + { + return this.TestMethod.GetPlatformSkipReason() ?? base.GetSkipReason(factAttribute); + } } diff --git a/src/Xunit.SkippableFact/Sdk/SkippableTheoryTestCase.cs b/src/Xunit.SkippableFact/Sdk/SkippableTheoryTestCase.cs index 09d5903..800ebeb 100644 --- a/src/Xunit.SkippableFact/Sdk/SkippableTheoryTestCase.cs +++ b/src/Xunit.SkippableFact/Sdk/SkippableTheoryTestCase.cs @@ -86,4 +86,10 @@ public override void Deserialize(IXunitSerializationInfo data) base.Deserialize(data); this.SkippingExceptionNames = data.GetValue(nameof(this.SkippingExceptionNames)); } + + /// + protected override string GetSkipReason(IAttributeInfo factAttribute) + { + return this.TestMethod.GetPlatformSkipReason() ?? base.GetSkipReason(factAttribute); + } } diff --git a/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs new file mode 100644 index 0000000..e17efcf --- /dev/null +++ b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the Microsoft Public License (Ms-PL). See LICENSE.txt file in the project root for full license information. + +using System.Runtime.InteropServices; +using Xunit.Abstractions; + +namespace Xunit.Sdk; + +/// +/// Extensions methods on . +/// +internal static class TestMethodExtensions +{ + /// + /// Assesses whether the test method can run on the current platform by looking at the [SupportedOSPlatform] attributes on the test method and on the test class. + /// + /// The . + /// A description of the supported platforms if the test can not run on the current platform or if the test can run on the current platform. + public static string? GetPlatformSkipReason(this ITestMethod testMethod) + { +#if NET462 + return null; +#else + var platforms = new HashSet(StringComparer.OrdinalIgnoreCase); + AddPlatforms(platforms, testMethod.Method.GetCustomAttributes("System.Runtime.Versioning.SupportedOSPlatformAttribute")); + AddPlatforms(platforms, testMethod.Method.Type.GetCustomAttributes("System.Runtime.Versioning.SupportedOSPlatformAttribute")); + + if (platforms.Count == 0 || platforms.Any(platform => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)))) + { + return null; + } + + string platformsDescription = platforms.Count == 1 ? platforms.First() : string.Join(", ", platforms.Reverse().Skip(1).Reverse()) + " and " + platforms.Last(); + return $"Only supported on {platformsDescription}"; +#endif + } + +#if !NET462 + private static void AddPlatforms(HashSet platforms, IEnumerable supportedPlatformAttributes) + { + foreach (IAttributeInfo supportedPlatformAttribute in supportedPlatformAttributes) + { + platforms.Add(supportedPlatformAttribute.GetNamedArgument("PlatformName")); + } + } +#endif +} diff --git a/test/Xunit.SkippableFact.Tests/SampleTests.cs b/test/Xunit.SkippableFact.Tests/SampleTests.cs index fd1aa0d..5598c88 100644 --- a/test/Xunit.SkippableFact.Tests/SampleTests.cs +++ b/test/Xunit.SkippableFact.Tests/SampleTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the Microsoft Public License (Ms-PL). See LICENSE.txt file in the project root for full license information. -using System; +using System.Runtime.Versioning; namespace Xunit.SkippableFact.Tests; @@ -76,4 +76,45 @@ public void SkipInsideAssertThrows() throw new Exception(); })); } + +#if NET5_0_OR_GREATER + [SkippableFact, SupportedOSPlatform("Linux")] + public void LinuxOnly() + { + Assert.True(OperatingSystem.IsLinux(), "This should only run on Linux"); + } + + [SkippableFact, SupportedOSPlatform("macOS")] + public void MacOsOnly() + { + Assert.True(OperatingSystem.IsMacOS(), "This should only run on macOS"); + } + + [SkippableFact, SupportedOSPlatform("Windows")] + public void WindowsOnly() + { + Assert.True(OperatingSystem.IsWindows(), "This should only run on Windows"); + } + + [SkippableFact, SupportedOSPlatform("Android"), SupportedOSPlatform("Browser")] + public void AndroidAndBrowserFact() + { + Assert.True(OperatingSystem.IsAndroid() || OperatingSystem.IsBrowser(), "This should only run on Android and Browser"); + } + + [SkippableTheory, SupportedOSPlatform("Android"), SupportedOSPlatform("Browser")] + [InlineData(1)] + [InlineData(2)] + public void AndroidAndBrowserTheory(int number) + { + _ = number; + Assert.True(OperatingSystem.IsAndroid() || OperatingSystem.IsBrowser(), "This should only run on Android and Browser"); + } + + [SkippableFact, SupportedOSPlatform("Android"), SupportedOSPlatform("Browser"), SupportedOSPlatform("Wasi")] + public void AndroidAndBrowserAndWasiOnly() + { + Assert.True(OperatingSystem.IsAndroid() || OperatingSystem.IsBrowser() || OperatingSystem.IsWasi(), "This should only run on Android, Browser and Wasi"); + } +#endif } From 00b459f3ad8baf7c509a8568fbc9aa7b49ee98b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Fri, 29 Nov 2024 11:46:01 +0100 Subject: [PATCH 2/4] Add support for platform versions --- .../Sdk/TestMethodExtensions.cs | 49 ++++++++++++++++++- test/Xunit.SkippableFact.Tests/SampleTests.cs | 24 +++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs index e17efcf..2f316b6 100644 --- a/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs +++ b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs @@ -25,7 +25,7 @@ internal static class TestMethodExtensions AddPlatforms(platforms, testMethod.Method.GetCustomAttributes("System.Runtime.Versioning.SupportedOSPlatformAttribute")); AddPlatforms(platforms, testMethod.Method.Type.GetCustomAttributes("System.Runtime.Versioning.SupportedOSPlatformAttribute")); - if (platforms.Count == 0 || platforms.Any(platform => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)))) + if (platforms.Count == 0 || platforms.Any(MatchesCurrentPlatform)) { return null; } @@ -36,6 +36,53 @@ internal static class TestMethodExtensions } #if !NET462 + private static bool MatchesCurrentPlatform(string platform) + { + int versionIndex = platform.IndexOfAny(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); + bool matchesVersion; + if (versionIndex >= 0 && Version.TryParse(platform[versionIndex..], out Version version)) + { + platform = platform[..versionIndex]; + matchesVersion = MatchesCurrentVersion(version.Major, version.Minor, version.Build, version.Revision); + } + else + { + matchesVersion = true; + } + + return matchesVersion && RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); + } + + // Adapted from OperatingSystem.IsOSVersionAtLeast() which is private, see https://github.com/dotnet/runtime/blob/d6eb35426ebdb09ee5c754aa9afb9ad6e96a3dec/src/libraries/System.Private.CoreLib/src/System/OperatingSystem.cs#L326-L351 + private static bool MatchesCurrentVersion(int major, int minor, int build, int revision) + { + Version current = Environment.OSVersion.Version; + + if (current.Major != major) + { + return current.Major > major; + } + + if (current.Minor != minor) + { + return current.Minor > minor; + } + + // Unspecified build component is to be treated as zero + int currentBuild = current.Build < 0 ? 0 : current.Build; + build = build < 0 ? 0 : build; + if (currentBuild != build) + { + return currentBuild > build; + } + + // Unspecified revision component is to be treated as zero + int currentRevision = current.Revision < 0 ? 0 : current.Revision; + revision = revision < 0 ? 0 : revision; + + return currentRevision >= revision; + } + private static void AddPlatforms(HashSet platforms, IEnumerable supportedPlatformAttributes) { foreach (IAttributeInfo supportedPlatformAttribute in supportedPlatformAttributes) diff --git a/test/Xunit.SkippableFact.Tests/SampleTests.cs b/test/Xunit.SkippableFact.Tests/SampleTests.cs index 5598c88..9bd28f8 100644 --- a/test/Xunit.SkippableFact.Tests/SampleTests.cs +++ b/test/Xunit.SkippableFact.Tests/SampleTests.cs @@ -90,12 +90,36 @@ public void MacOsOnly() Assert.True(OperatingSystem.IsMacOS(), "This should only run on macOS"); } + [SkippableFact, SupportedOSPlatform("macOS10.6")] + public void MacOs10_6Minimum() + { + Assert.True(OperatingSystem.IsMacOSVersionAtLeast(10, 6), "This should only run on macOS 10.6 onwards"); + } + + [SkippableFact, SupportedOSPlatform("macOS77.7")] + public void MacOs77_7Minimum() + { + Assert.True(OperatingSystem.IsMacOSVersionAtLeast(77, 7), "This should only run on macOS 77.7 onwards"); + } + [SkippableFact, SupportedOSPlatform("Windows")] public void WindowsOnly() { Assert.True(OperatingSystem.IsWindows(), "This should only run on Windows"); } + [SkippableFact, SupportedOSPlatform("Windows10.0")] + public void Windows10Minimum() + { + Assert.True(OperatingSystem.IsWindowsVersionAtLeast(10), "This should only run on Windows 10.0 onwards"); + } + + [SkippableFact, SupportedOSPlatform("Windows77.7")] + public void Windows77_7Minimum() + { + Assert.True(OperatingSystem.IsWindowsVersionAtLeast(77, 7), "This should only run on Windows 77.7 onwards"); + } + [SkippableFact, SupportedOSPlatform("Android"), SupportedOSPlatform("Browser")] public void AndroidAndBrowserFact() { From 229598725742a6508b5248d292103b09cd202d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Fri, 29 Nov 2024 11:32:32 +0100 Subject: [PATCH 3/4] Add support for the [UnsupportedOSPlatform] attribute --- .../Sdk/TestMethodExtensions.cs | 45 ++++++++++++++++--- test/Xunit.SkippableFact.Tests/SampleTests.cs | 8 ++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs index 2f316b6..1bb3fcd 100644 --- a/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs +++ b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs @@ -21,16 +21,20 @@ internal static class TestMethodExtensions #if NET462 return null; #else - var platforms = new HashSet(StringComparer.OrdinalIgnoreCase); - AddPlatforms(platforms, testMethod.Method.GetCustomAttributes("System.Runtime.Versioning.SupportedOSPlatformAttribute")); - AddPlatforms(platforms, testMethod.Method.Type.GetCustomAttributes("System.Runtime.Versioning.SupportedOSPlatformAttribute")); + HashSet unsupportedPlatforms = GetPlatforms(testMethod, "UnsupportedOSPlatform"); + string? unsupportedPlatform = unsupportedPlatforms.FirstOrDefault(MatchesCurrentPlatform); + if (unsupportedPlatform is not null) + { + return $"Unsupported on {unsupportedPlatform}"; + } - if (platforms.Count == 0 || platforms.Any(MatchesCurrentPlatform)) + HashSet supportedPlatforms = GetPlatforms(testMethod, "SupportedOSPlatform"); + if (supportedPlatforms.Count == 0 || supportedPlatforms.Any(MatchesCurrentPlatform)) { return null; } - string platformsDescription = platforms.Count == 1 ? platforms.First() : string.Join(", ", platforms.Reverse().Skip(1).Reverse()) + " and " + platforms.Last(); + string platformsDescription = supportedPlatforms.Count == 1 ? supportedPlatforms.First() : string.Join(", ", supportedPlatforms.Reverse().Skip(1).Reverse()) + " and " + supportedPlatforms.Last(); return $"Only supported on {platformsDescription}"; #endif } @@ -83,6 +87,37 @@ private static bool MatchesCurrentVersion(int major, int minor, int build, int r return currentRevision >= revision; } + /// + /// Returns the collection of platforms defined by the specified that decorate the test method and the test class. + /// + /// The . + /// Either SupportedOSPlatform or UnsupportedOSPlatform. + /// + /// + /// Calling GetPlatforms(testMethod, "SupportedOSPlatform") where represents MyTest returns ["Linux", "macOS"]. + /// + /// + /// [SupportedOSPlatform("macOS")] + /// public class MyTests + /// { + /// [SkippableFact] + /// [SupportedOSPlatform("Linux")] + /// public void MyTest() + /// { + /// } + /// } + /// + /// + /// The collection of platforms defined by the specified that decorate the test method and the test class. + private static HashSet GetPlatforms(ITestMethod testMethod, string platformAttributeName) + { + string platformAttribute = $"System.Runtime.Versioning.{platformAttributeName}Attribute"; + var platforms = new HashSet(StringComparer.OrdinalIgnoreCase); + AddPlatforms(platforms, testMethod.Method.GetCustomAttributes(platformAttribute)); + AddPlatforms(platforms, testMethod.Method.Type.GetCustomAttributes(platformAttribute)); + return platforms; + } + private static void AddPlatforms(HashSet platforms, IEnumerable supportedPlatformAttributes) { foreach (IAttributeInfo supportedPlatformAttribute in supportedPlatformAttributes) diff --git a/test/Xunit.SkippableFact.Tests/SampleTests.cs b/test/Xunit.SkippableFact.Tests/SampleTests.cs index 9bd28f8..8b91925 100644 --- a/test/Xunit.SkippableFact.Tests/SampleTests.cs +++ b/test/Xunit.SkippableFact.Tests/SampleTests.cs @@ -140,5 +140,13 @@ public void AndroidAndBrowserAndWasiOnly() { Assert.True(OperatingSystem.IsAndroid() || OperatingSystem.IsBrowser() || OperatingSystem.IsWasi(), "This should only run on Android, Browser and Wasi"); } + + [SkippableFact, UnsupportedOSPlatform("Linux"), UnsupportedOSPlatform("macOS"), UnsupportedOSPlatform("Windows")] + public void UnsupportedPlatforms() + { + Assert.False(OperatingSystem.IsLinux()); + Assert.False(OperatingSystem.IsMacOS()); + Assert.False(OperatingSystem.IsWindows()); + } #endif } From 0dd9ae77de240e3e773e80dd216cf975463bb550 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 30 Nov 2024 08:21:17 -0700 Subject: [PATCH 4/4] Touch-ups on README --- README.md | 8 ++++---- src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 18b6fd3..a4f0b0e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ public void TestFunctionalityWhichIsNotSupportedOnSomePlatforms() } ``` -### The [SupportedOSPlatform] attribute +## The `[SupportedOSPlatform]` attribute Since version 1.5, `Xunit.SkippableFact` understands the `SupportedOSPlatform` attribute to skip tests on unsupported platforms. @@ -51,10 +51,10 @@ public void TestCngKey() } ``` -Without `[SupportedOSPlatform("Windows")` the [CA1416](CA1416) code analysis warning would trigger: +Without `[SupportedOSPlatform("Windows")]` the [CA1416][CA1416] code analysis warning would trigger: > This call site is reachable on all platforms. 'CngKey. Create(CngAlgorithm)' is only supported on: 'windows'. -Adding `[SupportedOSPlatform("Windows")` both suppresses this platform compatibility warning and skips the test when running on Linux or macOS. +Adding `[SupportedOSPlatform("Windows")]` both suppresses this platform compatibility warning and skips the test when running on Linux or macOS. [NuPkg]: https://www.nuget.org/packages/Xunit.SkippableFact -[CA1416]: https://learn.microsoft.com/en-gb/dotnet/fundamentals/code-analysis/quality-rules/ca1416 +[CA1416]: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416 diff --git a/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs index 1bb3fcd..e3d0151 100644 --- a/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs +++ b/src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs @@ -16,7 +16,7 @@ internal static class TestMethodExtensions /// /// The . /// A description of the supported platforms if the test can not run on the current platform or if the test can run on the current platform. - public static string? GetPlatformSkipReason(this ITestMethod testMethod) + internal static string? GetPlatformSkipReason(this ITestMethod testMethod) { #if NET462 return null;