* Fix ForEach completion from nested flowchart * Assert ForEach complete output regression * Use break flag helper consistently * Cover parent flowchart ForEach completion
324 lines
11 KiB
C#
324 lines
11 KiB
C#
using Elsa.Extensions;
|
|
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 Elsa.Workflows.Management.Activities.SetOutput;
|
|
using Elsa.Workflows.Models;
|
|
using Elsa.Workflows.Options;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace Elsa.Activities.IntegrationTests;
|
|
|
|
public class ForEachTests(ITestOutputHelper testOutputHelper)
|
|
{
|
|
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
|
private const string CurrentValueVar = "CurrentValue";
|
|
|
|
[Fact(DisplayName = "ForEach executes each activity for every item in the collection")]
|
|
public async Task ForEach_ExecutesEachActivity_ForEveryItem()
|
|
{
|
|
var expectedLines = new[]
|
|
{
|
|
"a", "b", "c"
|
|
};
|
|
var forEach = new ForEach<string>(expectedLines)
|
|
{
|
|
Body = WriteCurrentValue()
|
|
};
|
|
await RunAndAssertLines(forEach, expectedLines);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach executes each activity for every item in the collection even if there's only one item")]
|
|
public async Task ForEach_ExecutesEachActivity_ForSingleItem()
|
|
{
|
|
var expectedLines = new[]
|
|
{
|
|
"a"
|
|
};
|
|
var forEach = new ForEach<string>(expectedLines)
|
|
{
|
|
Body = WriteCurrentValue()
|
|
};
|
|
await RunAndAssertLines(forEach, expectedLines);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach completes when the collection is empty")]
|
|
public async Task ForEach_Completes_WhenCollectionIsEmpty()
|
|
{
|
|
string[] expectedLines = [];
|
|
var forEach = new ForEach<string>(expectedLines)
|
|
{
|
|
Body = WriteCurrentValue()
|
|
};
|
|
var result = await _fixture.RunActivityAsync(forEach);
|
|
var journal = result.Journal;
|
|
var forEachContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is ForEach<string>);
|
|
Assert.NotNull(forEachContext);
|
|
Assert.Equal(ActivityStatus.Completed, forEachContext.Status);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach faults when the collection is null")]
|
|
public async Task ForEach_Faults_WhenCollectionIsNull()
|
|
{
|
|
var forEach = new ForEach<string>((ICollection<string>)null!)
|
|
{
|
|
Body = WriteCurrentValue()
|
|
};
|
|
var result = await _fixture.RunActivityAsync(forEach);
|
|
var journal = result.Journal;
|
|
var forEachContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is ForEach<string>);
|
|
Assert.NotNull(forEachContext);
|
|
Assert.Equal(ActivityStatus.Faulted, forEachContext.Status);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach breaks when the Break activity executed")]
|
|
public async Task ForEach_BreaksOutOfLoop_WhenExecutingBreakActivity()
|
|
{
|
|
var dataSource = new[]
|
|
{
|
|
"a", "b", "c"
|
|
};
|
|
var expectedLines = new[]
|
|
{
|
|
"a"
|
|
};
|
|
|
|
var forEach = new ForEach<string>(dataSource)
|
|
{
|
|
Body = new If
|
|
{
|
|
Condition = new(context => context.GetVariable<string>(CurrentValueVar) == "b"),
|
|
Then = new Break(),
|
|
Else = WriteCurrentValue()
|
|
}
|
|
};
|
|
await RunAndAssertLines(forEach, expectedLines);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach executes each activity for different item types")]
|
|
public async Task ForEach_ExecutesEachActivity_ForDifferentItemTypes()
|
|
{
|
|
var dataSource = new object?[]
|
|
{
|
|
"a", 2, null, new Foo()
|
|
};
|
|
var expectedLines = new object[]
|
|
{
|
|
"a", "2", "", "Baz"
|
|
};
|
|
var forEach = new ForEach<object?>(dataSource)
|
|
{
|
|
Body = new WriteLine(context => context.GetVariable<object>(CurrentValueVar)?.ToString() ?? "")
|
|
};
|
|
await RunAndAssertLines(forEach, expectedLines);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach completes when the end of the collection is reached even when an item is added to the collection at runtime")]
|
|
public async Task ForEach_Completes_WhenItemAddedToCollectionAtRuntime()
|
|
{
|
|
var dataSource = new[]
|
|
{
|
|
"a", "b", "c"
|
|
}.ToList();
|
|
var expectedLines = new[]
|
|
{
|
|
"a", "b", "c", "e"
|
|
};
|
|
var dataSourceInput = new Input<ICollection<string>>(() => dataSource.ToList());
|
|
var forEach = new ForEach<string>(dataSourceInput)
|
|
{
|
|
Body = new Sequence
|
|
{
|
|
Activities =
|
|
[
|
|
new If(context => context.GetVariable<string>(CurrentValueVar) == "b")
|
|
{
|
|
Then = new Inline(inlineContext =>
|
|
{
|
|
dataSource.Add("e");
|
|
inlineContext.Set(dataSourceInput.MemoryBlockReference(), dataSource.ToList());
|
|
})
|
|
},
|
|
WriteCurrentValue()
|
|
]
|
|
}
|
|
};
|
|
await RunAndAssertLines(forEach, expectedLines);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach completes when the end of the collection is reached even when an item is removed from the collection at runtime")]
|
|
public async Task ForEach_Completes_WhenItemRemovedFromCollectionAtRuntime()
|
|
{
|
|
var dataSource = new[]
|
|
{
|
|
"a", "b", "c"
|
|
}.ToList();
|
|
var expectedLines = new[]
|
|
{
|
|
"a", "b"
|
|
};
|
|
var dataSourceInput = new Input<ICollection<string>>(() => dataSource.ToList());
|
|
var forEach = new ForEach<string>(dataSourceInput)
|
|
{
|
|
Body = new Sequence
|
|
{
|
|
Activities =
|
|
[
|
|
new If(context => context.GetVariable<string>(CurrentValueVar) == "a")
|
|
{
|
|
Then = new Inline(inlineContext =>
|
|
{
|
|
dataSource.Remove("c");
|
|
inlineContext.Set(dataSourceInput.MemoryBlockReference(), dataSource.ToList());
|
|
})
|
|
},
|
|
WriteCurrentValue()
|
|
]
|
|
}
|
|
};
|
|
await RunAndAssertLines(forEach, expectedLines);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach completes when the end of the collection is reached even when an item is changed at runtime")]
|
|
public async Task ForEach_Completes_WhenItemChangedAtRuntime()
|
|
{
|
|
var dataSource = new[]
|
|
{
|
|
"a", "b", "c"
|
|
}.ToList();
|
|
var expectedLines = new[]
|
|
{
|
|
"a", "b", "d"
|
|
};
|
|
var dataSourceInput = new Input<ICollection<string>>(() => dataSource.ToList());
|
|
var forEach = new ForEach<string>(dataSourceInput)
|
|
{
|
|
Body = new Sequence
|
|
{
|
|
Activities =
|
|
[
|
|
new If(context => context.GetVariable<string>(CurrentValueVar) == "b")
|
|
{
|
|
Then = new Inline(inlineContext =>
|
|
{
|
|
dataSource.Remove("c");
|
|
dataSource.Add("d");
|
|
inlineContext.Set(dataSourceInput.MemoryBlockReference(), dataSource.ToList());
|
|
})
|
|
},
|
|
WriteCurrentValue()
|
|
]
|
|
}
|
|
};
|
|
await RunAndAssertLines(forEach, expectedLines);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach remains in the Running state when an activity faults")]
|
|
public async Task ForEach_Suspends_WhenActivityFaults()
|
|
{
|
|
var dataSource = new[]
|
|
{
|
|
"a", "b", "c"
|
|
}.ToList();
|
|
var expectedLines = new[]
|
|
{
|
|
"a", "b"
|
|
};
|
|
var forEach = new ForEach<string>(dataSource)
|
|
{
|
|
Body = new Sequence
|
|
{
|
|
Activities =
|
|
[
|
|
new If(context => context.GetVariable<string>(CurrentValueVar) == "c")
|
|
{
|
|
Then = new Fault
|
|
{
|
|
Message = new("Faulted"),
|
|
}
|
|
},
|
|
WriteCurrentValue()
|
|
]
|
|
}
|
|
};
|
|
var result = await _fixture.RunActivityAsync(forEach);
|
|
var journal = result.Journal;
|
|
var forEachContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is ForEach<string>);
|
|
Assert.Equal(expectedLines, _fixture.CapturingTextWriter.Lines);
|
|
Assert.NotNull(forEachContext);
|
|
Assert.Equal(ActivityStatus.Running, forEachContext.Status);
|
|
Assert.Equal(1, forEachContext.AggregateFaultCount);
|
|
}
|
|
|
|
[Fact(DisplayName = "ForEach completes when a nested flowchart completes the composite")]
|
|
public async Task ForEach_Completes_WhenNestedFlowchartCompletesComposite()
|
|
{
|
|
var dataSource = new[]
|
|
{
|
|
"a", "b", "c"
|
|
};
|
|
var writeLine = WriteCurrentValue();
|
|
var decision = new FlowDecision(context => context.GetVariable<string>(CurrentValueVar) == "b");
|
|
var setOutput = new SetOutput
|
|
{
|
|
OutputName = new("Output"),
|
|
OutputValue = new(context => context.GetVariable<string>(CurrentValueVar))
|
|
};
|
|
var complete = new Complete(["True"]);
|
|
var forEach = new ForEach<string>(dataSource)
|
|
{
|
|
Body = new Flowchart
|
|
{
|
|
Activities =
|
|
{
|
|
writeLine,
|
|
decision,
|
|
setOutput,
|
|
complete
|
|
},
|
|
Connections =
|
|
{
|
|
new() { Source = new(writeLine, "Done"), Target = new(decision) },
|
|
new() { Source = new(decision, "True"), Target = new(setOutput) },
|
|
new() { Source = new(setOutput, "Done"), Target = new(complete) }
|
|
}
|
|
}
|
|
};
|
|
var outerComplete = new Complete(["False"]);
|
|
var outerFlowchart = new Flowchart
|
|
{
|
|
Activities =
|
|
{
|
|
forEach,
|
|
outerComplete
|
|
},
|
|
Connections =
|
|
{
|
|
new() { Source = new(forEach, "Done"), Target = new(outerComplete) }
|
|
}
|
|
};
|
|
var options = new RunWorkflowOptions().WithCounterBasedFlowchart();
|
|
|
|
var result = await _fixture.RunActivityAsync(outerFlowchart, options);
|
|
|
|
Assert.Equal(new[] { "a", "b" }, _fixture.CapturingTextWriter.Lines);
|
|
Assert.Equal("b", result.WorkflowState.Output["Output"]);
|
|
}
|
|
|
|
private static WriteLine WriteCurrentValue() => new(context => context.GetVariable<string>(CurrentValueVar));
|
|
|
|
private async Task RunAndAssertLines(IActivity activity, System.Collections.IEnumerable expected)
|
|
{
|
|
await _fixture.RunActivityAsync(activity);
|
|
Assert.Equal(expected, _fixture.CapturingTextWriter.Lines);
|
|
}
|
|
}
|
|
|
|
record Foo(string Bar = "Baz")
|
|
{
|
|
public override string ToString() => Bar;
|
|
}
|