Skip to content

Commit b2cf26b

Browse files
authored
feature: Added ExcludeOnAttribute and RunOnAttribute (#2512)
* feature: Added `ExcludeOnAttribute` and `RunOnAttribute` as discussed in #2508 * chore: Updated skip reasons * test: Added tests for `ExcludeOn` and `RunOn` * chore: Marked `SkipMacOsAttribute` as obsolete * chore: Simplified * fix(test): Updated Public API definition
1 parent 092864b commit b2cf26b

File tree

9 files changed

+291
-0
lines changed

9 files changed

+291
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Runtime.InteropServices;
2+
using TUnit.Core.Enums;
3+
4+
namespace TUnit.Core;
5+
6+
/// <summary>
7+
/// Attribute that excludes a test from running on specific operating systems.
8+
/// </summary>
9+
/// <param name="OperatingSystem">
10+
/// Defines the operating systems on which the test should not run.
11+
/// </param>
12+
/// <remarks>
13+
/// <para>
14+
/// The <see cref="ExcludeOnAttribute"/> is used to specify that a test should not run on certain operating systems.
15+
/// Tests with this attribute will be skipped on operating systems that match the specified criteria.
16+
/// </para>
17+
/// <para>
18+
/// You can specify multiple operating systems by combining the <see cref="OS"/> enum values with the bitwise OR operator.
19+
/// </para>
20+
/// </remarks>
21+
/// <example>
22+
/// <code>
23+
/// // Skip on Windows
24+
/// [Test, ExcludeOn(OS.Windows)]
25+
/// public void NonWindowsOnlyTest()
26+
/// {
27+
/// // This test will run on Linux and macOS, but not on Windows
28+
/// }
29+
///
30+
/// // Skip on both Windows and Linux
31+
/// [Test, ExcludeOn(OS.Windows | OS.Linux)]
32+
/// public void MacOsOnlyTest()
33+
/// {
34+
/// // This test will only run on macOS
35+
/// }
36+
///
37+
/// // Skip on all supported platforms (essentially always skips the test)
38+
/// [Test, ExcludeOn(OS.Windows | OS.Linux | OS.MacOs)]
39+
/// public void NeverRunTest()
40+
/// {
41+
/// // This test will not run on any supported platform
42+
/// }
43+
/// </code>
44+
/// </example>
45+
/// <seealso cref="SkipAttribute"/>
46+
/// <seealso cref="RunOnAttribute"/>
47+
/// <seealso cref="OS"/>
48+
public sealed class ExcludeOnAttribute(OS OperatingSystem) : SkipAttribute($"This test is excluded on the following operating systems: `{OperatingSystem}`.")
49+
{
50+
/// <inheritdoc />
51+
public override Task<bool> ShouldSkip(BeforeTestContext context)
52+
{
53+
// Check if the current platform matches any of the excluded operating systems
54+
bool shouldSkip =
55+
(OperatingSystem.HasFlag(OS.Windows) && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
56+
#if NET
57+
// Only validate Linux and macOS on .NET 5+ where these OS flags are available
58+
|| (OperatingSystem.HasFlag(OS.Linux) && RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
59+
|| (OperatingSystem.HasFlag(OS.MacOs) && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
60+
#endif
61+
|| true;
62+
63+
// Return true if the test should be skipped (if we're on an excluded OS)
64+
return Task.FromResult(shouldSkip);
65+
}
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using TUnit.Core.Enums;
4+
5+
namespace TUnit.Core;
6+
7+
/// <summary>
8+
/// Attribute that restricts a test to run only on specific operating systems.
9+
/// </summary>
10+
/// <param name="OperatingSystem">
11+
/// Defines the operating systems on which the test should run.
12+
/// </param>
13+
/// <remarks>
14+
/// <para>
15+
/// The <see cref="RunOnAttribute"/> is used to specify that a test should only run on certain operating systems.
16+
/// Tests with this attribute will be skipped on operating systems that do not match the specified criteria.
17+
/// </para>
18+
/// <para>
19+
/// You can specify multiple operating systems by combining the <see cref="OS"/> enum values with the bitwise OR operator.
20+
/// </para>
21+
/// </remarks>
22+
/// <example>
23+
/// <code>
24+
/// // Run only on Windows
25+
/// [Test, RunOn(OS.Windows)]
26+
/// public void WindowsOnlyTest()
27+
/// {
28+
/// // This test will only run on Windows
29+
/// }
30+
///
31+
/// // Run on both Windows and Linux
32+
/// [Test, RunOn(OS.Windows | OS.Linux)]
33+
/// public void WindowsAndLinuxTest()
34+
/// {
35+
/// // This test will run on Windows and Linux, but not on macOS
36+
/// }
37+
///
38+
/// // Run on all supported platforms
39+
/// [Test, RunOn(OS.Windows | OS.Linux | OS.MacOs)]
40+
/// public void AllPlatformsTest()
41+
/// {
42+
/// // This test will run on all supported platforms
43+
/// }
44+
/// </code>
45+
/// </example>
46+
/// <seealso cref="SkipAttribute"/>
47+
/// <seealso cref="OS"/>
48+
public sealed class RunOnAttribute(OS OperatingSystem) : SkipAttribute($"Test is restricted to run on the following operating systems: `{OperatingSystem}`.")
49+
{
50+
/// <inheritdoc />
51+
public override Task<bool> ShouldSkip(BeforeTestContext context)
52+
{
53+
// Check if the current platform matches any of the allowed operating systems
54+
bool shouldRun =
55+
(OperatingSystem.HasFlag(OS.Windows) && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
56+
#if NET
57+
// Only validate Linux and macOS on .NET 5+ where these OS flags are available
58+
|| (OperatingSystem.HasFlag(OS.Linux) && RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
59+
|| (OperatingSystem.HasFlag(OS.MacOs) && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
60+
#endif
61+
;
62+
63+
// Return true if the test should be skipped (opposite of shouldRun)
64+
return Task.FromResult(!shouldRun);
65+
}
66+
}

TUnit.Core/Enums/OS.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace TUnit.Core.Enums;
2+
3+
/// <summary>
4+
/// Represents operating systems that can be specified for test execution constraints.
5+
/// </summary>
6+
/// <remarks>
7+
/// <para>
8+
/// This enum is marked with the <see cref="FlagsAttribute"/>, which allows combining multiple operating systems
9+
/// using bitwise operations when used with attributes like <see cref="RunOnAttribute"/>.
10+
/// </para>
11+
/// <para>
12+
/// The primary use case is to restrict test execution to specific operating systems through
13+
/// the <see cref="RunOnAttribute"/>.
14+
/// </para>
15+
/// </remarks>
16+
/// <example>
17+
/// <code>
18+
/// // Specify a test should run only on Windows
19+
/// [RunOn(OS.Windows)]
20+
///
21+
/// // Specify a test should run on either Windows or Linux
22+
/// [RunOn(OS.Windows | OS.Linux)]
23+
///
24+
/// // Specify a test should run on all supported platforms
25+
/// [RunOn(OS.Windows | OS.Linux | OS.MacOs)]
26+
/// </code>
27+
/// </example>
28+
/// <seealso cref="RunOnAttribute"/>
29+
[Flags]
30+
public enum OS
31+
{
32+
/// <summary>
33+
/// Represents the Linux operating system.
34+
/// </summary>
35+
/// <remarks>
36+
/// Tests with this flag will be executed on Linux platforms when used with <see cref="RunOnAttribute"/>.
37+
/// </remarks>
38+
Linux = 1,
39+
40+
/// <summary>
41+
/// Represents the Windows operating system.
42+
/// </summary>
43+
/// <remarks>
44+
/// Tests with this flag will be executed on Windows platforms when used with <see cref="RunOnAttribute"/>.
45+
/// </remarks>
46+
Windows = 2,
47+
48+
/// <summary>
49+
/// Represents the macOS operating system.
50+
/// </summary>
51+
/// <remarks>
52+
/// Tests with this flag will be executed on macOS platforms when used with <see cref="RunOnAttribute"/>.
53+
/// </remarks>
54+
MacOs = 4
55+
}

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,11 @@ namespace TUnit.Core
398398
public System.Threading.CancellationToken Token { get; }
399399
public void Dispose() { }
400400
}
401+
public sealed class ExcludeOnAttribute : TUnit.Core.SkipAttribute
402+
{
403+
public ExcludeOnAttribute(TUnit.Core.Enums.OS OperatingSystem) { }
404+
public override System.Threading.Tasks.Task<bool> ShouldSkip(TUnit.Core.BeforeTestContext context) { }
405+
}
401406
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)]
402407
public sealed class ExplicitAttribute : TUnit.Core.TUnitAttribute
403408
{
@@ -641,6 +646,11 @@ namespace TUnit.Core
641646
public void OnTestDiscovery(TUnit.Core.DiscoveredTestContext discoveredTestContext) { }
642647
public virtual System.Threading.Tasks.Task<bool> ShouldRetry(TUnit.Core.TestContext context, System.Exception exception, int currentRetryCount) { }
643648
}
649+
public sealed class RunOnAttribute : TUnit.Core.SkipAttribute
650+
{
651+
public RunOnAttribute(TUnit.Core.Enums.OS OperatingSystem) { }
652+
public override System.Threading.Tasks.Task<bool> ShouldSkip(TUnit.Core.BeforeTestContext context) { }
653+
}
644654
public class RunOnDiscoveryAttribute : TUnit.Core.TUnitAttribute, TUnit.Core.Interfaces.IEventReceiver, TUnit.Core.Interfaces.ITestDiscoveryEventReceiver
645655
{
646656
public RunOnDiscoveryAttribute() { }
@@ -1022,6 +1032,13 @@ namespace TUnit.Core.Enums
10221032
Error = 4,
10231033
Critical = 5,
10241034
}
1035+
[System.Flags]
1036+
public enum OS
1037+
{
1038+
Linux = 1,
1039+
Windows = 2,
1040+
MacOs = 4,
1041+
}
10251042
public enum Status
10261043
{
10271044
None = 0,

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ namespace TUnit.Core
429429
public System.Threading.CancellationToken Token { get; }
430430
public void Dispose() { }
431431
}
432+
public sealed class ExcludeOnAttribute : TUnit.Core.SkipAttribute
433+
{
434+
public ExcludeOnAttribute(TUnit.Core.Enums.OS OperatingSystem) { }
435+
public override System.Threading.Tasks.Task<bool> ShouldSkip(TUnit.Core.BeforeTestContext context) { }
436+
}
432437
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)]
433438
public sealed class ExplicitAttribute : TUnit.Core.TUnitAttribute
434439
{
@@ -681,6 +686,11 @@ namespace TUnit.Core
681686
public void OnTestDiscovery(TUnit.Core.DiscoveredTestContext discoveredTestContext) { }
682687
public virtual System.Threading.Tasks.Task<bool> ShouldRetry(TUnit.Core.TestContext context, System.Exception exception, int currentRetryCount) { }
683688
}
689+
public sealed class RunOnAttribute : TUnit.Core.SkipAttribute
690+
{
691+
public RunOnAttribute(TUnit.Core.Enums.OS OperatingSystem) { }
692+
public override System.Threading.Tasks.Task<bool> ShouldSkip(TUnit.Core.BeforeTestContext context) { }
693+
}
684694
public class RunOnDiscoveryAttribute : TUnit.Core.TUnitAttribute, TUnit.Core.Interfaces.IEventReceiver, TUnit.Core.Interfaces.ITestDiscoveryEventReceiver
685695
{
686696
public RunOnDiscoveryAttribute() { }
@@ -1080,6 +1090,13 @@ namespace TUnit.Core.Enums
10801090
Error = 4,
10811091
Critical = 5,
10821092
}
1093+
[System.Flags]
1094+
public enum OS
1095+
{
1096+
Linux = 1,
1097+
Windows = 2,
1098+
MacOs = 4,
1099+
}
10831100
public enum Status
10841101
{
10851102
None = 0,

TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ namespace TUnit.Core
429429
public System.Threading.CancellationToken Token { get; }
430430
public void Dispose() { }
431431
}
432+
public sealed class ExcludeOnAttribute : TUnit.Core.SkipAttribute
433+
{
434+
public ExcludeOnAttribute(TUnit.Core.Enums.OS OperatingSystem) { }
435+
public override System.Threading.Tasks.Task<bool> ShouldSkip(TUnit.Core.BeforeTestContext context) { }
436+
}
432437
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method)]
433438
public sealed class ExplicitAttribute : TUnit.Core.TUnitAttribute
434439
{
@@ -681,6 +686,11 @@ namespace TUnit.Core
681686
public void OnTestDiscovery(TUnit.Core.DiscoveredTestContext discoveredTestContext) { }
682687
public virtual System.Threading.Tasks.Task<bool> ShouldRetry(TUnit.Core.TestContext context, System.Exception exception, int currentRetryCount) { }
683688
}
689+
public sealed class RunOnAttribute : TUnit.Core.SkipAttribute
690+
{
691+
public RunOnAttribute(TUnit.Core.Enums.OS OperatingSystem) { }
692+
public override System.Threading.Tasks.Task<bool> ShouldSkip(TUnit.Core.BeforeTestContext context) { }
693+
}
684694
public class RunOnDiscoveryAttribute : TUnit.Core.TUnitAttribute, TUnit.Core.Interfaces.IEventReceiver, TUnit.Core.Interfaces.ITestDiscoveryEventReceiver
685695
{
686696
public RunOnDiscoveryAttribute() { }
@@ -1080,6 +1090,13 @@ namespace TUnit.Core.Enums
10801090
Error = 4,
10811091
Critical = 5,
10821092
}
1093+
[System.Flags]
1094+
public enum OS
1095+
{
1096+
Linux = 1,
1097+
Windows = 2,
1098+
MacOs = 4,
1099+
}
10831100
public enum Status
10841101
{
10851102
None = 0,

TUnit.TestProject/Attributes/SkipMacOSAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace TUnit.TestProject.Attributes;
44

5+
[Obsolete("Use `[ExcludeOnAttribute(OS.MacOS)]` instead.")]
56
public class SkipMacOSAttribute(string reason) : SkipAttribute(reason)
67
{
78
public override Task<bool> ShouldSkip(BeforeTestContext context)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace TUnit.TestProject;
2+
3+
using System.Runtime.InteropServices;
4+
using TUnit.Core.Enums;
5+
6+
public class ExcludeOnTests
7+
{
8+
[Test]
9+
[ExcludeOn(OS.Windows)]
10+
public async Task ExcludeOnWindowsOnlyTest()
11+
{
12+
// This test will not run on Windows
13+
var isSupportedPlatform = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ||
14+
RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
15+
16+
await Assert.That(isSupportedPlatform).IsTrue();
17+
}
18+
19+
[Test]
20+
[ExcludeOn(OS.Windows | OS.Linux | OS.MacOs)]
21+
public void ExcludeOnAllPlatformsTest()
22+
{
23+
Assert.Fail("This message should never be seen.");
24+
}
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace TUnit.TestProject;
2+
3+
using System.Runtime.InteropServices;
4+
using TUnit.Core.Enums;
5+
6+
public class RunOnSkipTests
7+
{
8+
[Test]
9+
[RunOn(OS.Windows)]
10+
public async Task RunOnWindowsOnlyTest()
11+
{
12+
// This test will only run on Windows
13+
var isSupportedPlatform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
14+
await Assert.That(isSupportedPlatform).IsTrue();
15+
}
16+
17+
[Test]
18+
[RunOn(OS.Windows | OS.Linux | OS.MacOs)]
19+
public async Task RunOnAllPlatformsTest()
20+
{
21+
// This test will run on all supported platforms
22+
var isSupportedPlatform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
23+
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ||
24+
RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
25+
await Assert.That(isSupportedPlatform).IsTrue();
26+
}
27+
}

0 commit comments

Comments
 (0)