From 8bf58c5c60fd5bf43ecf51d5bedddf081ea8a0a2 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 31 Mar 2025 18:10:59 +0200 Subject: [PATCH] Add support for activity output testing (#6529) * Add support for activity output testing Removed obsolete test workflows and updated the If activity tests to align with simplified testing practices. Introduced `GetActivityOutput` extension for retrieving activity outputs from workflow results. Refactored related APIs to simplify execution flow and unified cancellation token handling in service methods. * Remove unused Parlot.Fluent namespace in IfTests.cs This commit removes an unused "using" directive for the Parlot.Fluent namespace in the IfTests.cs file. Cleaning up unused references helps maintain code clarity and reduces potential confusion. --- .../ServiceProviderExtensions.cs | 2 +- .../Elsa.Workflows.Core/Activities/Inline.cs | 20 ++++---- .../WorkflowExecutionContextExtensions.cs | 6 +++ .../WorkflowExecutionResultExtensions.cs | 12 +++++ .../Models/RunWorkflowResult.cs | 4 +- .../Services/WorkflowRunner.cs | 4 +- .../Actors/WorkflowInstance.cs | 2 +- .../Services/WorkflowHost.cs | 2 +- .../Activities/If/ComplexIfWorkflow.cs | 44 ----------------- .../Activities/If/IfTests.cs | 47 +++++++++++++------ .../Activities/If/IfThenWorkflow.cs | 22 --------- 11 files changed, 68 insertions(+), 97 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionResultExtensions.cs delete mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Activities/If/ComplexIfWorkflow.cs delete mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfThenWorkflow.cs diff --git a/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs b/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs index fe08250ad..037086ff8 100644 --- a/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs +++ b/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs @@ -153,7 +153,7 @@ public static class ServiceProviderExtensions public static async Task GetWorkflowDefinitionAsync(this IServiceProvider services, string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default) { var workflowDefinitionService = services.GetRequiredService(); - var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions); + var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken); return workflowDefinition!; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Inline.cs b/src/modules/Elsa.Workflows.Core/Activities/Inline.cs index daea21359..ad209f296 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Inline.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Inline.cs @@ -14,26 +14,26 @@ namespace Elsa.Workflows.Activities; [PublicAPI] public class Inline : CodeActivity { - private readonly Func _activity = default!; + private readonly Func _activity = _ => default; /// - public Inline([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + public Inline([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } /// - public Inline(Func activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + public Inline(Func activity, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { _activity = activity; } /// - public Inline(Func activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(_ => activity(), source, line) + public Inline(Func activity, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(_ => activity(), source, line) { } /// - public Inline(Action activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(c => + public Inline(Action activity, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(c => { activity(c); return new ValueTask(); @@ -42,7 +42,7 @@ public class Inline : CodeActivity } /// - public Inline(Action activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(c => + public Inline(Action activity, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(c => { activity(); return new ValueTask(); @@ -103,20 +103,20 @@ public class Inline : CodeActivity private readonly Func> _activity; /// - public Inline(Func> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + public Inline(Func> activity, MemoryBlockReference? output = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(output, source, line) { _activity = activity; } /// - public Inline(Func> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + public Inline(Func> activity, MemoryBlockReference? output = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(_ => activity(), output, source, line) { } /// - public Inline(Func activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + public Inline(Func activity, MemoryBlockReference? output = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(c => { var result = activity(c); @@ -126,7 +126,7 @@ public class Inline : CodeActivity } /// - public Inline(Func activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + public Inline(Func activity, MemoryBlockReference? output = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(c => { var result = activity(); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index 76f25d0c9..57c01a6e6 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -139,4 +139,10 @@ public static class WorkflowExecutionContextExtensions /// Returns true if all activities have completed or canceled, false otherwise. /// public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => workflowExecutionContext.ActivityExecutionContexts.All(x => x.IsCompleted); + + public static object? GetOutputByActivityId(this WorkflowExecutionContext workflowExecutionContext, string activityId, string? outputName = null) + { + var outputRegister = workflowExecutionContext.GetActivityOutputRegister(); + return outputRegister.FindOutputByActivityId(activityId, outputName); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionResultExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionResultExtensions.cs new file mode 100644 index 000000000..3103ed1a6 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionResultExtensions.cs @@ -0,0 +1,12 @@ +using Elsa.Extensions; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +public static class WorkflowExecutionResultExtensions +{ + public static T GetActivityOutput(this RunWorkflowResult result, IActivity activity, string? outputName = null) + { + return (T)result.WorkflowExecutionContext.GetOutputByActivityId(activity.Id, outputName)!; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/RunWorkflowResult.cs b/src/modules/Elsa.Workflows.Core/Models/RunWorkflowResult.cs index 422d6dd9a..7cb280819 100644 --- a/src/modules/Elsa.Workflows.Core/Models/RunWorkflowResult.cs +++ b/src/modules/Elsa.Workflows.Core/Models/RunWorkflowResult.cs @@ -6,9 +6,9 @@ namespace Elsa.Workflows.Models; /// /// Contains information about a workflow run, such as . /// -public record RunWorkflowResult(WorkflowState WorkflowState, Workflow Workflow, object? Result); +public record RunWorkflowResult(WorkflowExecutionContext WorkflowExecutionContext, WorkflowState WorkflowState, Workflow Workflow, object? Result); /// /// Contains information about a workflow run, such as . /// -public record RunWorkflowResult(WorkflowState WorkflowState, Workflow Workflow, TResult Result); \ No newline at end of file +public record RunWorkflowResult(WorkflowExecutionContext WorkflowExecutionContext, WorkflowState WorkflowState, Workflow Workflow, TResult Result); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs index 80c8be63d..f426d1aa9 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs @@ -45,7 +45,7 @@ public class WorkflowRunner( public async Task> RunAsync(WorkflowBase workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var result = await RunAsync((IWorkflow)workflow, options, cancellationToken); - return new(result.WorkflowState, result.Workflow, (TResult)result.Result!); + return new(result.WorkflowExecutionContext, result.WorkflowState, result.Workflow, (TResult)result.Result!); } /// @@ -219,6 +219,6 @@ public class WorkflowRunner( var result = workflow.ResultVariable?.Get(workflowExecutionContext.MemoryRegister); await notificationSender.SendAsync(new WorkflowExecuted(workflow, workflowState, workflowExecutionContext), cancellationToken); await commitStateHandler.CommitAsync(workflowExecutionContext, workflowState, cancellationToken); - return new(workflowState, workflowExecutionContext.Workflow, result); + return new(workflowExecutionContext, workflowState, workflowExecutionContext.Workflow, result); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs index 2039857a7..6253ed724 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs @@ -200,7 +200,7 @@ internal class WorkflowInstance( if (_isRunning) { _queuedRunWorkflowOptions.Enqueue(runWorkflowOptions); - return new(null!, null!, null); + return new(null!, null!, null!, null); } _isRunning = true; diff --git a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs index 1ae3cc4b9..01e816184 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs @@ -71,7 +71,7 @@ public class WorkflowHost : IWorkflowHost if (WorkflowState.Status != WorkflowStatus.Running) { _logger.LogWarning("Attempt to resume workflow {WorkflowInstanceId} that is not in the Running state. The actual state is {ActualWorkflowStatus}", WorkflowState.Id, WorkflowState.Status); - return new RunWorkflowResult(WorkflowState, Workflow, null); + return new RunWorkflowResult(null!, WorkflowState, Workflow, null); } var runOptions = new RunWorkflowOptions diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/ComplexIfWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/ComplexIfWorkflow.cs deleted file mode 100644 index 893262273..000000000 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/ComplexIfWorkflow.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Elsa.Workflows.Activities; - -namespace Elsa.Workflows.IntegrationTests.Activities; - -public class ComplexIfWorkflow : WorkflowBase -{ - private readonly Func _condition; - - public ComplexIfWorkflow(Func condition) - { - _condition = condition; - } - - protected override void Build(IWorkflowBuilder workflow) - { - workflow.Root = new Sequence - { - Activities = - { - new WriteLine("Start"), - new If(_condition) - { - Then = new Sequence - { - Activities = - { - new WriteLine("Executing"), - new WriteLine("True!") - } - }, - Else = new Sequence - { - Activities = - { - new WriteLine("Executing"), - new WriteLine("False!") - } - } - }, - new WriteLine("End") - } - }; - } -} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfTests.cs index 8d7ece3e1..6de0dfdd9 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfTests.cs @@ -1,4 +1,5 @@ using Elsa.Testing.Shared; +using Elsa.Workflows.Activities; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; @@ -17,24 +18,42 @@ public class IfTests } [Theory(DisplayName = "The correct branch executes when condition is true")] - [InlineData(true, "True!")] - [InlineData(false, "False!")] - public async Task Test1(bool conditionResult, string expectedLine) + [InlineData(true)] + [InlineData(false)] + public async Task Test1(bool conditionResult) { - await _services.PopulateRegistriesAsync(); - await _workflowRunner.RunAsync(new IfThenWorkflow(() => conditionResult)); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(new[] { expectedLine }, lines); + var result = default(bool?); + + var activity = new If(() => conditionResult) + { + Then = new Inline(() => result = true), + Else = new Inline(() => result = false) + }; + await _services.RunActivityAsync(activity); + Assert.Equal(conditionResult, result); } [Theory(DisplayName = "The If activity completes only after either one of its branches completed")] - [InlineData(true, new[] { "Start", "Executing", "True!", "End" })] - [InlineData(false, new[] { "Start", "Executing", "False!", "End" })] - public async Task Test2(bool conditionResult, string[] expectedLines) + [InlineData(true)] + [InlineData(false)] + public async Task Test2(bool conditionResult) { - await _services.PopulateRegistriesAsync(); - await _workflowRunner.RunAsync(new ComplexIfWorkflow(() => conditionResult)); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(expectedLines, lines); + var activity = new If(() => conditionResult) + { + Then = new Inline(), + Else = new Inline() + }; + var result = await _services.RunActivityAsync(activity); + Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status); + } + + [Fact(DisplayName = "The If activity produces a result when one of its branches completes")] + public async Task Test3() + { + var activity = new If(() => true); + var result = await _services.RunActivityAsync(activity); + var activityResult = result.GetActivityOutput(activity); + + Assert.True(activityResult); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfThenWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfThenWorkflow.cs deleted file mode 100644 index 0d69089ad..000000000 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/If/IfThenWorkflow.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Elsa.Workflows.Activities; - -namespace Elsa.Workflows.IntegrationTests.Activities; - -public class IfThenWorkflow : WorkflowBase -{ - private readonly Func _condition; - - public IfThenWorkflow(Func condition) - { - _condition = condition; - } - - protected override void Build(IWorkflowBuilder workflow) - { - workflow.Root = new If(_condition) - { - Then = new WriteLine("True!"), - Else = new WriteLine("False!") - }; - } -} \ No newline at end of file