Skip to content
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
11 changes: 11 additions & 0 deletions src/TUnit.Core/Attributes/TestMetadata/SkipAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ namespace TUnit.Core;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
public class SkipAttribute : Attribute, ITestRegisteredEventReceiver
{
/// <summary>
/// The skip reason reported when a skip is requested with a null or empty reason.
/// The engine treats an empty skip reason as "not skipped", so it must never be stored as-is.
/// </summary>
internal const string DefaultReason = "No reason provided";

/// <summary>
/// Returns <paramref name="reason"/>, or <see cref="DefaultReason"/> when it is null or empty.
/// </summary>
internal static string NormalizeReason(string? reason) => string.IsNullOrEmpty(reason) ? DefaultReason : reason!;

public string Reason { get; }

public SkipAttribute(string reason)
Expand Down
3 changes: 2 additions & 1 deletion src/TUnit.Core/Contexts/TestRegisteredContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ internal void SetExplicitParallelLimiter(IParallelLimit parallelLimit)
/// <param name="reason">The reason why the test is being skipped</param>
public void SetSkipped(string reason)
{
TestContext.SkipReason = reason;
// An empty reason would otherwise read as "not skipped" (#7077).
TestContext.SkipReason = SkipAttribute.NormalizeReason(reason);
TestContext.Metadata.TestDetails.ClassInstance = SkippedTestInstance.Instance;
}

Expand Down
5 changes: 3 additions & 2 deletions src/TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1206,8 +1206,9 @@ private static void CollectAllDependencies(AbstractExecutableTest test, HashSet<
}

// All skip attributes are basic SkipAttribute instances
// Return the first reason (they all should skip), or null if no skip attributes
return firstSkipAttribute?.Reason;
// Return the first reason (they all should skip), or null if no skip attributes.
// An empty reason is normalized, because empty means "custom skip attribute" here (#7077).
return firstSkipAttribute is null ? null : SkipAttribute.NormalizeReason(firstSkipAttribute.Reason);
}

private async ValueTask<TestContext> CreateTestContextAsync(string testId, TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext, CancellationToken cancellationToken = default)
Expand Down
35 changes: 35 additions & 0 deletions tests/TUnit.Engine.Tests/Issue7077Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Shouldly;
using TUnit.Engine.Tests.Enums;

namespace TUnit.Engine.Tests;

public class Issue7077Tests(TestMode testMode) : InvokableTestBase(testMode)
{
[Test]
public async Task Skip_With_Empty_Or_Null_Reason_Skips_The_Test()
{
await RunTestsWithFilter(
"/*/TUnit.TestProject.Bugs._7077/EmptySkipReasonTests/*",
[
result => result.ResultSummary.Outcome.ShouldBe("Completed"),
result => result.ResultSummary.Counters.Total.ShouldBe(4),
result => result.ResultSummary.Counters.Passed.ShouldBe(0),
result => result.ResultSummary.Counters.Failed.ShouldBe(0),
result => result.ResultSummary.Counters.NotExecuted.ShouldBe(4)
]);
}

[Test]
public async Task Class_Level_Skip_With_Empty_Reason_Skips_Without_Constructing()
{
await RunTestsWithFilter(
"/*/TUnit.TestProject.Bugs._7077/EmptySkipReasonClassTests/*",
[
result => result.ResultSummary.Outcome.ShouldBe("Completed"),
result => result.ResultSummary.Counters.Total.ShouldBe(1),
result => result.ResultSummary.Counters.Passed.ShouldBe(0),
result => result.ResultSummary.Counters.Failed.ShouldBe(0),
result => result.ResultSummary.Counters.NotExecuted.ShouldBe(1)
]);
}
}
38 changes: 38 additions & 0 deletions tests/TUnit.TestProject/Bugs/_7077/EmptySkipReasonTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._7077;

[EngineTest(ExpectedResult.Pass)]
public class EmptySkipReasonTests
{
[Test, Skip("")]
public void EmptyReason_IsSkipped() => throw new Exception("test ran");

[Test, Skip(null!)]
public void NullReason_IsSkipped() => throw new Exception("test ran");

[Test, Skip("reason")]
public void NonEmptyReason_IsSkipped() => throw new Exception("control ran");

[Test, EmptyReasonSkip]
public void DerivedAttributeWithEmptyReason_IsSkipped() => throw new Exception("test ran");
}

[EngineTest(ExpectedResult.Pass)]
[Skip("")]
public class EmptySkipReasonClassTests
{
public EmptySkipReasonClassTests() => throw new Exception("constructor ran");

[Test]
public void ClassLevelEmptyReason_IsSkipped() => throw new Exception("test ran");
}

/// <summary>
/// A derived skip attribute is evaluated at registration time rather than discovery time,
/// so it exercises the runtime skip path with an empty reason.
/// </summary>
public class EmptyReasonSkipAttribute() : SkipAttribute("")
{
public override Task<bool> ShouldSkip(TestRegisteredContext context) => Task.FromResult(true);
}
Loading