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
8 changes: 7 additions & 1 deletion TUnit.Core/Hooks/InstanceHookMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ public record InstanceHookMethod : IExecutableHook<TestContext>

public ValueTask ExecuteAsync(TestContext context, CancellationToken cancellationToken)
{
// Skip instance hooks if this is a pre-skipped test
if (context.TestDetails.ClassInstance is SkippedTestInstance)
{
return new ValueTask();
}

return HookExecutor.ExecuteBeforeTestHook(MethodInfo, context,
() => Body!.Invoke(context.TestDetails.ClassInstance ?? throw new InvalidOperationException("ClassInstance is null"), context, cancellationToken)
() => Body!.Invoke(context.TestDetails.ClassInstance, context, cancellationToken)
);
}
}
14 changes: 14 additions & 0 deletions TUnit.Core/SkippedTestInstance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace TUnit.Core;

/// <summary>
/// A placeholder instance used for tests that are skipped at discovery time to avoid calling constructors.
/// </summary>
internal sealed class SkippedTestInstance
{
public static readonly SkippedTestInstance Instance = new();

private SkippedTestInstance()
{
// Private constructor to ensure singleton pattern
}
}
53 changes: 52 additions & 1 deletion TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,21 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
{
throw new InvalidOperationException($"Cannot create instance of generic type {metadata.TestClassType.Name} with empty type arguments");
}
var instance = await CreateInstance(metadata, resolvedClassGenericArgs, classData, contextAccessor.Current);

// Check for basic skip attributes that can be evaluated at discovery time
var basicSkipReason = GetBasicSkipReason(metadata);
object instance;

if (!string.IsNullOrEmpty(basicSkipReason))
{
// Use placeholder instance for basic skip attributes to avoid calling constructor
instance = SkippedTestInstance.Instance;
}
else
{
// No skip attributes or custom skip attributes - create instance normally
instance = await CreateInstance(metadata, resolvedClassGenericArgs, classData, contextAccessor.Current);
}

var testData = new TestData
{
Expand All @@ -279,6 +293,12 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
};

var test = await BuildTestAsync(metadata, testData, contextAccessor.Current);

// If we have a basic skip reason, set it immediately
if (!string.IsNullOrEmpty(basicSkipReason))
{
test.Context.SkipReason = basicSkipReason;
}
tests.Add(test);

contextAccessor.Current = new TestBuilderContext
Expand Down Expand Up @@ -545,6 +565,37 @@ public async Task<AbstractExecutableTest> BuildTestAsync(TestMetadata metadata,
return metadata.CreateExecutableTestFactory(creationContext, metadata);
}

/// <summary>
/// Checks if a test has basic SkipAttribute instances that can be evaluated at discovery time.
/// Returns null if no skip attributes, a skip reason if basic skip attributes are found,
/// or empty string if custom skip attributes requiring runtime evaluation are found.
/// </summary>
private static string? GetBasicSkipReason(TestMetadata metadata)
{
var attributes = metadata.AttributeFactory();
var skipAttributes = attributes.OfType<SkipAttribute>().ToList();

if (skipAttributes.Count == 0)
{
return null; // No skip attributes
}

// Check if all skip attributes are basic (non-derived) SkipAttribute instances
foreach (var skipAttribute in skipAttributes)
{
var attributeType = skipAttribute.GetType();
if (attributeType != typeof(SkipAttribute))
{
// This is a derived skip attribute that might have custom ShouldSkip logic
return string.Empty; // Indicates custom skip attributes that need runtime evaluation
}
}

// All skip attributes are basic SkipAttribute instances
// Return the first reason (they all should skip)
return skipAttributes[0].Reason;
}


private ValueTask<TestContext> CreateTestContextAsync(string testId, TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext)
{
Expand Down
8 changes: 7 additions & 1 deletion TUnit.Engine/Services/HookCollectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,16 @@ private static Func<TestContext, CancellationToken, Task> CreateInstanceHookDele
{
return async (context, cancellationToken) =>
{
// Skip instance hooks if this is a pre-skipped test
if (context.TestDetails.ClassInstance is SkippedTestInstance)
{
return;
}

if (hook.Body != null)
{
await hook.Body(
context.TestDetails.ClassInstance ?? throw new InvalidOperationException("ClassInstance is null"),
context.TestDetails.ClassInstance,
context,
cancellationToken);
}
Expand Down
12 changes: 12 additions & 0 deletions TUnit.Engine/Services/SingleTestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public async Task<TestNodeUpdateMessage> ExecuteTestAsync(
test.StartTime = DateTimeOffset.Now;
test.State = TestState.Running;

// Check if test is already marked as skipped (from basic SkipAttribute during discovery)
if (!string.IsNullOrEmpty(test.Context.SkipReason))
{
return await HandleSkippedTestAsync(test, cancellationToken);
}

// Check if we already have a skipped test instance from discovery
if (test.Context.TestDetails.ClassInstance is SkippedTestInstance)
{
return await HandleSkippedTestAsync(test, cancellationToken);
}

var instance = await test.CreateInstanceAsync();
test.Context.TestDetails.ClassInstance = instance;

Expand Down
23 changes: 23 additions & 0 deletions TUnit.TestProject/SimpleSkipTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using TUnit.Core;

namespace TUnit.TestProject;

public class SimpleSkipTest
{
static SimpleSkipTest()
{
Console.WriteLine("Static constructor called for SimpleSkipTest");
}

public SimpleSkipTest()
{
Console.WriteLine("CONSTRUCTOR CALLED FOR SKIPPED TEST - This should NOT appear!");
}

[Test]
[Skip("This test should be skipped")]
public void BasicSkippedTest()
{
Console.WriteLine("This test method should not run");
}
}
29 changes: 29 additions & 0 deletions TUnit.TestProject/SkipConstructorTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using TUnit.Core;

namespace TUnit.TestProject;

public class SkipConstructorTest : IAsyncDisposable
{
public static bool ConstructorCalled { get; set; }
public static bool DisposeCalled { get; set; }

public SkipConstructorTest()
{
ConstructorCalled = true;
Console.WriteLine("SkipConstructorTest constructor called");
}

[Test]
[Skip("Test should be skipped")]
public void SkippedTestShouldNotCallConstructor()
{
Console.WriteLine("This test method should not run");
}

public ValueTask DisposeAsync()
{
DisposeCalled = true;
Console.WriteLine("SkipConstructorTest dispose called");
return ValueTask.CompletedTask;
}
}