Merge branch 'develop/3.6.0' into feat/unit-test-coverage-while
This commit is contained in:
commit
c87c363e23
|
|
@ -195,7 +195,6 @@ public async Task Should_Return_Default_Outcome()
|
|||
Assert.True(context.HasOutcome("Default"));
|
||||
}
|
||||
```
|
||||
|
||||
#### **Integration tests:**
|
||||
- Place the activity inside a minimal workflow definition and run via [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs). Assert outputs/variables and that the activity integrates correctly with preceding/following activities.
|
||||
- If activity creates bookmarks or relies on scheduler semantics, integration tests should resume bookmarks via the engine APIs to validate resumption.
|
||||
|
|
|
|||
|
|
@ -128,12 +128,15 @@ public class ActivityTestFixture
|
|||
{
|
||||
var activityType = activity.GetType();
|
||||
var variableProperties = activityType.GetProperties()
|
||||
.Where(p => p.PropertyType.IsGenericType &&
|
||||
p.PropertyType.GetGenericTypeDefinition() == typeof(Variable<>))
|
||||
.Where(p => typeof(Variable).IsAssignableFrom(p.PropertyType))
|
||||
.ToList();
|
||||
|
||||
foreach (var variable in variableProperties.Select(property => (Variable)property.GetValue(activity)!))
|
||||
foreach (var variable in variableProperties.Select(property => (Variable?)property.GetValue(activity)))
|
||||
{
|
||||
if(variable == null)
|
||||
continue;
|
||||
|
||||
context.WorkflowExecutionContext.MemoryRegister.Declare(variable);
|
||||
variable.Set(context.ExpressionExecutionContext, variable.Value);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,12 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
[Output(Description = "The uploaded files, if any.", IsSerializable = false)]
|
||||
public Output<IFormFile[]> Files { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The first uploaded file, if any.
|
||||
/// </summary>
|
||||
[Output(Description = "The first uploaded file, if any.", IsSerializable = false)]
|
||||
public Output<IFormFile?> File { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The parsed route data, if any.
|
||||
/// </summary>
|
||||
|
|
@ -246,6 +252,7 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
}
|
||||
|
||||
Files.Set(context, files.ToArray());
|
||||
File.Set(context, files.FirstOrDefault());
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -497,4 +504,4 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
|
||||
return routeData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ namespace Elsa.Workflows.Activities.Flowchart.Activities;
|
|||
/// Note that this activity is no longer necessary for either AND or OR merges, because all activities inherit the Join Kind property.
|
||||
/// Use this activity if an explicit join step is desired.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "Branching", "[Obsolete] - Explicitly merge multiple branches into a single branch of execution.", DisplayName = "Join")]
|
||||
[Activity("Elsa", "Branching", "Explicitly merge multiple branches into a single branch of execution.", DisplayName = "Join")]
|
||||
[UsedImplicitly]
|
||||
[Obsolete("Each activity now supports the MergeMode property, making the use of this activity obsolete.", false)]
|
||||
public class FlowJoin : Activity, IJoinNode
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -38,8 +37,9 @@ public class FlowJoin : Activity, IJoinNode
|
|||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
if(!Flowchart.UseTokenFlow)
|
||||
await context.ParentActivityExecutionContext.CancelInboundAncestorsAsync(this);
|
||||
|
||||
if (context.ParentActivityExecutionContext != null)
|
||||
await context.ParentActivityExecutionContext.CancelInboundAncestorsAsync(this);
|
||||
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,16 @@ public static class ActivityExtensions
|
|||
{
|
||||
public static MergeMode? GetMergeMode(this IActivity activity)
|
||||
{
|
||||
activity.CustomProperties.TryGetValue("mergeMode", out var mergeModeString);
|
||||
return Enum.TryParse<MergeMode>((string?)mergeModeString, true, out var mergeMode) ? mergeMode : null;
|
||||
if (!activity.CustomProperties.TryGetValue("mergeMode", out var value))
|
||||
return null;
|
||||
|
||||
// Handle both string and enum values for backwards compatibility
|
||||
return value switch
|
||||
{
|
||||
MergeMode mode => mode,
|
||||
string str when Enum.TryParse<MergeMode>(str, true, out var mode) => mode,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public static void SetMergeMode(this IActivity activity, MergeMode? value)
|
||||
|
|
@ -17,7 +25,7 @@ public static class ActivityExtensions
|
|||
if (value == null)
|
||||
activity.CustomProperties.Remove("mergeMode");
|
||||
else
|
||||
activity.CustomProperties["mergeMode"] = value;
|
||||
activity.CustomProperties["mergeMode"] = value.ToString()!;
|
||||
}
|
||||
|
||||
public static async Task<MergeMode?> GetMergeModeAsync(this IActivity activity, ActivityExecutionContext context)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,353 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
using Xunit.Abstractions;
|
||||
using static Elsa.Activities.IntegrationTests.Flow.FlowchartTestHelpers;
|
||||
|
||||
namespace Elsa.Activities.IntegrationTests.Flow;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for counter-based flowchart execution strategy.
|
||||
/// </summary>
|
||||
[Collection("FlowchartTests")]
|
||||
public class FlowchartCounterBasedTests : IDisposable
|
||||
{
|
||||
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")]
|
||||
public async Task ExecutesSimpleLinearFlowchart()
|
||||
{
|
||||
// Arrange
|
||||
var flowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("First"),
|
||||
new WriteLine("Second"),
|
||||
new WriteLine("Third")
|
||||
);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, _output.Lines.Count);
|
||||
Assert.Equal("First", _output.Lines.ElementAt(0));
|
||||
Assert.Equal("Second", _output.Lines.ElementAt(1));
|
||||
Assert.Equal("Third", _output.Lines.ElementAt(2));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes both branches in parallel flowchart")]
|
||||
public async Task ExecutesBothBranches()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var flowchart = CreateBranchingFlowchart(start, branch1, branch2);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, _output.Lines.Count);
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Branch1", _output.Lines);
|
||||
Assert.Contains("Branch2", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles flowchart with no connections")]
|
||||
public async Task HandlesNoConnections()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new WriteLine("Isolated");
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = activity,
|
||||
Activities = { activity }
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Single(_output.Lines);
|
||||
Assert.Equal("Isolated", _output.Lines.ElementAt(0));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Completes when start activity is null")]
|
||||
public async Task CompletesWhenStartIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(_output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Follows conditional branches with If activity")]
|
||||
public async Task FollowsConditionalBranches()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If
|
||||
{
|
||||
Condition = new(true),
|
||||
Then = new WriteLine("Then branch"),
|
||||
Else = new WriteLine("Else branch")
|
||||
};
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = ifActivity,
|
||||
Activities = { ifActivity }
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Single(_output.Lines);
|
||||
Assert.Equal("Then branch", _output.Lines.ElementAt(0));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes join node with WaitAny mode")]
|
||||
public async Task ExecutesJoinNodeWaitAny()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) };
|
||||
var afterJoin = new WriteLine("AfterJoin");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, join, afterJoin },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, join),
|
||||
CreateConnection(branch2, join),
|
||||
CreateConnection(join, afterJoin)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("AfterJoin", _output.Lines);
|
||||
// At least one branch should execute
|
||||
Assert.True(_output.Lines.Contains("Branch1") || _output.Lines.Contains("Branch2"));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes join node with WaitAll mode")]
|
||||
public async Task ExecutesJoinNodeWaitAll()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
|
||||
var afterJoin = new WriteLine("AfterJoin");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, join, afterJoin },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, join),
|
||||
CreateConnection(branch2, join),
|
||||
CreateConnection(join, afterJoin)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Branch1", _output.Lines);
|
||||
Assert.Contains("Branch2", _output.Lines);
|
||||
Assert.Contains("AfterJoin", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles multiple sequential joins")]
|
||||
public async Task HandlesMultipleSequentialJoins()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var a1 = new WriteLine("A1");
|
||||
var a2 = new WriteLine("A2");
|
||||
var join1 = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
|
||||
var b1 = new WriteLine("B1");
|
||||
var b2 = new WriteLine("B2");
|
||||
var join2 = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, a1, a2, join1, b1, b2, join2, end },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, a1),
|
||||
CreateConnection(start, a2),
|
||||
CreateConnection(a1, join1),
|
||||
CreateConnection(a2, join1),
|
||||
CreateConnection(join1, b1),
|
||||
CreateConnection(join1, b2),
|
||||
CreateConnection(b1, join2),
|
||||
CreateConnection(b2, join2),
|
||||
CreateConnection(join2, end)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("A1", _output.Lines);
|
||||
Assert.Contains("A2", _output.Lines);
|
||||
Assert.Contains("B1", _output.Lines);
|
||||
Assert.Contains("B2", _output.Lines);
|
||||
Assert.Contains("End", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles complex diamond pattern")]
|
||||
public async Task HandlesComplexDiamondPattern()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var left1 = new WriteLine("Left1");
|
||||
var left2 = new WriteLine("Left2");
|
||||
var right1 = new WriteLine("Right1");
|
||||
var right2 = new WriteLine("Right2");
|
||||
var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, left1, left2, right1, right2, join, end },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, left1),
|
||||
CreateConnection(start, right1),
|
||||
CreateConnection(left1, left2),
|
||||
CreateConnection(right1, right2),
|
||||
CreateConnection(left2, join),
|
||||
CreateConnection(right2, join),
|
||||
CreateConnection(join, end)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Left1", _output.Lines);
|
||||
Assert.Contains("Left2", _output.Lines);
|
||||
Assert.Contains("Right1", _output.Lines);
|
||||
Assert.Contains("Right2", _output.Lines);
|
||||
Assert.Contains("End", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes activities in correct order for sequential flow")]
|
||||
public async Task ExecutesInCorrectOrderForSequential()
|
||||
{
|
||||
// Arrange
|
||||
var flowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("1"),
|
||||
new WriteLine("2"),
|
||||
new WriteLine("3"),
|
||||
new WriteLine("4")
|
||||
);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, _output.Lines.Count);
|
||||
Assert.Equal("1", _output.Lines.ElementAt(0));
|
||||
Assert.Equal("2", _output.Lines.ElementAt(1));
|
||||
Assert.Equal("3", _output.Lines.ElementAt(2));
|
||||
Assert.Equal("4", _output.Lines.ElementAt(3));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles nested flowcharts")]
|
||||
public async Task HandlesNestedFlowcharts()
|
||||
{
|
||||
// Arrange
|
||||
var innerFlowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("Inner1"),
|
||||
new WriteLine("Inner2")
|
||||
);
|
||||
|
||||
var outerFlowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("Outer1"),
|
||||
innerFlowchart,
|
||||
new WriteLine("Outer2")
|
||||
);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, outerFlowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Outer1", _output.Lines);
|
||||
Assert.Contains("Inner1", _output.Lines);
|
||||
Assert.Contains("Inner2", _output.Lines);
|
||||
Assert.Contains("Outer2", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles unconnected activities in flowchart")]
|
||||
public async Task HandlesUnconnectedActivities()
|
||||
{
|
||||
// Arrange
|
||||
var connected = new WriteLine("Connected");
|
||||
var unconnected = new WriteLine("Unconnected");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = connected,
|
||||
Activities = { connected, unconnected }
|
||||
// No connection to unconnected activity
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Single(_output.Lines);
|
||||
Assert.Equal("Connected", _output.Lines.ElementAt(0));
|
||||
Assert.DoesNotContain("Unconnected", _output.Lines);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Elsa.Activities.IntegrationTests.Flow;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a test collection to ensure flowchart tests don't run in parallel.
|
||||
/// This is necessary because the tests modify the process-wide static Flowchart.UseTokenFlow flag.
|
||||
/// </summary>
|
||||
[CollectionDefinition("FlowchartTests", DisableParallelization = true)]
|
||||
public class FlowchartTestCollection
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Models;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Activities.IntegrationTests.Flow;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper methods for Flowchart integration tests.
|
||||
/// </summary>
|
||||
public static class FlowchartTestHelpers
|
||||
{
|
||||
public static IServiceProvider CreateServiceProvider(ITestOutputHelper testOutputHelper, CapturingTextWriter? capturingTextWriter = null)
|
||||
{
|
||||
var builder = new TestApplicationBuilder(testOutputHelper);
|
||||
if (capturingTextWriter != null)
|
||||
builder.WithCapturingTextWriter(capturingTextWriter);
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
public static async Task<RunWorkflowResult> RunFlowchartAsync(IServiceProvider services, Flowchart flowchart)
|
||||
{
|
||||
return await services.RunActivityAsync(flowchart);
|
||||
}
|
||||
|
||||
public static Connection CreateConnection(IActivity source, IActivity target, string? outcome = "Done")
|
||||
{
|
||||
return new(new(source, outcome), new Endpoint(target));
|
||||
}
|
||||
|
||||
public static Flowchart CreateSimpleLinearFlowchart(params IActivity[] activities)
|
||||
{
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = activities.FirstOrDefault(),
|
||||
Activities = new List<IActivity>(activities)
|
||||
};
|
||||
|
||||
for (var i = 0; i < activities.Length - 1; i++)
|
||||
{
|
||||
flowchart.Connections.Add(CreateConnection(activities[i], activities[i + 1]));
|
||||
}
|
||||
|
||||
return flowchart;
|
||||
}
|
||||
|
||||
public static Flowchart CreateBranchingFlowchart(IActivity start, IActivity branch1, IActivity branch2, IActivity? join = null)
|
||||
{
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2 }
|
||||
};
|
||||
|
||||
flowchart.Connections.Add(CreateConnection(start, branch1));
|
||||
flowchart.Connections.Add(CreateConnection(start, branch2));
|
||||
|
||||
if (join == null)
|
||||
return flowchart;
|
||||
|
||||
flowchart.Activities.Add(join);
|
||||
flowchart.Connections.Add(CreateConnection(branch1, join));
|
||||
flowchart.Connections.Add(CreateConnection(branch2, join));
|
||||
|
||||
return flowchart;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,549 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Extensions;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
using Xunit.Abstractions;
|
||||
using static Elsa.Activities.IntegrationTests.Flow.FlowchartTestHelpers;
|
||||
|
||||
namespace Elsa.Activities.IntegrationTests.Flow;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for token-based flowchart execution strategy.
|
||||
/// </summary>
|
||||
[Collection("FlowchartTests")]
|
||||
public class FlowchartTokenBasedTests : IDisposable
|
||||
{
|
||||
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")]
|
||||
public async Task ExecutesSimpleLinearFlowchart()
|
||||
{
|
||||
// Arrange
|
||||
var flowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("First"),
|
||||
new WriteLine("Second"),
|
||||
new WriteLine("Third")
|
||||
);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, _output.Lines.Count);
|
||||
Assert.Equal("First", _output.Lines.ElementAt(0));
|
||||
Assert.Equal("Second", _output.Lines.ElementAt(1));
|
||||
Assert.Equal("Third", _output.Lines.ElementAt(2));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes both branches in parallel flowchart")]
|
||||
public async Task ExecutesBothBranches()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var flowchart = CreateBranchingFlowchart(start, branch1, branch2);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, _output.Lines.Count);
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Branch1", _output.Lines);
|
||||
Assert.Contains("Branch2", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles flowchart with no connections")]
|
||||
public async Task HandlesNoConnections()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new WriteLine("Isolated");
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = activity,
|
||||
Activities = { activity }
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Single(_output.Lines);
|
||||
Assert.Equal("Isolated", _output.Lines.ElementAt(0));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Completes when start activity is null")]
|
||||
public async Task CompletesWhenStartIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(_output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Follows conditional branches with If activity")]
|
||||
public async Task FollowsConditionalBranches()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If
|
||||
{
|
||||
Condition = new(true),
|
||||
Then = new WriteLine("Then branch"),
|
||||
Else = new WriteLine("Else branch")
|
||||
};
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = ifActivity,
|
||||
Activities = { ifActivity }
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Single(_output.Lines);
|
||||
Assert.Equal("Then branch", _output.Lines.ElementAt(0));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes Stream merge mode - schedules immediately")]
|
||||
public async Task ExecutesStreamMergeMode()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var afterJoin = new WriteLine("AfterJoin");
|
||||
afterJoin.SetMergeMode(MergeMode.Stream);
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, afterJoin },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, afterJoin),
|
||||
CreateConnection(branch2, afterJoin)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("AfterJoin", _output.Lines);
|
||||
// In Stream mode, afterJoin executes as soon as first branch arrives
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes Race merge mode - cancels other branches")]
|
||||
public async Task ExecutesRaceMergeMode()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var afterRace = new WriteLine("AfterRace");
|
||||
afterRace.SetMergeMode(MergeMode.Race);
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, afterRace },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, afterRace),
|
||||
CreateConnection(branch2, afterRace)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("AfterRace", _output.Lines);
|
||||
// In Race mode, afterRace executes on first arrival and blocks others
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes Converge merge mode - waits for all branches")]
|
||||
public async Task ExecutesConvergeMergeMode()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var converge = new WriteLine("Converge");
|
||||
converge.SetMergeMode(MergeMode.Converge);
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, converge, end },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, converge),
|
||||
CreateConnection(branch2, converge),
|
||||
CreateConnection(converge, end)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Branch1", _output.Lines);
|
||||
Assert.Contains("Branch2", _output.Lines);
|
||||
Assert.Contains("Converge", _output.Lines);
|
||||
Assert.Contains("End", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes None merge mode correctly")]
|
||||
public async Task ExecutesNoneMergeMode()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var noneMode = new WriteLine("NoneMode");
|
||||
noneMode.SetMergeMode(MergeMode.None);
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, noneMode },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, noneMode),
|
||||
CreateConnection(branch2, noneMode)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Branch1", _output.Lines);
|
||||
Assert.Contains("Branch2", _output.Lines);
|
||||
Assert.Contains("NoneMode", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles token consumption correctly")]
|
||||
public async Task HandlesTokenConsumption()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var middle = new WriteLine("Middle");
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = CreateSimpleLinearFlowchart(start, middle, end);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
// Tokens should be consumed after each activity completes
|
||||
Assert.Equal(3, _output.Lines.Count);
|
||||
Assert.Equal("Start", _output.Lines.ElementAt(0));
|
||||
Assert.Equal("Middle", _output.Lines.ElementAt(1));
|
||||
Assert.Equal("End", _output.Lines.ElementAt(2));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles multiple sequential converge nodes")]
|
||||
public async Task HandlesMultipleSequentialConvergeNodes()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var a1 = new WriteLine("A1");
|
||||
var a2 = new WriteLine("A2");
|
||||
var converge1 = new WriteLine("Converge1");
|
||||
converge1.SetMergeMode(MergeMode.Converge);
|
||||
var b1 = new WriteLine("B1");
|
||||
var b2 = new WriteLine("B2");
|
||||
var converge2 = new WriteLine("Converge2");
|
||||
converge2.SetMergeMode(MergeMode.Converge);
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, a1, a2, converge1, b1, b2, converge2, end },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, a1),
|
||||
CreateConnection(start, a2),
|
||||
CreateConnection(a1, converge1),
|
||||
CreateConnection(a2, converge1),
|
||||
CreateConnection(converge1, b1),
|
||||
CreateConnection(converge1, b2),
|
||||
CreateConnection(b1, converge2),
|
||||
CreateConnection(b2, converge2),
|
||||
CreateConnection(converge2, end)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("A1", _output.Lines);
|
||||
Assert.Contains("A2", _output.Lines);
|
||||
Assert.Contains("Converge1", _output.Lines);
|
||||
Assert.Contains("B1", _output.Lines);
|
||||
Assert.Contains("B2", _output.Lines);
|
||||
Assert.Contains("Converge2", _output.Lines);
|
||||
Assert.Contains("End", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles complex diamond pattern with tokens")]
|
||||
public async Task HandlesComplexDiamondPattern()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var left1 = new WriteLine("Left1");
|
||||
var left2 = new WriteLine("Left2");
|
||||
var right1 = new WriteLine("Right1");
|
||||
var right2 = new WriteLine("Right2");
|
||||
var converge = new WriteLine("Converge");
|
||||
converge.SetMergeMode(MergeMode.Converge);
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, left1, left2, right1, right2, converge, end },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, left1),
|
||||
CreateConnection(start, right1),
|
||||
CreateConnection(left1, left2),
|
||||
CreateConnection(right1, right2),
|
||||
CreateConnection(left2, converge),
|
||||
CreateConnection(right2, converge),
|
||||
CreateConnection(converge, end)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Left1", _output.Lines);
|
||||
Assert.Contains("Left2", _output.Lines);
|
||||
Assert.Contains("Right1", _output.Lines);
|
||||
Assert.Contains("Right2", _output.Lines);
|
||||
Assert.Contains("Converge", _output.Lines);
|
||||
Assert.Contains("End", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes activities in correct order for sequential flow")]
|
||||
public async Task ExecutesInCorrectOrderForSequential()
|
||||
{
|
||||
// Arrange
|
||||
var flowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("1"),
|
||||
new WriteLine("2"),
|
||||
new WriteLine("3"),
|
||||
new WriteLine("4")
|
||||
);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, _output.Lines.Count);
|
||||
Assert.Equal("1", _output.Lines.ElementAt(0));
|
||||
Assert.Equal("2", _output.Lines.ElementAt(1));
|
||||
Assert.Equal("3", _output.Lines.ElementAt(2));
|
||||
Assert.Equal("4", _output.Lines.ElementAt(3));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles nested flowcharts with tokens")]
|
||||
public async Task HandlesNestedFlowcharts()
|
||||
{
|
||||
// Arrange
|
||||
var innerFlowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("Inner1"),
|
||||
new WriteLine("Inner2")
|
||||
);
|
||||
|
||||
var outerFlowchart = CreateSimpleLinearFlowchart(
|
||||
new WriteLine("Outer1"),
|
||||
innerFlowchart,
|
||||
new WriteLine("Outer2")
|
||||
);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, outerFlowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Outer1", _output.Lines);
|
||||
Assert.Contains("Inner1", _output.Lines);
|
||||
Assert.Contains("Inner2", _output.Lines);
|
||||
Assert.Contains("Outer2", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles unconnected activities in flowchart")]
|
||||
public async Task HandlesUnconnectedActivities()
|
||||
{
|
||||
// Arrange
|
||||
var connected = new WriteLine("Connected");
|
||||
var unconnected = new WriteLine("Unconnected");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = connected,
|
||||
Activities = { connected, unconnected }
|
||||
// No connection to unconnected activity
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Single(_output.Lines);
|
||||
Assert.Equal("Connected", _output.Lines.ElementAt(0));
|
||||
Assert.DoesNotContain("Unconnected", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles mixed merge modes in complex flow")]
|
||||
public async Task HandlesMixedMergeModes()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var branch1 = new WriteLine("Branch1");
|
||||
var branch2 = new WriteLine("Branch2");
|
||||
var stream = new WriteLine("Stream");
|
||||
stream.SetMergeMode(MergeMode.Stream);
|
||||
var branch3 = new WriteLine("Branch3");
|
||||
var branch4 = new WriteLine("Branch4");
|
||||
var converge = new WriteLine("Converge");
|
||||
converge.SetMergeMode(MergeMode.Converge);
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = start,
|
||||
Activities = { start, branch1, branch2, stream, branch3, branch4, converge, end },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(start, branch1),
|
||||
CreateConnection(start, branch2),
|
||||
CreateConnection(branch1, stream),
|
||||
CreateConnection(branch2, stream),
|
||||
CreateConnection(stream, branch3),
|
||||
CreateConnection(stream, branch4),
|
||||
CreateConnection(branch3, converge),
|
||||
CreateConnection(branch4, converge),
|
||||
CreateConnection(converge, end)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Stream", _output.Lines);
|
||||
Assert.Contains("Converge", _output.Lines);
|
||||
Assert.Contains("End", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles converge with single inbound connection")]
|
||||
public async Task HandlesConvergeWithSingleInbound()
|
||||
{
|
||||
// Arrange
|
||||
var start = new WriteLine("Start");
|
||||
var single = new WriteLine("Single");
|
||||
single.SetMergeMode(MergeMode.Converge);
|
||||
var end = new WriteLine("End");
|
||||
|
||||
var flowchart = CreateSimpleLinearFlowchart(start, single, end);
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Start", _output.Lines);
|
||||
Assert.Contains("Single", _output.Lines);
|
||||
Assert.Contains("End", _output.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Emits and consumes tokens correctly across multiple steps")]
|
||||
public async Task EmitsAndConsumesTokensCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var step1 = new WriteLine("Step1");
|
||||
var step2a = new WriteLine("Step2a");
|
||||
var step2b = new WriteLine("Step2b");
|
||||
var step3 = new WriteLine("Step3");
|
||||
step3.SetMergeMode(MergeMode.Converge);
|
||||
var step4 = new WriteLine("Step4");
|
||||
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = step1,
|
||||
Activities = { step1, step2a, step2b, step3, step4 },
|
||||
Connections =
|
||||
{
|
||||
CreateConnection(step1, step2a),
|
||||
CreateConnection(step1, step2b),
|
||||
CreateConnection(step2a, step3),
|
||||
CreateConnection(step2b, step3),
|
||||
CreateConnection(step3, step4)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunFlowchartAsync(_services, flowchart);
|
||||
|
||||
// Assert
|
||||
// Verify all activities executed in a valid order
|
||||
Assert.Contains("Step1", _output.Lines);
|
||||
Assert.Contains("Step2a", _output.Lines);
|
||||
Assert.Contains("Step2b", _output.Lines);
|
||||
Assert.Contains("Step3", _output.Lines);
|
||||
Assert.Contains("Step4", _output.Lines);
|
||||
|
||||
// Step3 should only appear once (tokens consumed properly)
|
||||
Assert.Single(_output.Lines, l => l == "Step3");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Activities.IntegrationTests;
|
||||
|
||||
public class SetVariableTests
|
||||
{
|
||||
private readonly IWorkflowRunner _workflowRunner;
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public SetVariableTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
|
||||
_workflowRunner = _services.GetRequiredService<IWorkflowRunner>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "SetVariable sets variable in nearest scope when multiple variables with same name exist")]
|
||||
public async Task SetVariable_SetsVariableInNearestScope_WhenMultipleVariablesWithSameNameExist()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<VariableScopingWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
|
||||
// The sequence-level variable should be set to "Sequence Value"
|
||||
Assert.Equal(new[] { "Sequence Value" }, lines);
|
||||
}
|
||||
}
|
||||
|
||||
class VariableScopingWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
var workflowLevelVariable = new Variable<string>("Foo", "Workflow Value");
|
||||
var sequenceLevelVariable = new Variable<string>("Foo", "Initial Value");
|
||||
|
||||
workflow.Root = new Sequence
|
||||
{
|
||||
Variables = { workflowLevelVariable },
|
||||
Activities =
|
||||
{
|
||||
new Sequence
|
||||
{
|
||||
Variables = { sequenceLevelVariable },
|
||||
Activities =
|
||||
{
|
||||
new SetVariable
|
||||
{
|
||||
Variable = sequenceLevelVariable,
|
||||
Value = new("Sequence Value")
|
||||
},
|
||||
new WriteLine(context => context.GetVariable<string>("Foo"))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
317
test/unit/Elsa.Activities.UnitTests/Branching/IfTests.cs
Normal file
317
test/unit/Elsa.Activities.UnitTests/Branching/IfTests.cs
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Exceptions;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Branching;
|
||||
|
||||
public class IfTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Should_Set_Result_To_Condition_Value_Regardless_Of_Branch_Presence(bool conditionValue)
|
||||
{
|
||||
// Arrange - Test with no branches to verify result is independent of branch activities
|
||||
var ifActivity = new If(() => conditionValue);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.Equal(conditionValue, resultValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Schedule_Then_Branch_When_Condition_Is_True_And_Then_Branch_Exists()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => true);
|
||||
var thenActivity = new WriteLine("then executed");
|
||||
ifActivity.Then = thenActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.True(resultValue);
|
||||
Assert.True(context.HasScheduledActivity(thenActivity), "Then branch should be scheduled when condition is true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Schedule_Else_Branch_When_Condition_Is_False_And_Else_Branch_Exists()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => false);
|
||||
var elseActivity = new WriteLine("else executed");
|
||||
ifActivity.Else = elseActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.False(resultValue);
|
||||
Assert.True(context.HasScheduledActivity(elseActivity), "Else branch should be scheduled when condition is false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Schedule_Only_Then_Branch_When_Condition_Is_True_And_Both_Branches_Exist()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => true);
|
||||
var thenActivity = new WriteLine("then executed");
|
||||
var elseActivity = new WriteLine("else executed");
|
||||
ifActivity.Then = thenActivity;
|
||||
ifActivity.Else = elseActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.True(resultValue);
|
||||
Assert.True(context.HasScheduledActivity(thenActivity), "Then branch should be scheduled when condition is true");
|
||||
Assert.False(context.HasScheduledActivity(elseActivity), "Else branch should not be scheduled when condition is true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Schedule_Only_Else_Branch_When_Condition_Is_False_And_Both_Branches_Exist()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => false);
|
||||
var thenActivity = new WriteLine("then executed");
|
||||
var elseActivity = new WriteLine("else executed");
|
||||
ifActivity.Then = thenActivity;
|
||||
ifActivity.Else = elseActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.False(resultValue);
|
||||
Assert.True(context.HasScheduledActivity(elseActivity), "Else branch should be scheduled when condition is false");
|
||||
Assert.False(context.HasScheduledActivity(thenActivity), "Then branch should not be scheduled when condition is false");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Should_Not_Throw_When_Only_Then_Branch_Is_Present(bool conditionValue)
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => conditionValue)
|
||||
{
|
||||
Then = new WriteLine("then branch")
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.Equal(conditionValue, resultValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Should_Not_Throw_When_Only_Else_Branch_Is_Present(bool conditionValue)
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => conditionValue)
|
||||
{
|
||||
Else = new WriteLine("else branch")
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.Equal(conditionValue, resultValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Not_Schedule_Then_Branch_When_Condition_Is_False_And_Only_Then_Branch_Exists()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => false);
|
||||
var thenActivity = new WriteLine("then executed");
|
||||
ifActivity.Then = thenActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.False(resultValue);
|
||||
Assert.False(context.HasScheduledActivity(thenActivity), "Then branch should not be scheduled when condition is false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Not_Schedule_Else_Branch_When_Condition_Is_True_And_Only_Else_Branch_Exists()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => true);
|
||||
var elseActivity = new WriteLine("else executed");
|
||||
ifActivity.Else = elseActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.True(resultValue);
|
||||
Assert.False(context.HasScheduledActivity(elseActivity), "Else branch should not be scheduled when condition is true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Return_False_When_Condition_Is_Not_Set()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(); // no condition provided
|
||||
var thenActivity = new WriteLine("then");
|
||||
ifActivity.Then = thenActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.False(resultValue, "If activity should return false when no condition is set");
|
||||
Assert.False(context.HasScheduledActivity(thenActivity), "Then branch should not be scheduled when condition defaults to false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Bubble_Exception_From_Condition_And_Not_Schedule_Any_Branch()
|
||||
{
|
||||
// Arrange
|
||||
var ifActivity = new If(() => throw new ApplicationException("boom"));
|
||||
var thenActivity = new WriteLine("then");
|
||||
var elseActivity = new WriteLine("else");
|
||||
ifActivity.Then = thenActivity;
|
||||
ifActivity.Else = elseActivity;
|
||||
|
||||
// Act - Throwing any kind of exception in the condition results in an InputEvaluationException
|
||||
var ex = await Assert.ThrowsAsync<InputEvaluationException>(() => ExecuteAsync(ifActivity));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Failed to evaluate", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Evaluate_Condition_Exactly_Once()
|
||||
{
|
||||
// Arrange
|
||||
var count = 0;
|
||||
var ifActivity = new If(() => { count++; return true; });
|
||||
var thenActivity = new WriteLine("then");
|
||||
ifActivity.Then = thenActivity;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, count);
|
||||
Assert.True(context.HasScheduledActivity(thenActivity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Use_Latest_Captured_State_When_Evaluating_Condition()
|
||||
{
|
||||
// Arrange
|
||||
var flag = false;
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
var ifActivity = new If(() => flag);
|
||||
var thenActivity = new WriteLine("then");
|
||||
var elseActivity = new WriteLine("else");
|
||||
ifActivity.Then = thenActivity;
|
||||
ifActivity.Else = elseActivity;
|
||||
|
||||
// Mutate after construction, before execution
|
||||
flag = true;
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(ifActivity);
|
||||
|
||||
// Assert
|
||||
var resultValue = (bool)context.GetActivityOutput(() => ifActivity.Result)!;
|
||||
Assert.True(resultValue);
|
||||
Assert.True(context.HasScheduledActivity(thenActivity));
|
||||
Assert.False(context.HasScheduledActivity(elseActivity));
|
||||
}
|
||||
|
||||
[Theory] // outer, inner
|
||||
[InlineData(true, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(false, false)]
|
||||
public async Task Should_Schedule_Correct_Branches_For_Nested_If(bool outerCondition, bool innerCondition)
|
||||
{
|
||||
// Arrange inner
|
||||
var innerThen = new WriteLine("inner-then");
|
||||
var innerElse = new WriteLine("inner-else");
|
||||
var innerIf = new If(() => innerCondition)
|
||||
{
|
||||
Then = innerThen,
|
||||
Else = innerElse
|
||||
};
|
||||
|
||||
// Arrange outer
|
||||
var outerElse = new WriteLine("outer-else");
|
||||
var outerIf = new If(() => outerCondition)
|
||||
{
|
||||
Then = innerIf,
|
||||
Else = outerElse
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(outerIf);
|
||||
|
||||
// Assert outer result & scheduling
|
||||
var outerResult = (bool)context.GetActivityOutput(() => outerIf.Result)!;
|
||||
Assert.Equal(outerCondition, outerResult);
|
||||
|
||||
if (outerCondition)
|
||||
{
|
||||
// When outer condition is true, the inner If should be scheduled
|
||||
Assert.True(context.HasScheduledActivity(innerIf),
|
||||
"Inner If should be scheduled when outer condition is true");
|
||||
|
||||
// The outer else should NOT be scheduled
|
||||
Assert.False(context.HasScheduledActivity(outerElse),
|
||||
"Outer else should not be scheduled when outer condition is true");
|
||||
|
||||
// The inner branches should NOT be scheduled yet because the inner If hasn't executed
|
||||
Assert.False(context.HasScheduledActivity(innerThen),
|
||||
"Inner then should not be scheduled yet - inner If hasn't executed");
|
||||
Assert.False(context.HasScheduledActivity(innerElse),
|
||||
"Inner else should not be scheduled yet - inner If hasn't executed");
|
||||
|
||||
// Note: The inner If result won't be available until it executes, so we can't assert it
|
||||
}
|
||||
else
|
||||
{
|
||||
// When outer condition is false, outer else should be scheduled
|
||||
Assert.True(context.HasScheduledActivity(outerElse),
|
||||
"Outer else should be scheduled when outer condition is false");
|
||||
|
||||
// The inner If and its branches should NOT be scheduled
|
||||
Assert.False(context.HasScheduledActivity(innerIf),
|
||||
"Inner If should not be scheduled when outer condition is false");
|
||||
Assert.False(context.HasScheduledActivity(innerThen),
|
||||
"Inner then should not be scheduled when outer condition is false");
|
||||
Assert.False(context.HasScheduledActivity(innerElse),
|
||||
"Inner else should not be scheduled when outer condition is false");
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
{
|
||||
return new ActivityTestFixture(activity).ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Flow;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper methods for Flowchart unit tests.
|
||||
/// </summary>
|
||||
public static class FlowchartTestHelpers
|
||||
{
|
||||
public static async Task<ActivityExecutionContext> ExecuteFlowchartAsync(Flowchart flowchart)
|
||||
{
|
||||
var fixture = new ActivityTestFixture(flowchart);
|
||||
return await fixture.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
93
test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs
Normal file
93
test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using static Elsa.Activities.UnitTests.Flow.FlowchartTestHelpers;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Flow;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for common Flowchart behavior (both counter and token-based strategies).
|
||||
/// </summary>
|
||||
public class FlowchartTests
|
||||
{
|
||||
[Fact(DisplayName = "Schedules start activity when specified")]
|
||||
public async Task SchedulesStartActivity()
|
||||
{
|
||||
// Arrange
|
||||
var startActivity = new WriteLine("Start");
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = startActivity
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteFlowchartAsync(flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(startActivity));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Executes without error when no start activity specified")]
|
||||
public async Task ExecutesWithoutErrorWhenNoStartActivity()
|
||||
{
|
||||
// Arrange
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteFlowchartAsync(flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context);
|
||||
Assert.False(context.HasScheduledActivity(new WriteLine("NonExistent")));
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Respects UseTokenFlow flag")]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task RespectsUseTokenFlowFlag(bool useTokenFlow)
|
||||
{
|
||||
// Arrange
|
||||
var originalValue = Flowchart.UseTokenFlow;
|
||||
Flowchart.UseTokenFlow = useTokenFlow;
|
||||
|
||||
try
|
||||
{
|
||||
var activity = new WriteLine("Test");
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = activity,
|
||||
Activities = { activity }
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteFlowchartAsync(flowchart);
|
||||
|
||||
// Assert - just verify it executes without error
|
||||
Assert.NotNull(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Flowchart.UseTokenFlow = originalValue;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Accepts empty connections collection")]
|
||||
public async Task AcceptsEmptyConnections()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new WriteLine("Isolated");
|
||||
var flowchart = new Flowchart
|
||||
{
|
||||
Start = activity,
|
||||
Activities = { activity }
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteFlowchartAsync(flowchart);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(activity));
|
||||
}
|
||||
}
|
||||
|
|
@ -285,4 +285,4 @@ public class SendHttpRequestTests
|
|||
{
|
||||
return (_, _) => throw ((TException)Activator.CreateInstance(typeof(TException), message)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using Elsa.Testing.Shared;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Primitives;
|
||||
|
||||
public class SetVariableOfTTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Should_Set_Variable_Integer()
|
||||
{
|
||||
// Arrange
|
||||
const int expected = 42; // The answer to life, the universe and everything.
|
||||
var variable = new Variable<int>("myVar", 0, "myVar");
|
||||
var setVariable = new SetVariable<int>(variable, new Input<int>(expected));
|
||||
|
||||
// Act
|
||||
var fixture = new ActivityTestFixture(setVariable);
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var result = variable.Get(context);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Throw_When_Variable_Is_Null()
|
||||
{
|
||||
// Arrange
|
||||
var setVariable = new SetVariable<string>(null!, new Input<string>("test value"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Record.ExceptionAsync(() => new ActivityTestFixture(setVariable).ExecuteAsync());
|
||||
|
||||
Assert.NotNull(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Set_Variable_To_Null_Value()
|
||||
{
|
||||
// Arrange
|
||||
var variable = new Variable<string?>("myVar", "initial value", "myVar");
|
||||
var setVariable = new SetVariable<string?>(variable, new Input<string?>((string?)null));
|
||||
|
||||
// Act
|
||||
var fixture = new ActivityTestFixture(setVariable);
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var result = variable.Get(context);
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Primitives;
|
||||
|
|
@ -6,12 +8,16 @@ namespace Elsa.Activities.UnitTests.Primitives;
|
|||
public class SetVariableTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Should_Set_Variable_Integer()
|
||||
public async Task Should_Set_Variable()
|
||||
{
|
||||
// Arrange
|
||||
const int expected = 42; // The answer to life, the universe and everything.
|
||||
var variable = new Variable<int>("myVar", 0, "myVar");
|
||||
var setVariable = new SetVariable<int>(variable, new Input<int>(expected));
|
||||
const int expected = 42;
|
||||
var variable = new Variable("myVar", 0, "myVar");
|
||||
var setVariable = new SetVariable
|
||||
{
|
||||
Variable = variable,
|
||||
Value = new(expected)
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(setVariable);
|
||||
|
|
@ -25,12 +31,17 @@ public class SetVariableTests
|
|||
public async Task Should_Throw_When_Variable_Is_Null()
|
||||
{
|
||||
// Arrange
|
||||
var setVariable = new SetVariable<string>(null!, new Input<string>("test value"));
|
||||
var setVariable = new SetVariable
|
||||
{
|
||||
Variable = null,
|
||||
Value = new("test value")
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Record.ExceptionAsync(() => ExecuteAsync(setVariable));
|
||||
|
||||
Assert.NotNull(exception);
|
||||
Assert.IsType<InvalidOperationException>(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -38,7 +49,11 @@ public class SetVariableTests
|
|||
{
|
||||
// Arrange
|
||||
var variable = new Variable<string?>("myVar", "initial value", "myVar");
|
||||
var setVariable = new SetVariable<string?>(variable, new Input<string?>((string?)null));
|
||||
var setVariable = new SetVariable
|
||||
{
|
||||
Variable = variable,
|
||||
Value = new(new Literal(null))
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(setVariable);
|
||||
|
|
@ -47,9 +62,9 @@ public class SetVariableTests
|
|||
var result = variable.Get(context);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
|
||||
private static async Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
{
|
||||
return new ActivityTestFixture(activity).ExecuteAsync();
|
||||
return await new ActivityTestFixture(activity).ExecuteAsync();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue