From 6a42857f224ec54c7efe83e4dec8e116a3219a97 Mon Sep 17 00:00:00 2001 From: Bob Hauser Date: Sat, 8 Mar 2025 19:59:55 -0500 Subject: [PATCH] Fixes issue with FlowJoin(WaitAny) where activities can be executed multiple times --- .../Activities/Flowchart/Models/FlowScope.cs | 5 +- .../Scenarios/FlowchartNextActivity/Tests.cs | 213 ++++++++++++------ 2 files changed, 145 insertions(+), 73 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs index d0f74dd42..0fa2790a3 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs @@ -89,7 +89,10 @@ public class FlowScope public bool HasFollowedInboundConnection(FlowGraph flowGraph, IActivity activity) { var forwardInboundConnections = flowGraph.GetForwardInboundConnections(activity); - return forwardInboundConnections.Any(c => GetConnectionLastVisitFollowed(c)); + var outboundActivityVisitCount = GetActivityVisitCount(activity); + var maxConnectionVisitCount = forwardInboundConnections.Max(c => GetConnectionVisitCount(c)); + return maxConnectionVisitCount > outboundActivityVisitCount + && forwardInboundConnections.Any(c => GetConnectionVisitCount(c) == maxConnectionVisitCount && GetConnectionLastVisitFollowed(c)); } /// diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs index bfad64033..82eb8c22b 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs @@ -1,41 +1,41 @@ -using Elsa.Expressions.Models; -using Elsa.Testing.Shared; -using Elsa.Workflows.Activities; -using Elsa.Workflows.Activities.Flowchart.Activities; -using Elsa.Workflows.Activities.Flowchart.Models; -using Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity.Workflows; -using Elsa.Workflows.Memory; -using Microsoft.Extensions.DependencyInjection; -using Xunit.Abstractions; - -namespace Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity; - -public class FlowchartNextActivityTests -{ - private readonly CapturingTextWriter _capturingTextWriter = new(); - private readonly IServiceProvider _services; - private readonly IWorkflowRunner _workflowRunner; - - public FlowchartNextActivityTests(ITestOutputHelper testOutputHelper) - { - _services = new TestApplicationBuilder(testOutputHelper) - .WithCapturingTextWriter(_capturingTextWriter) - .AddActivitiesFrom() - .Build(); - - _workflowRunner = _services.GetRequiredService(); - } - - [Fact(DisplayName = "Flowchart only schedules next activity connected to outcome of previous activity.")] - public async Task Test1() - { - await _services.PopulateRegistriesAsync(); - await _workflowRunner.RunAsync(); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(new[] { "Line 1" }, lines); +using Elsa.Expressions.Models; +using Elsa.Testing.Shared; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity.Workflows; +using Elsa.Workflows.Memory; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity; + +public class FlowchartNextActivityTests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IServiceProvider _services; + private readonly IWorkflowRunner _workflowRunner; + + public FlowchartNextActivityTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .AddActivitiesFrom() + .Build(); + + _workflowRunner = _services.GetRequiredService(); } - [Fact(DisplayName = "Flowchart with backward connections and a dangling activity")] + [Fact(DisplayName = "Flowchart only schedules next activity connected to outcome of previous activity.")] + public async Task Test1() + { + await _services.PopulateRegistriesAsync(); + await _workflowRunner.RunAsync(); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "Line 1" }, lines); + } + + [Fact(DisplayName = "Flowchart with backward connections and a dangling activity")] public async Task BackwardConnectionTest() { var workflow = new TestWorkflow(workflowBuilder => @@ -76,12 +76,12 @@ public class FlowchartNextActivityTests workflowBuilder.Root = new Flowchart { - Variables = - { - loopVariable + Variables = + { + loopVariable }, - Activities = - { + Activities = + { start, dangling, writeLineDecision, @@ -93,7 +93,7 @@ public class FlowchartNextActivityTests d, e, f, - end + end }, Connections = { @@ -117,14 +117,14 @@ public class FlowchartNextActivityTests }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); - Assert.Equal(new[] { "A", "B", "C", "D", "E", "A", "B", "E", "F" }, lines); + await _services.PopulateRegistriesAsync(); + var result = await _workflowRunner.RunAsync(workflow); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); + Assert.Equal(new[] { "A", "B", "C", "D", "E", "A", "B", "E", "F" }, lines); } - [Fact(DisplayName = "Flowchart with an invalid backward connection")] + [Fact(DisplayName = "Flowchart with an invalid backward connection")] public async Task InvalidBackwardConnectionTest() { var workflow = new TestWorkflow(workflowBuilder => @@ -138,8 +138,8 @@ public class FlowchartNextActivityTests workflowBuilder.Root = new Flowchart { - Activities = - { + Activities = + { start, a, b, @@ -160,18 +160,18 @@ public class FlowchartNextActivityTests }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowState.SubStatus); - Assert.Equal(1, result.WorkflowState.Incidents.Count()); - Assert.Equal("Invalid backward connection: Every path from the source ('WriteLineE') must go through the target ('WriteLineC') when tracing back to the start.", result.WorkflowState.Incidents.First().Message); - Assert.Equal(new[] { "A", "B", "C", "D", "E" }, lines); + await _services.PopulateRegistriesAsync(); + var result = await _workflowRunner.RunAsync(workflow); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowState.SubStatus); + Assert.Equal(1, result.WorkflowState.Incidents.Count()); + Assert.Equal("Invalid backward connection: Every path from the source ('WriteLineE') must go through the target ('WriteLineC') when tracing back to the start.", result.WorkflowState.Incidents.First().Message); + Assert.Equal(new[] { "A", "B", "C", "D", "E" }, lines); } - [Theory(DisplayName = "Flowchart with a Join activity executed multiple times")] - [InlineData(FlowJoinMode.WaitAll)] - [InlineData(FlowJoinMode.WaitAny)] + [Theory(DisplayName = "Flowchart with a Join activity executed multiple times")] + [InlineData(FlowJoinMode.WaitAll)] + [InlineData(FlowJoinMode.WaitAny)] public async Task WaitAnyLoopTest(FlowJoinMode joinMode) { var workflow = new TestWorkflow(workflowBuilder => @@ -204,12 +204,12 @@ public class FlowchartNextActivityTests workflowBuilder.Root = new Flowchart { - Variables = - { - loopVariable + Variables = + { + loopVariable }, - Activities = - { + Activities = + { start, a, b, @@ -218,7 +218,7 @@ public class FlowchartNextActivityTests join, incrementLoop, loopbackDecision, - end + end }, Connections = { @@ -237,10 +237,79 @@ public class FlowchartNextActivityTests }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); - Assert.Equal(new[] { "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D"}, lines); - } + await _services.PopulateRegistriesAsync(); + var result = await _workflowRunner.RunAsync(workflow); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); + Assert.Equal(new[] { "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D"}, lines); + + } + + [Theory(DisplayName = "Flowchart with a Join activity executed multiple times, bug 6479")] + [InlineData(FlowJoinMode.WaitAll)] + [InlineData(FlowJoinMode.WaitAny)] + public async Task WaitLoopBug6479Test(FlowJoinMode joinMode) + { + var workflow = new TestWorkflow(workflowBuilder => + { + var loopVariable = new Variable("LoopCount", 0); + + var start = new Start(); + var loopbackSwitch = new FlowSwitch() + { + Cases = { + new FlowSwitchCase("DoLoopback", new Expression("JavaScript", "getVariable('LoopCount') < 3")), + }, + Mode = new(SwitchMode.MatchFirst) + }; + var a = new WriteLine("A"); + var incrementLoop = new SetVariable() + { + Variable = loopVariable, + Value = new Models.Input(new Expression("JavaScript", "getVariable('LoopCount') + 1")) + }; + var join = new FlowJoin() + { + Mode = new(joinMode) + }; + var b = new WriteLine("B"); + var end = new End(); + + + workflowBuilder.Root = new Flowchart + { + Variables = + { + loopVariable + }, + Activities = + { + start, + loopbackSwitch, + a, + incrementLoop, + join, + b, + end + }, + Connections = + { + new(start, loopbackSwitch), + new(new Endpoint(loopbackSwitch, "DoLoopback"), new Endpoint(a)), + new(new Endpoint(loopbackSwitch, "DoLoopback"), new Endpoint(incrementLoop)), + new(new Endpoint(loopbackSwitch, "Default"), new Endpoint(b)), + new(a, join), + new(incrementLoop, join), + new(join, loopbackSwitch), + new(b, end), + } + }; + }); + + await _services.PopulateRegistriesAsync(); + var result = await _workflowRunner.RunAsync(workflow); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); + Assert.Equal(new[] { "A", "A", "A", "B" }, lines); + } } \ No newline at end of file