Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Changes before error encountered
Co-authored-by: thomhurst <[email protected]>
  • Loading branch information
Copilot and thomhurst committed Aug 3, 2025
commit 6d7721bd2f8efab53e9c655948e81b3d7e5cfe2c
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 ClassInstance is null (e.g., for pre-skipped tests)
if (context.TestDetails.ClassInstance == null)
{
return ValueTask.CompletedTask;
}

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)
);
}
}
2 changes: 1 addition & 1 deletion TUnit.Core/TestDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class TestDetails
public required string TestName { get; init; }
public required Type ClassType { get; init; }
public required string MethodName { get; init; }
public required object ClassInstance { get; set; }
public required object? ClassInstance { get; set; }
public required object?[] TestMethodArguments { get; set; }
public required object?[] TestClassArguments { get; set; }
public TimeSpan? Timeout { get; set; }
Expand Down
51 changes: 49 additions & 2 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,17 @@ 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 = null;

if (basicSkipReason == null || basicSkipReason == string.Empty)
{
// No skip attributes or custom skip attributes - create instance normally
instance = await CreateInstance(metadata, resolvedClassGenericArgs, classData, contextAccessor.Current);
}
// If basicSkipReason is not null and not empty, it's a basic skip - don't create instance

var testData = new TestData
{
Expand All @@ -279,6 +289,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 +561,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 Expand Up @@ -874,7 +921,7 @@ private static bool IsTypeCompatible(Type actualType, Type expectedType)

internal class TestData
{
public required object TestClassInstance { get; init; }
public required object? TestClassInstance { get; init; }

public required int ClassDataSourceAttributeIndex { get; init; }
public required int ClassDataLoopIndex { get; init; }
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 ClassInstance is null (e.g., for pre-skipped tests)
if (context.TestDetails.ClassInstance == null)
{
return;
}

if (hook.Body != null)
{
await hook.Body(
context.TestDetails.ClassInstance ?? throw new InvalidOperationException("ClassInstance is null"),
context.TestDetails.ClassInstance,
context,
cancellationToken);
}
Expand Down
6 changes: 6 additions & 0 deletions TUnit.Engine/Services/SingleTestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ 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);
}

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");
}
}