Add ForEach tests, introduce asynchronous workflow runner and enhance workflow events. (#6926)
* Introduce asynchronous workflow runner and enhance workflow events. - Added `AsyncWorkflowRunner` to enable asynchronous workflow execution and result tracking. - Introduced new event arguments, such as `ActivityExecutedEventArgs` and `WorkflowStateCommittedEventArgs`. - Expanded `WorkflowEvents` class to include `ActivityExecuted`, `ActivityExecutedLogUpdated`, and `WorkflowStateCommitted` events. - Refactored event arguments into the `Elsa.Testing.Shared.EventArgs` namespace. - Enhanced tests with `AsyncWorkflowRunner` and new event-driven workflow scenarios. * Refactor event argument classes to unify namespace and simplify inheritance * Add shared component DotSettings file to support namespace exclusions
This commit is contained in:
parent
54c8a010fe
commit
bdfbd0886f
|
|
@ -0,0 +1,2 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=eventargs/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
public class ActivityExecutedEventArgs(ActivityExecutionContext activityExecutionContext) : EventArgs
|
||||
{
|
||||
public ActivityExecutionContext ActivityExecutionContext { get; } = activityExecutionContext;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
public class ActivityExecutedLogUpdatedEventArgs(WorkflowExecutionContext workflowExecutionContext, ICollection<ActivityExecutionRecord> records) : EventArgs
|
||||
{
|
||||
public WorkflowExecutionContext WorkflowExecutionContext { get; } = workflowExecutionContext;
|
||||
public ICollection<ActivityExecutionRecord> Records { get; } = records;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.State;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
public class WorkflowStateCommittedEventArgs(WorkflowExecutionContext workflowExecutionContext, WorkflowState workflowState, WorkflowInstance workflowInstance) : EventArgs
|
||||
{
|
||||
public WorkflowExecutionContext WorkflowExecutionContext { get; } = workflowExecutionContext;
|
||||
public WorkflowState WorkflowState { get; } = workflowState;
|
||||
public WorkflowInstance WorkflowInstance { get; } = workflowInstance;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using Elsa.Mediator.Contracts;
|
|||
using Elsa.Testing.Shared.Services;
|
||||
using Elsa.Workflows.Management.Notifications;
|
||||
using Elsa.Workflows.Notifications;
|
||||
using Elsa.Workflows.Runtime.Notifications;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Testing.Shared.Handlers;
|
||||
|
|
@ -9,17 +10,38 @@ namespace Elsa.Testing.Shared.Handlers;
|
|||
[UsedImplicitly]
|
||||
public class WorkflowEventHandlers(WorkflowEvents workflowEvents) :
|
||||
INotificationHandler<WorkflowFinished>,
|
||||
INotificationHandler<WorkflowInstanceSaved>
|
||||
INotificationHandler<WorkflowInstanceSaved>,
|
||||
INotificationHandler<WorkflowStateCommitted>,
|
||||
INotificationHandler<ActivityExecuted>,
|
||||
INotificationHandler<ActivityExecutionLogUpdated>
|
||||
{
|
||||
public Task HandleAsync(WorkflowFinished notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnWorkflowFinished(new WorkflowFinishedEventArgs(notification.Workflow, notification.WorkflowState));
|
||||
workflowEvents.OnWorkflowFinished(new(notification.Workflow, notification.WorkflowState));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(WorkflowInstanceSaved notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnWorkflowInstanceSaved(new WorkflowInstanceSavedEventArgs(notification.WorkflowInstance));
|
||||
workflowEvents.OnWorkflowInstanceSaved(new(notification.WorkflowInstance));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(WorkflowStateCommitted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnWorkflowStateCommitted(new(notification.WorkflowExecutionContext, notification.WorkflowState, notification.WorkflowInstance));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(ActivityExecuted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnActivityExecuted(new(notification.ActivityExecutionContext));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(ActivityExecutionLogUpdated notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnActivityExecutedLogUpdated(new(notification.WorkflowExecutionContext, notification.Records));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,12 @@ public class WorkflowEvents
|
|||
{
|
||||
public event EventHandler<WorkflowFinishedEventArgs>? WorkflowFinished;
|
||||
public event EventHandler<WorkflowInstanceSavedEventArgs>? WorkflowInstanceSaved;
|
||||
public event EventHandler<WorkflowStateCommittedEventArgs>? WorkflowStateCommitted;
|
||||
public event EventHandler<ActivityExecutedEventArgs>? ActivityExecuted;
|
||||
public event EventHandler<ActivityExecutedLogUpdatedEventArgs>? ActivityExecutedLogUpdated;
|
||||
public void OnWorkflowFinished(WorkflowFinishedEventArgs args) => WorkflowFinished?.Invoke(this, args);
|
||||
public void OnWorkflowInstanceSaved(WorkflowInstanceSavedEventArgs args) => WorkflowInstanceSaved?.Invoke(this, args);
|
||||
public void OnWorkflowStateCommitted(WorkflowStateCommittedEventArgs args) => WorkflowStateCommitted?.Invoke(this, args);
|
||||
public void OnActivityExecuted(ActivityExecutedEventArgs args) => ActivityExecuted?.Invoke(this, args);
|
||||
public void OnActivityExecutedLogUpdated(ActivityExecutedLogUpdatedEventArgs args) => ActivityExecutedLogUpdated?.Invoke(this, args);
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ public class LoggingMiddleware : IActivityExecutionMiddleware
|
|||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_stopwatch = new Stopwatch();
|
||||
_stopwatch = new();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public class WorkflowDefinitionEventConsumer(WorkflowDefinitionEvents workflowDe
|
|||
{
|
||||
public Task Consume(ConsumeContext<WorkflowDefinitionDeleted> context)
|
||||
{
|
||||
workflowDefinitionEvents.OnWorkflowDefinitionDeleted(new WorkflowDefinitionDeletedEventArgs(context.Message.Id));
|
||||
workflowDefinitionEvents.OnWorkflowDefinitionDeleted(new(context.Message.Id));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ public class EventPublishingChangeTokenSignaler(IChangeTokenSignaler decoratedSe
|
|||
|
||||
public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
triggerChangeTokenSignalEvents.RaiseChangeTokenSignalTriggered(new TriggerChangeTokenSignalEventArgs(key));
|
||||
triggerChangeTokenSignalEvents.RaiseChangeTokenSignalTriggered(new(key));
|
||||
return decoratedService.TriggerTokenAsync(key, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ using Elsa.Testing.Shared.Services;
|
|||
using Elsa.Workflows.ComponentTests.Consumers;
|
||||
using Elsa.Workflows.ComponentTests.Decorators;
|
||||
using Elsa.Workflows.ComponentTests.Materializers;
|
||||
using Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach;
|
||||
using Elsa.Workflows.ComponentTests.Services;
|
||||
using Elsa.Workflows.ComponentTests.WorkflowProviders;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Runtime.Distributed.Extensions;
|
||||
|
|
@ -127,12 +129,13 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
|
|||
{
|
||||
services
|
||||
.AddSingleton<SignalManager>()
|
||||
.AddScoped<WorkflowEvents>()
|
||||
.AddScoped<AsyncWorkflowRunner>()
|
||||
.AddSingleton<WorkflowEvents>()
|
||||
.AddScoped<WorkflowDefinitionEvents>()
|
||||
.AddSingleton<TriggerChangeTokenSignalEvents>()
|
||||
.AddScoped<IWorkflowMaterializer, TestWorkflowMaterializer>()
|
||||
.AddNotificationHandlersFrom<WorkflowServer>()
|
||||
.AddWorkflowDefinitionProvider<TestWorkflowProvider>()
|
||||
.AddWorkflowsProvider<TestWorkflowProvider>()
|
||||
.AddNotificationHandlersFrom<WorkflowEventHandlers>()
|
||||
.Decorate<IChangeTokenSignaler, EventPublishingChangeTokenSignaler>()
|
||||
;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Workflows.Runtime.Entities;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a test workflow execution, including the workflow execution context and activity execution records.
|
||||
/// </summary>
|
||||
/// <param name="WorkflowExecutionContext">The workflow execution context after completion.</param>
|
||||
/// <param name="ActivityExecutionRecords">The collection of activity execution records for the workflow.</param>
|
||||
public record TestWorkflowExecutionResult(WorkflowExecutionContext WorkflowExecutionContext, ICollection<ActivityExecutionRecord> ActivityExecutionRecords);
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Testing.Shared.Services;
|
||||
using Elsa.Workflows.ComponentTests.Models;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Runtime.Messages;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to execute workflows asynchronously and await their completion for testing purposes.
|
||||
/// Tracks activity execution records and workflow completion signals.
|
||||
/// </summary>
|
||||
public class AsyncWorkflowRunner : IDisposable
|
||||
{
|
||||
private readonly IWorkflowRuntime _workflowRuntime;
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly SignalManager _signalManager;
|
||||
private readonly WorkflowEvents _workflowEvents;
|
||||
private readonly ConcurrentDictionary<string, ActivityExecutionRecord> _activityExecutionRecords = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncWorkflowRunner"/> class.
|
||||
/// </summary>
|
||||
public AsyncWorkflowRunner(IWorkflowRuntime workflowRuntime, IIdentityGenerator identityGenerator, SignalManager signalManager, WorkflowEvents workflowEvents)
|
||||
{
|
||||
_workflowRuntime = workflowRuntime;
|
||||
_identityGenerator = identityGenerator;
|
||||
_signalManager = signalManager;
|
||||
_workflowEvents = workflowEvents;
|
||||
|
||||
_workflowEvents.WorkflowStateCommitted += OnWorkflowStateCommitted;
|
||||
_workflowEvents.ActivityExecutedLogUpdated += OnActivityExecutedLogUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified workflow definition asynchronously and waits for its completion.
|
||||
/// Returns the workflow execution context and activity execution records.
|
||||
/// </summary>
|
||||
/// <param name="workflowDefinitionHandle">The handle of the workflow definition to execute.</param>
|
||||
/// <returns>A <see cref="TestWorkflowExecutionResult"/> containing the workflow execution context and activity execution records.</returns>
|
||||
public async Task<TestWorkflowExecutionResult> RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle workflowDefinitionHandle)
|
||||
{
|
||||
var workflowInstanceId = _identityGenerator.GenerateId();
|
||||
var workflowClient = await _workflowRuntime.CreateClientAsync(workflowInstanceId);
|
||||
await workflowClient.CreateInstanceAsync(new()
|
||||
{
|
||||
WorkflowDefinitionHandle = workflowDefinitionHandle
|
||||
});
|
||||
_activityExecutionRecords.Clear();
|
||||
await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
|
||||
var signalName = GetSignalName(workflowInstanceId);
|
||||
var workflowExecutionContext = await _signalManager.WaitAsync<WorkflowExecutionContext>(signalName);
|
||||
return new(workflowExecutionContext, _activityExecutionRecords.Values.ToList());
|
||||
}
|
||||
|
||||
private void OnWorkflowStateCommitted(object? sender, WorkflowStateCommittedEventArgs e)
|
||||
{
|
||||
if (e.WorkflowExecutionContext.Status != WorkflowStatus.Finished)
|
||||
return;
|
||||
|
||||
var signalName = GetSignalName(e.WorkflowExecutionContext.Id);
|
||||
_signalManager.Trigger(signalName, e.WorkflowExecutionContext);
|
||||
}
|
||||
|
||||
private void OnActivityExecutedLogUpdated(object? sender, ActivityExecutedLogUpdatedEventArgs e)
|
||||
{
|
||||
foreach (var record in e.Records)
|
||||
_activityExecutionRecords[record.Id] = record;
|
||||
}
|
||||
|
||||
private static string GetSignalName(string workflowInstanceId) => $"WorkflowInstanceCompleted-{workflowInstanceId}";
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from workflow events and releases resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_workflowEvents.WorkflowStateCommitted -= OnWorkflowStateCommitted;
|
||||
_workflowEvents.ActivityExecutedLogUpdated -= OnActivityExecutedLogUpdated;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Elsa.Common.Models;
|
||||
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||
using Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach.Workflows;
|
||||
using Elsa.Workflows.ComponentTests.Services;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach;
|
||||
|
||||
public class ForEachWorkflowTests : AppComponentTest
|
||||
{
|
||||
private readonly AsyncWorkflowRunner _workflowRunner;
|
||||
|
||||
public ForEachWorkflowTests(App app) : base(app)
|
||||
{
|
||||
_workflowRunner = Scope.ServiceProvider.GetRequiredService<AsyncWorkflowRunner>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ForEach activity executes child activity for each collection item and supports blocking activities")]
|
||||
public async Task ForEachActivity_ExecutesChildActivity_ForEachCollectionItem_AndSupportsBlocking()
|
||||
{
|
||||
var result = await _workflowRunner.RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle.ByDefinitionId(ForEachWorkflow.DefinitionId, VersionOptions.Published));
|
||||
var writeLineExecutionRecords = result.ActivityExecutionRecords.Where(x => x.ActivityId == "WriteLine1").ToList();
|
||||
|
||||
// Assert that the workflow executed the expected number of activities.
|
||||
Assert.Equal(3, writeLineExecutionRecords.Count);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Scheduling.Activities;
|
||||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach.Workflows;
|
||||
|
||||
public class ForEachWorkflow : WorkflowBase
|
||||
{
|
||||
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||
protected override void Build(IWorkflowBuilder builder)
|
||||
{
|
||||
builder.WithDefinitionId(DefinitionId);
|
||||
builder.Root = new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new ForEach<string>(["a", "b", "c"])
|
||||
{
|
||||
Body = new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new WriteLine(context => $"Processing item: {context.GetVariable<string>("CurrentValue")}"),
|
||||
Delay.FromMilliseconds(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -26,11 +26,11 @@ public class DispatchWorkflowsTests : AppComponentTest
|
|||
public async Task DispatchAndWaitWorkflow_ShouldWaitForChildWorkflowToComplete()
|
||||
{
|
||||
var workflowClient = await _workflowRuntime.CreateClientAsync();
|
||||
await workflowClient.CreateInstanceAsync(new CreateWorkflowInstanceRequest
|
||||
await workflowClient.CreateInstanceAsync(new()
|
||||
{
|
||||
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(DispatchAndWaitWorkflow.DefinitionId, VersionOptions.Published)
|
||||
});
|
||||
await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
|
||||
await _signalManager.WaitAsync<string>("Completed");
|
||||
await _signalManager.WaitAsync<object>("Completed");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Activities.IntegrationTests;
|
||||
|
||||
public class ForEachTests
|
||||
{
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ForEachTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_serviceProvider = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ForEach executes each activity for every item in the collection")]
|
||||
public async Task ForEach_ExecutesEachActivity_ForEveryItem()
|
||||
{
|
||||
var expectedLines = new[] {"a", "b", "c"};
|
||||
var forEach = new ForEach<string>(expectedLines)
|
||||
{
|
||||
Body = new WriteLine(context => context.GetVariable<string>("CurrentValue"))
|
||||
};
|
||||
await _serviceProvider.RunActivityAsync(forEach);
|
||||
Assert.Equal(expectedLines, _capturingTextWriter.Lines);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue