diff --git a/Internal/Xcepto.Internal.Http/Xcepto.Internal.Http.csproj b/Internal/Xcepto.Internal.Http/Xcepto.Internal.Http.csproj index b3a48fa..d1f4155 100644 --- a/Internal/Xcepto.Internal.Http/Xcepto.Internal.Http.csproj +++ b/Internal/Xcepto.Internal.Http/Xcepto.Internal.Http.csproj @@ -15,11 +15,13 @@ https://github.com/xcepto/Xcepto.NET git Copyright © 2025 themassiveone, Xcepto + xcepto256.png + diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/FailingInitAdapter.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/FailingInitAdapter.cs new file mode 100644 index 0000000..7ff3bb5 --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/FailingInitAdapter.cs @@ -0,0 +1,9 @@ +namespace Samples.CleanupExecution.Tests.Adapters; + +public class FailingInitAdapter: TrackableCleanupAdapter +{ + protected override Task Initialize(IServiceProvider serviceProvider) + { + throw new Exception(); + } +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/SuccessfulAdapter.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/SuccessfulAdapter.cs new file mode 100644 index 0000000..ca8cffc --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/SuccessfulAdapter.cs @@ -0,0 +1,6 @@ +namespace Samples.CleanupExecution.Tests.Adapters; + +public class SuccessfulAdapter: TrackableCleanupAdapter +{ + +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/TrackableCleanupAdapter.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/TrackableCleanupAdapter.cs new file mode 100644 index 0000000..4dddfba --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Adapters/TrackableCleanupAdapter.cs @@ -0,0 +1,13 @@ +using Xcepto.Adapters; + +namespace Samples.CleanupExecution.Tests.Adapters; + +public abstract class TrackableCleanupAdapter: XceptoAdapter +{ + public bool CleanedUp { get; private set; } = false; + protected override Task Cleanup(IServiceProvider serviceProvider) + { + CleanedUp = true; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Samples.CleanupExecution.Tests.csproj b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Samples.CleanupExecution.Tests.csproj new file mode 100644 index 0000000..bcfd0a5 --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Samples.CleanupExecution.Tests.csproj @@ -0,0 +1,27 @@ + + + + enable + enable + + false + true + net9.0 + + + + + + + + + + + + + + + + + + diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/FailingInitDoScenario.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/FailingInitDoScenario.cs new file mode 100644 index 0000000..a66ef48 --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/FailingInitDoScenario.cs @@ -0,0 +1,14 @@ +using Xcepto.Builder; +using Xcepto.Data; +using Xcepto.Scenarios; + +namespace Samples.CleanupExecution.Tests.Scenario; + +public class FailingInitDoScenario : TrackableCleanupScenario +{ + protected override ScenarioSetup Setup(ScenarioSetupBuilder builder) => builder.Build(); + + protected override ScenarioInitialization Initialize(ScenarioInitializationBuilder builder) => builder + .Do(_ => throw new Exception()) + .Build(); +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/FailingInitFireScenario.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/FailingInitFireScenario.cs new file mode 100644 index 0000000..98a6669 --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/FailingInitFireScenario.cs @@ -0,0 +1,14 @@ +using Xcepto.Builder; +using Xcepto.Data; +using Xcepto.Scenarios; + +namespace Samples.CleanupExecution.Tests.Scenario; + +public class FailingInitFireScenario : TrackableCleanupScenario +{ + protected override ScenarioSetup Setup(ScenarioSetupBuilder builder) => builder.Build(); + + protected override ScenarioInitialization Initialize(ScenarioInitializationBuilder builder) => builder + .FireAndForget(_ => throw new Exception()) + .Build(); +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/SuccessfulScenario.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/SuccessfulScenario.cs new file mode 100644 index 0000000..040dbf5 --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/SuccessfulScenario.cs @@ -0,0 +1,9 @@ +using Xcepto.Builder; +using Xcepto.Data; + +namespace Samples.CleanupExecution.Tests.Scenario; + +public class SuccessfulScenario: TrackableCleanupScenario +{ + protected override ScenarioSetup Setup(ScenarioSetupBuilder builder) => builder.Build(); +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/TrackableCleanupScenario.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/TrackableCleanupScenario.cs new file mode 100644 index 0000000..99b325f --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Scenario/TrackableCleanupScenario.cs @@ -0,0 +1,13 @@ +using Xcepto.Builder; +using Xcepto.Data; +using Xcepto.Scenarios; + +namespace Samples.CleanupExecution.Tests.Scenario; + +public abstract class TrackableCleanupScenario : XceptoScenario +{ + public bool CleanupRan { get; private set; } = false; + protected override ScenarioCleanup Cleanup(ScenarioCleanupBuilder builder) => builder + .Do(() => CleanupRan = true) + .Build(); +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Test/AdapterCleanupTests.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Test/AdapterCleanupTests.cs new file mode 100644 index 0000000..3833526 --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Test/AdapterCleanupTests.cs @@ -0,0 +1,47 @@ +using Samples.CleanupExecution.Tests.Adapters; +using Samples.CleanupExecution.Tests.Scenario; +using Xcepto; +using Xcepto.Exceptions; +using Xcepto.Strategies; +using Xcepto.Strategies.Execution; + +namespace Samples.CleanupExecution.Tests.Test; + + +[TestFixtureSource(typeof(StrategyCombinations), nameof(StrategyCombinations.AllCombinations))] +public class AdapterCleanupTests +{ + private readonly XceptoTest _test; + public AdapterCleanupTests(BaseExecutionStrategy executionStrategy) + { + _test = new XceptoTest(executionStrategy); + } + + + [Test] + public async Task SuccessfulAdapter_CleanedUp() + { + var adapter = new SuccessfulAdapter(); + await _test.GivenWithStrategies(new SuccessfulScenario(), builder => + { + builder.RegisterAdapter(adapter); + }); + + Assert.That(adapter.CleanedUp, Is.True); + } + + [Test] + public void FailingInitAdapter_CleanedUp() + { + var adapter = new FailingInitAdapter(); + Assert.That(async () => + { + await _test.GivenWithStrategies(new SuccessfulScenario(), builder => + { + builder.RegisterAdapter(adapter); + }); + }, Throws.InstanceOf()); + + Assert.That(adapter.CleanedUp, Is.True); + } +} \ No newline at end of file diff --git a/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Test/ScenarioCleanupTests.cs b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Test/ScenarioCleanupTests.cs new file mode 100644 index 0000000..eb17348 --- /dev/null +++ b/Samples/CleanupExecution/Samples.CleanupExecution.Tests/Test/ScenarioCleanupTests.cs @@ -0,0 +1,52 @@ +using Samples.CleanupExecution.Tests.Scenario; +using Xcepto; +using Xcepto.Exceptions; +using Xcepto.Strategies; +using Xcepto.Strategies.Execution; + +namespace Samples.CleanupExecution.Tests.Test; + +[TestFixtureSource(typeof(StrategyCombinations), nameof(StrategyCombinations.AllCombinations))] +public class ScenarioCleanupTests +{ + private readonly XceptoTest _test; + public ScenarioCleanupTests(BaseExecutionStrategy executionStrategy) + { + _test = new XceptoTest(executionStrategy); + } + + [Test] + public async Task CleanupHappens_OnSuccessfulRun() + { + SuccessfulScenario scenario = new SuccessfulScenario(); + await _test.GivenWithStrategies(scenario, _ => { }); + + Assert.That(scenario.CleanupRan, Is.True); + } + + [Test] + public void CleanupHappens_OnFailingScenarioInitDo() + { + FailingInitDoScenario scenario = new FailingInitDoScenario(); + + Assert.That(async () => + { + await _test.GivenWithStrategies(scenario, _ => { }); + }, Throws.InstanceOf()); + + Assert.That(scenario.CleanupRan, Is.True); + } + + [Test] + public void CleanupHappens_OnFailingScenarioInitFire() + { + FailingInitFireScenario scenario = new FailingInitFireScenario(); + + Assert.That(async () => + { + await _test.GivenWithStrategies(scenario, _ => { }); + }, Throws.InstanceOf()); + + Assert.That(scenario.CleanupRan, Is.True); + } +} \ No newline at end of file diff --git a/Xcepto.NET.sln b/Xcepto.NET.sln index 8dfa778..f107ca2 100644 --- a/Xcepto.NET.sln +++ b/Xcepto.NET.sln @@ -59,6 +59,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExceptionDetail", "Exceptio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.ExceptionDetail.Tests", "Samples\ExceptionDetail\Samples.ExceptionDetail.Tests\Samples.ExceptionDetail.Tests.csproj", "{A1F50247-A6CB-479E-98C8-1E3AC815C0E6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CleanupExecution", "CleanupExecution", "{AA06077D-7B5D-41BC-8822-7FA52BC15191}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.CleanupExecution.Tests", "Samples\CleanupExecution\Samples.CleanupExecution.Tests\Samples.CleanupExecution.Tests.csproj", "{64BC5387-B5C0-4BFE-A5FD-B837B9D983DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -136,6 +140,10 @@ Global {A1F50247-A6CB-479E-98C8-1E3AC815C0E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1F50247-A6CB-479E-98C8-1E3AC815C0E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1F50247-A6CB-479E-98C8-1E3AC815C0E6}.Release|Any CPU.Build.0 = Release|Any CPU + {64BC5387-B5C0-4BFE-A5FD-B837B9D983DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64BC5387-B5C0-4BFE-A5FD-B837B9D983DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64BC5387-B5C0-4BFE-A5FD-B837B9D983DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64BC5387-B5C0-4BFE-A5FD-B837B9D983DC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {BABA2E33-65CD-4EB3-8FDC-3DF7B2DC2EAD} = {1C2E2691-2BCB-4F59-9222-DDA2C58EC928} @@ -159,5 +167,7 @@ Global {4315D42A-16FC-438E-8439-238FAB7FC248} = {31BF92D6-8D80-4E2C-8B58-2E212C86F5A2} {33A8048C-6324-475C-AC7F-F27B47805228} = {1C2E2691-2BCB-4F59-9222-DDA2C58EC928} {A1F50247-A6CB-479E-98C8-1E3AC815C0E6} = {33A8048C-6324-475C-AC7F-F27B47805228} + {AA06077D-7B5D-41BC-8822-7FA52BC15191} = {1C2E2691-2BCB-4F59-9222-DDA2C58EC928} + {64BC5387-B5C0-4BFE-A5FD-B837B9D983DC} = {AA06077D-7B5D-41BC-8822-7FA52BC15191} EndGlobalSection EndGlobal diff --git a/Xcepto.NewtonsoftJson/Xcepto.NewtonsoftJson.csproj b/Xcepto.NewtonsoftJson/Xcepto.NewtonsoftJson.csproj index 4c26fba..1934381 100644 --- a/Xcepto.NewtonsoftJson/Xcepto.NewtonsoftJson.csproj +++ b/Xcepto.NewtonsoftJson/Xcepto.NewtonsoftJson.csproj @@ -15,11 +15,13 @@ https://github.com/xcepto/Xcepto.NET git Copyright © 2025 themassiveone, Xcepto + xcepto256.png + diff --git a/Xcepto.Rest/Xcepto.Rest.csproj b/Xcepto.Rest/Xcepto.Rest.csproj index ec97f94..067dd93 100644 --- a/Xcepto.Rest/Xcepto.Rest.csproj +++ b/Xcepto.Rest/Xcepto.Rest.csproj @@ -15,11 +15,13 @@ https://github.com/xcepto/Xcepto.NET git Copyright © 2025 themassiveone, Xcepto + xcepto256.png + diff --git a/Xcepto.SSR/Xcepto.SSR.csproj b/Xcepto.SSR/Xcepto.SSR.csproj index 227fb38..f596ce8 100644 --- a/Xcepto.SSR/Xcepto.SSR.csproj +++ b/Xcepto.SSR/Xcepto.SSR.csproj @@ -15,11 +15,13 @@ https://github.com/xcepto/Xcepto.NET git Copyright © 2025 themassiveone, Xcepto + xcepto256.png + diff --git a/Xcepto.Testcontainers/Xcepto.Testcontainers.csproj b/Xcepto.Testcontainers/Xcepto.Testcontainers.csproj index 0b44777..f947c0d 100644 --- a/Xcepto.Testcontainers/Xcepto.Testcontainers.csproj +++ b/Xcepto.Testcontainers/Xcepto.Testcontainers.csproj @@ -15,11 +15,13 @@ https://github.com/xcepto/Xcepto.NET git Copyright © 2025 themassiveone, Xcepto + xcepto256.png + diff --git a/Xcepto/Internal/TestInstance.cs b/Xcepto/Internal/TestInstance.cs index a8b7130..b2fec20 100644 --- a/Xcepto/Internal/TestInstance.cs +++ b/Xcepto/Internal/TestInstance.cs @@ -27,7 +27,6 @@ internal class TestInstance private IEnumerable? _states; private IEnumerable? _adapters; private Func>? _propagatedTasksSupplier; - public IServiceProvider ServiceProvider { get; private set; } public AcceptanceStateMachine? StateMachine { get; private set; } internal TestInstance(TimeoutConfig timeout, XceptoScenario scenario, TransitionBuilder transitionBuilder) @@ -38,17 +37,20 @@ internal TestInstance(TimeoutConfig timeout, XceptoScenario scenario, Transition _timeout = timeout; } - internal async Task InitializeAsync() + internal async Task SetupAsync() { try { - ServiceProvider = await _scenario.CallSetup(); + return await _scenario.CallSetup(); } catch (Exception e) { throw new ScenarioSetupException($"Scenario setup failed: {_scenario.GetType().Name}").Promote(e); } - var loggingProvider = ServiceProvider.GetRequiredService(); + } + internal async Task InitializeAsync(IServiceProvider serviceProvider) + { + var loggingProvider = serviceProvider.GetRequiredService(); loggingProvider.LogDebug("Setting up acceptance test"); loggingProvider.LogDebug(""); @@ -58,9 +60,11 @@ internal async Task InitializeAsync() } catch (Exception e) { - throw new ScenarioInitException($"Scenario initialization failed: {_scenario.GetType().Name}").Promote(e); + var message = $"Scenario initialization failed: {_scenario.GetType().Name}"; + loggingProvider.LogDebug(message); + throw new ScenarioInitException(message).Promote(e); } - loggingProvider.LogDebug("Initialized scenario successfully ✅"); + loggingProvider.LogDebug($"Initialized scenario ({_scenario.GetType().Name}) successfully ✅"); loggingProvider.LogDebug(""); try @@ -69,7 +73,9 @@ internal async Task InitializeAsync() } catch (Exception e) { - throw new ArrangeTestException("Failed arranging the test").Promote(e); + var message = "Failed arranging the test"; + loggingProvider.LogDebug(message); + throw new ArrangeTestException(message).Promote(e); } _states = _transitionBuilder.GetStates().ToArray(); _adapters = _transitionBuilder.GetAdapters().ToArray(); @@ -84,11 +90,13 @@ internal async Task InitializeAsync() { try { - await state.Initialize(ServiceProvider); + await state.Initialize(serviceProvider); } catch (Exception e) { - throw new StateInitException($"State failed to initialize: [{state.Name}] ({state.GetType().Name}, state #{counter})").Promote(e); + string message = $"State failed to initialize: [{state.Name}] ({state.GetType().Name}, state #{counter})"; + loggingProvider.LogDebug(message); + throw new StateInitException(message).Promote(e); } loggingProvider.LogDebug($"State initialized: {state} ({counter}/{_states.Count()+2})"); } @@ -101,11 +109,13 @@ internal async Task InitializeAsync() { try { - await adapter.CallInitialize(ServiceProvider); + await adapter.CallInitialize(serviceProvider); } catch (Exception e) { - throw new AdapterInitException($"Adapter #{counter} failed to initialize: {adapter.GetType().Name}").Promote(e); + string message = $"Adapter #{counter} failed to initialize: {adapter.GetType().Name}"; + loggingProvider.LogDebug(message); + throw new AdapterInitException(message).Promote(e); } loggingProvider.LogDebug($"Adapter initialized: {adapter} ({counter}/{_adapters.Count()})"); } @@ -116,8 +126,7 @@ internal async Task InitializeAsync() loggingProvider.LogDebug("---------------------------------"); _startTime = DateTime.Now; - await StateMachine.Start(ServiceProvider); - return ServiceProvider; + await StateMachine.Start(serviceProvider); } private bool _firstStep = true; @@ -148,36 +157,40 @@ internal async Task CleanupAsync(IServiceProvider serviceProvider) var loggingProvider = serviceProvider.GetRequiredService(); var disposeProvider = serviceProvider.GetRequiredService(); loggingProvider.LogDebug("---------------------------------"); - loggingProvider.LogDebug("Test completed ✅"); - loggingProvider.LogDebug(""); - loggingProvider.LogDebug("Cleaning up:"); - foreach (var (adapter, counter) in _adapters.Select((adapter, i) => (adapter, i + 1))) + if (_adapters is not null) { - try - { - await adapter.CallCleanup(serviceProvider); - } - catch (Exception e) - { - throw new AdapterCleanupException($"Failed to cleanup adapter #{counter}: {adapter.GetType().Name}") - .Promote(e); - } - finally + foreach (var (adapter, counter) in _adapters.Select((adapter, i) => (adapter, i + 1))) { - disposeProvider?.DisposeAll(); + try + { + await adapter.CallCleanup(serviceProvider); + } + catch (Exception e) + { + string message = $"Failed to cleanup adapter #{counter}: {adapter.GetType().Name}"; + loggingProvider.LogDebug(message); + throw new AdapterCleanupException(message).Promote(e); + } + finally + { + disposeProvider?.DisposeAll(); + } + loggingProvider.LogDebug($"Adapter cleanup: {adapter} ({counter}/{_adapters.Count()})"); } - loggingProvider.LogDebug($"Adapter cleanup: {adapter} ({counter}/{_adapters.Count()})"); } - loggingProvider.LogDebug($"All {_adapters.Count()} adapters were successfully cleaned up ✅"); + loggingProvider.LogDebug($"All {(_adapters is not null ? _adapters.Count() : "0")} adapters were successfully cleaned up ✅"); try { await _scenario.CallCleanup(); + loggingProvider.LogDebug($"Scenario ({_scenario.GetType().Name}) was successfully cleaned up ✅"); } catch (Exception e) { - throw new ScenarioCleanupException($"Scenario cleanup failed: {_scenario.GetType().Name}").Promote(e); + string message = $"Scenario cleanup failed: {_scenario.GetType().Name}"; + loggingProvider.LogDebug(message); + throw new ScenarioCleanupException(message).Promote(e); } loggingProvider.LogDebug(""); diff --git a/Xcepto/Strategies/Execution/AsyncExecutionStrategy.cs b/Xcepto/Strategies/Execution/AsyncExecutionStrategy.cs index 5ad288c..c7b1f0f 100644 --- a/Xcepto/Strategies/Execution/AsyncExecutionStrategy.cs +++ b/Xcepto/Strategies/Execution/AsyncExecutionStrategy.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Runtime.ExceptionServices; using System.Threading.Tasks; -using Xcepto.Data; using Xcepto.Internal; namespace Xcepto.Strategies.Execution; @@ -12,16 +9,15 @@ public sealed class AsyncExecutionStrategy : BaseExecutionStrategy { public async Task RunAsync() { - if (_testInstance is null) + if (testInstance is null) throw new Exception("Execution strategy not primed yet"); - var propagatedTasksSupplier = _testInstance.GetPropagatedTasksSupplier(); - var timeout = _testInstance.GetTimeout(); + var propagatedTasksSupplier = testInstance.GetPropagatedTasksSupplier(); + var timeout = testInstance.GetTimeout(); var totalDeadline = DateTime.UtcNow + timeout.Total; - // INIT - var init = _testInstance.InitializeAsync(); - while (!init.IsCompleted) + var setup = testInstance.SetupAsync(); + while (!setup.IsCompleted) { await Task.Yield(); CheckTimeouts(totalDeadline); @@ -29,44 +25,61 @@ public async Task RunAsync() } CheckTimeouts(totalDeadline); CheckPropagated(propagatedTasksSupplier); - var serviceProvider = init.GetAwaiter().GetResult();; + serviceProvider = setup.GetAwaiter().GetResult(); - // EXECUTION LOOP - StartTest(); - while (true) + try { - var stepTask = _testInstance.StepAsync(serviceProvider); - - while (!stepTask.IsCompleted) + // INIT + var init = testInstance.InitializeAsync(serviceProvider); + while (!init.IsCompleted) { await Task.Yield(); - CheckTestTimeout(); CheckTimeouts(totalDeadline); CheckPropagated(propagatedTasksSupplier); } - CheckTestTimeout(); CheckTimeouts(totalDeadline); CheckPropagated(propagatedTasksSupplier); - if (stepTask.GetAwaiter().GetResult() == StepResult.Finished) - break; + init.GetAwaiter().GetResult(); - await Task.Yield(); - CheckTestTimeout(); - CheckTimeouts(totalDeadline); - CheckPropagated(propagatedTasksSupplier); - } + // EXECUTION LOOP + StartTest(); + while (true) + { + var stepTask = testInstance.StepAsync(serviceProvider); + + while (!stepTask.IsCompleted) + { + await Task.Yield(); + CheckTestTimeout(); + CheckTimeouts(totalDeadline); + CheckPropagated(propagatedTasksSupplier); + } + CheckTestTimeout(); + CheckTimeouts(totalDeadline); + CheckPropagated(propagatedTasksSupplier); + if (stepTask.GetAwaiter().GetResult() == StepResult.Finished) + break; - // CLEANUP - var cleanup = _testInstance.CleanupAsync(serviceProvider); - while (!cleanup.IsCompleted) + await Task.Yield(); + CheckTestTimeout(); + CheckTimeouts(totalDeadline); + CheckPropagated(propagatedTasksSupplier); + } + } + finally { - await Task.Yield(); + // CLEANUP + var cleanup = testInstance.CleanupAsync(serviceProvider); + while (!cleanup.IsCompleted) + { + await Task.Yield(); + CheckTimeouts(totalDeadline); + CheckPropagated(propagatedTasksSupplier); + } + if (cleanup.IsFaulted) + throw cleanup.Exception?.InnerExceptions.First() ?? new Exception("cleanup task failed without exception"); CheckTimeouts(totalDeadline); CheckPropagated(propagatedTasksSupplier); } - if (cleanup.IsFaulted) - throw cleanup.Exception?.InnerExceptions.First() ?? new Exception("cleanup task failed without exception"); - CheckTimeouts(totalDeadline); - CheckPropagated(propagatedTasksSupplier); } } diff --git a/Xcepto/Strategies/Execution/BaseExecutionStrategy.cs b/Xcepto/Strategies/Execution/BaseExecutionStrategy.cs index 74e953b..29d3996 100644 --- a/Xcepto/Strategies/Execution/BaseExecutionStrategy.cs +++ b/Xcepto/Strategies/Execution/BaseExecutionStrategy.cs @@ -13,7 +13,8 @@ namespace Xcepto.Strategies.Execution; public abstract class BaseExecutionStrategy { - internal TestInstance? _testInstance; + internal TestInstance? testInstance; + internal IServiceProvider? serviceProvider; private DateTime _testStartTime; protected void StartTest() @@ -23,11 +24,17 @@ protected void StartTest() protected void CheckTestTimeout() { FlushLogs(); - if (DateTime.UtcNow >= (_testStartTime + _testInstance.GetTimeout().Test)) + if (DateTime.UtcNow >= (_testStartTime + testInstance.GetTimeout().Test)) { - var failingResult = _testInstance.StateMachine?.CurrentXceptoState.MostRecentFailingResult; - string currentState = _testInstance?.StateMachine?.CurrentXceptoState.Name ?? ""; - var timeoutMessage = $"Test exceeded TEST timeout: {_testInstance.GetTimeout().Test} during [{currentState}]"; + var failingResult = testInstance.StateMachine?.CurrentXceptoState.MostRecentFailingResult; + string currentState = testInstance?.StateMachine?.CurrentXceptoState.Name ?? ""; + var timeoutMessage = $"Test exceeded TEST timeout: {testInstance.GetTimeout().Test} during [{currentState}]"; + if (serviceProvider is not null) + { + var loggingProvider = serviceProvider.GetRequiredService(); + loggingProvider.LogDebug(timeoutMessage); + FlushLogs(); + } if(failingResult is null) throw new TestTimeoutException(timeoutMessage); throw new TestTimeoutException(timeoutMessage).Promote(new AssertionException(failingResult.FailureDescription)); @@ -38,8 +45,14 @@ protected void CheckTimeouts(DateTime deadline) FlushLogs(); if (DateTime.UtcNow >= deadline) { - var failingResult = _testInstance.StateMachine?.CurrentXceptoState.MostRecentFailingResult; - var timeoutMessage = $"Test exceeded TOTAL timeout: {_testInstance.GetTimeout().Total}"; + var failingResult = testInstance.StateMachine?.CurrentXceptoState.MostRecentFailingResult; + var timeoutMessage = $"Test exceeded TOTAL timeout: {testInstance.GetTimeout().Total}"; + if (serviceProvider is not null) + { + var loggingProvider = serviceProvider.GetRequiredService(); + loggingProvider.LogDebug(timeoutMessage); + FlushLogs(); + } if(failingResult is null) throw new TotalTimeoutException(timeoutMessage); throw new TotalTimeoutException(timeoutMessage).Promote(new AssertionException(failingResult.FailureDescription)); @@ -49,9 +62,9 @@ protected void CheckTimeouts(DateTime deadline) private void FlushLogs() { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (_testInstance is null || _testInstance.ServiceProvider is null) + if (testInstance is null || serviceProvider is null) return; - var loggingProvider = _testInstance.ServiceProvider.GetRequiredService(); + var loggingProvider = serviceProvider.GetRequiredService(); loggingProvider.Flush(); } @@ -64,15 +77,21 @@ protected void CheckPropagated(Func> propagatedTasksSupplier) if (firstFaulted is null) return; - + // Unwrap AggregateException EXACTLY like before var ex = firstFaulted.Exception; var inner = ex.InnerException ?? ex; + if (serviceProvider is not null) + { + var loggingProvider = serviceProvider.GetRequiredService(); + loggingProvider.LogDebug($"Propagated task failed: {inner}"); + FlushLogs(); + } ExceptionDispatchInfo.Capture(inner).Throw(); } internal void Prime(TestInstance testInstance) { - _testInstance = testInstance; + this.testInstance = testInstance; } } \ No newline at end of file diff --git a/Xcepto/Strategies/Execution/EnumeratedExecutionStrategy.cs b/Xcepto/Strategies/Execution/EnumeratedExecutionStrategy.cs index 1f2c089..a5d671c 100644 --- a/Xcepto/Strategies/Execution/EnumeratedExecutionStrategy.cs +++ b/Xcepto/Strategies/Execution/EnumeratedExecutionStrategy.cs @@ -11,65 +11,94 @@ namespace Xcepto.Strategies.Execution; public sealed class EnumeratedExecutionStrategy: BaseExecutionStrategy { + private DateTime _deadline; + private Func> _propagatedTasksSupplier; + public IEnumerator RunEnumerated() { - if (_testInstance is null) + if (testInstance is null) throw new Exception("Execution strategy not primed yet"); - var propagatedTasksSupplier = _testInstance.GetPropagatedTasksSupplier(); - var timeout = _testInstance.GetTimeout(); - var deadline = DateTime.UtcNow + timeout.Total; + _propagatedTasksSupplier = testInstance.GetPropagatedTasksSupplier(); + var timeout = testInstance.GetTimeout(); + _deadline = DateTime.UtcNow + timeout.Total; - // INIT - var init = _testInstance.InitializeAsync(); - while (!init.IsCompleted) + var setup = testInstance.SetupAsync(); + while (!setup.IsCompleted) { yield return null; - CheckTimeouts(deadline); - CheckPropagated(propagatedTasksSupplier); + CheckTimeouts(_deadline); } - CheckTimeouts(deadline); - CheckPropagated(propagatedTasksSupplier); - var serviceProvider = init.GetAwaiter().GetResult(); + CheckTimeouts(_deadline); + serviceProvider = setup.GetAwaiter().GetResult(); - // EXECUTION LOOP - StartTest(); - while (true) + try { - var stepTask = _testInstance.StepAsync(serviceProvider); - - while (!stepTask.IsCompleted) + // INIT + var init = testInstance.InitializeAsync(serviceProvider); + while (!init.IsCompleted) { yield return null; - CheckTestTimeout(); - CheckTimeouts(deadline); - CheckPropagated(propagatedTasksSupplier); + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); } - CheckTestTimeout(); - CheckTimeouts(deadline); - CheckPropagated(propagatedTasksSupplier); + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); + init.GetAwaiter().GetResult(); - if (stepTask.GetAwaiter().GetResult() == StepResult.Finished) - break; + // EXECUTION LOOP + StartTest(); + while (true) + { + var stepTask = testInstance.StepAsync(serviceProvider); - // a frame passes - yield return null; - CheckTestTimeout(); - CheckTimeouts(deadline); - CheckPropagated(propagatedTasksSupplier); + while (!stepTask.IsCompleted) + { + yield return null; + CheckTestTimeout(); + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); + } + CheckTestTimeout(); + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); + + if (stepTask.GetAwaiter().GetResult() == StepResult.Finished) + break; + + // a frame passes + yield return null; + CheckTestTimeout(); + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); + } + } + finally + { + var enumerator = Cleanup(); + while (enumerator.MoveNext()) + { + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); + } + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); } + } + private IEnumerator Cleanup() + { // CLEANUP - var cleanup = _testInstance.CleanupAsync(serviceProvider); + var cleanup = testInstance.CleanupAsync(serviceProvider); while (!cleanup.IsCompleted) { yield return null; - CheckTimeouts(deadline); - CheckPropagated(propagatedTasksSupplier); + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); } if (cleanup.IsFaulted) throw cleanup.Exception?.InnerExceptions.First() ?? new Exception("cleanup task failed without exception"); - CheckTimeouts(deadline); - CheckPropagated(propagatedTasksSupplier); + CheckTimeouts(_deadline); + CheckPropagated(_propagatedTasksSupplier); } } \ No newline at end of file diff --git a/Xcepto/Xcepto.csproj b/Xcepto/Xcepto.csproj index 97757df..f94799c 100644 --- a/Xcepto/Xcepto.csproj +++ b/Xcepto/Xcepto.csproj @@ -15,11 +15,13 @@ https://github.com/xcepto/Xcepto.NET git Copyright © 2025 themassiveone, Xcepto + xcepto256.png + diff --git a/Xcepto/XceptoTest.cs b/Xcepto/XceptoTest.cs index d236fab..07b8957 100644 --- a/Xcepto/XceptoTest.cs +++ b/Xcepto/XceptoTest.cs @@ -53,12 +53,7 @@ public static IEnumerator GivenEnumerated(XceptoScenario scenario, TimeoutConfig return xceptoTest.GivenEnumeratedWithStrategies(scenario, timeout, builder); } - public static IEnumerator GivenEnumerated(XceptoScenario scenario, Action builder) => - GivenEnumerated(scenario, DefaultTimeout, builder); - - public IEnumerator GivenEnumeratedWithStrategies(XceptoScenario scenario, Action builder) => - GivenEnumeratedWithStrategies(scenario, DefaultTimeout, builder); - public IEnumerator GivenEnumeratedWithStrategies(XceptoScenario scenario, TimeoutConfig timeout, Action builder) + private IEnumerator GivenEnumeratedWithStrategies(XceptoScenario scenario, TimeoutConfig timeout, Action builder) { if (_executionStrategy is not EnumeratedExecutionStrategy enumeratedExecutionStrategy) throw new ArgumentException("Only enumerated strategy allowed"); diff --git a/media/xcepto256.png b/media/xcepto256.png new file mode 100644 index 0000000..17453b7 Binary files /dev/null and b/media/xcepto256.png differ diff --git a/media/xcepto256.xcf b/media/xcepto256.xcf new file mode 100644 index 0000000..7933bac Binary files /dev/null and b/media/xcepto256.xcf differ