From cebf6f12e347ba10116d7612375beb770c72d5bb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Mar 2023 22:25:48 +0100 Subject: [PATCH] Add integration test for reading composite activity output --- .../TestApplicationBuilder.cs | 31 +++++--- .../Services/JintJavaScriptEvaluator.cs | 12 ++- .../Activities/Composite.cs | 3 +- .../Elsa.Workflows.Core/Models/Variable.cs | 6 ++ .../Elsa.IntegrationTests.csproj | 1 + .../Scenarios/Composites/Tests.cs | 40 ++++++++++ .../Scenarios/Composites/Workflows.cs | 74 +++++++++++++++++++ 7 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/Composites/Tests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/Composites/Workflows.cs diff --git a/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs b/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs index 57892c30a..8f3f8d0d6 100644 --- a/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs +++ b/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs @@ -1,4 +1,5 @@ using Elsa.Extensions; +using Elsa.Features.Services; using Elsa.Workflows.Core.Contracts; using Elsa.Workflows.Core.Services; using Microsoft.Extensions.DependencyInjection; @@ -11,26 +12,36 @@ public class TestApplicationBuilder { private readonly ITestOutputHelper _testOutputHelper; private readonly ServiceCollection _services; + private Action _configureElsa; public TestApplicationBuilder(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _services = new ServiceCollection(); - - _services.AddElsa(elsa => elsa - .UseWorkflows(workflows => workflows - .WithStandardOutStreamProvider(_ => new StandardOutStreamProvider(new XunitConsoleTextWriter(testOutputHelper))) - ) - ); _services .AddSingleton(testOutputHelper) .AddLogging(logging => logging.AddProvider(new XunitLoggerProvider(testOutputHelper)).SetMinimumLevel(LogLevel.Debug)); + + _configureElsa += elsa => elsa + .UseWorkflows(workflows => workflows + .WithStandardOutStreamProvider(_ => new StandardOutStreamProvider(new XunitConsoleTextWriter(_testOutputHelper))) + ); } - public IServiceProvider Build() => _services.BuildServiceProvider(); + public IServiceProvider Build() + { + _services.AddElsa(_configureElsa); + return _services.BuildServiceProvider(); + } - public TestApplicationBuilder Configure(Action configure) + public TestApplicationBuilder ConfigureElsa(Action configure) + { + _configureElsa += configure; + return this; + } + + public TestApplicationBuilder ConfigureServices(Action configure) { configure(_services); return this; @@ -39,7 +50,9 @@ public class TestApplicationBuilder public TestApplicationBuilder WithCapturingTextWriter(CapturingTextWriter capturingTextWriter) { var combinedTextWriter = new CombinedTextWriter(capturingTextWriter, new XunitConsoleTextWriter(_testOutputHelper)); - _services.AddSingleton(new StandardOutStreamProvider(combinedTextWriter)); + var provider = new StandardOutStreamProvider(combinedTextWriter); + + ConfigureElsa(elsa => elsa.UseWorkflows(workflows => workflows.WithStandardOutStreamProvider(_ => provider))); return this; } } \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index daf49acbe..453d02d4f 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -65,8 +65,8 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator engine.SetValue("getVariable", (Func)(id => context.GetVariable(id))); engine.SetValue("getInput", (Func)(name => context.GetWorkflowExecutionContext().Input.GetValue(name))); - // Create variable & input setters and getters for each variable. - CreateMemoryBlockAccessors(engine, context); + // Create variable getters and setters for each variable. + CreateVariableAccessors(engine, context); engine.SetValue("isNullOrWhiteSpace", (Func)(value => string.IsNullOrWhiteSpace(value))); engine.SetValue("isNullOrEmpty", (Func)(value => string.IsNullOrEmpty(value))); @@ -88,7 +88,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator return engine; } - private static void CreateMemoryBlockAccessors(Engine engine, ExpressionExecutionContext context) + private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context) { var variableNames = GetVariableNamesInScope(context).ToList(); @@ -100,7 +100,11 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator } } - private static IEnumerable GetVariableNamesInScope(ExpressionExecutionContext context) => EnumerateVariablesInScope(context).Select(x => x.Name).Distinct(); + private static IEnumerable GetVariableNamesInScope(ExpressionExecutionContext context) => + EnumerateVariablesInScope(context) + .Select(x => x.Name) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(); private static object GetVariableInScope(ExpressionExecutionContext context, string variableName) { diff --git a/src/modules/Elsa.Workflows.Core/Activities/Composite.cs b/src/modules/Elsa.Workflows.Core/Activities/Composite.cs index 4cf934063..820b37411 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Composite.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Composite.cs @@ -48,10 +48,11 @@ public abstract class Composite : Activity, IVariableContainer /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { + ConfigureActivities(context); + // Register variables. context.ExpressionExecutionContext.Memory.Declare(Variables); - ConfigureActivities(context); await context.ScheduleActivityAsync(Root, OnRootCompletedAsync); } diff --git a/src/modules/Elsa.Workflows.Core/Models/Variable.cs b/src/modules/Elsa.Workflows.Core/Models/Variable.cs index 8d2b31cd4..21fb86ee9 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Variable.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Variable.cs @@ -45,6 +45,12 @@ public class Variable : Variable { Value = value; } + + public Variable(string name, T value) + { + Name = name; + Value = value; + } public T? Get(ActivityExecutionContext context) => Get(context.ExpressionExecutionContext).ConvertTo(); public new T? Get(ExpressionExecutionContext context) => base.Get(context).ConvertTo(); diff --git a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj index 0872a73d6..7eed97924 100644 --- a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj +++ b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj @@ -22,6 +22,7 @@ + diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/Composites/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/Composites/Tests.cs new file mode 100644 index 000000000..a5ab9a577 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/Composites/Tests.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System.Threading.Tasks; +using Elsa.Extensions; +using Elsa.Testing.Shared; +using Elsa.Workflows.Core.Contracts; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.Composites; + +public class Tests +{ + private readonly IWorkflowRunner _workflowRunner; + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IWorkflowBuilderFactory _workflowBuilderFactory; + + public Tests(ITestOutputHelper testOutputHelper) + { + var services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .ConfigureElsa(elsa => elsa.UseJavaScript()) + .Build(); + _workflowBuilderFactory = services.GetRequiredService(); + _workflowRunner = services.GetRequiredService(); + } + + [Fact(DisplayName = "The main workflow can capture the result of the composite activity")] + public async Task Test1() + { + var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync(); + + // Start workflow. + await _workflowRunner.RunAsync(workflow); + + // Verify expected output. + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "Sum: 3" }, lines); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/Composites/Workflows.cs b/test/integration/Elsa.IntegrationTests/Scenarios/Composites/Workflows.cs new file mode 100644 index 000000000..f7f7d227d --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/Composites/Workflows.cs @@ -0,0 +1,74 @@ +using Elsa.Extensions; +using Elsa.JavaScript.Activities; +using Elsa.Workflows.Core.Abstractions; +using Elsa.Workflows.Core.Activities; +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; + +namespace Elsa.IntegrationTests.Scenarios.Composites; + +/// +/// A composite activity that sums two inputs and returns the result. +/// +public class Sum : Composite +{ + private readonly RunJavaScript _runJavaScript; + + public Input A { get; set; } = default!; + public Input B { get; set; } = default!; + + public Sum() + { + _runJavaScript = new RunJavaScript + { + Script = new("getA() + getB();"), + }; + + Root = new Sequence + { + Activities = + { + _runJavaScript + } + }; + } + + protected override void ConfigureActivities(ActivityExecutionContext context) + { + // Copy input into variables so that JS activity can access them. + var a = A.Get(context); + var b = B.Get(context); + Variables.Add(new Variable("A", a)); + Variables.Add(new Variable("B", b)); + + // If the call site set a result variable, assign it to the JavaScript activity's result. + if (Result != null) + _runJavaScript.Result = new Output(Result.MemoryBlockReference)!; + } +} + +/// +/// A workflow that uses the composite activity. +/// +public class SumWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder workflow) + { + var sum = new Variable(); + + workflow.Root = new Sequence + { + Variables = { sum }, + Activities = + { + new Sum + { + A = new(1), + B = new(2), + Result = new(sum) + }, + new WriteLine(context => $"Sum: {sum.Get(context)}") + } + }; + } +} \ No newline at end of file