diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 10bb34007..7e23ef7d4 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -1,4 +1,5 @@ name: Packages + on: workflow_dispatch: push: @@ -12,18 +13,86 @@ on: - 'rc/*' - 'develop/*' - 'codex/*' + pull_request: + branches: + - 'main' + - 'bug/*' + - 'perf/*' + - 'patch/*' + - 'feat/*' + - 'enh/*' + - 'rc/*' + - 'develop/*' + - 'codex/*' release: - types: [ prereleased, published ] + types: [prereleased, published] + env: base_version: '3.6.0' feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json' nuget_feed_source: 'https://api.nuget.org/v3/index.json' jobs: - build: - name: Build packages + test: + name: Test with coverage runs-on: ubuntu-latest timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore solution + run: dotnet restore Elsa.sln + + - name: Build solution + run: dotnet build Elsa.sln --no-restore --configuration Release + + - name: Prepare coverage directory + run: | + rm -rf artifacts/coverage + mkdir -p artifacts/coverage + + - name: Run tests with coverage + run: | + set -euo pipefail + mapfile -t projects < <(find test -type f -name '*.csproj' | sort) + if [ ${#projects[@]} -eq 0 ]; then + echo "No test projects were found in the test directory." >&2 + exit 1 + fi + for project in "${projects[@]}"; do + echo "Running tests with coverage for ${project}" + dotnet test "$project" --configuration Release --no-build --logger "GitHubActions;report-warnings=false" /p:CollectCoverage=true + done + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: artifacts/coverage + + build: + name: Build packages + needs: test + runs-on: ubuntu-latest + timeout-minutes: 30 + if: ${{ github.event_name != 'pull_request' }} steps: - name: Extract branch name run: | @@ -31,28 +100,31 @@ jobs: BRANCH_NAME=${BRANCH_NAME#refs/heads/} # remove the refs/heads/ prefix # Extract the last part after the last slash of the branch name, if any, e.g., feature/issue-123 -> issue-123 and use it as the version prefix. PACKAGE_PREFIX=$(echo $BRANCH_NAME | rev | cut -d/ -f1 | rev | tr '_' '-') - + # If the branch name is main, use the preview version. Otherwise, use the branch name as the version prefix. if [[ "${BRANCH_NAME}" == "main" || "${BRANCH_NAME}" =~ ^develop/ ]]; then PACKAGE_PREFIX="preview" fi - + echo "Ref: ${{ github.ref }}" echo "Branch name: ${BRANCH_NAME}" echo "Package prefix: ${PACKAGE_PREFIX}" echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_ENV echo "PACKAGE_PREFIX=${PACKAGE_PREFIX}" >> $GITHUB_ENV + - name: Checkout uses: actions/checkout@v4 + - name: Verify commit exists in branch run: | - if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased")]]; then + if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased") ]]; then git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/develop/3.6.0 else git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/${BRANCH_NAME} fi + - name: Set VERSION variable run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased") ]]; then @@ -62,35 +134,21 @@ jobs: else echo "VERSION=${{env.base_version}}-preview.${{github.run_number}}" >> $GITHUB_ENV fi - # - name: Set up JDK 17 - # uses: actions/setup-java@v2 - # with: - # java-version: '17' - # distribution: 'adopt' + - uses: actions/setup-dotnet@v4 with: dotnet-version: 9.x - # - name: Install SonarScanner for .NET - # run: dotnet tool install --global dotnet-sonarscanner - # - name: Install Coverlet for code coverage - # run: dotnet tool install --global coverlet.console - # - name: Begin SonarCloud analysis - # env: - # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - # run: dotnet sonarscanner begin /k:"elsa-workflows_elsa-core" /o:"elsa-workflows" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.exclusions=**/obj/**,**/*.dll,build/**,samples/**,src/apps/** /d:"sonar.verbose=true" /d:sonar.cs.opencover.reportsPaths=**/testresults/**/coverage.opencover.xml + - name: Compile+Test+Pack run: ./build.sh Compile+Test+Pack --version ${VERSION} --analyseCode true - # - name: End SonarCloud analysis - # env: - # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - # run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: elsa-nuget-packages path: packages/*nupkg if: ${{ github.event_name == 'release' || github.event_name == 'push'}} - + publish_preview_feedz: name: Publish to feedz.io needs: build diff --git a/Directory.Packages.props b/Directory.Packages.props index 4768c5cc5..232f8c561 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,7 @@ + diff --git a/Elsa.sln b/Elsa.sln index 9fce3e932..f92369873 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -62,8 +62,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{18453B51-25EB-4317-A4B3-B10518252E92}" + ProjectSection(SolutionItems) = preProject + test\unit\Directory.Build.props = test\unit\Directory.Build.props + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration", "{1B8D5897-902E-4632-8698-E89CAF3DDF54}" + ProjectSection(SolutionItems) = preProject + test\integration\Directory.Build.props = test\integration\Directory.Build.props + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "component", "component", "{08B41FFA-CEE3-46A7-B5C0-3EB65D37A16C}" ProjectSection(SolutionItems) = preProject @@ -298,6 +304,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EFCore.Sql EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.IntegrationTests", "test\integration\Elsa.Alterations.IntegrationTests\Elsa.Alterations.IntegrationTests.csproj", "{51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qa", "qa", "{0478E6EA-DCB2-4667-ADC2-37C62C9C2574}" ProjectSection(SolutionItems) = preProject doc\qa\test-guidelines.md = doc\qa\test-guidelines.md @@ -607,6 +615,10 @@ Global {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}.Release|Any CPU.Build.0 = Release|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -633,7 +645,6 @@ Global {5948B0A5-7873-4DBB-BA03-EB283D6EA91B} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {B08B4E00-C2AB-48F3-8389-449F42AEF179} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {28818676-F6AF-4203-8B65-BD33A50CB9A2} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} - {DC9CCAD0-7363-4691-B964-FF5B3AEA3F95} = {18453B51-25EB-4317-A4B3-B10518252E92} {29638A67-E79F-44FE-AC05-DA499EBA929E} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912} {A516931E-EDBB-4FC3-BB94-1BB824D5BC61} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {BBCE36D1-6767-4ED1-B3E8-84D2567A962A} = {A516931E-EDBB-4FC3-BB94-1BB824D5BC61} @@ -700,6 +711,8 @@ Global {71D5178D-2490-4681-8621-BF8DED964F33} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {698051E0-7981-43D4-B7BA-F3D8B65004A1} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} + {DC9CCAD0-7363-4691-B964-FF5B3AEA3F95} = {18453B51-25EB-4317-A4B3-B10518252E92} + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA} = {18453B51-25EB-4317-A4B3-B10518252E92} {0478E6EA-DCB2-4667-ADC2-37C62C9C2574} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs index a12b35086..689e847e6 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs @@ -89,7 +89,7 @@ public class SetVariable : CodeActivity /// The variable to assign the value to. /// [Input(Description = "The variable to assign the value to.")] - public Variable Variable { get; set; } = null!; + public Variable? Variable { get; set; } /// /// The value to assign. @@ -101,9 +101,13 @@ public class SetVariable : CodeActivity protected override void Execute(ActivityExecutionContext context) { // Always refer to the variable by ID to ensure that the variable is resolved from the correct scope. - var variableId = Variable.Id; + var variableId = Variable?.Id; var variable = context.ExpressionExecutionContext.EnumerateVariablesInScope().FirstOrDefault(x => x.Id == variableId); + + if (variable == null) + throw new InvalidOperationException($"Variable '{variableId}' not found."); + var value = context.Get(Value); - variable?.Set(context, value); + variable.Set(context, value); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs index 282d07be3..f5b4482ca 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs @@ -57,7 +57,6 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable< var activityDescriptor = await activityDescriber.DescribeActivityAsync(activityType, cancellationToken); - Add(activityDescriptor, _activityDescriptors, _manualActivityDescriptors); _manualActivityDescriptors.Add(activityDescriptor); } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 03f0023c2..108bef4c9 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -9,6 +9,15 @@ false true false + true + cobertura,lcov,opencover + $(MSBuildThisFileDirectory)../artifacts/coverage/$(MSBuildProjectName)/coverage + 10 + line + total + + [*Elsa.Testing.Shared*]* + @@ -17,7 +26,7 @@ - + @@ -26,4 +35,4 @@ - \ No newline at end of file + diff --git a/test/component/Directory.Build.props b/test/component/Directory.Build.props index 94e6bc5b1..e6ff014e4 100644 --- a/test/component/Directory.Build.props +++ b/test/component/Directory.Build.props @@ -1,7 +1,11 @@ - + + + 40 + + diff --git a/test/integration/Directory.Build.props b/test/integration/Directory.Build.props new file mode 100644 index 000000000..d4fc658e9 --- /dev/null +++ b/test/integration/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + 10 + + + diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj b/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj index 79007a43c..f3487fa1f 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj +++ b/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj @@ -32,5 +32,8 @@ Always + + Always + diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs index c3aca5a2a..f3d678175 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs @@ -31,6 +31,22 @@ public class ForkDecisionJoinTests await RunAndAssert("fork-decision-join-waitall.json", ["A", "C", "B", "D"]); } + [Fact(DisplayName = "An implicit join from the True and False branches should execute because the join mode is None and by default, all active branches are joined.")] + public async Task ImplicitJoinFromBranchesShouldExecute() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import workflow. + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json"); + + // Execute. + var workflowState = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId); + + // Assert. + Assert.Equal(WorkflowStatus.Finished, workflowState.Status); + } + private async Task RunAndAssert(string workflowFileName, string[] expectedLines) { // Populate registries. diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json new file mode 100644 index 000000000..81820da59 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json @@ -0,0 +1 @@ +{"$schema":"https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json","id":"c2601be150ec7739","definitionId":"b8c92c080faf734f","name":"Decision Merge","createdAt":"2025-10-08T19:00:25.918638\u002B00:00","version":5,"toolVersion":"3.6.0.0","variables":[],"inputs":[],"outputs":[],"outcomes":[],"customProperties":{},"isReadonly":false,"isSystem":false,"isLatest":true,"isPublished":true,"options":{"autoUpdateConsumingWorkflows":false},"root":{"id":"990d07168c5223de","nodeId":"Workflow1:990d07168c5223de","name":"Flowchart1","type":"Elsa.Flowchart","version":1,"customProperties":{"notFoundConnections":[],"canStartWorkflow":false,"runAsynchronously":false},"metadata":{},"activities":[{"condition":{"typeName":"Boolean","expression":{"type":"Literal","value":false}},"id":"a38215f28410f472","nodeId":"Workflow1:990d07168c5223de:a38215f28410f472","name":"FlowDecision1","type":"Elsa.FlowDecision","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false},"metadata":{"designer":{"position":{"x":-237.5,"y":-255.5},"size":{"width":149.84375,"height":67.9765625}}}},{"id":"50e042cfab596891","nodeId":"Workflow1:990d07168c5223de:50e042cfab596891","name":"Start1","type":"Elsa.Start","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false},"metadata":{"designer":{"position":{"x":-485.5,"y":-255.5},"size":{"width":122.6484375,"height":67.9765625}}}},{"id":"d5b9c2affe09a93b","nodeId":"Workflow1:990d07168c5223de:d5b9c2affe09a93b","name":"End1","type":"Elsa.End","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false,"mergeMode":"None"},"metadata":{"designer":{"position":{"x":75.5,"y":-255.5},"size":{"width":115.3125,"height":67.9765625}}}}],"variables":[],"connections":[{"source":{"activity":"50e042cfab596891","port":"Done"},"target":{"activity":"a38215f28410f472","port":"In"},"vertices":[]},{"source":{"activity":"a38215f28410f472","port":"True"},"target":{"activity":"d5b9c2affe09a93b","port":"In"},"vertices":[]},{"source":{"activity":"a38215f28410f472","port":"False"},"target":{"activity":"d5b9c2affe09a93b","port":"In"},"vertices":[]}]}} \ No newline at end of file diff --git a/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj b/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj index ee58b4574..244833891 100644 --- a/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj +++ b/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj @@ -7,6 +7,7 @@ false Exe + false diff --git a/test/unit/Directory.Build.props b/test/unit/Directory.Build.props new file mode 100644 index 000000000..7e5383630 --- /dev/null +++ b/test/unit/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + 0 + + + diff --git a/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj b/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj new file mode 100644 index 000000000..3017b8a33 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs new file mode 100644 index 000000000..b8f623ed6 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs @@ -0,0 +1,123 @@ +using Elsa.Common; +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Services; +using Elsa.Extensions; +using Elsa.Mediator.Contracts; +using Elsa.Workflows; +using Elsa.Workflows.CommitStates; +using Elsa.Workflows.Management.Providers; +using Elsa.Workflows.Management.Services; +using Elsa.Workflows.PortResolvers; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace Elsa.Activities.UnitTests.Helpers; + +/// +/// Helper class for unit testing activities in isolation. +/// Provides methods to execute activities with minimal setup, abstracting away the complexity +/// of creating WorkflowExecutionContext and ActivityExecutionContext. +/// +public static class ActivityTestHelper +{ + /// + /// Executes an activity in isolation for unit testing purposes and returns the execution context. + /// This method handles all the complexity of setting up the execution context, + /// evaluating inputs, and executing the activity. + /// + /// The activity to execute + /// An optional action to configure the service collection for dependency injection during activity execution. + /// The ActivityExecutionContext used for execution + public static async Task ExecuteActivityAsync(IActivity activity, Action? configureServices = null) + { + var context = await CreateMinimalActivityExecutionContext(activity, configureServices); + + // Set up variables and inputs, then execute the activity + await SetupExistingVariablesAsync(activity, context); + await context.EvaluateInputPropertiesAsync(); + await activity.ExecuteAsync(context); + + return context; + } + + /// + /// Creates a minimal ActivityExecutionContext suitable for isolated unit testing of activities. + /// This helper method creates a real WorkflowExecutionContext using the minimal workflow pattern + /// to provide proper context for activities. + /// + private static async Task CreateMinimalActivityExecutionContext(IActivity activity, Action? configureServices) + { + // Create a minimal service provider with the required services for expression evaluation + var services = new ServiceCollection(); + + // Add core services + services.AddLogging(); + services.AddSingleton(_ => Substitute.For()); + services.AddSingleton(_ => Substitute.For()); + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // Add the default expression descriptor provider which includes Literal expressions + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(_ => Substitute.For()); + + services.AddSingleton(_ => Substitute.For()); + services.AddSingleton(_ => Substitute.For()); + services.AddSingleton(_ => Substitute.For()); + + // Call the configure services action if provided. + configureServices?.Invoke(services); + + var serviceProvider = services.BuildServiceProvider(); + var activityRegistry = serviceProvider.GetRequiredService(); + var workflowGraphBuilder = serviceProvider.GetRequiredService(); + await activityRegistry.RegisterAsync(activity.GetType()); + var workflow = Workflow.FromActivity(activity); + var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow); + + // Create workflow execution context using the static factory method + var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync( + serviceProvider, + workflowGraph, + $"test-instance-{Guid.NewGuid()}", + CancellationToken.None + ); + + // Create ActivityExecutionContext for the actual activity we want to test + return await workflowExecutionContext.CreateActivityExecutionContextAsync(activity); + } + + /// + /// Sets up existing variables found on the activity in the execution context. + /// This is necessary because in unit tests, variables need to be initialized. + /// + private static Task SetupExistingVariablesAsync(IActivity activity, ActivityExecutionContext context) + { + var activityType = activity.GetType(); + var variableProperties = activityType.GetProperties() + .Where(p => p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition() == typeof(Variable<>)) + .ToList(); + + foreach (var variable in variableProperties.Select(property => (Variable)property.GetValue(activity)!)) + { + variable.Set(context.ExpressionExecutionContext, variable.Value); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs new file mode 100644 index 000000000..2f5c7ec5d --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -0,0 +1,49 @@ +using Elsa.Activities.UnitTests.Helpers; + +namespace Elsa.Activities.UnitTests.Primitives; + +public class SetVariableTests +{ + [Fact] + public async Task Should_Set_Variable_Integer() + { + // Arrange + const int expected = 42; // The answer to life, the universe and everything. + var variable = new Variable("myVar", 0, "myVar"); + var setVariable = new SetVariable(variable, new Input(expected)); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal(expected, result); + } + + [Fact] + public async Task Should_Throw_When_Variable_Is_Null() + { + // Arrange + var setVariable = new SetVariable(null!, new Input("test value")); + + // Act & Assert + var exception = await Record.ExceptionAsync(async () => await ActivityTestHelper.ExecuteActivityAsync(setVariable)); + + Assert.NotNull(exception); + } + + [Fact] + public async Task Should_Set_Variable_To_Null_Value() + { + // Arrange + var variable = new Variable("myVar", "initial value", "myVar"); + var setVariable = new SetVariable(variable, new Input((string?)null)); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Null(result); + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Activities.UnitTests/Usings.cs b/test/unit/Elsa.Activities.UnitTests/Usings.cs new file mode 100644 index 000000000..b45935d97 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Usings.cs @@ -0,0 +1,3 @@ +global using Elsa.Workflows.Activities; +global using Elsa.Workflows.Memory; +global using Elsa.Workflows.Models; \ No newline at end of file diff --git a/test/unit/Elsa.Common.Core/Elsa.Common.Core.csproj b/test/unit/Elsa.Common.Core/Elsa.Common.Core.csproj deleted file mode 100644 index 0d2efc651..000000000 --- a/test/unit/Elsa.Common.Core/Elsa.Common.Core.csproj +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/test/unit/Elsa.Common.Core/EnumerableExtensionsTests.cs b/test/unit/Elsa.Common.Core/EnumerableExtensionsTests.cs deleted file mode 100644 index 3c1598fe8..000000000 --- a/test/unit/Elsa.Common.Core/EnumerableExtensionsTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Extensions; -using Xunit; - -namespace Elsa.Common.Core; - -public class EnumerableExtensionsTests -{ - [Fact] - public void ToBatches_ReturnsItemsInListOfPages() - { - var items = new List - { - 1, - 2, - 3, - 4, - 5 - }; - var batchSize = 2; - var batches = items.ToBatches(batchSize).ToList(); - - Assert.Equal(3, batches.Count); - Assert.Equal(2, batches.ElementAt(0).Count()); - Assert.Equal(2, batches.ElementAt(1).Count()); - Assert.Equal(1, batches.ElementAt(2).Count()); - Assert.Equal(3, batches.ElementAt(1).ElementAt(0)); - Assert.Equal(4, batches.ElementAt(1).ElementAt(1)); - } -} \ No newline at end of file