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.
This commit is contained in:
Sipke Schoorstra 2025-03-31 18:10:59 +02:00 committed by GitHub
parent ede5d5cad2
commit 8bf58c5c60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 68 additions and 97 deletions

View file

@ -153,7 +153,7 @@ public static class ServiceProviderExtensions
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);
var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken);
return workflowDefinition!;
}
}

View file

@ -14,26 +14,26 @@ namespace Elsa.Workflows.Activities;
[PublicAPI]
public class Inline : CodeActivity
{
private readonly Func<ActivityExecutionContext, ValueTask> _activity = default!;
private readonly Func<ActivityExecutionContext, ValueTask> _activity = _ => default;
/// <inheritdoc />
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)
{
}
/// <inheritdoc />
public Inline(Func<ActivityExecutionContext, ValueTask> activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
public Inline(Func<ActivityExecutionContext, ValueTask> activity, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
_activity = activity;
}
/// <inheritdoc />
public Inline(Func<ValueTask> activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(_ => activity(), source, line)
public Inline(Func<ValueTask> activity, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(_ => activity(), source, line)
{
}
/// <inheritdoc />
public Inline(Action<ActivityExecutionContext> activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(c =>
public Inline(Action<ActivityExecutionContext> activity, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(c =>
{
activity(c);
return new ValueTask();
@ -42,7 +42,7 @@ public class Inline : CodeActivity
}
/// <inheritdoc />
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<T> : CodeActivity<T>
private readonly Func<ActivityExecutionContext, ValueTask<T>> _activity;
/// <inheritdoc />
public Inline(Func<ActivityExecutionContext, ValueTask<T>> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
public Inline(Func<ActivityExecutionContext, ValueTask<T>> activity, MemoryBlockReference? output = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null)
: base(output, source, line)
{
_activity = activity;
}
/// <inheritdoc />
public Inline(Func<ValueTask<T>> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
public Inline(Func<ValueTask<T>> activity, MemoryBlockReference? output = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null)
: this(_ => activity(), output, source, line)
{
}
/// <inheritdoc />
public Inline(Func<ActivityExecutionContext, T> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
public Inline(Func<ActivityExecutionContext, T> 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<T> : CodeActivity<T>
}
/// <inheritdoc />
public Inline(Func<T> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
public Inline(Func<T> activity, MemoryBlockReference? output = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null)
: this(c =>
{
var result = activity();

View file

@ -139,4 +139,10 @@ public static class WorkflowExecutionContextExtensions
/// Returns true if all activities have completed or canceled, false otherwise.
/// </summary>
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);
}
}

View file

@ -0,0 +1,12 @@
using Elsa.Extensions;
using Elsa.Workflows.Models;
namespace Elsa.Workflows;
public static class WorkflowExecutionResultExtensions
{
public static T GetActivityOutput<T>(this RunWorkflowResult result, IActivity activity, string? outputName = null)
{
return (T)result.WorkflowExecutionContext.GetOutputByActivityId(activity.Id, outputName)!;
}
}

View file

@ -6,9 +6,9 @@ namespace Elsa.Workflows.Models;
/// <summary>
/// Contains information about a workflow run, such as <see cref="WorkflowState"/>.
/// </summary>
public record RunWorkflowResult(WorkflowState WorkflowState, Workflow Workflow, object? Result);
public record RunWorkflowResult(WorkflowExecutionContext WorkflowExecutionContext, WorkflowState WorkflowState, Workflow Workflow, object? Result);
/// <summary>
/// Contains information about a workflow run, such as <see cref="WorkflowState"/>.
/// </summary>
public record RunWorkflowResult<TResult>(WorkflowState WorkflowState, Workflow Workflow, TResult Result);
public record RunWorkflowResult<TResult>(WorkflowExecutionContext WorkflowExecutionContext, WorkflowState WorkflowState, Workflow Workflow, TResult Result);

View file

@ -45,7 +45,7 @@ public class WorkflowRunner(
public async Task<RunWorkflowResult<TResult>> RunAsync<TResult>(WorkflowBase<TResult> 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!);
}
/// <inheritdoc />
@ -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);
}
}

View file

@ -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;

View file

@ -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

View file

@ -1,44 +0,0 @@
using Elsa.Workflows.Activities;
namespace Elsa.Workflows.IntegrationTests.Activities;
public class ComplexIfWorkflow : WorkflowBase
{
private readonly Func<bool> _condition;
public ComplexIfWorkflow(Func<bool> 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")
}
};
}
}

View file

@ -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<bool>(activity);
Assert.True(activityResult);
}
}

View file

@ -1,22 +0,0 @@
using Elsa.Workflows.Activities;
namespace Elsa.Workflows.IntegrationTests.Activities;
public class IfThenWorkflow : WorkflowBase
{
private readonly Func<bool> _condition;
public IfThenWorkflow(Func<bool> condition)
{
_condition = condition;
}
protected override void Build(IWorkflowBuilder workflow)
{
workflow.Root = new If(_condition)
{
Then = new WriteLine("True!"),
Else = new WriteLine("False!")
};
}
}