Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add skipping tests based on the [SupportedOSPlatform] attribute #48

Merged
merged 4 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/dotnet/fundamentals/code-analysis/quality-rules/ca1416
6 changes: 6 additions & 0 deletions src/Xunit.SkippableFact/Sdk/SkippableFactTestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,10 @@ public override void Deserialize(IXunitSerializationInfo data)
base.Deserialize(data);
this.SkippingExceptionNames = data.GetValue<string[]>(nameof(this.SkippingExceptionNames));
}

/// <inheritdoc/>
protected override string GetSkipReason(IAttributeInfo factAttribute)
{
return this.TestMethod.GetPlatformSkipReason() ?? base.GetSkipReason(factAttribute);
}
}
6 changes: 6 additions & 0 deletions src/Xunit.SkippableFact/Sdk/SkippableTheoryTestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,10 @@ public override void Deserialize(IXunitSerializationInfo data)
base.Deserialize(data);
this.SkippingExceptionNames = data.GetValue<string[]>(nameof(this.SkippingExceptionNames));
}

/// <inheritdoc/>
protected override string GetSkipReason(IAttributeInfo factAttribute)
{
return this.TestMethod.GetPlatformSkipReason() ?? base.GetSkipReason(factAttribute);
}
}
129 changes: 129 additions & 0 deletions src/Xunit.SkippableFact/Sdk/TestMethodExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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;

/// <summary>
/// Extensions methods on <see cref="ITestMethod"/>.
/// </summary>
internal static class TestMethodExtensions
{
/// <summary>
/// Assesses whether the test method can run on the current platform by looking at the <c>[SupportedOSPlatform]</c> attributes on the test method and on the test class.
/// </summary>
/// <param name="testMethod">The <see cref="ITestMethod"/>.</param>
/// <returns>A description of the supported platforms if the test can not run on the current platform or <see langword="null"/> if the test can run on the current platform.</returns>
internal static string? GetPlatformSkipReason(this ITestMethod testMethod)
{
#if NET462
return null;
#else
HashSet<string> unsupportedPlatforms = GetPlatforms(testMethod, "UnsupportedOSPlatform");
string? unsupportedPlatform = unsupportedPlatforms.FirstOrDefault(MatchesCurrentPlatform);
if (unsupportedPlatform is not null)
{
return $"Unsupported on {unsupportedPlatform}";
}

HashSet<string> supportedPlatforms = GetPlatforms(testMethod, "SupportedOSPlatform");
if (supportedPlatforms.Count == 0 || supportedPlatforms.Any(MatchesCurrentPlatform))
{
return null;
}

string platformsDescription = supportedPlatforms.Count == 1 ? supportedPlatforms.First() : string.Join(", ", supportedPlatforms.Reverse().Skip(1).Reverse()) + " and " + supportedPlatforms.Last();
return $"Only supported on {platformsDescription}";
#endif
}

#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;
}

/// <summary>
/// Returns the collection of platforms defined by the specified <paramref name="platformAttributeName"/> that decorate the test method and the test class.
/// </summary>
/// <param name="testMethod">The <see cref="ITestMethod"/>.</param>
/// <param name="platformAttributeName">Either <c>SupportedOSPlatform</c> or <c>UnsupportedOSPlatform</c>.</param>
/// <example>
/// <para>
/// Calling GetPlatforms(testMethod, "SupportedOSPlatform") where <paramref name="testMethod"/> represents <c>MyTest</c> returns ["Linux", "macOS"].
/// </para>
/// <code>
/// [SupportedOSPlatform("macOS")]
/// public class MyTests
/// {
/// [SkippableFact]
/// [SupportedOSPlatform("Linux")]
/// public void MyTest()
/// {
/// }
/// }
/// </code>
/// </example>
/// <returns>The collection of platforms defined by the specified <paramref name="platformAttributeName"/> that decorate the test method and the test class.</returns>
private static HashSet<string> GetPlatforms(ITestMethod testMethod, string platformAttributeName)
{
string platformAttribute = $"System.Runtime.Versioning.{platformAttributeName}Attribute";
var platforms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
AddPlatforms(platforms, testMethod.Method.GetCustomAttributes(platformAttribute));
AddPlatforms(platforms, testMethod.Method.Type.GetCustomAttributes(platformAttribute));
return platforms;
}

private static void AddPlatforms(HashSet<string> platforms, IEnumerable<IAttributeInfo> supportedPlatformAttributes)
{
foreach (IAttributeInfo supportedPlatformAttribute in supportedPlatformAttributes)
{
platforms.Add(supportedPlatformAttribute.GetNamedArgument<string>("PlatformName"));
}
}
#endif
}
75 changes: 74 additions & 1 deletion test/Xunit.SkippableFact.Tests/SampleTests.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -76,4 +76,77 @@ 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("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()
{
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");
}

[SkippableFact, UnsupportedOSPlatform("Linux"), UnsupportedOSPlatform("macOS"), UnsupportedOSPlatform("Windows")]
public void UnsupportedPlatforms()
{
Assert.False(OperatingSystem.IsLinux());
Assert.False(OperatingSystem.IsMacOS());
Assert.False(OperatingSystem.IsWindows());
}
#endif
}