Improves Fork activity and break signal handling (#7104)
* Add unit and integration tests for `Container` activity, covering behavior such as variable scoping, child activity execution, and mixed variable types. * Refactor `RunWorkflowAndCaptureOutput` method in `ContainerTests` for better code organization * Move `Sequence` activity tests to a dedicated namespace and add new unit and integration tests for enhanced coverage. - Deleted outdated `SequenceTests` and related workflows. - Introduced `SequenceActivity` namespace with improved organization. - Added comprehensive unit and integration test coverage for sequential execution, nested sequences, conditional breaking, variables, and dynamic activities. * Relocate `SequenceActivity` tests to `Activities` namespace to improve organization and update references in related test classes. * Refactor `SequenceTests` to move `DynamicSequenceWorkflow` to its own file for better test organization. * Extract `TestContainer` to `Elsa.Testing.Shared.Activities` for reuse across test projects. * Add unit and integration tests for `Break` activity; refactor workflow tests for improved organization - Introduced `BreakInForkWorkflow` and deprecated `BreakWhileForkWorkflow`. - Added `BreakTests` unit tests to validate behavior of the `Break` activity, including terminal node implementation and execution completion. - Enhanced integration tests for `Break` activity, covering multiple looping constructs (`ForEach`, `For`, `While`, `Fork`) and nested workflows. - Updated `Fork` activity to handle the `BreakSignal` asynchronously. - Simplified workflow definitions by removing redundant constructors and using concise variable initialization syntax. - Improved test clarity with better organization, comments, and consistent naming conventions. * Refactor `Fork` activity tests and workflows for improved organization and coverage - Relocated `BasicForkWorkflow` and `JoinAnyForkWorkflow` to `Fork/Workflows` namespace. - Introduced `EmptyForkWorkflow` to test Fork behavior with no branches. - Enhanced `ForkTests` with scenarios for `Fork` execution with different join modes and branch configurations. - Refactored `Fork` activity to handle empty branches and simplified `BreakSignal` handling. - Improved consistency and clarity of test cases, including better assertions and comments. * Update test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update GitHub Actions workflow to use .NET 10.x * Remove outdated GitHub workflows and update configurations to .NET 10.x * Remove unused result variable assignments in integration tests (#7105) * Initial plan * Remove unused result variable assignments in BreakTests and ForkTests Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
This commit is contained in:
parent
7c7561b509
commit
3340e14b74
54
.github/copilot-instructions.md
vendored
54
.github/copilot-instructions.md
vendored
|
|
@ -51,7 +51,7 @@ docker/ # Docker configurations
|
|||
## Build Instructions
|
||||
|
||||
### Prerequisites
|
||||
- **.NET 9.0 SDK** (verified working version: 9.0.305)
|
||||
- **.NET 10.0 SDK**
|
||||
- **Build time**: Initial restore ~1-2 minutes, full compile ~5-10 minutes
|
||||
|
||||
### Critical Build Information
|
||||
|
|
@ -111,11 +111,10 @@ find test/unit -name "*.csproj" | head -5 | xargs -I {} dotnet build {}
|
|||
### Expected Build Warnings
|
||||
- `NU1900`: Unable to load service index for external feeds (safe to ignore)
|
||||
- `NU1801`: Service index warnings for feedz.io sources (safe to ignore)
|
||||
- `NU1101`: Missing Elsa.Studio packages (blocks studio app builds)
|
||||
|
||||
### Successful Build Indicators
|
||||
- Core modules (Elsa.Workflows.Core, etc.) compile successfully
|
||||
- Server applications (Elsa.Server.Web) build without the studio UI
|
||||
- Server applications (Elsa.Server.Web) build
|
||||
- Most modules show "succeeded with X warning(s)" (warnings are acceptable)
|
||||
|
||||
## Testing
|
||||
|
|
@ -142,7 +141,7 @@ dotnet test --no-build --no-restore [project-path]
|
|||
|
||||
### Code Standards
|
||||
- **Language version**: C# latest
|
||||
- **Target framework**: .NET 9.0
|
||||
- **Target framework**: .NET 10.0
|
||||
- **Nullable reference types**: Enabled
|
||||
- **Implicit usings**: Enabled
|
||||
- **EditorConfig**: Configured (4-space indentation, CRLF line endings)
|
||||
|
|
@ -154,23 +153,22 @@ dotnet test --no-build --no-restore [project-path]
|
|||
- **Async/await**: Extensive use throughout for scalability
|
||||
|
||||
### Common Gotchas
|
||||
1. **External Dependencies**: Studio-related projects require external packages
|
||||
2. **NuGet Source Mapping**: Configured in NuGet.Config, restricts where packages can be sourced
|
||||
3. **Multiple Target Frameworks**: Some projects conditionally target different frameworks
|
||||
4. **Build Warnings**: Many NU1900/NU1801 warnings are expected and safe
|
||||
1. **NuGet Source Mapping**: Configured in NuGet.Config, restricts where packages can be sourced
|
||||
2**Multiple Target Frameworks**: Some projects conditionally target different frameworks
|
||||
3**Build Warnings**: Many NU1900/NU1801 warnings are expected and safe
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Workflow
|
||||
- **Trigger**: Pull requests to `main` branch
|
||||
- **Runner**: ubuntu-latest
|
||||
- **.NET Version**: 9.x (latest)
|
||||
- **.NET Version**: 10.x (latest)
|
||||
- **Commands**: `./build.cmd Compile Test Pack`
|
||||
- **File**: `.github/workflows/pr.yml` (auto-generated by NUKE)
|
||||
|
||||
### CI Pipeline Steps
|
||||
1. Checkout code
|
||||
2. Setup .NET 9.x SDK
|
||||
2. Setup .NET 10.x SDK
|
||||
3. Execute: Compile → Test → Pack
|
||||
4. Expected warnings for external feed access
|
||||
5. Studio apps may be excluded from CI builds
|
||||
|
|
@ -230,29 +228,6 @@ dotnet run
|
|||
|
||||
The server will start on the configured ports (check `appsettings.json` or environment variables).
|
||||
|
||||
### Combined Server and Studio
|
||||
|
||||
To run both the workflow server and visual studio together:
|
||||
|
||||
```bash
|
||||
cd src/apps/Elsa.ServerAndStudio.Web
|
||||
dotnet restore --ignore-failed-sources
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Access the studio at the configured URL (typically `http://localhost:5000` or as configured).
|
||||
|
||||
### Using Docker
|
||||
|
||||
For quick testing with Docker (see README for full details):
|
||||
|
||||
```bash
|
||||
docker pull elsaworkflows/elsa-server-and-studio-v3:latest
|
||||
docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e HTTP_PORTS=8080 -e HTTP__BASEURL=http://localhost:13000 -p 13000:8080 elsaworkflows/elsa-server-and-studio-v3:latest
|
||||
```
|
||||
|
||||
Default credentials: username `admin`, password `password`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
|
@ -296,12 +271,11 @@ Default credentials: username `admin`, password `password`
|
|||
## Important Notes for Coding Agents
|
||||
|
||||
1. **Always use `--ignore-failed-sources`** when restoring packages
|
||||
2. **Focus on core workflow functionality** rather than studio UI components
|
||||
3. **Studio apps require external packages** that may not be accessible
|
||||
4. **Build warnings are normal** - don't try to fix NU1900/NU1801 warnings
|
||||
5. **Test individual modules** rather than solution-wide tests when external deps fail
|
||||
6. **Use direct dotnet commands** for building specific components when NUKE fails
|
||||
7. **Check project references** before attempting builds - some projects have conditional references
|
||||
8. **Start with core modules** like `Elsa.Workflows.Core`, `Elsa.Workflows.Runtime` which are more likely to build successfully
|
||||
2. **Focus on core workflow functionality**
|
||||
3. **Build warnings are normal** - don't try to fix NU1900/NU1801 warnings
|
||||
4. **Test individual modules** rather than solution-wide tests when external deps fail
|
||||
5. **Use direct dotnet commands** for building specific components when NUKE fails
|
||||
6. **Check project references** before attempting builds - some projects have conditional references
|
||||
7. **Start with core modules** like `Elsa.Workflows.Core`, `Elsa.Workflows.Runtime` which are more likely to build successfully
|
||||
|
||||
Trust these instructions for build and development workflows. Only search for additional information if these instructions are incomplete or found to be incorrect.
|
||||
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up .NET 9 SDK
|
||||
- name: Set up .NET 10 SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "9.0.x"
|
||||
dotnet-version: "10.0.x"
|
||||
2
.github/workflows/docker-ca.yml
vendored
2
.github/workflows/docker-ca.yml
vendored
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Publish TLS smoke test app
|
||||
run: dotnet publish test/TlsSmoke/TlsSmoke.csproj -c Release -o artifacts/tls-smoke
|
||||
|
|
|
|||
61
.github/workflows/elsa-server-and-studio.yml
vendored
61
.github/workflows/elsa-server-and-studio.yml
vendored
|
|
@ -1,61 +0,0 @@
|
|||
name: Elsa Server + Studio Docker image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- develop/3.6.0
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elsaworkflows/elsa-server-and-studio-v3-6-preview
|
||||
flavor: |
|
||||
latest=true
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=sha
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/ElsaServerAndStudio.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
9.x
|
||||
10.x
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run: Compile, Test'
|
||||
run: ./build.cmd Compile Test
|
||||
|
|
|
|||
41
Elsa.sln
41
Elsa.sln
|
|
@ -94,17 +94,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Core.UnitTes
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Tenants", "src\modules\Elsa.Tenants\Elsa.Tenants.csproj", "{29638A67-E79F-44FE-AC05-DA499EBA929E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{B789B743-0FBE-4BBC-9F45-84C20C8D4814}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.github\workflows\bounty.yml = .github\workflows\bounty.yml
|
||||
.github\workflows\elsa-server-and-studio.yml = .github\workflows\elsa-server-and-studio.yml
|
||||
.github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml
|
||||
.github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml
|
||||
.github\workflows\packages.yml = .github\workflows\packages.yml
|
||||
.github\workflows\pr.yml = .github\workflows\pr.yml
|
||||
.github\workflows\stale.yml = .github\workflows\stale.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C80C8231-D35C-4ACC-9ED6-9F3DB221535E}"
|
||||
|
|
@ -313,6 +302,34 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scheduling.UnitTests",
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Common.IntegrationTests", "test\integration\Elsa.Common.IntegrationTests\Elsa.Common.IntegrationTests.csproj", "{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{06DC6553-AAEF-4FC8-B16F-039F292E86A3}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.github\copilot-instructions.md = .github\copilot-instructions.md
|
||||
.github\dependabot.yml = .github\dependabot.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{2158FC82-6253-47F2-9216-B266710D50F1}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.github\workflows\stale.yml = .github\workflows\stale.yml
|
||||
.github\workflows\pr.yml = .github\workflows\pr.yml
|
||||
.github\workflows\packages.yml = .github\workflows\packages.yml
|
||||
.github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml
|
||||
.github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml
|
||||
.github\workflows\docker-ca.yml = .github\workflows\docker-ca.yml
|
||||
.github\workflows\bounty.yml = .github\workflows\bounty.yml
|
||||
.github\workflows\copilot-setup-steps.yml = .github\workflows\copilot-setup-steps.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issue_templates", "issue_templates", "{36556068-64BF-4D4B-827E-EC8CDC23D516}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md
|
||||
.github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md
|
||||
.github\ISSUE_TEMPLATE\improvement.md = .github\ISSUE_TEMPLATE\improvement.md
|
||||
.github\ISSUE_TEMPLATE\performance.md = .github\ISSUE_TEMPLATE\performance.md
|
||||
.github\ISSUE_TEMPLATE\task.md = .github\ISSUE_TEMPLATE\task.md
|
||||
.github\ISSUE_TEMPLATE\documentation.md = .github\ISSUE_TEMPLATE\documentation.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -721,6 +738,8 @@ Global
|
|||
{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{9AEE941A-3F23-4E4B-9B59-1A2F57FC762D} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||
{2158FC82-6253-47F2-9216-B266710D50F1} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
|
||||
{36556068-64BF-4D4B-827E-EC8CDC23D516} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ public class For : Activity
|
|||
/// <inheritdoc />
|
||||
public For(int start, int end, int step, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line)
|
||||
{
|
||||
Start = new Input<int>(start);
|
||||
End = new Input<int>(end);
|
||||
Step = new Input<int>(step);
|
||||
Start = new(start);
|
||||
End = new(end);
|
||||
Step = new(step);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Attributes;
|
||||
using Elsa.Workflows.Signals;
|
||||
using Elsa.Workflows.UIHints;
|
||||
|
|
@ -38,7 +39,17 @@ public class Fork : Activity
|
|||
public ICollection<IActivity> Branches { get; set; } = new List<IActivity>();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => context.ScheduleActivities(Branches, CompleteChildAsync);
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
// If there are no branches, complete immediately
|
||||
if (Branches.Count == 0)
|
||||
{
|
||||
await context.CompleteActivityAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await context.ScheduleActivities(Branches, CompleteChildAsync);
|
||||
}
|
||||
|
||||
private async ValueTask CompleteChildAsync(ActivityCompletedContext context)
|
||||
{
|
||||
|
|
@ -57,7 +68,7 @@ public class Fork : Activity
|
|||
// Append activity to the set of completed activities.
|
||||
var completedActivityIds = targetContext.UpdateProperty<HashSet<string>>("Completed", set =>
|
||||
{
|
||||
set ??= new HashSet<string>();
|
||||
set ??= new();
|
||||
set.Add(completedChildActivityId);
|
||||
return set;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class While : Activity
|
|||
/// </summary>
|
||||
public static While True(IActivity body) => new(body)
|
||||
{
|
||||
Condition = new Input<bool>(true)
|
||||
Condition = new(true)
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -1,64 +1,58 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities;
|
||||
|
||||
public class BreakTests
|
||||
/// <summary>
|
||||
/// Integration tests for the <see cref="Workflows.Activities.Break"/> activity.
|
||||
/// Tests Break behavior across different looping constructs (ForEach, For, While, Fork).
|
||||
/// </summary>
|
||||
public class BreakTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
private readonly IWorkflowRunner _workflowRunner;
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
||||
|
||||
public BreakTests(ITestOutputHelper testOutputHelper)
|
||||
[Fact(DisplayName = "Break exits ForEach loop")]
|
||||
public async Task Break_ExitsForEachLoop()
|
||||
{
|
||||
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
|
||||
_workflowRunner = _services.GetRequiredService<IWorkflowRunner>();
|
||||
}
|
||||
// Act
|
||||
await _fixture.RunWorkflowAsync(new BreakForEachWorkflow());
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
|
||||
[Fact(DisplayName = "Break exits out of ForEach")]
|
||||
public async Task Test1()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<BreakForEachWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
// Assert
|
||||
Assert.Equal(new[] { "Start", "C#", "End" }, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Break exits out of immediate ForEach only")]
|
||||
public async Task Test2()
|
||||
[Fact(DisplayName = "Break exits only immediate loop in nested ForEach")]
|
||||
public async Task Break_ExitsOnlyImmediateLoopInNestedForEach()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<NestedForEachWithBreakWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
// Act
|
||||
await _fixture.RunWorkflowAsync(new NestedForEachWithBreakWorkflow());
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "C#", "Classes", "Rust", "Classes", "Go", "Classes" }, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Break exits out of For")]
|
||||
public async Task Test3()
|
||||
[Fact(DisplayName = "Break exits For loop")]
|
||||
public async Task Break_ExitsForLoop()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<BreakForWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
// Act
|
||||
await _fixture.RunWorkflowAsync(new BreakForWorkflow());
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "Start", "0", "1", "End" }, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Break exits out of While")]
|
||||
public async Task Test4()
|
||||
|
||||
[Fact(DisplayName = "Break exits While loop")]
|
||||
public async Task Break_ExitsWhileLoop()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<BreakWhileWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
// Act
|
||||
await _fixture.RunWorkflowAsync(new BreakWhileWorkflow());
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "Start", "1", "2", "End" }, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Break removes the right bookmarks")]
|
||||
public async Task Test5()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<BreakWhileWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(new[] { "Start", "1", "2", "End" }, lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using Elsa.Workflows.Memory;
|
|||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
|
||||
class BreakForEachWorkflow : WorkflowBase
|
||||
public class BreakForEachWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using Elsa.Workflows.Memory;
|
|||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
|
||||
class BreakForWorkflow : WorkflowBase
|
||||
public class BreakForWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
|
||||
public class BreakWhileForkWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
var currentValue = new Variable<int?>("CurrentValue", 0);
|
||||
|
||||
workflow.Root = new Sequence
|
||||
{
|
||||
Variables = { currentValue },
|
||||
Activities =
|
||||
{
|
||||
While.True(new Fork
|
||||
{
|
||||
Branches =
|
||||
{
|
||||
new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new SetVariable
|
||||
{
|
||||
Variable = currentValue,
|
||||
Value = new (context => currentValue.Get(context) + 1)
|
||||
},
|
||||
new If(context => currentValue.Get(context) == 3)
|
||||
{
|
||||
Then = new Break()
|
||||
}
|
||||
}
|
||||
},
|
||||
new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new WriteLine("Waiting for event..."),
|
||||
new Runtime.Activities.Event("Some event") { Id = "SomeEvent" },
|
||||
new WriteLine("Resuming"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ using Elsa.Workflows.Memory;
|
|||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities;
|
||||
|
||||
class NestedForEachWithBreakWorkflow : WorkflowBase
|
||||
public class NestedForEachWithBreakWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,54 +1,64 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
using Elsa.Workflows.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities;
|
||||
|
||||
public class ForkTests
|
||||
/// <summary>
|
||||
/// Integration tests for the <see cref="Workflows.Activities.Fork"/> activity.
|
||||
/// Tests Fork behavior with different join modes and branch configurations.
|
||||
/// </summary>
|
||||
public class ForkTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
private readonly IWorkflowRunner _workflowRunner;
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
||||
|
||||
public ForkTests(ITestOutputHelper testOutputHelper)
|
||||
[Fact(DisplayName = "Fork executes all branches with WaitAll")]
|
||||
public async Task Fork_ExecutesAllBranchesWithWaitAll()
|
||||
{
|
||||
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
|
||||
_workflowBuilderFactory = _services.GetRequiredService<IWorkflowBuilderFactory>();
|
||||
_workflowRunner = _services.GetRequiredService<IWorkflowRunner>();
|
||||
}
|
||||
// Act
|
||||
await _fixture.RunWorkflowAsync(new BasicForkWorkflow());
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
|
||||
[Fact(DisplayName = "Each branch executes")]
|
||||
public async Task Test1()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync<BasicForkWorkflow>();
|
||||
await _workflowRunner.RunAsync(workflow);
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
// Assert
|
||||
Assert.Equal(new[] { "Branch 1", "Branch 2", "Branch 3" }, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Wait AnyAsync causes workflow to continue")]
|
||||
public async Task Test2()
|
||||
[Fact(DisplayName = "Fork with WaitAny continues after first branch completes")]
|
||||
public async Task Fork_WaitAnyContinuesAfterFirstBranch()
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync<JoinAnyForkWorkflow>();
|
||||
// Arrange & build services
|
||||
await _fixture.BuildAsync();
|
||||
var workflowBuilderFactory = _fixture.Services.GetRequiredService<IWorkflowBuilderFactory>();
|
||||
var workflow = await workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync<JoinAnyForkWorkflow>();
|
||||
|
||||
// First run.
|
||||
var result = await _workflowRunner.RunAsync(workflow);
|
||||
// Act - First run
|
||||
var workflowRunner = _fixture.Services.GetRequiredService<IWorkflowRunner>();
|
||||
var result = await workflowRunner.RunAsync(workflow);
|
||||
|
||||
// Collect one of the bookmarks to resume the workflow.
|
||||
// Collect one of the bookmarks to resume the workflow
|
||||
var bookmark = result.WorkflowState.Bookmarks.FirstOrDefault(x => x.ActivityId == "Event2");
|
||||
Assert.NotNull(bookmark);
|
||||
|
||||
// Resume the workflow.
|
||||
// Resume the workflow
|
||||
var runOptions = new RunWorkflowOptions { BookmarkId = bookmark.Id };
|
||||
await _workflowRunner.RunAsync(workflow, result.WorkflowState, runOptions);
|
||||
await workflowRunner.RunAsync(workflow, result.WorkflowState, runOptions);
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
|
||||
// Verify output.
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
// Assert
|
||||
Assert.Equal(new[] { "Start", "Branch 2", "End" }, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Fork with no branches completes successfully")]
|
||||
public async Task Fork_WithNoBranchesCompletesSuccessfully()
|
||||
{
|
||||
// Act
|
||||
await _fixture.RunWorkflowAsync(new EmptyForkWorkflow());
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { "Before fork", "After fork" }, lines);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities;
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
|
||||
public class BasicForkWorkflow : WorkflowBase
|
||||
{
|
||||
|
|
@ -16,4 +16,4 @@ public class BasicForkWorkflow : WorkflowBase
|
|||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
|
||||
public class EmptyForkWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
workflow.Root = new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new WriteLine("Before fork"),
|
||||
new Fork(),
|
||||
new WriteLine("After fork")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities;
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
|
||||
|
||||
public class JoinAnyForkWorkflow : WorkflowBase
|
||||
{
|
||||
|
|
@ -56,4 +56,4 @@ public class JoinAnyForkWorkflow : WorkflowBase
|
|||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ public class BreakWhileFromForkWorkflow : WorkflowBase
|
|||
new Event("Branch 1") { Id = "Branch 1" },
|
||||
new WriteLine("Branch 1 - Resumed"),
|
||||
|
||||
// This should break the while loop, not matter how high up in the tree it is.
|
||||
// This should break the while loop, no matter how high up in the tree it is.
|
||||
new Break()
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Signals;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Flow;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the Fork activity covering various join modes, branching scenarios, and signal handling.
|
||||
/// Unit tests for the <see cref="Fork"/> activity.
|
||||
/// </summary>
|
||||
public class ForkTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SchedulesAllBranches()
|
||||
[Fact(DisplayName = "Fork schedules all branches")]
|
||||
public async Task Fork_SchedulesAllBranches()
|
||||
{
|
||||
// Arrange
|
||||
var branches = CreateBranches(3);
|
||||
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = ForkJoinMode.WaitAll,
|
||||
|
|
@ -31,36 +29,30 @@ public class ForkTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutesWithNoBranches()
|
||||
[Fact(DisplayName = "Fork with no branches completes immediately")]
|
||||
public async Task Fork_WithNoBranchesCompletesImmediately()
|
||||
{
|
||||
// Arrange
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = ForkJoinMode.WaitAll
|
||||
};
|
||||
var fork = new Fork { JoinMode = ForkJoinMode.WaitAll };
|
||||
|
||||
// Act
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context);
|
||||
Assert.Equal(ActivityStatus.Completed, context.Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Theory(DisplayName = "Fork respects join mode")]
|
||||
[InlineData(ForkJoinMode.WaitAll)]
|
||||
[InlineData(ForkJoinMode.WaitAny)]
|
||||
public async Task ExecutesWithSpecifiedJoinMode(ForkJoinMode joinMode)
|
||||
public async Task Fork_RespectsJoinMode(ForkJoinMode joinMode)
|
||||
{
|
||||
// Arrange
|
||||
var branch = new WriteLine("Test Branch");
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = joinMode,
|
||||
Branches =
|
||||
{
|
||||
branch
|
||||
}
|
||||
Branches = { branch }
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -71,305 +63,11 @@ public class ForkTests
|
|||
Assert.True(context.HasScheduledActivity(branch));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ForkJoinMode.WaitAll, 1, 1)] // WaitAll: complete 1 of 2, should track 1
|
||||
[InlineData(ForkJoinMode.WaitAll, 2, 2)] // WaitAll: complete 2 of 2, should track 2
|
||||
[InlineData(ForkJoinMode.WaitAny, 1, 1)] // WaitAny: complete 1 of 2, should track 1
|
||||
public async Task CompletionTrackingWithJoinModes(ForkJoinMode joinMode, int branchesToComplete, int expectedCompleted)
|
||||
{
|
||||
// Arrange
|
||||
var branches = CreateBranches(2);
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = joinMode,
|
||||
Branches = branches.Cast<IActivity>().ToList()
|
||||
};
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
private static Task<ActivityExecutionContext> ExecuteForkAsync(Fork fork) =>
|
||||
new ActivityTestFixture(fork).ExecuteAsync();
|
||||
|
||||
// Act
|
||||
HashSet<string> completedSet = null!;
|
||||
for (var i = 0; i < branchesToComplete; i++)
|
||||
{
|
||||
completedSet = await CompleteBranchAsync(fork, context, branches[i]);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(completedSet);
|
||||
Assert.Equal(expectedCompleted, completedSet.Count);
|
||||
for (var i = 0; i < branchesToComplete; i++)
|
||||
{
|
||||
Assert.Contains(branches[i].Id, completedSet);
|
||||
}
|
||||
|
||||
for (var i = branchesToComplete; i < branches.Length; i++)
|
||||
{
|
||||
Assert.DoesNotContain(branches[i].Id, completedSet);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlesBreakSignal()
|
||||
{
|
||||
// Arrange
|
||||
var fork = new Fork
|
||||
{
|
||||
Branches =
|
||||
{
|
||||
new WriteLine("Branch")
|
||||
}
|
||||
};
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
|
||||
// Act
|
||||
var breakSignal = new BreakSignal();
|
||||
var signalContext = new SignalContext(context, context, CancellationToken.None);
|
||||
await InvokePrivateMethodAsync(fork, "OnBreakSignalReceived", breakSignal, signalContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GetIsBreaking());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletesImmediatelyWhenBreaking()
|
||||
{
|
||||
// Arrange
|
||||
var branch = new WriteLine("Branch");
|
||||
var fork = new Fork
|
||||
{
|
||||
Branches =
|
||||
{
|
||||
branch
|
||||
}
|
||||
};
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
context.SetIsBreaking();
|
||||
|
||||
// Act
|
||||
await CompleteBranchAsync(fork, context, branch);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GetIsBreaking());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SingleBranchCompletesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var branch = new WriteLine("Single Branch");
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = ForkJoinMode.WaitAll,
|
||||
Branches =
|
||||
{
|
||||
branch
|
||||
}
|
||||
};
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
|
||||
// Act
|
||||
var completedSet = await CompleteBranchAsync(fork, context, branch);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(completedSet);
|
||||
Assert.Single(completedSet);
|
||||
Assert.Contains(branch.Id, completedSet);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ForkJoinMode.WaitAll, 3, 3)] // WaitAll requires all 3 branches to complete
|
||||
[InlineData(ForkJoinMode.WaitAny, 3, 1)] // WaitAny only needs 1 branch to complete
|
||||
public async Task HandlesMultipleBranchesWithJoinModes(ForkJoinMode joinMode, int totalBranches, int branchesToComplete)
|
||||
{
|
||||
// Arrange
|
||||
var branches = CreateBranches(totalBranches);
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = joinMode,
|
||||
Branches = branches.Cast<IActivity>().ToList()
|
||||
};
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
|
||||
// Act
|
||||
HashSet<string> completedSet = null!;
|
||||
for (var i = 0; i < branchesToComplete; i++)
|
||||
{
|
||||
completedSet = await CompleteBranchAsync(fork, context, branches[i]);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(completedSet);
|
||||
Assert.Equal(branchesToComplete, completedSet.Count);
|
||||
for (var i = 0; i < branchesToComplete; i++)
|
||||
{
|
||||
Assert.Contains(branches[i].Id, completedSet);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TracksAllActivityCompletions() // Including non-branch activities
|
||||
{
|
||||
// Arrange
|
||||
var branch = new WriteLine("Valid Branch")
|
||||
{
|
||||
Id = "branch-activity"
|
||||
};
|
||||
var nonBranch = new WriteLine("Non-Branch Activity")
|
||||
{
|
||||
Id = "non-branch-activity"
|
||||
};
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = ForkJoinMode.WaitAll,
|
||||
Branches =
|
||||
{
|
||||
branch
|
||||
}
|
||||
};
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
|
||||
// Act - complete a non-branch activity
|
||||
var completedSet = await CompleteBranchAsync(fork, context, nonBranch);
|
||||
|
||||
// Assert - Fork tracks ALL completions, even non-branch activities
|
||||
Assert.NotNull(completedSet);
|
||||
Assert.Contains(nonBranch.Id, completedSet);
|
||||
Assert.Single(completedSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JoinLogicOnlyConsidersBranchActivities()
|
||||
{
|
||||
// Arrange
|
||||
var branch = new WriteLine("Valid Branch")
|
||||
{
|
||||
Id = "branch-activity"
|
||||
};
|
||||
var nonBranch = new WriteLine("Non-Branch Activity")
|
||||
{
|
||||
Id = "non-branch-activity"
|
||||
};
|
||||
var fork = new Fork
|
||||
{
|
||||
JoinMode = ForkJoinMode.WaitAll,
|
||||
Branches =
|
||||
{
|
||||
branch
|
||||
}
|
||||
};
|
||||
var context = await ExecuteForkAsync(fork);
|
||||
|
||||
// Act - complete non-branch activity first, then branch activity
|
||||
await CompleteBranchAsync(fork, context, nonBranch);
|
||||
var completedSet = await CompleteBranchAsync(fork, context, branch);
|
||||
|
||||
// Assert - both activities should be tracked
|
||||
Assert.NotNull(completedSet);
|
||||
Assert.Contains(branch.Id, completedSet);
|
||||
Assert.Contains(nonBranch.Id, completedSet);
|
||||
Assert.Equal(2, completedSet.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MaintainsSeparateCompletionTracking()
|
||||
{
|
||||
// Arrange
|
||||
var branch1 = new WriteLine("Branch 1")
|
||||
{
|
||||
Id = "branch1-unique"
|
||||
};
|
||||
var branch2 = new WriteLine("Branch 2")
|
||||
{
|
||||
Id = "branch2-unique"
|
||||
};
|
||||
var fork1 = new Fork
|
||||
{
|
||||
Branches =
|
||||
{
|
||||
branch1
|
||||
}
|
||||
};
|
||||
var fork2 = new Fork
|
||||
{
|
||||
Branches =
|
||||
{
|
||||
branch2
|
||||
}
|
||||
};
|
||||
var context1 = await ExecuteForkAsync(fork1);
|
||||
var context2 = await ExecuteForkAsync(fork2);
|
||||
|
||||
// Act
|
||||
var completed1 = await CompleteBranchAsync(fork1, context1, branch1);
|
||||
var completed2 = await CompleteBranchAsync(fork2, context2, branch2);
|
||||
|
||||
// Assert - each fork should track its own completions
|
||||
Assert.NotNull(completed1);
|
||||
Assert.NotNull(completed2);
|
||||
Assert.Contains(branch1.Id, completed1);
|
||||
Assert.DoesNotContain(branch2.Id, completed1);
|
||||
Assert.Contains(branch2.Id, completed2);
|
||||
Assert.DoesNotContain(branch1.Id, completed2);
|
||||
}
|
||||
|
||||
private static async Task<ActivityExecutionContext> ExecuteForkAsync(Fork fork)
|
||||
{
|
||||
var fixture = new ActivityTestFixture(fork);
|
||||
return await fixture.ExecuteAsync();
|
||||
}
|
||||
|
||||
private static WriteLine[] CreateBranches(int count, string namePrefix = "Branch")
|
||||
{
|
||||
return Enumerable
|
||||
.Range(1, count)
|
||||
.Select(i => new WriteLine($"{namePrefix} {i}")
|
||||
{
|
||||
Id = $"{namePrefix.ToLower()}-{i}"
|
||||
})
|
||||
private static WriteLine[] CreateBranches(int count) =>
|
||||
Enumerable.Range(1, count)
|
||||
.Select(i => new WriteLine($"Branch {i}") { Id = $"branch-{i}" })
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static async Task<ActivityExecutionContext> CreateChildContextAsync(IActivity childActivity)
|
||||
{
|
||||
var childFixture = new ActivityTestFixture(childActivity);
|
||||
var childContext = await childFixture.ExecuteAsync();
|
||||
return childContext;
|
||||
}
|
||||
|
||||
private static async Task<HashSet<string>> CompleteBranchAsync(Fork fork, ActivityExecutionContext parentContext, IActivity branch)
|
||||
{
|
||||
var childContext = await CreateChildContextAsync(branch);
|
||||
var completedContext = new ActivityCompletedContext(parentContext, childContext);
|
||||
await InvokeCompleteChildAsync(fork, completedContext);
|
||||
return parentContext.GetProperty<HashSet<string>>("Completed") ?? new HashSet<string>();
|
||||
}
|
||||
|
||||
private static async Task InvokePrivateMethodAsync(object instance, string methodName, params object[] parameters)
|
||||
{
|
||||
var method = instance.GetType().GetMethod(methodName,
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException($"{methodName} method not found on {instance.GetType().Name} class. This may indicate a breaking change in the implementation.");
|
||||
|
||||
try
|
||||
{
|
||||
var result = method.Invoke(instance, parameters);
|
||||
switch (result)
|
||||
{
|
||||
case ValueTask valueTask:
|
||||
await valueTask;
|
||||
break;
|
||||
case Task task:
|
||||
await task;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (System.Reflection.TargetInvocationException ex) when (ex.InnerException != null)
|
||||
{
|
||||
throw ex.InnerException;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task InvokeCompleteChildAsync(Fork fork, ActivityCompletedContext completedContext)
|
||||
{
|
||||
return InvokePrivateMethodAsync(fork, "CompleteChildAsync", completedContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs
Normal file
35
test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Looping;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Break"/> activity.
|
||||
/// </summary>
|
||||
public class BreakTests
|
||||
{
|
||||
[Fact(DisplayName = "Break implements ITerminalNode interface")]
|
||||
public void Break_ImplementsITerminalNode()
|
||||
{
|
||||
// Arrange
|
||||
var breakActivity = new Break();
|
||||
|
||||
// Assert
|
||||
Assert.IsAssignableFrom<ITerminalNode>(breakActivity);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Break completes execution")]
|
||||
public async Task Break_CompletesExecution()
|
||||
{
|
||||
// Arrange
|
||||
var breakActivity = new Break();
|
||||
var fixture = new ActivityTestFixture(breakActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ActivityStatus.Completed, context.Status);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue