diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index b056f5010..b7c7dc84f 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -12,6 +12,7 @@ using Elsa.Tenants.AspNetCore; using Elsa.Tenants.Extensions; using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Extensions; using Elsa.Workflows; +using Elsa.Workflows.Activities.Flowchart.Extensions; using Elsa.Workflows.Api; using Elsa.Workflows.CommitStates.Strategies; using Elsa.Workflows.IncidentStrategies; @@ -60,6 +61,7 @@ services strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10))); }); }) + .UseFlowchart(flowchart => flowchart.UseCounterBasedExecution()) .UseWorkflowManagement(management => { management.UseEntityFrameworkCore(ef => ef.UseSqlite()); diff --git a/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs b/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs index 818f7787e..5fdb20243 100644 --- a/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs +++ b/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs @@ -20,70 +20,76 @@ namespace Elsa.Testing.Shared; [PublicAPI] public static class RunWorkflowExtensions { - /// - /// Runs a workflow until its end, automatically resuming any bookmark it encounters. - /// /// The services. - /// The ID of the workflow definition. - /// An optional dictionary of input values. - /// An optional correlation id of the workflow. - /// An optional set of options to specify the version of the workflow definition to retrieve. - /// The workflow state. - public static async Task RunWorkflowUntilEndAsync(this IServiceProvider services, - string workflowDefinitionId, - IDictionary? input = null, - string? correlationId = null, - VersionOptions? versionOptions = null) + extension(IServiceProvider services) { - var workflowDefinitionService = services.GetRequiredService(); - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, versionOptions ?? VersionOptions.Published); - - if (workflowGraph == null) - throw new InvalidOperationException($"Workflow definition with ID '{workflowDefinitionId}' not found."); - - var workflowRuntime = services.GetRequiredService(); - var workflowClient = await workflowRuntime.CreateClientAsync(); - var response = await workflowClient.CreateAndRunInstanceAsync(new() + /// + /// Runs a workflow until its end, automatically resuming any bookmark it encounters. + /// + /// The ID of the workflow definition. + /// An optional dictionary of input values. + /// An optional correlation id of the workflow. + /// An optional set of options to specify the version of the workflow definition to retrieve. + /// Optional workflow execution options. + /// The workflow state. + public async Task RunWorkflowUntilEndAsync(string workflowDefinitionId, + IDictionary? input = null, + string? correlationId = null, + VersionOptions? versionOptions = null, + RunWorkflowOptions? runWorkflowOptions = null) { - WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph.Workflow.Identity.Id), - Input = input, - CorrelationId = correlationId - }); - - var bookmarkStore = services.GetRequiredService(); + var workflowDefinitionService = services.GetRequiredService(); + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, versionOptions ?? VersionOptions.Published); - // Continue resuming the workflow for as long as there are bookmarks to resume and the workflow is not Finished. - while (response.Status != WorkflowStatus.Finished) - { - var bookmarks = (await bookmarkStore.FindManyAsync(new() + if (workflowGraph == null) + throw new InvalidOperationException($"Workflow definition with ID '{workflowDefinitionId}' not found."); + + var workflowRuntime = services.GetRequiredService(); + var workflowClient = await workflowRuntime.CreateClientAsync(); + var response = await workflowClient.CreateAndRunInstanceAsync(new() { - WorkflowInstanceId = response.WorkflowInstanceId - })).ToList(); + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph.Workflow.Identity.Id), + Input = input, + CorrelationId = correlationId, + Properties = runWorkflowOptions?.Properties + }); - if (!bookmarks.Any()) - break; + var bookmarkStore = services.GetRequiredService(); - foreach (var bookmark in bookmarks) + // Continue resuming the workflow for as long as there are bookmarks to resume and the workflow is not Finished. + while (response.Status != WorkflowStatus.Finished) { - var runRequest = new RunWorkflowInstanceRequest + var bookmarks = (await bookmarkStore.FindManyAsync(new() { - BookmarkId = bookmark.Id, - Input = input - }; - response = await workflowClient.RunInstanceAsync(runRequest); + WorkflowInstanceId = response.WorkflowInstanceId + })).ToList(); + + if (!bookmarks.Any()) + break; + + foreach (var bookmark in bookmarks) + { + var runRequest = new RunWorkflowInstanceRequest + { + BookmarkId = bookmark.Id, + Input = input, + Properties = runWorkflowOptions?.Properties + }; + response = await workflowClient.RunInstanceAsync(runRequest); + } } + + // Return the workflow state. + return await workflowClient.ExportStateAsync(); } - // Return the workflow state. - return await workflowClient.ExportStateAsync(); - } - - /// - /// Runs a workflow until its end, automatically resuming any bookmark it encounters. - /// - public static async Task RunWorkflowUntilEndAsync(this IServiceProvider services, IDictionary? input = null) where TWorkflow : IWorkflow - { - var workflowDefinitionId = typeof(TWorkflow).Name; - return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input); + /// + /// Runs a workflow until its end, automatically resuming any bookmark it encounters. + /// + public async Task RunWorkflowUntilEndAsync(IDictionary? input = null) where TWorkflow : IWorkflow + { + var workflowDefinitionId = typeof(TWorkflow).Name; + return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input); + } } } \ No newline at end of file diff --git a/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs b/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs index b4dfd6eb8..18cbcfd79 100644 --- a/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs +++ b/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs @@ -20,51 +20,52 @@ namespace Elsa.Testing.Shared; [PublicAPI] public static class ServiceProviderExtensions { - /// - /// Updates the registries. - /// /// The services. - public static Task PopulateRegistriesAsync(this IServiceProvider services) + extension(IServiceProvider services) { - var registriesPopulator = services.GetRequiredService(); - return registriesPopulator.PopulateAsync(); - } - - /// - /// Imports a workflow definition from a file. - /// - /// The services. - /// The file name. - /// The workflow definition. - public static async Task ImportWorkflowDefinitionAsync(this IServiceProvider services, string fileName) - { - var json = await File.ReadAllTextAsync(fileName); - var serializer = services.GetRequiredService(); - var model = serializer.Deserialize(json); - - var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest + /// + /// Updates the registries. + /// + public Task PopulateRegistriesAsync() { - Model = model, - Publish = true - }; + var registriesPopulator = services.GetRequiredService(); + return registriesPopulator.PopulateAsync(); + } - var workflowDefinitionImporter = services.GetRequiredService(); - var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest); - return result.WorkflowDefinition; - } + /// + /// Imports a workflow definition from a file. + /// + /// The file name. + /// The workflow definition. + public async Task ImportWorkflowDefinitionAsync(string fileName) + { + var json = await File.ReadAllTextAsync(fileName); + var serializer = services.GetRequiredService(); + var model = serializer.Deserialize(json); - /// - /// Retrieves a workflow definition by its ID. - /// - /// The service provider. - /// The definition ID of the workflow definition. - /// Options to specify the version of the workflow definition to retrieve. - /// A cancellation token to cancel the operation. - /// The retrieved workflow definition. - public static async Task GetWorkflowDefinitionAsync(this IServiceProvider services, string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default) - { - var workflowDefinitionService = services.GetRequiredService(); - var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken); - return workflowDefinition!; + var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest + { + Model = model, + Publish = true + }; + + var workflowDefinitionImporter = services.GetRequiredService(); + var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest); + return result.WorkflowDefinition; + } + + /// + /// Retrieves a workflow definition by its ID. + /// + /// The definition ID of the workflow definition. + /// Options to specify the version of the workflow definition to retrieve. + /// A cancellation token to cancel the operation. + /// The retrieved workflow definition. + public async Task GetWorkflowDefinitionAsync(string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default) + { + var workflowDefinitionService = services.GetRequiredService(); + var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken); + return workflowDefinition!; + } } } \ No newline at end of file diff --git a/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs b/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs index 6e4c9ecda..a85e765db 100644 --- a/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs +++ b/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs @@ -2,8 +2,10 @@ using Elsa.Expressions.Models; using Elsa.Features.Services; using Elsa.Workflows; using Elsa.Workflows.Activities; +using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; +using Elsa.Workflows.Options; using Elsa.Workflows.State; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; @@ -102,6 +104,9 @@ public class WorkflowTestFixture /// public async Task BuildAsync() { + if (_services != null) + return this; + _services = _testApplicationBuilder.Build(); await Services.PopulateRegistriesAsync(); return this; @@ -116,12 +121,52 @@ public class WorkflowTestFixture /// The workflow result after execution public async Task RunWorkflowAsync(IWorkflow workflow, CancellationToken cancellationToken = default) { - if (_services == null) - await BuildAsync(); - + await BuildAsync(); var workflowRunner = Services.GetRequiredService(); return await workflowRunner.RunAsync(workflow, cancellationToken: cancellationToken); } + + /// + /// Runs the specified workflow and returns the workflow result. + /// Automatically builds the fixture if not already built. + /// + /// Cancellation token + /// The workflow result after execution + public async Task RunWorkflowAsync(CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new() + { + await BuildAsync(); + var workflowRunner = Services.GetRequiredService(); + return await workflowRunner.RunAsync(cancellationToken: cancellationToken); + } + + /// + /// Runs a workflow with the specified options and returns the workflow result. + /// Automatically builds the fixture if not already built. + /// + /// The workflow to run + /// Workflow execution options + /// Cancellation token + /// The workflow result after execution + public async Task RunWorkflowAsync(IWorkflow workflow, RunWorkflowOptions options, CancellationToken cancellationToken = default) + { + await BuildAsync(); + var workflowRunner = Services.GetRequiredService(); + return await workflowRunner.RunAsync(workflow, options, cancellationToken); + } + + /// + /// Runs the specified workflow with the specified options and returns the workflow result. + /// Automatically builds the fixture if not already built. + /// + /// Workflow execution options + /// Cancellation token + /// The workflow result after execution + public async Task RunWorkflowAsync(RunWorkflowOptions options, CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new() + { + await BuildAsync(); + var workflowRunner = Services.GetRequiredService(); + return await workflowRunner.RunAsync(options, cancellationToken); + } /// /// Runs an activity wrapped in a workflow and returns the workflow result. @@ -132,26 +177,45 @@ public class WorkflowTestFixture /// The workflow result after execution public async Task RunActivityAsync(IActivity activity, CancellationToken cancellationToken = default) { - if (_services == null) - await BuildAsync(); - + await BuildAsync(); var workflowRunner = Services.GetRequiredService(); return await workflowRunner.RunAsync(activity, cancellationToken: cancellationToken); } + /// + /// Runs an activity wrapped in a workflow with the specified options and returns the workflow result. + /// Automatically builds the fixture if not already built. + /// + /// The activity to run + /// Workflow execution options + /// Cancellation token + /// The workflow result after execution + public async Task RunActivityAsync(IActivity activity, RunWorkflowOptions options, CancellationToken cancellationToken = default) + { + await BuildAsync(); + var workflowRunner = Services.GetRequiredService(); + return await workflowRunner.RunAsync(activity, options, cancellationToken); + } + /// /// Runs a workflow by definition ID and returns the workflow state. /// Automatically builds the fixture if not already built. /// /// The workflow definition ID /// Optional input dictionary + /// Optional workflow execution options /// The workflow state after execution - public async Task RunWorkflowAsync(string definitionId, IDictionary? input = null) + public async Task RunWorkflowAsync(string definitionId, IDictionary? input = null, RunWorkflowOptions? options = null) { - if (_services == null) - await BuildAsync(); + + await BuildAsync(); + return await Services.RunWorkflowUntilEndAsync(definitionId, input, runWorkflowOptions: options); + } - return await Services.RunWorkflowUntilEndAsync(definitionId, input); + public async Task ImportWorkflowDefinitionAsync(string fileName) + { + await BuildAsync(); + return await Services.ImportWorkflowDefinitionAsync(fileName); } /// diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 8fa1c5d06..08fee2028 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -1,8 +1,11 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Activities.Flowchart.Options; using Elsa.Workflows.Attributes; using Elsa.Workflows.Signals; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Elsa.Workflows.Activities.Flowchart.Activities; @@ -14,9 +17,17 @@ namespace Elsa.Workflows.Activities.Flowchart.Activities; public partial class Flowchart : Container { /// - /// Set this to false from your program file in case you wish to use the old counter based model. + /// The property key used to store the flowchart execution mode in . /// - public static bool UseTokenFlow = true; + public const string ExecutionModePropertyKey = "Flowchart:ExecutionMode"; + + /// + /// Set this to false from your program file in case you wish to use the old counter based model. + /// This static field is used as a final fallback when no execution mode is specified via options or workflow execution context properties. + /// Note: Prefer using configured via DI for application-wide settings. + /// + // ReSharper disable once GrammarMistakeInComment + public static bool UseTokenFlow = false; // Default to false to maintain the same behavior with 3.5.2 out of the box. /// public Flowchart([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) @@ -29,7 +40,7 @@ public partial class Flowchart : Container /// /// The activity to execute when the flowchart starts. /// - [Port][Browsable(false)] public IActivity? Start { get; set; } + [Port] [Browsable(false)] public IActivity? Start { get; set; } /// /// A list of connections between activities. @@ -78,16 +89,18 @@ public partial class Flowchart : Container private ValueTask OnChildCompletedAsync(ActivityCompletedContext context) { - return UseTokenFlow - ? OnChildCompletedTokenBasedLogicAsync(context) - : OnChildCompletedCounterBasedLogicAsync(context); + return ExecuteBasedOnMode( + context.TargetContext, + () => OnChildCompletedTokenBasedLogicAsync(context), + () => OnChildCompletedCounterBasedLogicAsync(context)); } private ValueTask OnActivityCanceledAsync(CancelSignal signal, SignalContext context) { - return UseTokenFlow - ? OnTokenFlowActivityCanceledAsync(signal, context) - : OnCounterFlowActivityCanceledAsync(signal, context); + return ExecuteBasedOnMode( + context.ReceiverActivityExecutionContext, + () => OnTokenFlowActivityCanceledAsync(signal, context), + () => OnCounterFlowActivityCanceledAsync(signal, context)); } private async Task CompleteIfNoPendingWorkAsync(ActivityExecutionContext context) @@ -104,4 +117,50 @@ public partial class Flowchart : Container } } } + + private static ValueTask ExecuteBasedOnMode(ActivityExecutionContext context, Func tokenBasedAction, Func counterBasedAction) + { + var mode = GetEffectiveExecutionMode(context); + + return mode switch + { + FlowchartExecutionMode.TokenBased => tokenBasedAction(), + FlowchartExecutionMode.CounterBased or FlowchartExecutionMode.Default or _ => counterBasedAction() + }; + } + + /// + /// Gets the effective execution mode for this flowchart execution. + /// Priority: WorkflowExecutionContext.Properties > FlowchartOptions (DI) > Static UseTokenFlow flag + /// + private static FlowchartExecutionMode GetEffectiveExecutionMode(ActivityExecutionContext context) + { + var workflowExecutionContext = context.WorkflowExecutionContext; + + if (!workflowExecutionContext.Properties.TryGetValue(ExecutionModePropertyKey, out var modeValue)) + return GetDefaultModeFromOptions(context); + + var mode = ParseExecutionMode(modeValue); + return mode != FlowchartExecutionMode.Default ? mode : GetDefaultModeFromOptions(context); + } + + private static FlowchartExecutionMode ParseExecutionMode(object modeValue) + { + return modeValue switch + { + FlowchartExecutionMode executionMode => executionMode, + string str when Enum.TryParse(str, true, out var parsed) => parsed, + int intValue when Enum.IsDefined(typeof(FlowchartExecutionMode), intValue) => (FlowchartExecutionMode)intValue, + _ => FlowchartExecutionMode.Default + }; + } + + private static FlowchartExecutionMode GetDefaultModeFromOptions(ActivityExecutionContext context) + { + var options = context.WorkflowExecutionContext.ServiceProvider.GetService>(); + var mode = options?.Value.DefaultExecutionMode ?? FlowchartExecutionMode.Default; + if (mode == FlowchartExecutionMode.Default) + return UseTokenFlow ? FlowchartExecutionMode.TokenBased : FlowchartExecutionMode.CounterBased; + return mode; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/FlowchartFeatureExtensions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/FlowchartFeatureExtensions.cs new file mode 100644 index 000000000..dae0c3af6 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/FlowchartFeatureExtensions.cs @@ -0,0 +1,47 @@ +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Activities.Flowchart.Options; +using Elsa.Workflows.Features; + +namespace Elsa.Workflows.Activities.Flowchart.Extensions; + +/// +/// Extension methods for . +/// +public static class FlowchartFeatureExtensions +{ + extension(FlowchartFeature feature) + { + /// + /// Configures the flowchart options. + /// + public FlowchartFeature ConfigureFlowchart(Action configure) + { + feature.FlowchartOptionsConfigurator = configure; + return feature; + } + + /// + /// Sets the default execution mode for flowcharts to token-based. + /// + public FlowchartFeature UseTokenBasedExecution() + { + return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.TokenBased); + } + + /// + /// Sets the default execution mode for flowcharts to counter-based (legacy mode). + /// + public FlowchartFeature UseCounterBasedExecution() + { + return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.CounterBased); + } + + /// + /// Sets the default execution mode for flowcharts to the specified mode. + /// + public FlowchartFeature UseExecution(FlowchartExecutionMode mode) + { + return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = mode); + } + } +} diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/RunWorkflowOptionsExtensions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/RunWorkflowOptionsExtensions.cs new file mode 100644 index 000000000..fce338735 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/RunWorkflowOptionsExtensions.cs @@ -0,0 +1,39 @@ +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Options; + +namespace Elsa.Workflows.Activities.Flowchart.Extensions; + +/// +/// Extension methods for to configure flowchart execution mode. +/// +public static class RunWorkflowOptionsExtensions +{ + extension(RunWorkflowOptions options) + { + /// + /// Sets the flowchart execution mode to token-based. + /// + public RunWorkflowOptions WithTokenBasedFlowchart() + { + return options.WithFlowchartExecutionMode(FlowchartExecutionMode.TokenBased); + } + + /// + /// Sets the flowchart execution mode to counter-based (legacy mode). + /// + public RunWorkflowOptions WithCounterBasedFlowchart() + { + return options.WithFlowchartExecutionMode(FlowchartExecutionMode.CounterBased); + } + + /// + /// Sets the flowchart execution mode. + /// + public RunWorkflowOptions WithFlowchartExecutionMode(FlowchartExecutionMode mode) + { + options.Properties ??= new Dictionary(); + options.Properties[Activities.Flowchart.ExecutionModePropertyKey] = mode; + return options; + } + } +} diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs new file mode 100644 index 000000000..0368f0c70 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs @@ -0,0 +1,22 @@ +namespace Elsa.Workflows.Activities.Flowchart.Models; + +/// +/// Specifies the execution mode for flowchart activities. +/// +public enum FlowchartExecutionMode +{ + /// + /// Use the default mode as specified by . + /// + Default = 0, + + /// + /// Use token-based flow logic. + /// + TokenBased = 1, + + /// + /// Use counter-based flow logic (legacy mode). + /// + CounterBased = 2 +} diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/FlowchartOptions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/FlowchartOptions.cs new file mode 100644 index 000000000..db1e92d62 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/FlowchartOptions.cs @@ -0,0 +1,15 @@ +using Elsa.Workflows.Activities.Flowchart.Models; + +namespace Elsa.Workflows.Activities.Flowchart.Options; + +/// +/// Options for configuring flowchart execution behavior. +/// +public class FlowchartOptions +{ + /// + /// Gets or sets the default execution mode for flowcharts when not explicitly specified. + /// Defaults to . + /// + public FlowchartExecutionMode DefaultExecutionMode { get; set; } = FlowchartExecutionMode.CounterBased; // Default to counter-based in order to maintain the same behavior with 3.5.2 out of the box. +} diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs index ccd559f5c..e456d0d39 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs @@ -6,9 +6,18 @@ namespace Elsa.Extensions; public static class ModuleExtensions { - public static IModule UseWorkflows(this IModule configuration, Action? configure = default) + extension(IModule configuration) { - configuration.Configure(configure); - return configuration; + public IModule UseWorkflows(Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } + + public IModule UseFlowchart(Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs b/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs index f489487bd..ab3a000a2 100644 --- a/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs @@ -2,7 +2,9 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Activities.Flowchart.Options; using Elsa.Workflows.Activities.Flowchart.Serialization; +using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Features; @@ -16,11 +18,21 @@ public class FlowchartFeature : FeatureBase { } + /// + /// A delegate to configure . + /// + public Action? FlowchartOptionsConfigurator { get; set; } + /// public override void Apply() { Services.AddSerializationOptionsConfigurator(); - + + // Register FlowchartOptions + Services.AddOptions(); + + if (FlowchartOptionsConfigurator != null) + Services.Configure(FlowchartOptionsConfigurator); } public override void Configure() diff --git a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs index 1853ca250..81f9b0dc8 100644 --- a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs +++ b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs @@ -1,6 +1,9 @@ using Elsa.Testing.Shared; using Elsa.Workflows.Activities; using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Extensions; +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Options; using Xunit.Abstractions; namespace Elsa.Activities.IntegrationTests.Branching; @@ -9,23 +12,15 @@ namespace Elsa.Activities.IntegrationTests.Branching; /// Integration tests for FlowDecision activity in flowchart scenarios. /// [Collection("FlowchartTests")] -public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable +public class FlowDecisionTests(ITestOutputHelper testOutputHelper) { private readonly WorkflowTestFixture _fixture = new(testOutputHelper); - private readonly bool _originalFlowMode = Flowchart.UseTokenFlow; - - public void Dispose() - { - Flowchart.UseTokenFlow = _originalFlowMode; - } [Theory(DisplayName = "FlowDecision follows correct path based on condition")] [MemberData(nameof(BasicPathTestCases))] - public async Task Should_Follow_Correct_Path_Based_On_Condition(bool useTokenFlow, bool condition, string[] expectedOutputs, string[] unexpectedOutputs) + public async Task Should_Follow_Correct_Path_Based_On_Condition(FlowchartExecutionMode executionMode, bool condition, string[] expectedOutputs, string[] unexpectedOutputs) { // Arrange - Flowchart.UseTokenFlow = useTokenFlow; - var start = new WriteLine("Start"); var decision = new FlowDecision(ctx => condition); var truePath = new WriteLine("TruePath"); @@ -43,8 +38,10 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable } }; + var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode); + // Act - await _fixture.RunActivityAsync(flowchart); + await _fixture.RunActivityAsync(flowchart, options); // Assert AssertOutputs(expectedOutputs, unexpectedOutputs); @@ -53,19 +50,17 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable public static IEnumerable BasicPathTestCases() { // useTokenFlow, condition, expectedOutputs, unexpectedOutputs - yield return [true, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }]; - yield return [true, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }]; - yield return [false, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }]; - yield return [false, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }]; + yield return [FlowchartExecutionMode.TokenBased, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }]; + yield return [FlowchartExecutionMode.TokenBased, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }]; + yield return [FlowchartExecutionMode.CounterBased, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }]; + yield return [FlowchartExecutionMode.CounterBased, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }]; } [Theory(DisplayName = "FlowDecision handles nested decisions")] [MemberData(nameof(NestedDecisionTestCases))] - public async Task Should_Handle_Nested_Decisions(bool useTokenFlow, bool outerCondition, bool innerCondition, string[] expectedOutputs, string[] unexpectedOutputs) + public async Task Should_Handle_Nested_Decisions(FlowchartExecutionMode executionMode, bool outerCondition, bool innerCondition, string[] expectedOutputs, string[] unexpectedOutputs) { // Arrange - Flowchart.UseTokenFlow = useTokenFlow; - var start = new WriteLine("Start"); var outerDecision = new FlowDecision(ctx => outerCondition); var innerDecision = new FlowDecision(ctx => innerCondition); @@ -87,8 +82,10 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable } }; + var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode); + // Act - await _fixture.RunActivityAsync(flowchart); + await _fixture.RunActivityAsync(flowchart, options); // Assert AssertOutputs(expectedOutputs, unexpectedOutputs); @@ -97,23 +94,21 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable public static IEnumerable NestedDecisionTestCases() { // useTokenFlow, outerCondition, innerCondition, expectedOutputs, unexpectedOutputs - yield return [true, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }]; - yield return [true, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }]; - yield return [true, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; - yield return [true, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; - yield return [false, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }]; - yield return [false, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }]; - yield return [false, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; - yield return [false, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; + yield return [FlowchartExecutionMode.TokenBased, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }]; + yield return [FlowchartExecutionMode.TokenBased, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }]; + yield return [FlowchartExecutionMode.TokenBased, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; + yield return [FlowchartExecutionMode.TokenBased, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; + yield return [FlowchartExecutionMode.CounterBased, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }]; + yield return [FlowchartExecutionMode.CounterBased, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }]; + yield return [FlowchartExecutionMode.CounterBased, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; + yield return [FlowchartExecutionMode.CounterBased, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }]; } [Theory(DisplayName = "FlowDecision works with only one path connected")] [MemberData(nameof(OnePathConnectedTestCases))] - public async Task Should_Work_With_Only_One_Path_Connected(bool useTokenFlow, bool condition, string[] expectedOutputs, string[] unexpectedOutputs) + public async Task Should_Work_With_Only_One_Path_Connected(FlowchartExecutionMode executionMode, bool condition, string[] expectedOutputs, string[] unexpectedOutputs) { // Arrange - Flowchart.UseTokenFlow = useTokenFlow; - var start = new WriteLine("Start"); var decision = new FlowDecision(ctx => condition); var truePath = new WriteLine("TruePath"); @@ -132,8 +127,10 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable } }; + var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode); + // Act - await _fixture.RunActivityAsync(flowchart); + await _fixture.RunActivityAsync(flowchart, options); // Assert AssertOutputs(expectedOutputs, unexpectedOutputs); @@ -142,19 +139,17 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable public static IEnumerable OnePathConnectedTestCases() { // useTokenFlow, condition, expectedOutputs, unexpectedOutputs - yield return [true, true, new[] { "Start", "TruePath", "End" }, Array.Empty()]; - yield return [true, false, new[] { "Start" }, new[] { "TruePath", "End" }]; - yield return [false, true, new[] { "Start", "TruePath", "End" }, Array.Empty()]; - yield return [false, false, new[] { "Start" }, new[] { "TruePath", "End" }]; + yield return [FlowchartExecutionMode.TokenBased, true, new[] { "Start", "TruePath", "End" }, Array.Empty()]; + yield return [FlowchartExecutionMode.TokenBased, false, new[] { "Start" }, new[] { "TruePath", "End" }]; + yield return [FlowchartExecutionMode.CounterBased, true, new[] { "Start", "TruePath", "End" }, Array.Empty()]; + yield return [FlowchartExecutionMode.CounterBased, false, new[] { "Start" }, new[] { "TruePath", "End" }]; } [Theory(DisplayName = "FlowDecision converges paths correctly")] [MemberData(nameof(ConvergePathsTestCases))] - public async Task Should_Converge_Paths_Correctly(bool useTokenFlow, bool condition, string[] expectedOutputs, string[] unexpectedOutputs) + public async Task Should_Converge_Paths_Correctly(FlowchartExecutionMode executionMode, bool condition, string[] expectedOutputs, string[] unexpectedOutputs) { // Arrange - Flowchart.UseTokenFlow = useTokenFlow; - var start = new WriteLine("Start"); var decision = new FlowDecision(ctx => condition); var truePath = new WriteLine("TruePath"); @@ -175,8 +170,10 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable } }; + var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode); + // Act - await _fixture.RunActivityAsync(flowchart); + await _fixture.RunActivityAsync(flowchart, options); // Assert AssertOutputs(expectedOutputs, unexpectedOutputs); @@ -185,22 +182,15 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable public static IEnumerable ConvergePathsTestCases() { // useTokenFlow, condition, expectedOutputs, unexpectedOutputs - yield return [true, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }]; - yield return [true, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }]; - yield return [false, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }]; - yield return [false, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }]; + yield return [FlowchartExecutionMode.TokenBased, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }]; + yield return [FlowchartExecutionMode.TokenBased, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }]; + yield return [FlowchartExecutionMode.CounterBased, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }]; + yield return [FlowchartExecutionMode.CounterBased, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }]; } private void AssertOutputs(string[] expectedOutputs, string[] unexpectedOutputs) { - foreach (var expected in expectedOutputs) - { - Assert.Contains(expected, _fixture.CapturingTextWriter.Lines); - } - - foreach (var unexpected in unexpectedOutputs) - { - Assert.DoesNotContain(unexpected, _fixture.CapturingTextWriter.Lines); - } + foreach (var expected in expectedOutputs) Assert.Contains(expected, _fixture.CapturingTextWriter.Lines); + foreach (var unexpected in unexpectedOutputs) Assert.DoesNotContain(unexpected, _fixture.CapturingTextWriter.Lines); } } diff --git a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs index faaaefdae..2f0d4b022 100644 --- a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs +++ b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs @@ -10,22 +10,15 @@ namespace Elsa.Activities.IntegrationTests.Branching; /// /// Integration tests for FlowJoin activity in complex flowchart scenarios. /// -public class FlowJoinTests : IDisposable +public class FlowJoinTests { private readonly IServiceProvider _services; private readonly CapturingTextWriter _output; - private readonly bool _originalFlowMode; public FlowJoinTests(ITestOutputHelper testOutputHelper) { _output = new(); _services = CreateServiceProvider(testOutputHelper, _output); - _originalFlowMode = Flowchart.UseTokenFlow; - } - - public void Dispose() - { - Flowchart.UseTokenFlow = _originalFlowMode; } [Theory] @@ -37,17 +30,17 @@ public class FlowJoinTests : IDisposable { // Test with a more complex flowchart that has multiple activities // Arrange - Flowchart.UseTokenFlow = useTokenFlow; + var executionMode = useTokenFlow ? FlowchartExecutionMode.TokenBased : FlowchartExecutionMode.CounterBased; var startActivity = new WriteLine("Start"); var flowJoin = new FlowJoin { Mode = new(joinMode) }; var afterJoin = new WriteLine("AfterJoin"); - + var flowchart = new Flowchart { Start = startActivity, Activities = { startActivity, flowJoin, afterJoin }, - Connections = + Connections = { new() { Source = new(startActivity, "Done"), Target = new(flowJoin) }, new() { Source = new(flowJoin, "Done"), Target = new(afterJoin) } @@ -55,7 +48,7 @@ public class FlowJoinTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, executionMode); // Assert Assert.Contains("Start", _output.Lines); @@ -82,7 +75,7 @@ public class FlowJoinTests : IDisposable { // Test FlowJoin with a Fork-Join pattern // Arrange - Flowchart.UseTokenFlow = useTokenFlow; + var executionMode = useTokenFlow ? FlowchartExecutionMode.TokenBased : FlowchartExecutionMode.CounterBased; var start = new WriteLine("Start"); var fork = new FlowFork { Branches = new(["Branch1", "Branch2"]) }; @@ -107,7 +100,7 @@ public class FlowJoinTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, executionMode); // Assert Assert.Contains("Start", _output.Lines); @@ -149,7 +142,7 @@ public class FlowJoinTests : IDisposable { // Test multiple FlowJoin activities in a complex flowchart // Arrange - Flowchart.UseTokenFlow = useTokenFlow; + var executionMode = useTokenFlow ? FlowchartExecutionMode.TokenBased : FlowchartExecutionMode.CounterBased; var start = new WriteLine("Start"); var fork1 = new FlowFork { Branches = new(["A", "B"]) }; @@ -185,7 +178,7 @@ public class FlowJoinTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, executionMode); // Assert Assert.Contains("Start", _output.Lines); diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs index 0623897de..fbcc4094c 100644 --- a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs +++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs @@ -11,23 +11,15 @@ namespace Elsa.Activities.IntegrationTests.Flow; /// Integration tests for counter-based flowchart execution strategy. /// [Collection("FlowchartTests")] -public class FlowchartCounterBasedTests : IDisposable +public class FlowchartCounterBasedTests { private readonly IServiceProvider _services; private readonly CapturingTextWriter _output; - private readonly bool _originalFlowMode; public FlowchartCounterBasedTests(ITestOutputHelper testOutputHelper) { _output = new(); _services = CreateServiceProvider(testOutputHelper, _output); - _originalFlowMode = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = false; - } - - public void Dispose() - { - Flowchart.UseTokenFlow = _originalFlowMode; } [Fact(DisplayName = "Executes simple linear flowchart")] @@ -41,7 +33,7 @@ public class FlowchartCounterBasedTests : IDisposable ); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Equal(3, _output.Lines.Count); @@ -60,7 +52,7 @@ public class FlowchartCounterBasedTests : IDisposable var flowchart = CreateBranchingFlowchart(start, branch1, branch2); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Equal(3, _output.Lines.Count); @@ -81,7 +73,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Single(_output.Lines); @@ -98,7 +90,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - var result = await RunFlowchartAsync(_services, flowchart); + var result = await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.NotNull(result); @@ -122,7 +114,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Single(_output.Lines); @@ -154,7 +146,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Contains("Start", _output.Lines); @@ -188,7 +180,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Contains("Start", _output.Lines); @@ -229,7 +221,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Contains("Start", _output.Lines); @@ -269,7 +261,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Contains("Start", _output.Lines); @@ -292,7 +284,7 @@ public class FlowchartCounterBasedTests : IDisposable ); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Equal(4, _output.Lines.Count); @@ -342,7 +334,7 @@ public class FlowchartCounterBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased); // Assert Assert.Single(_output.Lines); diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs index c5ee60959..db8384904 100644 --- a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs +++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs @@ -1,8 +1,10 @@ using Elsa.Testing.Shared; using Elsa.Workflows; using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Extensions; using Elsa.Workflows.Activities.Flowchart.Models; using Elsa.Workflows.Models; +using Elsa.Workflows.Options; using Xunit.Abstractions; namespace Elsa.Activities.IntegrationTests.Flow; @@ -20,9 +22,13 @@ public static class FlowchartTestHelpers return builder.Build(); } - public static async Task RunFlowchartAsync(IServiceProvider services, Flowchart flowchart) + public static async Task RunFlowchartAsync(IServiceProvider services, Flowchart flowchart, FlowchartExecutionMode? executionMode = null) { - return await services.RunActivityAsync(flowchart); + var options = executionMode.HasValue + ? new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode.Value) + : null; + + return await services.RunActivityAsync(flowchart, options); } public static Connection CreateConnection(IActivity source, IActivity target, string? outcome = "Done") diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs index b62d27882..e6ff586e9 100644 --- a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs +++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs @@ -12,23 +12,15 @@ namespace Elsa.Activities.IntegrationTests.Flow; /// Integration tests for token-based flowchart execution strategy. /// [Collection("FlowchartTests")] -public class FlowchartTokenBasedTests : IDisposable +public class FlowchartTokenBasedTests { private readonly IServiceProvider _services; private readonly CapturingTextWriter _output; - private readonly bool _originalFlowMode; public FlowchartTokenBasedTests(ITestOutputHelper testOutputHelper) { _output = new(); _services = CreateServiceProvider(testOutputHelper, _output); - _originalFlowMode = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = true; - } - - public void Dispose() - { - Flowchart.UseTokenFlow = _originalFlowMode; } [Fact(DisplayName = "Executes simple linear flowchart")] @@ -42,7 +34,7 @@ public class FlowchartTokenBasedTests : IDisposable ); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Equal(3, _output.Lines.Count); @@ -61,7 +53,7 @@ public class FlowchartTokenBasedTests : IDisposable var flowchart = CreateBranchingFlowchart(start, branch1, branch2); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Equal(3, _output.Lines.Count); @@ -82,7 +74,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Single(_output.Lines); @@ -99,7 +91,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - var result = await RunFlowchartAsync(_services, flowchart); + var result = await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.NotNull(result); @@ -123,7 +115,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Single(_output.Lines); @@ -154,7 +146,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -186,7 +178,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -220,7 +212,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -254,7 +246,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -274,7 +266,7 @@ public class FlowchartTokenBasedTests : IDisposable var flowchart = CreateSimpleLinearFlowchart(start, middle, end); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert // Tokens should be consumed after each activity completes @@ -318,7 +310,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -361,7 +353,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -385,7 +377,7 @@ public class FlowchartTokenBasedTests : IDisposable ); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Equal(4, _output.Lines.Count); @@ -435,7 +427,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Single(_output.Lines); @@ -477,7 +469,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -498,7 +490,7 @@ public class FlowchartTokenBasedTests : IDisposable var flowchart = CreateSimpleLinearFlowchart(start, single, end); // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert Assert.Contains("Start", _output.Lines); @@ -532,7 +524,7 @@ public class FlowchartTokenBasedTests : IDisposable }; // Act - await RunFlowchartAsync(_services, flowchart); + await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased); // Assert // Verify all activities executed in a valid order diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs index 1cb751446..b630e2858 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs @@ -2,40 +2,25 @@ using Elsa.Expressions.Models; using Elsa.Testing.Shared; using Elsa.Workflows.Activities; using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Extensions; using Elsa.Workflows.Activities.Flowchart.Models; using Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity.Workflows; using Elsa.Workflows.Memory; -using Microsoft.Extensions.DependencyInjection; +using Elsa.Workflows.Options; using Xunit.Abstractions; namespace Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity; -public class FlowchartNextActivityTests +public class FlowchartNextActivityTests(ITestOutputHelper testOutputHelper) { - 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(); - } + private readonly WorkflowTestFixture _fixture = new WorkflowTestFixture(testOutputHelper).AddActivitiesFrom(); [Fact(DisplayName = "Flowchart only schedules next activity connected to outcome of previous activity.")] - public async Task Test1() + public async Task FlowchartOnlySchedulesNextConnectedActivity() { - await _services.PopulateRegistriesAsync(); - await _workflowRunner.RunAsync(); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(new[] - { - "Line 1" - }, lines); + await _fixture.RunWorkflowAsync(); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); + Assert.Equal(["Line 1"], lines); } [Fact(DisplayName = "Flowchart with backward connections and a dangling activity")] @@ -117,9 +102,8 @@ public class FlowchartNextActivityTests }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); + var result = await _fixture.RunWorkflowAsync(workflow); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); Assert.Equal(new[] { @@ -127,12 +111,10 @@ public class FlowchartNextActivityTests }, lines); } - [Fact(DisplayName = "Flowchart with an invalid backward connection")] + [Fact(DisplayName = "Flowchart with an invalid backward connection (counter-based mode only)")] public async Task InvalidBackwardConnectionTest() { - if(Flowchart.UseTokenFlow) - return; - + // This test is only valid for counter-based mode var workflow = new TestWorkflow(workflowBuilder => { var start = new Start @@ -184,9 +166,9 @@ public class FlowchartNextActivityTests }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); + var options = new RunWorkflowOptions().WithCounterBasedFlowchart(); + var result = await _fixture.RunWorkflowAsync(workflow, options); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowState.SubStatus); Assert.Single(result.WorkflowState.Incidents); 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); @@ -222,13 +204,14 @@ public class FlowchartNextActivityTests }; var loopbackDecision = new FlowSwitch { - Cases = { + Cases = + { new("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")), }, Mode = new(SwitchMode.MatchFirst) }; var end = new End(); - + workflowBuilder.Root = new Flowchart { Variables = @@ -257,18 +240,20 @@ public class FlowchartNextActivityTests new(c, join), new(d, join), new(join, incrementLoop), - new(incrementLoop,loopbackDecision), + new(incrementLoop, loopbackDecision), new(new(loopbackDecision, "LessThanThree"), new Endpoint(a)), new(new(loopbackDecision, "Default"), new Endpoint(end)), } }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); + var result = await _fixture.RunWorkflowAsync(workflow); + var lines = _fixture.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); + 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")] @@ -284,7 +269,8 @@ public class FlowchartNextActivityTests var start = new Start(); var loopbackSwitch = new FlowSwitch { - Cases = { + Cases = + { new("DoLoopback", new Expression("JavaScript", "getVariable('LoopCount') < 3")), }, Mode = new(SwitchMode.MatchFirst) @@ -301,7 +287,7 @@ public class FlowchartNextActivityTests }; var b = new WriteLine("B"); var end = new End(); - + workflowBuilder.Root = new Flowchart { Variables = @@ -332,9 +318,8 @@ public class FlowchartNextActivityTests }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); + var result = await _fixture.RunWorkflowAsync(workflow); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); Assert.Equal(new[] { @@ -342,13 +327,31 @@ public class FlowchartNextActivityTests }, lines); } - [Theory(DisplayName = "Flowchart Join behaves correctly")] - [InlineData(false, FlowJoinMode.WaitAll, new[] { "A", "B", "C", "D", "F" })] // "E" is not scheduled because join has an unfollowed inbound connection - [InlineData(false, FlowJoinMode.WaitAllActive, new[] { "A", "B", "C", "D", "E", "F" })] // "E" gets scheduled by join with an unfollowed inbound connection - [InlineData(false, FlowJoinMode.WaitAny, new[] { "A", "B", "C", "D", "E", "F" })] // "E" only scheduled once - [InlineData(true, FlowJoinMode.WaitAll, new[] { "A", "B", "C", "E", "F" })] // all Join inbound connections followed, "E" gets scheduled - [InlineData(true, FlowJoinMode.WaitAllActive, new[] { "A", "B", "C", "E", "F" })] // all Join inbound connections followed, "E" gets scheduled - [InlineData(true, FlowJoinMode.WaitAny, new[] { "A", "B", "C", "E", "F" })] // "E" only scheduled once + [Theory(DisplayName = "Flowchart Join behaves correctly (counter-based mode)")] + [InlineData(false, FlowJoinMode.WaitAll, new[] + { + "A", "B", "C", "D", "F" + })] // "E" is not scheduled because join has an unfollowed inbound connection + [InlineData(false, FlowJoinMode.WaitAllActive, new[] + { + "A", "B", "C", "D", "E", "F" + })] // "E" gets scheduled by join with an unfollowed inbound connection + [InlineData(false, FlowJoinMode.WaitAny, new[] + { + "A", "B", "C", "D", "E", "F" + })] // "E" only scheduled once + [InlineData(true, FlowJoinMode.WaitAll, new[] + { + "A", "B", "C", "E", "F" + })] // all Join inbound connections followed, "E" gets scheduled + [InlineData(true, FlowJoinMode.WaitAllActive, new[] + { + "A", "B", "C", "E", "F" + })] // all Join inbound connections followed, "E" gets scheduled + [InlineData(true, FlowJoinMode.WaitAny, new[] + { + "A", "B", "C", "E", "F" + })] // "E" only scheduled once // Start // / | \ // / | \ @@ -362,54 +365,75 @@ public class FlowchartNextActivityTests // (false) \ | / // | Join // D | - // \ E + // \ E // \ / // \ / - // \ / + // \ / // F public async Task JoinBehavesCorrectly(bool decisionResult, FlowJoinMode joinMode, string[] expectedLines) { - Flowchart.UseTokenFlow = false; + // This test validates counter-based mode behavior var workflow = new TestWorkflow(workflowBuilder => { - var start = new Start() { Id = "Start" }; - var a = new WriteLine("A") { Id = "WriteLineA" }; - var b = new WriteLine("B") { Id = "WriteLineB" }; - var c = new WriteLine("C") { Id = "WriteLineC" }; + var start = new Start() + { + Id = "Start" + }; + var a = new WriteLine("A") + { + Id = "WriteLineA" + }; + var b = new WriteLine("B") + { + Id = "WriteLineB" + }; + var c = new WriteLine("C") + { + Id = "WriteLineC" + }; var decision = new FlowDecision() { Condition = new(new Literal(decisionResult)) }; - var d = new WriteLine("D") { Id = "WriteLineD" }; + var d = new WriteLine("D") + { + Id = "WriteLineD" + }; var join = new FlowJoin() { Mode = new(joinMode) }; - var e = new WriteLine("E") { Id = "WriteLineE" }; - var f = new WriteLine("F") { Id = "WriteLineF" }; + var e = new WriteLine("E") + { + Id = "WriteLineE" + }; + var f = new WriteLine("F") + { + Id = "WriteLineF" + }; workflowBuilder.Root = new Flowchart { Activities = - { - start, - a, - b, - c, - decision, - d, - join, - e, - f, - }, + { + start, + a, + b, + c, + decision, + d, + join, + e, + f, + }, Connections = { new(start, a), new(start, b), new(start, c), new(a, decision), - new(new Endpoint(decision, "True"), new Endpoint(join)), - new(new Endpoint(decision, "False"), new Endpoint(d)), + new(new(decision, "True"), new Endpoint(join)), + new(new(decision, "False"), new Endpoint(d)), new(b, join), new(c, join), new(d, f), @@ -419,11 +443,10 @@ public class FlowchartNextActivityTests }; }); - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(workflow); - var lines = _capturingTextWriter.Lines.ToList(); + var options = new RunWorkflowOptions().WithCounterBasedFlowchart(); + var result = await _fixture.RunWorkflowAsync(workflow, options); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); Assert.Equal(expectedLines, lines); - Flowchart.UseTokenFlow = true; } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs index 5c77b115e..640cde2d1 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs @@ -1,36 +1,29 @@ using Elsa.Testing.Shared; +using Elsa.Workflows.Activities.Flowchart.Extensions; using Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows; -using Microsoft.Extensions.DependencyInjection; +using Elsa.Workflows.Options; using Xunit.Abstractions; namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors; -public class BraidedWorkflowTests +public class BraidedWorkflowTests(ITestOutputHelper testOutputHelper) { - private readonly IWorkflowRunner _workflowRunner; - private readonly CapturingTextWriter _capturingTextWriter = new(); - private readonly IServiceProvider _services; - - public BraidedWorkflowTests(ITestOutputHelper testOutputHelper) - { - _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); - _workflowRunner = _services.GetRequiredService(); - } + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); [Fact(DisplayName = "Braided workflows are executed correctly")] public async Task Test1() { - await _services.PopulateRegistriesAsync(); - await _workflowRunner.RunAsync(); - var lines = _capturingTextWriter.Lines.ToList(); + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + await _fixture.RunWorkflowAsync(options); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); Assert.Equal(new[] { "WriteLine1", "WriteLine2", "WriteLine3", "WriteLine4", "WriteLine5", "WriteLine6", "WriteLine7" }, lines); } - + [Fact(DisplayName = "Braided workflows complete the workflow")] public async Task Test2() { - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(); + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + var result = await _fixture.RunWorkflowAsync(options); Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs index 34ad36849..64c544d00 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs @@ -1,17 +1,13 @@ using Elsa.Testing.Shared; +using Elsa.Workflows.Activities.Flowchart.Extensions; +using Elsa.Workflows.Options; using Xunit.Abstractions; namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors; -public class ForkDecisionJoinTests +public class ForkDecisionJoinTests(ITestOutputHelper testOutputHelper) { - private readonly CapturingTextWriter _capturingTextWriter = new(); - private readonly IServiceProvider _services; - - public ForkDecisionJoinTests(ITestOutputHelper testOutputHelper) - { - _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); - } + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); [Fact(DisplayName = "The implicit join configured with Stream merge mode should execute.")] public async Task ImplicitJoinStreamShouldExecute() @@ -36,36 +32,32 @@ public class ForkDecisionJoinTests { await RunAndAssert("fork-decision-join-waitall.json", ["A", "C", "B", "D"]); } - + [Fact(DisplayName = "An implicit join from the True and False branches should execute because the join mode is Stream and by default, all active branches are joined.")] public async Task ImplicitJoinFromBranchesShouldExecute() { - // Populate registries. - await _services.PopulateRegistriesAsync(); - // Import workflow. - var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json"); + var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json"); - // Execute. - var workflowState = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId); + // Execute with token-based mode. + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + var workflowState = await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options); // Assert. Assert.Equal(WorkflowStatus.Finished, workflowState.Status); } - + private async Task RunAndAssert(string workflowFileName, string[] expectedLines) { - // Populate registries. - await _services.PopulateRegistriesAsync(); - // Import workflow. - var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/{workflowFileName}"); + var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/{workflowFileName}"); + + // Execute with token-based mode. + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options); - // Execute. - await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId); - // Assert. - var lines = _capturingTextWriter.Lines.ToList(); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); Assert.Equal(expectedLines, lines); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs index 4d6e5ebbb..16a9661b9 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs @@ -1,36 +1,29 @@ using Elsa.Testing.Shared; +using Elsa.Workflows.Activities.Flowchart.Extensions; using Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows; -using Microsoft.Extensions.DependencyInjection; +using Elsa.Workflows.Options; using Xunit.Abstractions; namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors; -public class ImplicitWorkflowTests +public class ImplicitWorkflowTests(ITestOutputHelper testOutputHelper) { - private readonly IWorkflowRunner _workflowRunner; - private readonly CapturingTextWriter _capturingTextWriter = new(); - private readonly IServiceProvider _services; - - public ImplicitWorkflowTests(ITestOutputHelper testOutputHelper) - { - _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); - _workflowRunner = _services.GetRequiredService(); - } + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); [Fact(DisplayName = "Implicit loop workflows are executed correctly")] public async Task Test1() { - await _services.PopulateRegistriesAsync(); - await _workflowRunner.RunAsync(); - var lines = _capturingTextWriter.Lines.ToList(); + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + await _fixture.RunWorkflowAsync(options); + var lines = _fixture.CapturingTextWriter.Lines.ToList(); Assert.Equal(new[] { "Start", "Retry", "End" }, lines); } - + [Fact(DisplayName = "Implicit loop workflows complete the workflow")] public async Task Test2() { - await _services.PopulateRegistriesAsync(); - var result = await _workflowRunner.RunAsync(); + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + var result = await _fixture.RunWorkflowAsync(options); Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs index 36a60a563..fc3869a22 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs @@ -1,35 +1,29 @@ using Elsa.Common.Models; using Elsa.Testing.Shared; +using Elsa.Workflows.Activities.Flowchart.Extensions; +using Elsa.Workflows.Options; using Elsa.Workflows.Runtime; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors; -public class JoinRunsOnceTests +public class JoinRunsOnceTests(ITestOutputHelper testOutputHelper) { - private readonly CapturingTextWriter _capturingTextWriter = new(); - private readonly IServiceProvider _services; - - public JoinRunsOnceTests(ITestOutputHelper testOutputHelper) - { - _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); - } + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); [Fact(DisplayName = "The Join activity executes only once, not twice")] public async Task Test1() { - // Populate registries. - await _services.PopulateRegistriesAsync(); - // Import workflow. - var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/join.json"); + var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/join.json"); + + // Execute with token-based mode. + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + var state = await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options); - // Execute. - var state = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId); - // Assert. - var journal = await _services.GetRequiredService().FindManyAsync(new() + var journal = await _fixture.Services.GetRequiredService().FindManyAsync(new() { WorkflowInstanceId = state.Id, ActivityId = "802725996be1b582", diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs index 22c5d8a71..93bef7ef2 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs @@ -1,35 +1,29 @@ using Elsa.Common.Models; using Elsa.Testing.Shared; +using Elsa.Workflows.Activities.Flowchart.Extensions; +using Elsa.Workflows.Options; using Elsa.Workflows.Runtime; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors; -public class ParallelJoinCompletesTests +public class ParallelJoinCompletesTests(ITestOutputHelper testOutputHelper) { - private readonly CapturingTextWriter _capturingTextWriter = new(); - private readonly IServiceProvider _services; - - public ParallelJoinCompletesTests(ITestOutputHelper testOutputHelper) - { - _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); - } + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); [Fact(DisplayName = "The ParallelForEach activity completes when its Body contains a Join activity")] public async Task Test1() { - // Populate registries. - await _services.PopulateRegistriesAsync(); - // Import workflow. - var workflowDefinition = await _services.ImportWorkflowDefinitionAsync("Scenarios/JoinBehaviors/Workflows/parallel-join.json"); + var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync("Scenarios/JoinBehaviors/Workflows/parallel-join.json"); + + // Execute with token-based mode. + var options = new RunWorkflowOptions().WithTokenBasedFlowchart(); + var state = await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options); - // Execute. - var state = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId); - // Assert. - var journal = await _services.GetRequiredService().FindManyAsync(new() + var journal = await _fixture.Services.GetRequiredService().FindManyAsync(new() { WorkflowInstanceId = state.Id, ActivityId = "70fc1183cd5800f2", diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json index 81820da59..c3b454c63 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json @@ -1 +1,149 @@ -{"$schema":"https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json","id":"c2601be150ec7739","definitionId":"b8c92c080faf734f","name":"Decision Merge","createdAt":"2025-10-08T19:00:25.918638\u002B00:00","version":5,"toolVersion":"3.6.0.0","variables":[],"inputs":[],"outputs":[],"outcomes":[],"customProperties":{},"isReadonly":false,"isSystem":false,"isLatest":true,"isPublished":true,"options":{"autoUpdateConsumingWorkflows":false},"root":{"id":"990d07168c5223de","nodeId":"Workflow1:990d07168c5223de","name":"Flowchart1","type":"Elsa.Flowchart","version":1,"customProperties":{"notFoundConnections":[],"canStartWorkflow":false,"runAsynchronously":false},"metadata":{},"activities":[{"condition":{"typeName":"Boolean","expression":{"type":"Literal","value":false}},"id":"a38215f28410f472","nodeId":"Workflow1:990d07168c5223de:a38215f28410f472","name":"FlowDecision1","type":"Elsa.FlowDecision","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false},"metadata":{"designer":{"position":{"x":-237.5,"y":-255.5},"size":{"width":149.84375,"height":67.9765625}}}},{"id":"50e042cfab596891","nodeId":"Workflow1:990d07168c5223de:50e042cfab596891","name":"Start1","type":"Elsa.Start","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false},"metadata":{"designer":{"position":{"x":-485.5,"y":-255.5},"size":{"width":122.6484375,"height":67.9765625}}}},{"id":"d5b9c2affe09a93b","nodeId":"Workflow1:990d07168c5223de:d5b9c2affe09a93b","name":"End1","type":"Elsa.End","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false,"mergeMode":"None"},"metadata":{"designer":{"position":{"x":75.5,"y":-255.5},"size":{"width":115.3125,"height":67.9765625}}}}],"variables":[],"connections":[{"source":{"activity":"50e042cfab596891","port":"Done"},"target":{"activity":"a38215f28410f472","port":"In"},"vertices":[]},{"source":{"activity":"a38215f28410f472","port":"True"},"target":{"activity":"d5b9c2affe09a93b","port":"In"},"vertices":[]},{"source":{"activity":"a38215f28410f472","port":"False"},"target":{"activity":"d5b9c2affe09a93b","port":"In"},"vertices":[]}]}} \ No newline at end of file +{ + "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", + "id": "c2601be150ec7739", + "definitionId": "b8c92c080faf734f", + "name": "Decision Merge", + "createdAt": "2025-10-08T19:00:25.918638\u002B00:00", + "version": 5, + "toolVersion": "3.6.0.0", + "variables": [], + "inputs": [], + "outputs": [], + "outcomes": [], + "customProperties": {}, + "isReadonly": false, + "isSystem": false, + "isLatest": true, + "isPublished": true, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "id": "990d07168c5223de", + "nodeId": "Workflow1:990d07168c5223de", + "name": "Flowchart1", + "type": "Elsa.Flowchart", + "version": 1, + "customProperties": { + "notFoundConnections": [], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": {}, + "activities": [ + { + "condition": { + "typeName": "Boolean", + "expression": { + "type": "Literal", + "value": false + } + }, + "id": "a38215f28410f472", + "nodeId": "Workflow1:990d07168c5223de:a38215f28410f472", + "name": "FlowDecision1", + "type": "Elsa.FlowDecision", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -237.5, + "y": -255.5 + }, + "size": { + "width": 149.84375, + "height": 67.9765625 + } + } + } + }, + { + "id": "50e042cfab596891", + "nodeId": "Workflow1:990d07168c5223de:50e042cfab596891", + "name": "Start1", + "type": "Elsa.Start", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -485.5, + "y": -255.5 + }, + "size": { + "width": 122.6484375, + "height": 67.9765625 + } + } + } + }, + { + "id": "d5b9c2affe09a93b", + "nodeId": "Workflow1:990d07168c5223de:d5b9c2affe09a93b", + "name": "End1", + "type": "Elsa.End", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false, + "mergeMode": "None" + }, + "metadata": { + "designer": { + "position": { + "x": 75.5, + "y": -255.5 + }, + "size": { + "width": 115.3125, + "height": 67.9765625 + } + } + } + } + ], + "variables": [], + "connections": [ + { + "source": { + "activity": "50e042cfab596891", + "port": "Done" + }, + "target": { + "activity": "a38215f28410f472", + "port": "In" + }, + "vertices": [] + }, + { + "source": { + "activity": "a38215f28410f472", + "port": "True" + }, + "target": { + "activity": "d5b9c2affe09a93b", + "port": "In" + }, + "vertices": [] + }, + { + "source": { + "activity": "a38215f28410f472", + "port": "False" + }, + "target": { + "activity": "d5b9c2affe09a93b", + "port": "In" + }, + "vertices": [] + } + ] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json index 40ca9c9f2..b64e10f84 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json @@ -1,7 +1,7 @@ { "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", "id": "strict-converge-test", - "definitionId": "strict-converge-001", + "definitionId": "3d3412c458178ff4", "name": "Fork-Decision-Converge-Strict", "description": "Tests the strictest Converge mode that requires ALL inbound connections to execute", "createdAt": "2025-10-20T00:00:00.000000+00:00", diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json index 8bc40e337..2da7bbac3 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json @@ -1,7 +1,7 @@ { "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", "id": "b60374027329240a", - "definitionId": "3d3412c458178fff", + "definitionId": "3d3412c458178ff1", "name": "Fork-issues", "description": "show case", "createdAt": "2025-09-30T16:56:12.420213\u002B00:00", diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json index ddb3db562..3cdf365e5 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json @@ -1,7 +1,7 @@ { "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", "id": "eed88d95ad53e0f", - "definitionId": "3d3412c458178fff", + "definitionId": "3d3412c458178ff2", "name": "Fork-issues", "description": "show case", "createdAt": "2025-09-30T17:03:24.623714\u002B00:00", diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json index a10bcfcd7..1e03e3247 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json @@ -1,7 +1,7 @@ { "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", "id": "93277d4cd53f2dfc", - "definitionId": "3d3412c458178fff", + "definitionId": "3d3412c458178ff3", "name": "Fork-issues", "description": "show case", "createdAt": "2025-09-30T17:19:02.380015\u002B00:00", diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs index 0ab8e6d86..fbe18f8de 100644 --- a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs @@ -11,33 +11,33 @@ namespace Elsa.Activities.UnitTests.Branching; public class FlowJoinTests { [Theory] - [InlineData(true, FlowJoinMode.WaitAny)] - [InlineData(true, FlowJoinMode.WaitAll)] - [InlineData(false, FlowJoinMode.WaitAny)] - [InlineData(false, FlowJoinMode.WaitAll)] - public async Task Should_Complete_In_All_Flow_Mode_Combinations(bool useTokenFlow, FlowJoinMode joinMode) + [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAny)] + [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAll)] + [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAny)] + [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAll)] + public async Task Should_Complete_In_All_Flow_Mode_Combinations(FlowchartExecutionMode executionMode, FlowJoinMode joinMode) { // Arrange & Act - var context = await ExecuteWithFlowModeAsync(useTokenFlow, joinMode); + var context = await ExecuteWithFlowModeAsync(executionMode, joinMode); // Assert Assert.True(context.IsCompleted); } [Theory] - [InlineData(true, FlowJoinMode.WaitAny)] - [InlineData(true, FlowJoinMode.WaitAll)] - [InlineData(false, FlowJoinMode.WaitAny)] - [InlineData(false, FlowJoinMode.WaitAll)] - public async Task Should_Execute_In_Flowchart_Context_For_All_Combinations(bool useTokenFlow, FlowJoinMode joinMode) + [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAny)] + [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAll)] + [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAny)] + [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAll)] + public async Task Should_Execute_In_Flowchart_Context_For_All_Combinations(FlowchartExecutionMode executionMode, FlowJoinMode joinMode) { // Arrange & Act - var (context, flowJoin) = await ExecuteInFlowchartWithFlowModeAsync(useTokenFlow, joinMode); + var (context, flowJoin) = await ExecuteInFlowchartWithFlowModeAsync(executionMode, joinMode); // Assert Assert.NotNull(context); - var expectedMessage = useTokenFlow + var expectedMessage = executionMode == FlowchartExecutionMode.TokenBased ? $"Token flow mode with {joinMode} should schedule the join activity" : $"Counter flow mode with {joinMode} should schedule the join activity as start"; @@ -51,8 +51,8 @@ public class FlowJoinTests var joinMode = FlowJoinMode.WaitAll; // Use WaitAll to highlight differences // Act - var tokenContext = await ExecuteWithFlowModeAsync(true, joinMode); - var counterContext = await ExecuteWithFlowModeAsync(false, joinMode); + var tokenContext = await ExecuteWithFlowModeAsync(FlowchartExecutionMode.TokenBased, joinMode); + var counterContext = await ExecuteWithFlowModeAsync(FlowchartExecutionMode.CounterBased, joinMode); // Assert Assert.True(tokenContext.IsCompleted, "Token flow should always complete"); @@ -74,62 +74,48 @@ public class FlowJoinTests }; /// - /// Executes a FlowJoin activity with the specified flow mode, handling the UseTokenFlow setup and teardown. + /// Executes a FlowJoin activity with the specified flow mode. /// - private static async Task ExecuteWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode) + private static async Task ExecuteWithFlowModeAsync(FlowchartExecutionMode executionMode, FlowJoinMode joinMode) { - return await WithFlowModeAsync(useTokenFlow, async () => - { - var flowJoin = CreateFlowJoin(joinMode); - return await ExecuteAsync(flowJoin); - }); + var flowJoin = CreateFlowJoin(joinMode); + return await ExecuteAsync(flowJoin, executionMode); } /// /// Executes a FlowJoin activity within a flowchart context with the specified flow mode. /// - private static async Task<(ActivityExecutionContext context, FlowJoin flowJoin)> ExecuteInFlowchartWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode) + private static async Task<(ActivityExecutionContext context, FlowJoin flowJoin)> ExecuteInFlowchartWithFlowModeAsync(FlowchartExecutionMode executionMode, FlowJoinMode joinMode) { - return await WithFlowModeAsync(useTokenFlow, async () => - { - var flowJoin = CreateFlowJoin(joinMode); - var flowchart = CreateSimpleFlowchart(flowJoin); - var context = await ExecuteFlowchartAsync(flowchart); - return (context, flowJoin); - }); + var flowJoin = CreateFlowJoin(joinMode); + var flowchart = CreateSimpleFlowchart(flowJoin); + var context = await ExecuteFlowchartAsync(flowchart, executionMode); + return (context, flowJoin); } /// - /// Executes an action with the specified flow mode, ensuring proper setup and teardown of UseTokenFlow. + /// Executes a flowchart using the ActivityTestFixture with the specified execution mode. /// - private static async Task WithFlowModeAsync(bool useTokenFlow, Func> action) + private static Task ExecuteFlowchartAsync(Flowchart flowchart, FlowchartExecutionMode? executionMode = null) { - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = useTokenFlow; + return ExecuteAsync(flowchart, executionMode); + } - try + /// + /// Executes an activity using the ActivityTestFixture with the specified execution mode. + /// + private static async Task ExecuteAsync(IActivity activity, FlowchartExecutionMode? executionMode = null) + { + var fixture = new ActivityTestFixture(activity); + + if (executionMode.HasValue) { - return await action(); + fixture.ConfigureContext(context => + { + context.WorkflowExecutionContext.Properties[Flowchart.ExecutionModePropertyKey] = executionMode.Value; + }); } - finally - { - Flowchart.UseTokenFlow = originalValue; - } - } - /// - /// Executes an activity using the ActivityTestFixture. - /// - private static async Task ExecuteAsync(IActivity activity) - { - return await new ActivityTestFixture(activity).ExecuteAsync(); - } - - /// - /// Executes a flowchart using the ActivityTestFixture. - /// - private static async Task ExecuteFlowchartAsync(Flowchart flowchart) - { - return await new ActivityTestFixture(flowchart).ExecuteAsync(); + return await fixture.ExecuteAsync(); } } diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs index c504f48b7..b6c97a0c0 100644 --- a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs +++ b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs @@ -1,6 +1,7 @@ using Elsa.Testing.Shared; using Elsa.Workflows; using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Models; namespace Elsa.Activities.UnitTests.Flow; @@ -9,9 +10,18 @@ namespace Elsa.Activities.UnitTests.Flow; /// public static class FlowchartTestHelpers { - public static async Task ExecuteFlowchartAsync(Flowchart flowchart) + public static async Task ExecuteFlowchartAsync(Flowchart flowchart, FlowchartExecutionMode? executionMode = null) { var fixture = new ActivityTestFixture(flowchart); + + if (executionMode.HasValue) + { + fixture.ConfigureContext(context => + { + context.WorkflowExecutionContext.Properties[Flowchart.ExecutionModePropertyKey] = executionMode.Value; + }); + } + return await fixture.ExecuteAsync(); } } diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs index cbca233e9..7396d1733 100644 --- a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs @@ -1,5 +1,6 @@ using Elsa.Testing.Shared; using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Models; using static Elsa.Activities.UnitTests.Flow.FlowchartTestHelpers; namespace Elsa.Activities.UnitTests.Flow; @@ -43,34 +44,24 @@ public class FlowchartTests Assert.False(context.HasScheduledActivity(new WriteLine("NonExistent"))); } - [Theory(DisplayName = "Respects UseTokenFlow flag")] - [InlineData(true)] - [InlineData(false)] - public async Task RespectsUseTokenFlowFlag(bool useTokenFlow) + [Theory(DisplayName = "Respects execution mode")] + [InlineData(FlowchartExecutionMode.TokenBased)] + [InlineData(FlowchartExecutionMode.CounterBased)] + public async Task RespectsExecutionMode(FlowchartExecutionMode executionMode) { // Arrange - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = useTokenFlow; - - try + var activity = new WriteLine("Test"); + var flowchart = new Flowchart { - var activity = new WriteLine("Test"); - var flowchart = new Flowchart - { - Start = activity, - Activities = { activity } - }; + Start = activity, + Activities = { activity } + }; - // Act - var context = await ExecuteFlowchartAsync(flowchart); + // Act + var context = await ExecuteFlowchartAsync(flowchart, executionMode); - // Assert - just verify it executes without error - Assert.NotNull(context); - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } + // Assert - just verify it executes without error + Assert.NotNull(context); } [Fact(DisplayName = "Accepts empty connections collection")]