Configures Flowchart Execution via DI (#7141)
* Introduce `FlowchartExecutionMode` to streamline flowchart execution logic. - Added `FlowchartExecutionMode` enum to represent execution modes (Default, TokenBased, CounterBased). - Updated flowchart-related integration and unit tests to use the new execution mode. - Removed the global `UseTokenFlow` flag in favor of execution-specific configuration via `RunWorkflowOptions`. - Refactored flowchart-related APIs and test helpers for improved flexibility and modularity. * Add support for configuring flowchart execution behavior via `FlowchartOptions` and DI. - Introduced extensions for `FlowchartFeature` to simplify configuration. - Added DI support for setting default execution modes. - Refactored flowchart execution logic to prioritize configuration. * Update default Flowchart execution settings to align with version 3.5.2 behavior - Changed `DefaultExecutionMode` to `CounterBased`. - Updated `UseTokenFlow` default to `false`. * Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/FlowchartFeatureExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/RunWorkflowOptionsExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor flowchart integration tests for improved formatting and consistency * Refactor workflow tests and related services to improve reusability and align with updated Flowchart execution behavior * Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/FlowchartOptions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor flowchart execution logic to use `FlowchartExecutionMode` enum, replacing boolean checks for improved clarity and extensibility. * Refactor tests and workflow logic to replace boolean `useTokenFlow` with `FlowchartExecutionMode` enum for clarity and consistency. * Refactor flowchart execution logic to centralize mode-based behavior handling and simplify implementation. * Remove unnecessary whitespace in Flowchart.cs to improve code formatting * Remove unnecessary whitespace in FlowJoinTests.cs to improve code formatting * Update src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
8b9070df09
commit
4bbdd6f3a7
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -20,70 +20,76 @@ namespace Elsa.Testing.Shared;
|
|||
[PublicAPI]
|
||||
public static class RunWorkflowExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
|
||||
/// </summary>
|
||||
/// <param name="services">The services.</param>
|
||||
/// <param name="workflowDefinitionId">The ID of the workflow definition.</param>
|
||||
/// <param name="input">An optional dictionary of input values.</param>
|
||||
/// <param name="correlationId">An optional correlation id of the workflow.</param>
|
||||
/// <param name="versionOptions">An optional set of options to specify the version of the workflow definition to retrieve.</param>
|
||||
/// <returns>The workflow state.</returns>
|
||||
public static async Task<WorkflowState> RunWorkflowUntilEndAsync(this IServiceProvider services,
|
||||
string workflowDefinitionId,
|
||||
IDictionary<string, object>? input = null,
|
||||
string? correlationId = null,
|
||||
VersionOptions? versionOptions = null)
|
||||
extension(IServiceProvider services)
|
||||
{
|
||||
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
|
||||
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<IWorkflowRuntime>();
|
||||
var workflowClient = await workflowRuntime.CreateClientAsync();
|
||||
var response = await workflowClient.CreateAndRunInstanceAsync(new()
|
||||
/// <summary>
|
||||
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
|
||||
/// </summary>
|
||||
/// <param name="workflowDefinitionId">The ID of the workflow definition.</param>
|
||||
/// <param name="input">An optional dictionary of input values.</param>
|
||||
/// <param name="correlationId">An optional correlation id of the workflow.</param>
|
||||
/// <param name="versionOptions">An optional set of options to specify the version of the workflow definition to retrieve.</param>
|
||||
/// <param name="runWorkflowOptions">Optional workflow execution options.</param>
|
||||
/// <returns>The workflow state.</returns>
|
||||
public async Task<WorkflowState> RunWorkflowUntilEndAsync(string workflowDefinitionId,
|
||||
IDictionary<string, object>? 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<IBookmarkStore>();
|
||||
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
|
||||
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<IWorkflowRuntime>();
|
||||
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<IBookmarkStore>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
|
||||
/// </summary>
|
||||
public static async Task<WorkflowState> RunWorkflowUntilEndAsync<TWorkflow>(this IServiceProvider services, IDictionary<string, object>? input = null) where TWorkflow : IWorkflow
|
||||
{
|
||||
var workflowDefinitionId = typeof(TWorkflow).Name;
|
||||
return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input);
|
||||
/// <summary>
|
||||
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
|
||||
/// </summary>
|
||||
public async Task<WorkflowState> RunWorkflowUntilEndAsync<TWorkflow>(IDictionary<string, object>? input = null) where TWorkflow : IWorkflow
|
||||
{
|
||||
var workflowDefinitionId = typeof(TWorkflow).Name;
|
||||
return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,51 +20,52 @@ namespace Elsa.Testing.Shared;
|
|||
[PublicAPI]
|
||||
public static class ServiceProviderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the registries.
|
||||
/// </summary>
|
||||
/// <param name="services">The services.</param>
|
||||
public static Task PopulateRegistriesAsync(this IServiceProvider services)
|
||||
extension(IServiceProvider services)
|
||||
{
|
||||
var registriesPopulator = services.GetRequiredService<IRegistriesPopulator>();
|
||||
return registriesPopulator.PopulateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports a workflow definition from a file.
|
||||
/// </summary>
|
||||
/// <param name="services">The services.</param>
|
||||
/// <param name="fileName">The file name.</param>
|
||||
/// <returns>The workflow definition.</returns>
|
||||
public static async Task<WorkflowDefinition> ImportWorkflowDefinitionAsync(this IServiceProvider services, string fileName)
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(fileName);
|
||||
var serializer = services.GetRequiredService<IActivitySerializer>();
|
||||
var model = serializer.Deserialize<WorkflowDefinitionModel>(json);
|
||||
|
||||
var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest
|
||||
/// <summary>
|
||||
/// Updates the registries.
|
||||
/// </summary>
|
||||
public Task PopulateRegistriesAsync()
|
||||
{
|
||||
Model = model,
|
||||
Publish = true
|
||||
};
|
||||
var registriesPopulator = services.GetRequiredService<IRegistriesPopulator>();
|
||||
return registriesPopulator.PopulateAsync();
|
||||
}
|
||||
|
||||
var workflowDefinitionImporter = services.GetRequiredService<IWorkflowDefinitionImporter>();
|
||||
var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
|
||||
return result.WorkflowDefinition;
|
||||
}
|
||||
/// <summary>
|
||||
/// Imports a workflow definition from a file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file name.</param>
|
||||
/// <returns>The workflow definition.</returns>
|
||||
public async Task<WorkflowDefinition> ImportWorkflowDefinitionAsync(string fileName)
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(fileName);
|
||||
var serializer = services.GetRequiredService<IActivitySerializer>();
|
||||
var model = serializer.Deserialize<WorkflowDefinitionModel>(json);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a workflow definition by its ID.
|
||||
/// </summary>
|
||||
/// <param name="services">The service provider.</param>
|
||||
/// <param name="workflowDefinitionId">The definition ID of the workflow definition.</param>
|
||||
/// <param name="versionOptions">Options to specify the version of the workflow definition to retrieve.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
|
||||
/// <returns>The retrieved workflow definition.</returns>
|
||||
public static async Task<WorkflowDefinition> GetWorkflowDefinitionAsync(this IServiceProvider services, string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken);
|
||||
return workflowDefinition!;
|
||||
var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest
|
||||
{
|
||||
Model = model,
|
||||
Publish = true
|
||||
};
|
||||
|
||||
var workflowDefinitionImporter = services.GetRequiredService<IWorkflowDefinitionImporter>();
|
||||
var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
|
||||
return result.WorkflowDefinition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a workflow definition by its ID.
|
||||
/// </summary>
|
||||
/// <param name="workflowDefinitionId">The definition ID of the workflow definition.</param>
|
||||
/// <param name="versionOptions">Options to specify the version of the workflow definition to retrieve.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
|
||||
/// <returns>The retrieved workflow definition.</returns>
|
||||
public async Task<WorkflowDefinition> GetWorkflowDefinitionAsync(string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken);
|
||||
return workflowDefinition!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
/// </summary>
|
||||
public async Task<WorkflowTestFixture> BuildAsync()
|
||||
{
|
||||
if (_services != null)
|
||||
return this;
|
||||
|
||||
_services = _testApplicationBuilder.Build();
|
||||
await Services.PopulateRegistriesAsync();
|
||||
return this;
|
||||
|
|
@ -116,12 +121,52 @@ public class WorkflowTestFixture
|
|||
/// <returns>The workflow result after execution</returns>
|
||||
public async Task<RunWorkflowResult> RunWorkflowAsync(IWorkflow workflow, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_services == null)
|
||||
await BuildAsync();
|
||||
|
||||
await BuildAsync();
|
||||
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
|
||||
return await workflowRunner.RunAsync(workflow, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified workflow and returns the workflow result.
|
||||
/// Automatically builds the fixture if not already built.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The workflow result after execution</returns>
|
||||
public async Task<RunWorkflowResult> RunWorkflowAsync<TWorkflow>(CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new()
|
||||
{
|
||||
await BuildAsync();
|
||||
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
|
||||
return await workflowRunner.RunAsync<TWorkflow>(cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a workflow with the specified options and returns the workflow result.
|
||||
/// Automatically builds the fixture if not already built.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow to run</param>
|
||||
/// <param name="options">Workflow execution options</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The workflow result after execution</returns>
|
||||
public async Task<RunWorkflowResult> RunWorkflowAsync(IWorkflow workflow, RunWorkflowOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await BuildAsync();
|
||||
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
|
||||
return await workflowRunner.RunAsync(workflow, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified workflow with the specified options and returns the workflow result.
|
||||
/// Automatically builds the fixture if not already built.
|
||||
/// </summary>
|
||||
/// <param name="options">Workflow execution options</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The workflow result after execution</returns>
|
||||
public async Task<RunWorkflowResult> RunWorkflowAsync<TWorkflow>(RunWorkflowOptions options, CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new()
|
||||
{
|
||||
await BuildAsync();
|
||||
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
|
||||
return await workflowRunner.RunAsync<TWorkflow>(options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an activity wrapped in a workflow and returns the workflow result.
|
||||
|
|
@ -132,26 +177,45 @@ public class WorkflowTestFixture
|
|||
/// <returns>The workflow result after execution</returns>
|
||||
public async Task<RunWorkflowResult> RunActivityAsync(IActivity activity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_services == null)
|
||||
await BuildAsync();
|
||||
|
||||
await BuildAsync();
|
||||
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
|
||||
return await workflowRunner.RunAsync(activity, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an activity wrapped in a workflow with the specified options and returns the workflow result.
|
||||
/// Automatically builds the fixture if not already built.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to run</param>
|
||||
/// <param name="options">Workflow execution options</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The workflow result after execution</returns>
|
||||
public async Task<RunWorkflowResult> RunActivityAsync(IActivity activity, RunWorkflowOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await BuildAsync();
|
||||
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
|
||||
return await workflowRunner.RunAsync(activity, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a workflow by definition ID and returns the workflow state.
|
||||
/// Automatically builds the fixture if not already built.
|
||||
/// </summary>
|
||||
/// <param name="definitionId">The workflow definition ID</param>
|
||||
/// <param name="input">Optional input dictionary</param>
|
||||
/// <param name="options">Optional workflow execution options</param>
|
||||
/// <returns>The workflow state after execution</returns>
|
||||
public async Task<WorkflowState> RunWorkflowAsync(string definitionId, IDictionary<string, object>? input = null)
|
||||
public async Task<WorkflowState> RunWorkflowAsync(string definitionId, IDictionary<string, object>? 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<WorkflowDefinition> ImportWorkflowDefinitionAsync(string fileName)
|
||||
{
|
||||
await BuildAsync();
|
||||
return await Services.ImportWorkflowDefinitionAsync(fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Set this to <c>false</c> 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 <see cref="WorkflowExecutionContext.Properties"/>.
|
||||
/// </summary>
|
||||
public static bool UseTokenFlow = true;
|
||||
public const string ExecutionModePropertyKey = "Flowchart:ExecutionMode";
|
||||
|
||||
/// <summary>
|
||||
/// Set this to <c>false</c> 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 <see cref="FlowchartOptions"/> configured via DI for application-wide settings.
|
||||
/// </summary>
|
||||
// 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.
|
||||
|
||||
/// <inheritdoc />
|
||||
public Flowchart([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
|
|
@ -29,7 +40,7 @@ public partial class Flowchart : Container
|
|||
/// <summary>
|
||||
/// The activity to execute when the flowchart starts.
|
||||
/// </summary>
|
||||
[Port][Browsable(false)] public IActivity? Start { get; set; }
|
||||
[Port] [Browsable(false)] public IActivity? Start { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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<ValueTask> tokenBasedAction, Func<ValueTask> counterBasedAction)
|
||||
{
|
||||
var mode = GetEffectiveExecutionMode(context);
|
||||
|
||||
return mode switch
|
||||
{
|
||||
FlowchartExecutionMode.TokenBased => tokenBasedAction(),
|
||||
FlowchartExecutionMode.CounterBased or FlowchartExecutionMode.Default or _ => counterBasedAction()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective execution mode for this flowchart execution.
|
||||
/// Priority: WorkflowExecutionContext.Properties > FlowchartOptions (DI) > Static UseTokenFlow flag
|
||||
/// </summary>
|
||||
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<FlowchartExecutionMode>(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<IOptions<FlowchartOptions>>();
|
||||
var mode = options?.Value.DefaultExecutionMode ?? FlowchartExecutionMode.Default;
|
||||
if (mode == FlowchartExecutionMode.Default)
|
||||
return UseTokenFlow ? FlowchartExecutionMode.TokenBased : FlowchartExecutionMode.CounterBased;
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="FlowchartFeature"/>.
|
||||
/// </summary>
|
||||
public static class FlowchartFeatureExtensions
|
||||
{
|
||||
extension(FlowchartFeature feature)
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the flowchart options.
|
||||
/// </summary>
|
||||
public FlowchartFeature ConfigureFlowchart(Action<FlowchartOptions> configure)
|
||||
{
|
||||
feature.FlowchartOptionsConfigurator = configure;
|
||||
return feature;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default execution mode for flowcharts to token-based.
|
||||
/// </summary>
|
||||
public FlowchartFeature UseTokenBasedExecution()
|
||||
{
|
||||
return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.TokenBased);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default execution mode for flowcharts to counter-based (legacy mode).
|
||||
/// </summary>
|
||||
public FlowchartFeature UseCounterBasedExecution()
|
||||
{
|
||||
return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.CounterBased);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default execution mode for flowcharts to the specified mode.
|
||||
/// </summary>
|
||||
public FlowchartFeature UseExecution(FlowchartExecutionMode mode)
|
||||
{
|
||||
return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Options;
|
||||
|
||||
namespace Elsa.Workflows.Activities.Flowchart.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="RunWorkflowOptions"/> to configure flowchart execution mode.
|
||||
/// </summary>
|
||||
public static class RunWorkflowOptionsExtensions
|
||||
{
|
||||
extension(RunWorkflowOptions options)
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the flowchart execution mode to token-based.
|
||||
/// </summary>
|
||||
public RunWorkflowOptions WithTokenBasedFlowchart()
|
||||
{
|
||||
return options.WithFlowchartExecutionMode(FlowchartExecutionMode.TokenBased);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the flowchart execution mode to counter-based (legacy mode).
|
||||
/// </summary>
|
||||
public RunWorkflowOptions WithCounterBasedFlowchart()
|
||||
{
|
||||
return options.WithFlowchartExecutionMode(FlowchartExecutionMode.CounterBased);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the flowchart execution mode.
|
||||
/// </summary>
|
||||
public RunWorkflowOptions WithFlowchartExecutionMode(FlowchartExecutionMode mode)
|
||||
{
|
||||
options.Properties ??= new Dictionary<string, object>();
|
||||
options.Properties[Activities.Flowchart.ExecutionModePropertyKey] = mode;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
namespace Elsa.Workflows.Activities.Flowchart.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the execution mode for flowchart activities.
|
||||
/// </summary>
|
||||
public enum FlowchartExecutionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Use the default mode as specified by <see cref="Elsa.Workflows.Activities.Flowchart.Flowchart.UseTokenFlow"/>.
|
||||
/// </summary>
|
||||
Default = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Use token-based flow logic.
|
||||
/// </summary>
|
||||
TokenBased = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Use counter-based flow logic (legacy mode).
|
||||
/// </summary>
|
||||
CounterBased = 2
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
|
||||
namespace Elsa.Workflows.Activities.Flowchart.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring flowchart execution behavior.
|
||||
/// </summary>
|
||||
public class FlowchartOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default execution mode for flowcharts when not explicitly specified.
|
||||
/// Defaults to <see cref="FlowchartExecutionMode.CounterBased"/>.
|
||||
/// </summary>
|
||||
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.
|
||||
}
|
||||
|
|
@ -6,9 +6,18 @@ namespace Elsa.Extensions;
|
|||
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
public static IModule UseWorkflows(this IModule configuration, Action<WorkflowsFeature>? configure = default)
|
||||
extension(IModule configuration)
|
||||
{
|
||||
configuration.Configure(configure);
|
||||
return configuration;
|
||||
public IModule UseWorkflows(Action<WorkflowsFeature>? configure = null)
|
||||
{
|
||||
configuration.Configure(configure);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public IModule UseFlowchart(Action<FlowchartFeature>? configure = null)
|
||||
{
|
||||
configuration.Configure(configure);
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delegate to configure <see cref="FlowchartOptions"/>.
|
||||
/// </summary>
|
||||
public Action<FlowchartOptions>? FlowchartOptionsConfigurator { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddSerializationOptionsConfigurator<FlowchartSerializationOptionConfigurator>();
|
||||
|
||||
|
||||
// Register FlowchartOptions
|
||||
Services.AddOptions<FlowchartOptions>();
|
||||
|
||||
if (FlowchartOptionsConfigurator != null)
|
||||
Services.Configure(FlowchartOptionsConfigurator);
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[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<object[]> 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<object[]> 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<object[]> OnePathConnectedTestCases()
|
||||
{
|
||||
// useTokenFlow, condition, expectedOutputs, unexpectedOutputs
|
||||
yield return [true, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
|
||||
yield return [true, false, new[] { "Start" }, new[] { "TruePath", "End" }];
|
||||
yield return [false, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
|
||||
yield return [false, false, new[] { "Start" }, new[] { "TruePath", "End" }];
|
||||
yield return [FlowchartExecutionMode.TokenBased, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
|
||||
yield return [FlowchartExecutionMode.TokenBased, false, new[] { "Start" }, new[] { "TruePath", "End" }];
|
||||
yield return [FlowchartExecutionMode.CounterBased, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
|
||||
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<object[]> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,22 +10,15 @@ namespace Elsa.Activities.IntegrationTests.Branching;
|
|||
/// <summary>
|
||||
/// Integration tests for FlowJoin activity in complex flowchart scenarios.
|
||||
/// </summary>
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -11,23 +11,15 @@ namespace Elsa.Activities.IntegrationTests.Flow;
|
|||
/// Integration tests for counter-based flowchart execution strategy.
|
||||
/// </summary>
|
||||
[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);
|
||||
|
|
|
|||
|
|
@ -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<RunWorkflowResult> RunFlowchartAsync(IServiceProvider services, Flowchart flowchart)
|
||||
public static async Task<RunWorkflowResult> 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")
|
||||
|
|
|
|||
|
|
@ -12,23 +12,15 @@ namespace Elsa.Activities.IntegrationTests.Flow;
|
|||
/// Integration tests for token-based flowchart execution strategy.
|
||||
/// </summary>
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -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<FlowchartNextActivityTests>()
|
||||
.Build();
|
||||
|
||||
_workflowRunner = _services.GetRequiredService<IWorkflowRunner>();
|
||||
}
|
||||
private readonly WorkflowTestFixture _fixture = new WorkflowTestFixture(testOutputHelper).AddActivitiesFrom<FlowchartNextActivityTests>();
|
||||
|
||||
[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<FlowchartWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"Line 1"
|
||||
}, lines);
|
||||
await _fixture.RunWorkflowAsync<FlowchartWorkflow>();
|
||||
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<bool>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IWorkflowRunner>();
|
||||
}
|
||||
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
||||
|
||||
[Fact(DisplayName = "Braided workflows are executed correctly")]
|
||||
public async Task Test1()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<BraidedWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
|
||||
await _fixture.RunWorkflowAsync<BraidedWorkflow>(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<BraidedWorkflow>();
|
||||
var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
|
||||
var result = await _fixture.RunWorkflowAsync<BraidedWorkflow>(options);
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IWorkflowRunner>();
|
||||
}
|
||||
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
||||
|
||||
[Fact(DisplayName = "Implicit loop workflows are executed correctly")]
|
||||
public async Task Test1()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<ImplicitLoopWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
|
||||
await _fixture.RunWorkflowAsync<ImplicitLoopWorkflow>(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<ImplicitLoopWorkflow>();
|
||||
var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
|
||||
var result = await _fixture.RunWorkflowAsync<ImplicitLoopWorkflow>(options);
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IWorkflowExecutionLogStore>().FindManyAsync(new()
|
||||
var journal = await _fixture.Services.GetRequiredService<IWorkflowExecutionLogStore>().FindManyAsync(new()
|
||||
{
|
||||
WorkflowInstanceId = state.Id,
|
||||
ActivityId = "802725996be1b582",
|
||||
|
|
|
|||
|
|
@ -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<IWorkflowExecutionLogStore>().FindManyAsync(new()
|
||||
var journal = await _fixture.Services.GetRequiredService<IWorkflowExecutionLogStore>().FindManyAsync(new()
|
||||
{
|
||||
WorkflowInstanceId = state.Id,
|
||||
ActivityId = "70fc1183cd5800f2",
|
||||
|
|
|
|||
|
|
@ -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":[]}]}}
|
||||
{
|
||||
"$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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
|||
};
|
||||
|
||||
/// <summary>
|
||||
/// Executes a FlowJoin activity with the specified flow mode, handling the UseTokenFlow setup and teardown.
|
||||
/// Executes a FlowJoin activity with the specified flow mode.
|
||||
/// </summary>
|
||||
private static async Task<ActivityExecutionContext> ExecuteWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode)
|
||||
private static async Task<ActivityExecutionContext> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a FlowJoin activity within a flowchart context with the specified flow mode.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static async Task<T> WithFlowModeAsync<T>(bool useTokenFlow, Func<Task<T>> action)
|
||||
private static Task<ActivityExecutionContext> ExecuteFlowchartAsync(Flowchart flowchart, FlowchartExecutionMode? executionMode = null)
|
||||
{
|
||||
var originalValue = Flowchart.UseTokenFlow;
|
||||
Flowchart.UseTokenFlow = useTokenFlow;
|
||||
return ExecuteAsync(flowchart, executionMode);
|
||||
}
|
||||
|
||||
try
|
||||
/// <summary>
|
||||
/// Executes an activity using the ActivityTestFixture with the specified execution mode.
|
||||
/// </summary>
|
||||
private static async Task<ActivityExecutionContext> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an activity using the ActivityTestFixture.
|
||||
/// </summary>
|
||||
private static async Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
{
|
||||
return await new ActivityTestFixture(activity).ExecuteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a flowchart using the ActivityTestFixture.
|
||||
/// </summary>
|
||||
private static async Task<ActivityExecutionContext> ExecuteFlowchartAsync(Flowchart flowchart)
|
||||
{
|
||||
return await new ActivityTestFixture(flowchart).ExecuteAsync();
|
||||
return await fixture.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
/// </summary>
|
||||
public static class FlowchartTestHelpers
|
||||
{
|
||||
public static async Task<ActivityExecutionContext> ExecuteFlowchartAsync(Flowchart flowchart)
|
||||
public static async Task<ActivityExecutionContext> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Reference in a new issue