using Elsa.Testing.Shared; using Elsa.Workflows; using Elsa.Workflows.Activities; using Xunit.Abstractions; using Parallel = Elsa.Workflows.Activities.Parallel; namespace Elsa.Activities.IntegrationTests; public class ParallelTests(ITestOutputHelper testOutputHelper) { private readonly WorkflowTestFixture _fixture = new(testOutputHelper); [Fact(DisplayName = "Parallel executes all child activities and completes")] public async Task Parallel_ExecutesAllChildren_AndCompletes() { // Arrange var parallel = new Parallel( new WriteLine("Activity 1"), new WriteLine("Activity 2"), new WriteLine("Activity 3") ); // Act var result = await _fixture.RunActivityAsync(parallel); // Assert var journal = result.Journal; var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel); Assert.NotNull(parallelContext); Assert.Equal(ActivityStatus.Completed, parallelContext.Status); Assert.Equal(3, _fixture.CapturingTextWriter.Lines.Count); Assert.Contains("Activity 1", _fixture.CapturingTextWriter.Lines); Assert.Contains("Activity 2", _fixture.CapturingTextWriter.Lines); Assert.Contains("Activity 3", _fixture.CapturingTextWriter.Lines); } [Fact(DisplayName = "Parallel completes when empty")] public async Task Parallel_Completes_WhenEmpty() { // Arrange var parallel = new Parallel(); // Act var result = await _fixture.RunActivityAsync(parallel); // Assert var journal = result.Journal; var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel); Assert.NotNull(parallelContext); Assert.Equal(ActivityStatus.Completed, parallelContext.Status); Assert.Empty(_fixture.CapturingTextWriter.Lines); } [Fact(DisplayName = "Parallel executes single child activity and completes")] public async Task Parallel_ExecutesSingleChild_AndCompletes() { // Arrange var parallel = new Parallel( new WriteLine("Single Activity") ); // Act var result = await _fixture.RunActivityAsync(parallel); // Assert var journal = result.Journal; var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel); Assert.NotNull(parallelContext); Assert.Equal(ActivityStatus.Completed, parallelContext.Status); Assert.Single(_fixture.CapturingTextWriter.Lines); Assert.Equal("Single Activity", _fixture.CapturingTextWriter.Lines.Single()); } [Fact(DisplayName = "Parallel executes multiple different activity types")] public async Task Parallel_ExecutesMixedActivityTypes_AndCompletes() { // Arrange var parallel = new Parallel( new WriteLine("First"), new SetName("TestName"), new WriteLine("Second") ); // Act var result = await _fixture.RunActivityAsync(parallel); // Assert var journal = result.Journal; var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel); Assert.NotNull(parallelContext); Assert.Equal(ActivityStatus.Completed, parallelContext.Status); Assert.Equal(2, _fixture.CapturingTextWriter.Lines.Count); Assert.Contains("First", _fixture.CapturingTextWriter.Lines); Assert.Contains("Second", _fixture.CapturingTextWriter.Lines); } [Fact(DisplayName = "Parallel completes only after all children complete")] public async Task Parallel_CompletesOnlyAfterAllChildrenComplete() { // Arrange var parallel = new Parallel( new WriteLine("Child 1"), new WriteLine("Child 2"), new WriteLine("Child 3"), new WriteLine("Child 4") ); // Act var result = await _fixture.RunActivityAsync(parallel); // Assert var journal = result.Journal; var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel); Assert.NotNull(parallelContext); Assert.Equal(ActivityStatus.Completed, parallelContext.Status); Assert.Equal(4, _fixture.CapturingTextWriter.Lines.Count); } [Fact(DisplayName = "Parallel executes nested Parallel activities")] public async Task Parallel_ExecutesNestedParallel_AndCompletes() { // Arrange var innerParallel = new Parallel( new WriteLine("Inner 1"), new WriteLine("Inner 2") ); var outerParallel = new Parallel( new WriteLine("Outer 1"), innerParallel, new WriteLine("Outer 2") ); // Act var result = await _fixture.RunActivityAsync(outerParallel); // Assert var journal = result.Journal; var outerParallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity == outerParallel); var innerParallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity == innerParallel); Assert.NotNull(outerParallelContext); Assert.NotNull(innerParallelContext); Assert.Equal(ActivityStatus.Completed, outerParallelContext.Status); Assert.Equal(ActivityStatus.Completed, innerParallelContext.Status); Assert.Equal(4, _fixture.CapturingTextWriter.Lines.Count); Assert.Contains("Outer 1", _fixture.CapturingTextWriter.Lines); Assert.Contains("Outer 2", _fixture.CapturingTextWriter.Lines); Assert.Contains("Inner 1", _fixture.CapturingTextWriter.Lines); Assert.Contains("Inner 2", _fixture.CapturingTextWriter.Lines); } [Fact(DisplayName = "Parallel remains in Running state when a child activity faults")] public async Task Parallel_RemainsRunning_WhenChildFaults() { // Arrange var parallel = new Parallel( new WriteLine("Before Fault"), new Fault { Message = new("Test fault") }, new WriteLine("After Fault") ); // Act var result = await _fixture.RunActivityAsync(parallel); // Assert var journal = result.Journal; var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel); Assert.NotNull(parallelContext); Assert.Equal(ActivityStatus.Running, parallelContext.Status); Assert.Equal(1, parallelContext.AggregateFaultCount); // The non-faulted activities should still execute Assert.Contains("Before Fault", _fixture.CapturingTextWriter.Lines); Assert.Contains("After Fault", _fixture.CapturingTextWriter.Lines); } }