From b583c7f1a995b6ab0718e4ee3df3e6b4675bd6f5 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 24 Feb 2021 13:29:03 +0000 Subject: [PATCH] Resolves #485 - Add PurgeVariables functionality (#654) * Trivial - Add VS Code workspace * Trivial - Add VSCode build task & extra ignore On GNU/Linux, auto-generated .directory files should be ignored. * WIP #485 - Boilerplate for unit test project * WIP #485 - Add test coverage (Remove/RemoveAll) * WIP #485 - Implement RemoveAll * Remove redundant logic, covered by IDictionary Per the following, the contract for a generic IDictionary, the Remove method already deals with non-existent keys. https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.idictionary-2.remove?view=net-5.0#System_Collections_Generic_IDictionary_2_Remove__0_ * Resolve #485 - Add PurgeVariables methods These are convenience methods upon ActivityExecutionContext and WorkflowExecutionContext, consistent with their current APIs. Also in this commit are tests for those simple methods. There's a bit of test-scaffold as well included, such as: * Autofixture Xunit2 integration * New project for unit tests * Customize attribute for avoiding crashes on Autofixture recursion * Reusable specimen builder for creating IServiceProvider which resolves services from Autofixture * Custonize attribute for a parameter to use that ^^ specimen builder --- .gitignore | 5 +- .vscode/tasks.json | 33 ++++++++++ Elsa.code-workspace | 8 +++ Elsa.sln | 10 ++++ .../Elsa.Abstractions/Models/Variables.cs | 18 +++++- .../Models/ActivityExecutionContext.cs | 8 +++ .../Models/WorkflowExecutionContext.cs | 7 +++ .../AutofixtureServiceProviderAttribute.cs | 43 +++++++++++++ .../Attributes/OmitOnRecursionAttribute.cs | 25 ++++++++ ...tofixtureServiceProviderSpecimenBuilder.cs | 36 +++++++++++ .../Elsa.Testing.Shared.csproj | 2 + .../Elsa.UnitTests/AutoMoqDataAttribute.cs | 13 ++++ .../unit/Elsa.UnitTests/Elsa.UnitTests.csproj | 31 ++++++++++ .../Elsa.UnitTests/Models/VariablesTests.cs | 60 +++++++++++++++++++ .../Models/ActivityExecutionContextTests.cs | 23 +++++++ .../Models/WorkflowExecutionContextTests.cs | 23 +++++++ 16 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 Elsa.code-workspace create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/AutofixtureServiceProviderAttribute.cs create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/OmitOnRecursionAttribute.cs create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs create mode 100644 test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs create mode 100644 test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj create mode 100644 test/unit/Elsa.UnitTests/Models/VariablesTests.cs create mode 100644 test/unit/Elsa.UnitTests/Services/Models/ActivityExecutionContextTests.cs create mode 100644 test/unit/Elsa.UnitTests/Services/Models/WorkflowExecutionContextTests.cs diff --git a/.gitignore b/.gitignore index abf72e513..99e1e6c41 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,7 @@ FodyWeavers.xsd # Elsa Stencil Components src/designer/elsa-workflows-studio/dist src/designer/elsa-workflows-studio/www -src/designer/elsa-workflows-studio/loader \ No newline at end of file +src/designer/elsa-workflows-studio/loader + +# Created by developers using GNU/Linux +.directory \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..2f2312032 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "/property:GenerateFullPaths=true" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "test", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "/property:GenerateFullPaths=true" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Elsa.code-workspace b/Elsa.code-workspace new file mode 100644 index 000000000..876a1499c --- /dev/null +++ b/Elsa.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/Elsa.sln b/Elsa.sln index 3a5e560c0..e1f526d9c 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -232,6 +232,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Designer.Bindings.Blaz EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Timers.Quartz", "src\samples\worker\Elsa.Samples.Timers.Quartz\Elsa.Samples.Timers.Quartz.csproj", "{FE724143-B24C-43DA-BBF5-15431BCA6E27}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{F471267A-DA3A-48C7-8784-F8E4E46203A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.UnitTests", "test\unit\Elsa.UnitTests\Elsa.UnitTests.csproj", "{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -554,6 +558,10 @@ Global {FE724143-B24C-43DA-BBF5-15431BCA6E27}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE724143-B24C-43DA-BBF5-15431BCA6E27}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE724143-B24C-43DA-BBF5-15431BCA6E27}.Release|Any CPU.Build.0 = Release|Any CPU + {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -664,6 +672,8 @@ Global {5C005CC7-B5F2-49C3-8A8B-7640FCDBEE50} = {E42743A0-FBDD-4150-9D53-6000496D9B87} {45A06CA3-199B-4650-8880-30DDF2E38E19} = {8B6B40A8-DF21-4CD2-BABD-474B79D0C3AF} {FE724143-B24C-43DA-BBF5-15431BCA6E27} = {E42743A0-FBDD-4150-9D53-6000496D9B87} + {F471267A-DA3A-48C7-8784-F8E4E46203A2} = {AB1AE008-6FD6-414C-8E88-D735F42E1FA6} + {F822DE2F-A91D-416E-BF4A-A6C466C1BF0A} = {F471267A-DA3A-48C7-8784-F8E4E46203A2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158} diff --git a/src/core/Elsa.Abstractions/Models/Variables.cs b/src/core/Elsa.Abstractions/Models/Variables.cs index fdcc6a5be..bfbc52d96 100644 --- a/src/core/Elsa.Abstractions/Models/Variables.cs +++ b/src/core/Elsa.Abstractions/Models/Variables.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Elsa.Models @@ -29,11 +30,24 @@ namespace Elsa.Models return this; } + /// + /// Removes a variable of the specified name if it is present. + /// + /// The variable name + /// A reference to this same instance, so calls may be chained. public Variables Remove(string name) { - if (Has(name)) - Data.Remove(name); + Data.Remove(name); + return this; + } + /// + /// Removes all of the variables from the current instance, clearing it. + /// + /// A reference to this same instance, so calls may be chained. + public Variables RemoveAll() + { + Data.Clear(); return this; } diff --git a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs index 29caf1d65..d5a6d4e3b 100644 --- a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs @@ -116,6 +116,14 @@ namespace Elsa.Services.Models public object? GetVariable(string name) => WorkflowExecutionContext.GetVariable(name); public T? GetVariable(string name) => WorkflowExecutionContext.GetVariable(name); public T? GetVariable() => GetVariable(typeof(T).Name); + + /// + /// Clears all of the variables associated with the current . + /// + /// + /// + public void PurgeVariables() => WorkflowExecutionContext.PurgeVariables(); + public void SetTransientVariable(string name, object? value) => WorkflowExecutionContext.SetTransientVariable(name, value); public object? GetTransientVariable(string name) => WorkflowExecutionContext.GetTransientVariable(name); public T? GetTransientVariable(string name) => WorkflowExecutionContext.GetTransientVariable(name); diff --git a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs index 238ace335..253d1711e 100644 --- a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs @@ -146,6 +146,13 @@ namespace Elsa.Services.Models /// public object? GetWorkflowVariable(string name) => WorkflowInstance.Variables.Get(name); + /// + /// Clears all of the variables associated with the current . + /// + /// + public void PurgeVariables() => WorkflowInstance.Variables.RemoveAll(); + + public ActivityScope CurrentScope => WorkflowInstance.Scopes.Peek(); public ActivityScope GetScope(string activityId) => WorkflowInstance.Scopes.First(x => x.ActivityId == activityId); diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/AutofixtureServiceProviderAttribute.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/AutofixtureServiceProviderAttribute.cs new file mode 100644 index 000000000..bee3715cb --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/AutofixtureServiceProviderAttribute.cs @@ -0,0 +1,43 @@ +using System; +using System.Reflection; +using AutoFixture; +using AutoFixture.Kernel; +using AutoFixture.Xunit2; +using Elsa.Testing.Shared.AutoFixture.SpecimenBuilders; + +namespace Elsa.Testing.Shared.AutoFixture.Attributes +{ + /// + /// Customizes the parameter so that it creates an + /// using Moq. That mock service provider gets services by resolving them from Autofixture. + /// + /// + public class AutofixtureServiceProviderAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + => new AutofixtureServiceProviderCustomization(parameter); + + class AutofixtureServiceProviderCustomization : ICustomization + { + readonly ParameterInfo parameter; + + public void Customize(IFixture fixture) + { + fixture.Customizations.Insert(0, GetSpecimenBuilder()); + } + + ISpecimenBuilder GetSpecimenBuilder() + { + var paramSpec = new ParameterSpecification(parameter.ParameterType, parameter.Name); + var specimenBuilder = new AutofixtureServiceProviderSpecimenBuilder(); + return new FilteringSpecimenBuilder(specimenBuilder, paramSpec); + } + + public AutofixtureServiceProviderCustomization(ParameterInfo parameter) + { + this.parameter = parameter; + } + } + + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/OmitOnRecursionAttribute.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/OmitOnRecursionAttribute.cs new file mode 100644 index 000000000..cec1c2887 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/OmitOnRecursionAttribute.cs @@ -0,0 +1,25 @@ +using System.Linq; +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; + +namespace Elsa.Testing.Shared.AutoFixture.Attributes +{ + public class OmitOnRecursionAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + => new OmitOnRecursionCustomization(); + + class OmitOnRecursionCustomization : ICustomization + { + public void Customize(IFixture fixture) + { + var throwingBehaviours = fixture.Behaviors.OfType().ToList(); + foreach(var behaviour in throwingBehaviours) + fixture.Behaviors.Remove(behaviour); + + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + } + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs new file mode 100644 index 000000000..4a124efa8 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using AutoFixture.Kernel; +using Moq; + +namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders +{ + /// + /// Creates a mock which gets services + /// by resolving them from Autofixture. + /// + public class AutofixtureServiceProviderSpecimenBuilder : ISpecimenBuilder + { + public object Create(object request, ISpecimenContext context) + { + if(Equals(request, typeof(IServiceProvider))) + return GetServiceProvider(context); + if(request is ParameterInfo paramInfo && paramInfo.ParameterType == typeof(IServiceProvider)) + return GetServiceProvider(context); + + return new NoSpecimen(); + } + + static object GetServiceProvider(ISpecimenContext context) + { + var provider = new Mock(); + + provider.Name = $"Autofixture_{nameof(IServiceProvider)}-{Guid.NewGuid()}"; + provider + .Setup(x => x.GetService(It.IsAny())) + .Returns((Type t) => context.Resolve(t)); + + return provider.Object; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj index b95b976e1..5df535cd8 100644 --- a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj +++ b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj @@ -13,6 +13,8 @@ + + diff --git a/test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs b/test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs new file mode 100644 index 000000000..643588758 --- /dev/null +++ b/test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs @@ -0,0 +1,13 @@ +using AutoFixture; +using AutoFixture.AutoMoq; +using AutoFixture.Xunit2; + +namespace Elsa +{ + public class AutoMoqDataAttribute : AutoDataAttribute + { + public AutoMoqDataAttribute() : base(() => new Fixture().Customize(new AutoMoqCustomization())) + { + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj b/test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj new file mode 100644 index 000000000..68d5d38b9 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + net5.0 + false + Elsa + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/unit/Elsa.UnitTests/Models/VariablesTests.cs b/test/unit/Elsa.UnitTests/Models/VariablesTests.cs new file mode 100644 index 000000000..283ebdcf8 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Models/VariablesTests.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Xunit; + +namespace Elsa.Models +{ + public class VariablesTests + { + [Theory(DisplayName = "The Remove method should remove a variable if such a variable exists"), AutoMoqData] + public void Remove_removes_a_variable_by_name_if_present(string variableName, object variableData) + { + var sut = new Variables(new Dictionary { { variableName, variableData } }); + + sut.Remove(variableName); + + Assert.Empty(sut.Data); + } + + [Theory(DisplayName = "The Remove method should not throw if used with a non-existent variable"), AutoMoqData] + public void Remove_does_not_throw_when_trying_to_remove_a_variable_which_does_not_exist(string variableName) + { + var sut = new Variables(); + + sut.Remove(variableName); + + // No assertion, if the line about doesn't throw then this test passed + } + + [Theory(DisplayName = "The Remove method should support call-chaining by returning a self-reference"), AutoMoqData] + public void Remove_returns_a_reference_to_itself(Variables sut, string variableName) + { + var result = sut.Remove(variableName); + Assert.Same(sut, result); + } + + [Theory(DisplayName = "The RemoveAll method should leave the variables collection empty"), AutoMoqData] + public void RemoveAll_clears_all_variables(string variableName1, + string variableName2, + string variableName3, + object variableData) + { + var sut = new Variables(new Dictionary { + { variableName1, variableData }, + { variableName2, variableData }, + { variableName3, variableData }, + }); + + sut.RemoveAll(); + + Assert.Empty(sut.Data); + } + + [Theory(DisplayName = "The RemoveAll method should support call-chaining by returning a self-reference"), AutoMoqData] + public void RemoveAll_returns_a_reference_to_itself(Variables sut) + { + var result = sut.RemoveAll(); + Assert.Same(sut, result); + } + + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/Models/ActivityExecutionContextTests.cs b/test/unit/Elsa.UnitTests/Services/Models/ActivityExecutionContextTests.cs new file mode 100644 index 000000000..18c9455ce --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/Models/ActivityExecutionContextTests.cs @@ -0,0 +1,23 @@ +using Xunit; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using AutoFixture.Xunit2; +using System; + +namespace Elsa.Services.Models +{ + public class ActivityExecutionContextTests + { + [Theory(DisplayName = "The PurgeVariables method should clear the Variables instance associated with the WorkflowInstance associated with the Workflow Execution Context"), AutoMoqData] + public void PurgeVariables_clears_workflow_execution_context_workflow_instance_variables([AutofixtureServiceProvider, Frozen] IServiceProvider serviceProvider, + [OmitOnRecursion,NoAutoProperties] ActivityExecutionContext sut, + string variableName, + object variableValue) + { + sut.WorkflowExecutionContext.WorkflowInstance.Variables.Set(variableName, variableValue); + + sut.PurgeVariables(); + + Assert.Empty(sut.WorkflowExecutionContext.WorkflowInstance.Variables.Data); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/Models/WorkflowExecutionContextTests.cs b/test/unit/Elsa.UnitTests/Services/Models/WorkflowExecutionContextTests.cs new file mode 100644 index 000000000..160ff796b --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/Models/WorkflowExecutionContextTests.cs @@ -0,0 +1,23 @@ +using Xunit; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using AutoFixture.Xunit2; +using System; + +namespace Elsa.Services.Models +{ + public class WorkflowExecutionContextTests + { + [Theory(DisplayName = "The PurgeVariables method should clear the Variables instance associated with the WorkflowInstance"), AutoMoqData] + public void PurgeVariables_clears_workflow_execution_context_workflow_instance_variables([AutofixtureServiceProvider, Frozen] IServiceProvider serviceProvider, + [OmitOnRecursion,NoAutoProperties] WorkflowExecutionContext sut, + string variableName, + object variableValue) + { + sut.WorkflowInstance.Variables.Set(variableName, variableValue); + + sut.PurgeVariables(); + + Assert.Empty(sut.WorkflowInstance.Variables.Data); + } + } +} \ No newline at end of file