From 3340e14b74384cddbd951533f6b2d63bc9c2e2b6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 25 Nov 2025 19:29:02 +0100 Subject: [PATCH] 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> --- .github/copilot-instructions.md | 54 +-- ...-steps.yml.yml => copilot-setup-steps.yml} | 4 +- .github/workflows/docker-ca.yml | 2 +- .github/workflows/elsa-server-and-studio.yml | 61 ---- .github/workflows/pr.yml | 2 +- Elsa.sln | 41 ++- .../Elsa.Workflows.Core/Activities/For.cs | 6 +- .../Elsa.Workflows.Core/Activities/Fork.cs | 15 +- .../Elsa.Workflows.Core/Activities/While.cs | 2 +- .../Activities/Break/BreakTests.cs | 76 ++-- .../Break/Workflows/BreakForEachWorkflow.cs | 2 +- .../Break/Workflows/BreakForWorkflow.cs | 2 +- .../Break/Workflows/BreakWhileForkWorkflow.cs | 51 --- .../ForEach/NestedForEachWithBreakWorkflow.cs | 2 +- .../Activities/Fork/ForkTests.cs | 66 ++-- .../Fork/{ => Workflows}/BasicForkWorkflow.cs | 4 +- .../Fork/Workflows/EmptyForkWorkflow.cs | 19 + .../{ => Workflows}/JoinAnyForkWorkflow.cs | 4 +- .../Workflows/BreakWhileFromForkWorkflow.cs | 2 +- .../Flow/ForkTests.cs | 334 +----------------- .../Looping/BreakTests.cs | 35 ++ 21 files changed, 216 insertions(+), 568 deletions(-) rename .github/workflows/{copilot-setup-steps.yml.yml => copilot-setup-steps.yml} (80%) delete mode 100644 .github/workflows/elsa-server-and-studio.yml delete mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakWhileForkWorkflow.cs rename test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/{ => Workflows}/BasicForkWorkflow.cs (85%) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/EmptyForkWorkflow.cs rename test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/{ => Workflows}/JoinAnyForkWorkflow.cs (96%) create mode 100644 test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6d57db0ee..41f39a82b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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. \ No newline at end of file diff --git a/.github/workflows/copilot-setup-steps.yml.yml b/.github/workflows/copilot-setup-steps.yml similarity index 80% rename from .github/workflows/copilot-setup-steps.yml.yml rename to .github/workflows/copilot-setup-steps.yml index f5f80a5d3..9070ba5c4 100644 --- a/.github/workflows/copilot-setup-steps.yml.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -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" diff --git a/.github/workflows/docker-ca.yml b/.github/workflows/docker-ca.yml index e8a737d48..0a35f9c30 100644 --- a/.github/workflows/docker-ca.yml +++ b/.github/workflows/docker-ca.yml @@ -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 diff --git a/.github/workflows/elsa-server-and-studio.yml b/.github/workflows/elsa-server-and-studio.yml deleted file mode 100644 index 6df73dfd2..000000000 --- a/.github/workflows/elsa-server-and-studio.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8fac46a64..c3c434b40 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -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 diff --git a/Elsa.sln b/Elsa.sln index 66d805dc4..2d7a9e618 100644 --- a/Elsa.sln +++ b/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} diff --git a/src/modules/Elsa.Workflows.Core/Activities/For.cs b/src/modules/Elsa.Workflows.Core/Activities/For.cs index a607c73e6..0dcef9454 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/For.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/For.cs @@ -26,9 +26,9 @@ public class For : Activity /// public For(int start, int end, int step, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line) { - Start = new Input(start); - End = new Input(end); - Step = new Input(step); + Start = new(start); + End = new(end); + Step = new(step); } /// diff --git a/src/modules/Elsa.Workflows.Core/Activities/Fork.cs b/src/modules/Elsa.Workflows.Core/Activities/Fork.cs index 8f8f6e98b..da7fbe8d9 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Fork.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Fork.cs @@ -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 Branches { get; set; } = new List(); /// - 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>("Completed", set => { - set ??= new HashSet(); + set ??= new(); set.Add(completedChildActivityId); return set; }); diff --git a/src/modules/Elsa.Workflows.Core/Activities/While.cs b/src/modules/Elsa.Workflows.Core/Activities/While.cs index 215160f2a..3dc28bab1 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/While.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/While.cs @@ -21,7 +21,7 @@ public class While : Activity /// public static While True(IActivity body) => new(body) { - Condition = new Input(true) + Condition = new(true) }; /// diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs index 5233b5e1c..c293c2d96 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/BreakTests.cs @@ -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 +/// +/// Integration tests for the activity. +/// Tests Break behavior across different looping constructs (ForEach, For, While, Fork). +/// +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(); - } + // 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(); - 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(); - 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(); - 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(); - 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(); - var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(new[] { "Start", "1", "2", "End" }, lines); - } -} \ No newline at end of file +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs index 1a00cc3f3..d5b36e141 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForEachWorkflow.cs @@ -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) { diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs index 6766297f6..70aa5205b 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakForWorkflow.cs @@ -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) { diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakWhileForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakWhileForkWorkflow.cs deleted file mode 100644 index ddde01831..000000000 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Break/Workflows/BreakWhileForkWorkflow.cs +++ /dev/null @@ -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("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"), - } - } - } - }), - } - }; - } -} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs index 38b718be0..701015fe3 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/ForEach/NestedForEachWithBreakWorkflow.cs @@ -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) { diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs index ffabf4de1..0dbb49064 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/ForkTests.cs @@ -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 +/// +/// Integration tests for the activity. +/// Tests Fork behavior with different join modes and branch configurations. +/// +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(); - _workflowRunner = _services.GetRequiredService(); - } + // 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(); - 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(); + // Arrange & build services + await _fixture.BuildAsync(); + var workflowBuilderFactory = _fixture.Services.GetRequiredService(); + var workflow = await workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync(); - // First run. - var result = await _workflowRunner.RunAsync(workflow); + // Act - First run + var workflowRunner = _fixture.Services.GetRequiredService(); + 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); + } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/BasicForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/BasicForkWorkflow.cs similarity index 85% rename from test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/BasicForkWorkflow.cs rename to test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/BasicForkWorkflow.cs index 2cbb9763a..789aebae7 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/BasicForkWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/BasicForkWorkflow.cs @@ -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 } }; } -} \ No newline at end of file +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/EmptyForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/EmptyForkWorkflow.cs new file mode 100644 index 000000000..62fb163d1 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/EmptyForkWorkflow.cs @@ -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") + } + }; + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/JoinAnyForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/JoinAnyForkWorkflow.cs similarity index 96% rename from test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/JoinAnyForkWorkflow.cs rename to test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/JoinAnyForkWorkflow.cs index 72c7706be..e37e90262 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/JoinAnyForkWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Activities/Fork/Workflows/JoinAnyForkWorkflow.cs @@ -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 } }; } -} \ No newline at end of file +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs index 80c77f707..8fb0457b1 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BlockingAndBreaking/Workflows/BreakWhileFromForkWorkflow.cs @@ -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() } }, diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs b/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs index 21778bf96..1b9c53d77 100644 --- a/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs @@ -1,20 +1,18 @@ using Elsa.Testing.Shared; using Elsa.Workflows; -using Elsa.Workflows.Signals; namespace Elsa.Activities.UnitTests.Flow; /// -/// Unit tests for the Fork activity covering various join modes, branching scenarios, and signal handling. +/// Unit tests for the activity. /// 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().ToList() - }; - var context = await ExecuteForkAsync(fork); + private static Task ExecuteForkAsync(Fork fork) => + new ActivityTestFixture(fork).ExecuteAsync(); - // Act - HashSet 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().ToList() - }; - var context = await ExecuteForkAsync(fork); - - // Act - HashSet 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 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 CreateChildContextAsync(IActivity childActivity) - { - var childFixture = new ActivityTestFixture(childActivity); - var childContext = await childFixture.ExecuteAsync(); - return childContext; - } - - private static async Task> 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>("Completed") ?? new HashSet(); - } - - 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); - } -} \ No newline at end of file +} diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs new file mode 100644 index 000000000..5730e1b3b --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Looping/BreakTests.cs @@ -0,0 +1,35 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Activities.UnitTests.Looping; + +/// +/// Unit tests for the activity. +/// +public class BreakTests +{ + [Fact(DisplayName = "Break implements ITerminalNode interface")] + public void Break_ImplementsITerminalNode() + { + // Arrange + var breakActivity = new Break(); + + // Assert + Assert.IsAssignableFrom(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); + } +}