diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs
index 4a470817ee..7e81d243aa 100644
--- a/TUnit.Engine/Building/TestBuilder.cs
+++ b/TUnit.Engine/Building/TestBuilder.cs
@@ -1109,6 +1109,7 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
var testDetails = CreateFailedTestDetails(metadata, testId);
var context = CreateFailedTestContext(metadata, testDetails);
+ var now = DateTimeOffset.UtcNow;
return new FailedExecutableTest(exception)
{
@@ -1121,8 +1122,8 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
Result = new TestResult
{
State = TestState.Failed,
- Start = DateTimeOffset.UtcNow,
- End = DateTimeOffset.UtcNow,
+ Start = now,
+ End = now,
Duration = TimeSpan.Zero,
Exception = exception,
ComputerName = EnvironmentHelper.MachineName,
diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs
index 9cd01fd205..14a2c6dcb2 100644
--- a/TUnit.Engine/Building/TestBuilderPipeline.cs
+++ b/TUnit.Engine/Building/TestBuilderPipeline.cs
@@ -473,6 +473,7 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
context.Metadata.TestDetails = testDetails;
+ var now = DateTimeOffset.UtcNow;
return new FailedExecutableTest(exception)
{
@@ -485,8 +486,8 @@ private AbstractExecutableTest CreateFailedTestForDataGenerationError(TestMetada
Result = new TestResult
{
State = TestState.Failed,
- Start = DateTimeOffset.UtcNow,
- End = DateTimeOffset.UtcNow,
+ Start = now,
+ End = now,
Duration = TimeSpan.Zero,
Exception = exception,
ComputerName = EnvironmentHelper.MachineName,
@@ -525,6 +526,8 @@ private AbstractExecutableTest CreateFailedTestForGenericResolutionError(TestMet
context.Metadata.TestDetails = testDetails;
+ var now = DateTimeOffset.UtcNow;
+
return new FailedExecutableTest(exception)
{
TestId = testId,
@@ -536,8 +539,8 @@ private AbstractExecutableTest CreateFailedTestForGenericResolutionError(TestMet
Result = new TestResult
{
State = TestState.Failed,
- Start = DateTimeOffset.UtcNow,
- End = DateTimeOffset.UtcNow,
+ Start = now,
+ End = now,
Duration = TimeSpan.Zero,
Exception = exception,
ComputerName = EnvironmentHelper.MachineName,
diff --git a/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs b/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs
index 133e9c4a84..d7976fff03 100644
--- a/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs
+++ b/TUnit.Engine/Services/DiscoveryCircuitBreaker.cs
@@ -11,7 +11,11 @@ public sealed class DiscoveryCircuitBreaker
{
private readonly long _maxMemoryBytes;
private readonly TimeSpan _maxGenerationTime;
+#if NET
+ private readonly long _startTimestamp;
+#else
private readonly Stopwatch _stopwatch;
+#endif
private readonly long _initialMemoryUsage;
///
@@ -23,8 +27,12 @@ public DiscoveryCircuitBreaker(
{
_maxMemoryBytes = (long)(GetAvailableMemoryBytes() * maxMemoryPercentage);
_maxGenerationTime = maxGenerationTime ?? EngineDefaults.MaxGenerationTime;
+#if NET
+ _startTimestamp = Stopwatch.GetTimestamp();
+#else
_stopwatch = Stopwatch.StartNew();
-
+#endif
+
// Track initial memory to calculate growth
GC.Collect();
GC.WaitForPendingFinalizers();
@@ -32,6 +40,15 @@ public DiscoveryCircuitBreaker(
_initialMemoryUsage = GC.GetTotalMemory(false);
}
+ private TimeSpan GetElapsed()
+ {
+#if NET
+ return Stopwatch.GetElapsedTime(_startTimestamp);
+#else
+ return _stopwatch.Elapsed;
+#endif
+ }
+
///
/// Checks if the circuit breaker should trip based on current resource usage
///
@@ -39,14 +56,14 @@ public DiscoveryCircuitBreaker(
/// True if generation should continue, false if circuit breaker trips
public bool ShouldContinue(int currentTestCount = 0)
{
- if (_stopwatch.Elapsed > _maxGenerationTime)
+ if (GetElapsed() > _maxGenerationTime)
{
return false;
}
var currentMemoryUsage = GC.GetTotalMemory(false);
var memoryGrowth = currentMemoryUsage - _initialMemoryUsage;
-
+
if (memoryGrowth > _maxMemoryBytes)
{
return false;
@@ -62,14 +79,15 @@ internal DiscoveryResourceUsage GetResourceUsage()
{
var currentMemoryUsage = GC.GetTotalMemory(false);
var memoryGrowth = currentMemoryUsage - _initialMemoryUsage;
-
+ var elapsed = GetElapsed();
+
return new DiscoveryResourceUsage
{
- ElapsedTime = _stopwatch.Elapsed,
+ ElapsedTime = elapsed,
MaxTime = _maxGenerationTime,
MemoryGrowthBytes = memoryGrowth,
MaxMemoryBytes = _maxMemoryBytes,
- TimeUsagePercentage = _stopwatch.Elapsed.TotalMilliseconds / _maxGenerationTime.TotalMilliseconds,
+ TimeUsagePercentage = elapsed.TotalMilliseconds / _maxGenerationTime.TotalMilliseconds,
MemoryUsagePercentage = (double)memoryGrowth / _maxMemoryBytes
};
}
@@ -110,7 +128,9 @@ private static long GetAvailableMemoryBytes()
public void Dispose()
{
- _stopwatch?.Stop();
+#if !NET
+ _stopwatch.Stop();
+#endif
}
}
@@ -125,4 +145,4 @@ internal record DiscoveryResourceUsage
public long MaxMemoryBytes { get; init; }
public double TimeUsagePercentage { get; init; }
public double MemoryUsagePercentage { get; init; }
-}
\ No newline at end of file
+}
diff --git a/TUnit.Engine/Services/TestExecution/RetryHelper.cs b/TUnit.Engine/Services/TestExecution/RetryHelper.cs
index 7db0a93d73..7352cba6cb 100644
--- a/TUnit.Engine/Services/TestExecution/RetryHelper.cs
+++ b/TUnit.Engine/Services/TestExecution/RetryHelper.cs
@@ -4,7 +4,10 @@ namespace TUnit.Engine.Services.TestExecution;
internal static class RetryHelper
{
- public static async Task ExecuteWithRetry(TestContext testContext, Func action)
+ private static readonly Task s_shouldRetryTrue = Task.FromResult(true);
+ private static readonly Task s_shouldRetryFalse = Task.FromResult(false);
+
+ public static async Task ExecuteWithRetry(TestContext testContext, Func action)
{
var maxRetries = testContext.Metadata.TestDetails.RetryLimit;
@@ -57,29 +60,29 @@ public static async Task ExecuteWithRetry(TestContext testContext, Func ac
}
}
- private static async Task ShouldRetry(TestContext testContext, Exception ex, int attempt)
+ private static Task ShouldRetry(TestContext testContext, Exception ex, int attempt)
{
if (attempt >= testContext.Metadata.TestDetails.RetryLimit)
{
- return false;
+ return s_shouldRetryFalse;
}
if (testContext.RetryFunc == null)
{
// Default behavior: retry on any exception if within retry limit
- return true;
+ return s_shouldRetryTrue;
}
- return await testContext.RetryFunc(testContext, ex, attempt + 1).ConfigureAwait(false);
+ return testContext.RetryFunc(testContext, ex, attempt + 1);
}
- private static async Task ApplyBackoffDelay(TestContext testContext, int attempt)
+ private static Task ApplyBackoffDelay(TestContext testContext, int attempt)
{
var backoffMs = testContext.Metadata.TestDetails.RetryBackoffMs;
if (backoffMs <= 0)
{
- return;
+ return Task.CompletedTask;
}
var multiplier = testContext.Metadata.TestDetails.RetryBackoffMultiplier;
@@ -87,7 +90,9 @@ private static async Task ApplyBackoffDelay(TestContext testContext, int attempt
if (delayMs > 0)
{
- await Task.Delay(delayMs, testContext.CancellationToken).ConfigureAwait(false);
+ return Task.Delay(delayMs, testContext.CancellationToken);
}
+
+ return Task.CompletedTask;
}
}
diff --git a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs
index 70d0f0a553..c7ea325f1b 100644
--- a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs
+++ b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs
@@ -117,10 +117,8 @@ private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, Ca
// Slow path: use retry wrapper
// Timeout is handled inside TestExecutor.ExecuteAsync, wrapping only the test body
// (not hooks or data source initialization) — fixes #4772
- await RetryHelper.ExecuteWithRetry(test.Context, async () =>
- {
- await ExecuteTestLifecycleAsync(test, cancellationToken).ConfigureAwait(false);
- }).ConfigureAwait(false);
+ await RetryHelper.ExecuteWithRetry(test.Context,
+ () => ExecuteTestLifecycleAsync(test, cancellationToken)).ConfigureAwait(false);
}
_stateManager.MarkCompleted(test);
diff --git a/TUnit.Engine/Services/TestExecution/TestMethodInvoker.cs b/TUnit.Engine/Services/TestExecution/TestMethodInvoker.cs
index 451dddb20b..9e7a77e197 100644
--- a/TUnit.Engine/Services/TestExecution/TestMethodInvoker.cs
+++ b/TUnit.Engine/Services/TestExecution/TestMethodInvoker.cs
@@ -8,18 +8,14 @@ namespace TUnit.Engine.Services.TestExecution;
///
internal sealed class TestMethodInvoker
{
- public async Task InvokeTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
+ public ValueTask InvokeTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
{
if (test.Context.InternalDiscoveredTest?.TestExecutor is { } testExecutor)
{
- await testExecutor.ExecuteTest(test.Context,
- async () => await test.InvokeTestAsync(test.Context.Metadata.TestDetails.ClassInstance, cancellationToken))
- .ConfigureAwait(false);
- }
- else
- {
- await test.InvokeTestAsync(test.Context.Metadata.TestDetails.ClassInstance, cancellationToken)
- .ConfigureAwait(false);
+ return testExecutor.ExecuteTest(test.Context,
+ () => new ValueTask(test.InvokeTestAsync(test.Context.Metadata.TestDetails.ClassInstance, cancellationToken)));
}
+
+ return new ValueTask(test.InvokeTestAsync(test.Context.Metadata.TestDetails.ClassInstance, cancellationToken));
}
}
\ No newline at end of file
diff --git a/TUnit.Engine/TestExecutor.cs b/TUnit.Engine/TestExecutor.cs
index 125ad8e676..4f513d36aa 100644
--- a/TUnit.Engine/TestExecutor.cs
+++ b/TUnit.Engine/TestExecutor.cs
@@ -59,7 +59,7 @@ await _beforeHookTaskCache.GetOrCreateBeforeTestSessionTask(
// Register After Session hook to run on cancellation (guarantees cleanup)
_afterHookPairTracker.RegisterAfterTestSessionHook(
cancellationToken,
- () => new ValueTask>(_hookExecutor.ExecuteAfterTestSessionHooksAsync(CancellationToken.None).AsTask()));
+ () => _hookExecutor.ExecuteAfterTestSessionHooksAsync(CancellationToken.None));
}
///
@@ -95,7 +95,7 @@ await _beforeHookTaskCache.GetOrCreateBeforeAssemblyTask(
_afterHookPairTracker.RegisterAfterAssemblyHook(
testAssembly,
cancellationToken,
- (assembly) => new ValueTask>(_hookExecutor.ExecuteAfterAssemblyHooksAsync(assembly, CancellationToken.None).AsTask()));
+ (assembly) => _hookExecutor.ExecuteAfterAssemblyHooksAsync(assembly, CancellationToken.None));
await _eventReceiverOrchestrator.InvokeFirstTestInAssemblyEventReceiversAsync(
executableTest.Context,
@@ -323,7 +323,7 @@ private static async ValueTask ExecuteTestAsync(AbstractExecutableTest executabl
if (executableTest.Context.InternalDiscoveredTest?.TestExecutor is { } testExecutor)
{
await testExecutor.ExecuteTest(executableTest.Context,
- async () => await executableTest.InvokeTestAsync(executableTest.Context.Metadata.TestDetails.ClassInstance, cancellationToken)).ConfigureAwait(false);
+ () => new ValueTask(executableTest.InvokeTestAsync(executableTest.Context.Metadata.TestDetails.ClassInstance, cancellationToken))).ConfigureAwait(false);
}
else
{
@@ -351,7 +351,7 @@ internal async Task> ExecuteAfterClassAssemblyHooks(AbstractExec
// Use AfterHookPairTracker to prevent double execution if already triggered by cancellation
var assemblyExceptions = await _afterHookPairTracker.GetOrCreateAfterAssemblyTask(
testAssembly,
- (assembly) => new ValueTask>(_hookExecutor.ExecuteAfterAssemblyHooksAsync(assembly, cancellationToken).AsTask())).ConfigureAwait(false);
+ (assembly) => _hookExecutor.ExecuteAfterAssemblyHooksAsync(assembly, cancellationToken)).ConfigureAwait(false);
exceptions.AddRange(assemblyExceptions);
}
@@ -367,7 +367,7 @@ public async Task> ExecuteAfterTestSessionHooksAsync(Cancellatio
{
// Use AfterHookPairTracker to prevent double execution if already triggered by cancellation
var exceptions = await _afterHookPairTracker.GetOrCreateAfterTestSessionTask(
- () => new ValueTask>(_hookExecutor.ExecuteAfterTestSessionHooksAsync(cancellationToken).AsTask())).ConfigureAwait(false);
+ () => _hookExecutor.ExecuteAfterTestSessionHooksAsync(cancellationToken)).ConfigureAwait(false);
return exceptions;
}
@@ -375,17 +375,17 @@ public async Task> ExecuteAfterTestSessionHooksAsync(Cancellatio
///
/// Execute discovery-level before hooks.
///
- public async Task ExecuteBeforeTestDiscoveryHooksAsync(CancellationToken cancellationToken)
+ public ValueTask ExecuteBeforeTestDiscoveryHooksAsync(CancellationToken cancellationToken)
{
- await _hookExecutor.ExecuteBeforeTestDiscoveryHooksAsync(cancellationToken).ConfigureAwait(false);
+ return _hookExecutor.ExecuteBeforeTestDiscoveryHooksAsync(cancellationToken);
}
///
/// Execute discovery-level after hooks.
///
- public async Task ExecuteAfterTestDiscoveryHooksAsync(CancellationToken cancellationToken)
+ public ValueTask ExecuteAfterTestDiscoveryHooksAsync(CancellationToken cancellationToken)
{
- await _hookExecutor.ExecuteAfterTestDiscoveryHooksAsync(cancellationToken).ConfigureAwait(false);
+ return _hookExecutor.ExecuteAfterTestDiscoveryHooksAsync(cancellationToken);
}
///