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
41 changes: 35 additions & 6 deletions src/TUnit.Engine/Services/TestFilterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,41 @@ private static TestRegisteredContext CreateTestRegisteredContext(TestContext tes
}

private async Task RegisterTest(AbstractExecutableTest test, bool isForExecution)
{
try
{
await RegisterTestCore(test, isForExecution).ConfigureAwait(false);
}
catch (Exception ex)
{
// Registration callbacks are user code. Preserve the failure on this test so
// neither sequential nor parallel registration aborts the rest of the request.
if (isForExecution)
{
try
{
// Track only objects that already exist. Tracking is idempotent if
// argument registration completed before a later callback failed.
testArgumentRegistrationService.TrackSkippedTestArguments(test.Context);
}
catch (Exception trackingException)
{
logger.LogDebug($"Could not track the objects of registration-failed test '{test.TestId}' for disposal: {trackingException.Message}");
}
}

// A callback failure takes precedence over an earlier request to skip.
test.Context.SkipReason = null;
test.SetResult(TestState.Failed, ex);
Comment thread
thomhurst marked this conversation as resolved.
Resolved
Comment thread
thomhurst marked this conversation as resolved.
Resolved
Comment thread
thomhurst marked this conversation as resolved.
Resolved
}
finally
{
// Callbacks may have changed display formatting before failing.
test.Context.InvalidateDisplayNameCache();
}
}

private async Task RegisterTestCore(AbstractExecutableTest test, bool isForExecution)
{
var registeredReceivers = test.Context.GetTestRegisteredReceivers();
TestRegisteredContext? registeredContext = null;
Expand Down Expand Up @@ -178,7 +213,6 @@ async Task InvokeRegistrationDelegatesAsync()
}
}

test.Context.InvalidateDisplayNameCache();
return;
}

Expand Down Expand Up @@ -219,11 +253,6 @@ async Task InvokeRegistrationDelegatesAsync()
await InvokeRegistrationDelegatesAsync().ConfigureAwait(false);
}
}

// Clear the cached display name after registration events
// This ensures that ArgumentDisplayFormatterAttribute and similar attributes
// have a chance to register their formatters before the display name is finalized
test.Context.InvalidateDisplayNameCache();
}

/// <summary>
Expand Down
35 changes: 35 additions & 0 deletions tests/TUnit.Engine.Tests/RegistrationFailure7525Tests.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 RegistrationFailure7525Tests(TestMode testMode) : InvokableTestBase(testMode)
{
[Test]
[Arguments("RegistrationFailureTests", 3, 1)]
[Arguments("ParallelRegistrationFailureTests", 8, 6)]
public async Task RegistrationFailureDoesNotAbortRequest(string className, int total, int passed)
{
await RunTestsWithFilter($"/*/TUnit.TestProject.Bugs._7525/{className}/*",
[
result => result.ResultSummary.Counters.Total.ShouldBe(total),
result => result.ResultSummary.Counters.Failed.ShouldBe(2),
result => result.ResultSummary.Counters.Passed.ShouldBe(passed),
result => result.Results.Where(x => x.Outcome == "Failed")
.ShouldAllBe(x => x.Output!.ErrorInfo!.Message!.Contains("Registration ")
&& x.Output.ErrorInfo.Message.Contains("failed (#7525)"))
]);
}
[Test]
public async Task RegistrationFailureDisposesExistingArguments()
{
await RunTestsWithFilter("/*/TUnit.TestProject.Bugs._7525/RegistrationDisposalTests/*",
[
result => result.ResultSummary.Counters.Total.ShouldBe(2),
result => result.ResultSummary.Counters.Failed.ShouldBe(1),
result => result.ResultSummary.Counters.Passed.ShouldBe(1),
result => result.Results.Single(x => x.Outcome == "Failed").Output!.ErrorInfo!.Message!
.ShouldContain("Registration receiver failed (#7525)")
]);
}
}
79 changes: 79 additions & 0 deletions tests/TUnit.TestProject/Bugs/7525/Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using TUnit.Core.Interfaces;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._7525;

public class RegistrationFailureTests
{
public sealed class ThrowingReceiverAttribute : Attribute, ITestRegisteredEventReceiver
{
public int Order => 0;

public ValueTask OnTestRegistered(TestRegisteredContext context)
{
context.SetSkipped("skip before registration failure");
throw new InvalidOperationException("Registration receiver failed (#7525)");
}
}

public sealed class ThrowingDelegateAttribute : DataSourceGeneratorAttribute<int>
{
protected override IEnumerable<Func<int>> GenerateDataSources(DataGeneratorMetadata metadata)
{
yield return () =>
{
metadata.TestBuilderContext.Current.Events.OnTestRegistered += async (_, _) =>
{
await Task.Yield();
throw new InvalidOperationException("Registration delegate failed (#7525)");
};
return 1;
};
}
}

[Test, ThrowingReceiver, EngineTest(ExpectedResult.Failure)]
public void ReceiverFailure() => throw new Exception("Test body must not execute");

[Test, ThrowingDelegate, EngineTest(ExpectedResult.Failure)]
public void DelegateFailure(int value) => throw new Exception("Test body must not execute");

[Test, EngineTest(ExpectedResult.Pass)]
public void UnrelatedTest() { }
}

// Eight or more tests take the parallel registration path.
[InheritsTests]
public class ParallelRegistrationFailureTests : RegistrationFailureTests
{
[Test, Arguments(1), Arguments(2), Arguments(3), Arguments(4), Arguments(5), EngineTest(ExpectedResult.Pass)]
public void OtherTests(int value) { }
}

public sealed class RegistrationDisposable : IDisposable
{
public static List<RegistrationDisposable> Created { get; } = [];
public bool IsDisposed { get; private set; }
public RegistrationDisposable() => Created.Add(this);
public IAsyncInitializer Child => throw new InvalidOperationException("tracking getter failed");
public void Dispose() => IsDisposed = true;
}

[EngineTest(ExpectedResult.Failure)]
public class RegistrationDisposalTests
{
public static IEnumerable<RegistrationDisposable> Values()
{
yield return new RegistrationDisposable();
}

[Test, RegistrationFailureTests.ThrowingReceiver, MethodDataSource(nameof(Values))]
public void Fails(RegistrationDisposable value) => throw new Exception("Test body must not execute");

[Test, DependsOn(nameof(Fails), ProceedOnFailure = true)]
public async Task ArgumentWasDisposed()
{
await Assert.That(RegistrationDisposable.Created.Count).IsEqualTo(1);
await Assert.That(RegistrationDisposable.Created[0].IsDisposed).IsTrue();
}
}
Loading