From 6f847161d348046840d1ff592133a3a1a6c35f80 Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 17 Nov 2024 17:01:48 +0100 Subject: [PATCH] Merge ExecuteTest back into ExecuteTestInternalAsync (#1237) --- TUnit.Core/RunHelpers.cs | 16 +- .../Framework/TUnitServiceProvider.cs | 3 +- TUnit.Engine/Services/SingleTestExecutor.cs | 256 +++++++----------- 3 files changed, 104 insertions(+), 171 deletions(-) diff --git a/TUnit.Core/RunHelpers.cs b/TUnit.Core/RunHelpers.cs index 667c7e103..b68d83891 100644 --- a/TUnit.Core/RunHelpers.cs +++ b/TUnit.Core/RunHelpers.cs @@ -7,7 +7,7 @@ namespace TUnit.Core; internal static class RunHelpers { - internal static async Task RunWithTimeoutAsync(Func taskDelegate, TimeSpan? timeout, CancellationToken token) + internal static async Task RunWithTimeoutAsync(Func taskDelegate, TimeSpan timeout, CancellationToken token) { using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -23,20 +23,10 @@ internal static async Task RunWithTimeoutAsync(Func tas return; } - if (timeout.HasValue) - { - taskCompletionSource.TrySetException(new TimeoutException(timeout.Value)); - } - else - { - taskCompletionSource.TrySetCanceled(cancellationToken); - } + taskCompletionSource.TrySetException(new TimeoutException(timeout)); }); - if (timeout != null) - { - cancellationTokenSource.CancelAfter(timeout.Value); - } + cancellationTokenSource.CancelAfter(timeout); _ = taskDelegate(cancellationToken).ContinueWith(async t => { diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index 9335c34bc..b4ba848f6 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -95,7 +95,6 @@ public TUnitServiceProvider(IExtension extension, Disposer = Register(new Disposer(Logger)); - var cancellationTokenSource = Register(EngineCancellationToken.CancellationTokenSource); var testInvoker = Register(new TestInvoker(testHookOrchestrator, Disposer)); var explicitFilterService = Register(new ExplicitFilterService()); var parallelLimitProvider = Register(new ParallelLimitLockProvider()); @@ -103,7 +102,7 @@ public TUnitServiceProvider(IExtension extension, // TODO Register(new HookMessagePublisher(extension, messageBus)); - var singleTestExecutor = Register(new SingleTestExecutor(extension, cancellationTokenSource, instanceTracker, testInvoker, + var singleTestExecutor = Register(new SingleTestExecutor(extension, instanceTracker, testInvoker, explicitFilterService, parallelLimitProvider, AssemblyHookOrchestrator, classHookOrchestrator, TestFinder, TUnitMessageBus, Logger, EngineCancellationToken)); TestsExecutor = Register(new TestsExecutor(singleTestExecutor, Logger, CommandLineOptions, EngineCancellationToken)); diff --git a/TUnit.Engine/Services/SingleTestExecutor.cs b/TUnit.Engine/Services/SingleTestExecutor.cs index df00cbd7f..81bcc809a 100644 --- a/TUnit.Engine/Services/SingleTestExecutor.cs +++ b/TUnit.Engine/Services/SingleTestExecutor.cs @@ -18,8 +18,7 @@ namespace TUnit.Engine.Services; internal class SingleTestExecutor( IExtension extension, - CancellationTokenSource cancellationTokenSource, - InstanceTracker instanceTracker, + InstanceTracker instanceTracker, TestInvoker testInvoker, ExplicitFilterService explicitFilterService, ParallelLimitLockProvider parallelLimitLockProvider, @@ -48,17 +47,17 @@ private async Task ExecuteTestInternalAsync(DiscoveredTest test, ITestExecutionF { await semaphore.WaitAsync(); } - + try { var testContext = test.TestContext; var timings = testContext.Timings; - if (cancellationTokenSource.IsCancellationRequested) + if (engineCancellationToken.Token.IsCancellationRequested) { await messageBus.Cancelled(testContext); - - cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + engineCancellationToken.Token.ThrowIfCancellationRequested(); } await messageBus.InProgress(testContext); @@ -70,15 +69,36 @@ private async Task ExecuteTestInternalAsync(DiscoveredTest test, ITestExecutionF try { await WaitForDependsOnTests(test, filter, context); - + start = DateTimeOffset.Now; - - await ExecuteTest(test, testContext, filter, cleanUpExceptions); + + if (!explicitFilterService.CanRun(test.TestDetails, filter)) + { + throw new SkipTestException("Test with ExplicitAttribute was not explicitly run."); + } + + if (testContext.SkipReason != null) + { + throw new SkipTestException(testContext.SkipReason); + } + + if (engineCancellationToken.Token.IsCancellationRequested) + { + throw new SkipTestException("The test session has been cancelled..."); + } + + await ExecuteStaticBeforeHooks(test); + + TestContext.Current = testContext; + + await ExecuteOnTestStartEvents(testContext); + + await ExecuteWithRetries(test, cleanUpExceptions); ExceptionsHelper.ThrowIfAny(cleanUpExceptions); var timingProperty = GetTimingProperty(testContext, start); - + await messageBus.Passed(testContext, start); testContext.Result = new TestResult @@ -95,20 +115,21 @@ private async Task ExecuteTestInternalAsync(DiscoveredTest test, ITestExecutionF } catch (SkipTestException skipTestException) { + var timingProperty = GetTimingProperty(testContext, start); + await logger.LogInformationAsync($"Skipping {testContext.GetClassTypeName()}.{testContext.GetTestDisplayName()}..."); await messageBus.Skipped(testContext, skipTestException.Reason); - var now = DateTimeOffset.Now; - testContext.Result = new TestResult { - Duration = TimeSpan.Zero, - Start = timings.MinBy(x => x.Start)?.Start ?? now, - End = timings.MinBy(x => x.End)?.End ?? timings.MinBy(x => x.Start)?.Start ?? now, + Duration = timingProperty.GlobalTiming.Duration, + Start = timingProperty.GlobalTiming.StartTime, + End = timingProperty.GlobalTiming.EndTime, ComputerName = Environment.MachineName, Exception = null, Status = Status.Skipped, + Output = $"{testContext.GetErrorOutput()}{Environment.NewLine}{testContext.GetStandardOutput()}" }; } catch (Exception e) @@ -130,114 +151,39 @@ private async Task ExecuteTestInternalAsync(DiscoveredTest test, ITestExecutionF throw; } - } - finally - { - semaphore?.Release(); - } - } - - private void CheckCancelled() - { - if (engineCancellationToken.Token.IsCancellationRequested) - { - throw new SkipTestException("The test session has been cancelled..."); - } - - if (cancellationTokenSource.IsCancellationRequested) - { - throw new SkipTestException("The test has been cancelled..."); - } - } - - private async Task ExecuteTest(DiscoveredTest test, TestContext testContext, - ITestExecutionFilter? filter, - List cleanUpExceptions) - { - DateTimeOffset? start = null; - - try - { - if (!explicitFilterService.CanRun(test.TestDetails, filter)) - { - throw new SkipTestException("Test with ExplicitAttribute was not explicitly run."); - } - - if (testContext.SkipReason != null) - { - throw new SkipTestException(testContext.SkipReason); - } - - CheckCancelled(); - - start = DateTimeOffset.Now; - - await ExecuteStaticBeforeHooks(test); - - TestContext.Current = testContext; - - await ExecuteOnTestStartEvents(testContext); - - await ExecuteWithRetries(test, cleanUpExceptions); - - var timingProperty = GetTimingProperty(testContext, start.Value); - - testContext.Result = new TestResult - { - Duration = timingProperty.GlobalTiming.Duration, - Start = timingProperty.GlobalTiming.StartTime, - End = timingProperty.GlobalTiming.EndTime, - ComputerName = Environment.MachineName, - Exception = null, - Status = Status.Passed, - Output = $"{testContext.GetErrorOutput()}{Environment.NewLine}{testContext.GetStandardOutput()}" - }; - } - catch (Exception e) - { - var timingProperty = GetTimingProperty(testContext, start); - - testContext.Result = new TestResult + finally { - Duration = timingProperty.GlobalTiming.Duration, - Start = timingProperty.GlobalTiming.StartTime, - End = timingProperty.GlobalTiming.EndTime, - ComputerName = Environment.MachineName, - Exception = e is SkipTestException ? null : e, - Status = e is SkipTestException ? Status.Skipped : Status.Failed, - Output = $"{testContext.GetErrorOutput()}{Environment.NewLine}{testContext.GetStandardOutput()}" - }; - - throw; - } - finally - { - if (testContext.Result?.Status != Status.Skipped) - { - foreach (var testEndEventsObject in testContext.GetTestEndEventObjects()) + if (testContext.Result?.Status != Status.Skipped) { - await RunHelpers.RunValueTaskSafelyAsync(() => testEndEventsObject.OnTestEnd(testContext), - cleanUpExceptions); + foreach (var testEndEventsObject in testContext.GetTestEndEventObjects()) + { + await RunHelpers.RunValueTaskSafelyAsync(() => testEndEventsObject.OnTestEnd(testContext), + cleanUpExceptions); + } } - } - else - { - foreach (var testSkippedEventReceiver in testContext.GetTestSkippedEventObjects()) + else { - await RunHelpers.RunValueTaskSafelyAsync(() => testSkippedEventReceiver.OnTestSkipped(testContext), - cleanUpExceptions); + foreach (var testSkippedEventReceiver in testContext.GetTestSkippedEventObjects()) + { + await RunHelpers.RunValueTaskSafelyAsync(() => testSkippedEventReceiver.OnTestSkipped(testContext), + cleanUpExceptions); + } } - } - - TestContext.Current = null; - - await ExecuteStaticAfterHooks(test, testContext, cleanUpExceptions); - foreach (var artifact in testContext.Artifacts) - { - await messageBus.TestArtifact(testContext, artifact); + TestContext.Current = null; + + await ExecuteStaticAfterHooks(test, testContext, cleanUpExceptions); + + foreach (var artifact in testContext.Artifacts) + { + await messageBus.TestArtifact(testContext, artifact); + } } } + finally + { + semaphore?.Release(); + } } private async ValueTask ExecuteOnTestStartEvents(TestContext testContext) @@ -285,48 +231,41 @@ await RunHelpers.RunValueTaskSafelyAsync( return null; } - private static TimingProperty GetTimingProperty(TestContext testContext, DateTimeOffset? overallStart) + private static TimingProperty GetTimingProperty(TestContext testContext, DateTimeOffset overallStart) { var end = DateTimeOffset.Now; - overallStart ??= end; lock (testContext.Lock) { var stepTimings = testContext.Timings.Select(x => new StepTimingInfo(x.StepName, string.Empty, new TimingInfo(x.Start, x.End, x.Duration))); - return new TimingProperty(new TimingInfo(overallStart.Value, end, end - overallStart.Value), [..stepTimings]); + return new TimingProperty(new TimingInfo(overallStart, end, end - overallStart), [.. stepTimings]); } } - private Task RunTest(DiscoveredTest discoveredTest, CancellationToken cancellationToken, List cleanupExceptions) - { - return testInvoker.Invoke(discoveredTest, cancellationToken, cleanupExceptions); - } - private async ValueTask ExecuteWithRetries(DiscoveredTest discoveredTest, List cleanupExceptions) { - var testInformation = discoveredTest.TestContext.TestDetails; - var retryCount = testInformation.RetryLimit; - + var retryCount = discoveredTest.TestDetails.RetryLimit; + discoveredTest.TestContext.TestStart = DateTimeOffset.Now; - + // +1 for the original non-retry for (var i = 0; i < retryCount + 1; i++) { try { - await ExecuteCore(discoveredTest, cleanupExceptions); + await ExecuteWithCancellationTokens(discoveredTest, cleanupExceptions); break; } catch (Exception e) { - if (i == retryCount + if (i == retryCount || !await ShouldRetry(discoveredTest.TestContext, e, i + 1)) { throw; } - + cleanupExceptions.Clear(); await logger.LogWarningAsync($""" @@ -335,12 +274,12 @@ await logger.LogWarningAsync($""" """); await discoveredTest.ResetTestInstance(); - + foreach (var testRetryEventReceiver in discoveredTest.TestContext.GetTestRetryEventObjects()) { await testRetryEventReceiver.OnTestRetry(discoveredTest.TestContext, i + 1); } - + discoveredTest.TestContext.CurrentRetryAttempt++; } } @@ -366,17 +305,36 @@ private async ValueTask ShouldRetry(TestContext context, Exception e, int } } - private async ValueTask ExecuteCore(DiscoveredTest discoveredTest, List cleanupExceptions) + private async ValueTask ExecuteWithCancellationTokens(DiscoveredTest discoveredTest, List cleanupExceptions) { - using var linkedTokenSource = CreateLinkedToken(discoveredTest.TestContext, engineCancellationToken.CancellationTokenSource); - + var cancellationTokens = discoveredTest.TestContext.LinkedCancellationTokens; + + if (cancellationTokens.Count == 0) + { + await ExecuteTestMethodWithTimeout(discoveredTest, engineCancellationToken.Token, cleanupExceptions); + return; + } + + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource([engineCancellationToken.Token, .. cancellationTokens]); await ExecuteTestMethodWithTimeout(discoveredTest, linkedTokenSource.Token, cleanupExceptions); } - private static CancellationTokenSource CreateLinkedToken(TestContext testContext, - CancellationTokenSource cancellationTokenSource) + private async Task ExecuteTestMethodWithTimeout(DiscoveredTest discoveredTest, CancellationToken cancellationToken, List cleanupExceptions) + { + var timeout = discoveredTest.TestDetails.Timeout; + + if (timeout == null || timeout.Value == default) + { + await RunTest(discoveredTest, cancellationToken, cleanupExceptions); + return; + } + + await RunHelpers.RunWithTimeoutAsync(token => RunTest(discoveredTest, token, cleanupExceptions), timeout.Value, cancellationToken); + } + + private Task RunTest(DiscoveredTest discoveredTest, CancellationToken cancellationToken, List cleanupExceptions) { - return CancellationTokenSource.CreateLinkedTokenSource([cancellationTokenSource.Token, ..testContext.LinkedCancellationTokens.ToArray()]); + return testInvoker.Invoke(discoveredTest, cancellationToken, cleanupExceptions); } private async ValueTask WaitForDependsOnTests(DiscoveredTest testContext, ITestExecutionFilter? filter, @@ -413,12 +371,12 @@ private async ValueTask WaitForDependsOnTests(DiscoveredTest testContext, ITestE foreach (var dependency in dependencies) { currentChain.Add(dependency.TestDetails); - + if (dependency.TestDetails.IsSameTest(original)) { throw new DependencyConflictException(currentChain); } - + yield return (dependency.InternalDiscoveredTest, dependsOnAttribute.ProceedOnFailure); foreach (var nestedDependency in GetDependencies(original, dependency.TestDetails, currentChain)) @@ -438,7 +396,7 @@ private TestContext[] GetDependencies(TestDetails testDetails, DependsOnAttribut testsForClass = testsForClass .Where(x => x.TestDetails.TestClassArguments.SequenceEqual(testDetails.TestClassArguments)); } - + if (dependsOnAttribute.TestName != null) { testsForClass = testsForClass.Where(x => x.TestDetails.TestName == dependsOnAttribute.TestName); @@ -449,24 +407,10 @@ private TestContext[] GetDependencies(TestDetails testDetails, DependsOnAttribut testsForClass = testsForClass.Where(x => x.TestDetails.TestMethodParameterTypes.SequenceEqual(dependsOnAttribute.ParameterTypes)); } - - return testsForClass.ToArray(); - } - - private async Task ExecuteTestMethodWithTimeout(DiscoveredTest discoveredTest, CancellationToken cancellationToken, List cleanupExceptions) - { - var testDetails = discoveredTest.TestDetails; - - if (testDetails.Timeout == null || testDetails.Timeout.Value == default) - { - await RunTest(discoveredTest, cancellationToken, cleanupExceptions); - return; - } - await RunHelpers.RunWithTimeoutAsync(token => RunTest(discoveredTest, token, cleanupExceptions), testDetails.Timeout, cancellationToken); + return testsForClass.ToArray(); } - public Task IsEnabledAsync() { return extension.IsEnabledAsync();