Skip to content

Commit

Permalink
Merge pull request #48 from 0xced/SupportedOSPlatform
Browse files Browse the repository at this point in the history
Add skipping tests based on the [SupportedOSPlatform] attribute
  • Loading branch information
AArnott authored Nov 30, 2024
2 parents 8bc417a + 0dd9ae7 commit 2965bfa
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 1 deletion.
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
}

0 comments on commit 2965bfa

Please sign in to comment.