From 523c96a53546788cab492bb1e3082eccd88e21a3 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 17 Mar 2021 19:47:58 +0000 Subject: [PATCH 01/10] WIP #738 - Straight refactor to new service (SRP) There's no logic change here at all. I just moved a large function out of the TriggerIndexer into a service of its own. --- .../IGetsTriggersForWorkflowBlueprints.cs | 22 +++++ .../ElsaServiceCollectionExtensions.cs | 1 + src/core/Elsa.Core/Triggers/TriggerIndexer.cs | 59 ++----------- .../Triggers/TriggersForBlueprintsProvider.cs | 82 +++++++++++++++++++ 4 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs create mode 100644 src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs diff --git a/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs new file mode 100644 index 000000000..e7fb16a88 --- /dev/null +++ b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// An object which can get all of the workflow triggers for a collection of workflow blueprints. + /// + public interface IGetsTriggersForWorkflowBlueprints + { + /// + /// Gets the triggers for all of the specified workflow blueprints. + /// + /// The workflow blueprints for which to get triggers. + /// An optional cancellation token. + /// A task which exposes an enumerable collection of workflow triggers. + Task> GetTriggersAsync(IEnumerable workflowBlueprints, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs index 65187b275..82e89c63a 100644 --- a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs @@ -143,6 +143,7 @@ namespace Microsoft.Extensions.DependencyInjection .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddSingleton() .AddScoped() .AddBookmarkProvider() diff --git a/src/core/Elsa.Core/Triggers/TriggerIndexer.cs b/src/core/Elsa.Core/Triggers/TriggerIndexer.cs index b3a4d368a..3bd7d50ec 100644 --- a/src/core/Elsa.Core/Triggers/TriggerIndexer.cs +++ b/src/core/Elsa.Core/Triggers/TriggerIndexer.cs @@ -17,36 +17,24 @@ namespace Elsa.Triggers public class TriggerIndexer : ITriggerIndexer { private readonly IWorkflowRegistry _workflowRegistry; - private readonly IBookmarkHasher _bookmarkHasher; - private readonly IEnumerable _providers; - private readonly IServiceProvider _serviceProvider; - private readonly IWorkflowFactory _workflowFactory; - private readonly IActivityTypeService _activityTypeService; private readonly ITriggerStore _triggerStore; private readonly IMediator _mediator; private readonly ILogger _logger; private readonly Stopwatch _stopwatch = new(); + private readonly IGetsTriggersForWorkflowBlueprints _triggersForBookmarksProvider; public TriggerIndexer( IWorkflowRegistry workflowRegistry, - IBookmarkHasher bookmarkHasher, - IEnumerable providers, - IServiceProvider serviceProvider, - IWorkflowFactory workflowFactory, - IActivityTypeService activityTypeService, ITriggerStore triggerStore, IMediator mediator, - ILogger logger) + ILogger logger, + IGetsTriggersForWorkflowBlueprints triggersForBookmarksProvider) { _workflowRegistry = workflowRegistry; - _bookmarkHasher = bookmarkHasher; - _providers = providers; - _serviceProvider = serviceProvider; - _workflowFactory = workflowFactory; - _activityTypeService = activityTypeService; _triggerStore = triggerStore; _mediator = mediator; _logger = logger; + _triggersForBookmarksProvider = triggersForBookmarksProvider; } public async Task IndexTriggersAsync(CancellationToken cancellationToken = default) @@ -63,49 +51,12 @@ namespace Elsa.Triggers _logger.LogInformation("Indexing triggers"); var workflowBlueprintList = workflowBlueprints.ToList(); - var triggers = (await GetTriggersAsync(workflowBlueprintList, cancellationToken)).ToList(); + var triggers = (await _triggersForBookmarksProvider.GetTriggersAsync(workflowBlueprintList, cancellationToken)).ToList(); _stopwatch.Stop(); _logger.LogInformation("Indexed {TriggerCount} triggers in {ElapsedTime}", triggers.Count, _stopwatch.Elapsed); await _triggerStore.StoreAsync(triggers, cancellationToken); } - - private async Task> GetTriggersAsync(ICollection workflowBlueprints, CancellationToken cancellationToken) - { - var allTriggers = new List(); - var activityTypes = (await _activityTypeService.GetActivityTypesAsync(cancellationToken)).ToDictionary(x => x.TypeName); - - foreach (var workflowBlueprint in workflowBlueprints) - { - var startActivities = workflowBlueprint.GetStartActivities(); - var workflowInstance = await _workflowFactory.InstantiateAsync(workflowBlueprint, cancellationToken: cancellationToken); - var workflowExecutionContext = new WorkflowExecutionContext(_serviceProvider, workflowBlueprint, workflowInstance); - - foreach (var activity in startActivities) - { - var activityExecutionContext = new ActivityExecutionContext(_serviceProvider, workflowExecutionContext, activity, null, false, cancellationToken); - var activityType = activityTypes[activity.Type]; - var context = new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); - var providers = await FilterProvidersAsync(context).ToListAsync(cancellationToken); - - foreach (var provider in providers) - { - var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); - var triggers = bookmarks.Select(x => new WorkflowTrigger(workflowBlueprint, activity.Id, activity.Type, _bookmarkHasher.Hash(x), x)).ToList(); - allTriggers.AddRange(triggers); - } - } - } - - return allTriggers; - } - - private async IAsyncEnumerable FilterProvidersAsync(BookmarkProviderContext context) - { - foreach (var provider in _providers) - if (await provider.SupportsActivityAsync(context)) - yield return provider; - } } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs new file mode 100644 index 000000000..1538ec11d --- /dev/null +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// Default implementation of which + /// gets all of the workflow triggers for a collection of workflow blueprints. + /// + public class TriggersForBlueprintsProvider : IGetsTriggersForWorkflowBlueprints + { + private readonly IActivityTypeService _activityTypeService; + private readonly IBookmarkHasher _bookmarkHasher; + private readonly IEnumerable _providers; + private readonly IServiceProvider _serviceProvider; + private readonly IWorkflowFactory _workflowFactory; + + public TriggersForBlueprintsProvider(IActivityTypeService activityTypeService, + IBookmarkHasher bookmarkHasher, + IEnumerable providers, + IServiceProvider serviceProvider, + IWorkflowFactory workflowFactory) + { + _activityTypeService = activityTypeService; + _bookmarkHasher = bookmarkHasher; + _providers = providers; + _serviceProvider = serviceProvider; + _workflowFactory = workflowFactory; + } + + /// + /// Gets the triggers for all of the specified workflow blueprints. + /// + /// The workflow blueprints for which to get triggers. + /// An optional cancellation token. + /// A task which exposes an enumerable collection of workflow triggers. + public async Task> GetTriggersAsync(IEnumerable workflowBlueprints, + CancellationToken cancellationToken) + { + var allTriggers = new List(); + var activityTypes = (await _activityTypeService.GetActivityTypesAsync(cancellationToken)).ToDictionary(x => x.TypeName); + + foreach (var workflowBlueprint in workflowBlueprints) + { + var startActivities = workflowBlueprint.GetStartActivities(); + var workflowInstance = await _workflowFactory.InstantiateAsync(workflowBlueprint, cancellationToken: cancellationToken); + var workflowExecutionContext = new WorkflowExecutionContext(_serviceProvider, workflowBlueprint, workflowInstance); + + foreach (var activity in startActivities) + { + var activityExecutionContext = new ActivityExecutionContext(_serviceProvider, workflowExecutionContext, activity, null, false, cancellationToken); + var activityType = activityTypes[activity.Type]; + var context = new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); + var providers = await FilterProvidersAsync(context).ToListAsync(cancellationToken); + + foreach (var provider in providers) + { + var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); + var triggers = bookmarks.Select(x => new WorkflowTrigger(workflowBlueprint, activity.Id, activity.Type, _bookmarkHasher.Hash(x), x)).ToList(); + allTriggers.AddRange(triggers); + } + } + } + + return allTriggers; + } + + async IAsyncEnumerable FilterProvidersAsync(BookmarkProviderContext context) + { + foreach (var provider in _providers) + if (await provider.SupportsActivityAsync(context)) + yield return provider; + } + } +} \ No newline at end of file From 1dee17b90477a56516e1d105617258a459058676 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 17 Mar 2021 21:35:30 +0000 Subject: [PATCH 02/10] WIP #738 - Add test coverage for new service This test passes, but as you can see it's massive because of the complexity of what the class has to do. --- .../IGetsTriggersForWorkflowBlueprints.cs | 2 +- .../Triggers/TriggersForBlueprintsProvider.cs | 2 +- .../MockBookmarkProvidersAttribute.cs | 20 +++ .../MockBookmarkProvidersCustomization.cs | 19 +++ .../MockBookmarkProvidersSpecimenBuilder.cs | 28 ++++ .../TriggersForBlueprintsProviderTests.cs | 141 ++++++++++++++++++ 6 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/MockBookmarkProvidersAttribute.cs create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/MockBookmarkProvidersCustomization.cs create mode 100644 test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/MockBookmarkProvidersSpecimenBuilder.cs create mode 100644 test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs diff --git a/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs index e7fb16a88..c5f568697 100644 --- a/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs +++ b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForWorkflowBlueprints.cs @@ -17,6 +17,6 @@ namespace Elsa.Triggers /// An optional cancellation token. /// A task which exposes an enumerable collection of workflow triggers. Task> GetTriggersAsync(IEnumerable workflowBlueprints, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs index 1538ec11d..142baf372 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -42,7 +42,7 @@ namespace Elsa.Triggers /// An optional cancellation token. /// A task which exposes an enumerable collection of workflow triggers. public async Task> GetTriggersAsync(IEnumerable workflowBlueprints, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { var allTriggers = new List(); var activityTypes = (await _activityTypeService.GetActivityTypesAsync(cancellationToken)).ToDictionary(x => x.TypeName); diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/MockBookmarkProvidersAttribute.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/MockBookmarkProvidersAttribute.cs new file mode 100644 index 000000000..ae8a3187c --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Attributes/MockBookmarkProvidersAttribute.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Testing.Shared.AutoFixture.Customizations; + +namespace Elsa.Testing.Shared.AutoFixture.Attributes +{ + public class MockBookmarkProvidersAttribute : CustomizeAttribute + { + readonly int howMany; + + public override ICustomization GetCustomization(ParameterInfo parameter) + => new MockBookmarkProvidersCustomization(parameter, howMany); + + public MockBookmarkProvidersAttribute(int howMany = 3) + { + this.howMany = howMany; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/MockBookmarkProvidersCustomization.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/MockBookmarkProvidersCustomization.cs new file mode 100644 index 000000000..30d09bf43 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/MockBookmarkProvidersCustomization.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using AutoFixture.Kernel; +using Elsa.Testing.Shared.AutoFixture.SpecimenBuilders; + +namespace Elsa.Testing.Shared.AutoFixture.Customizations +{ + public class MockBookmarkProvidersCustomization : SpecimenBuilderForParameterCustomization + { + readonly int howMany; + + protected override ISpecimenBuilder GetUnfilteredSpecimenBuilder() + => new MockBookmarkProvidersSpecimenBuilder(howMany); + + public MockBookmarkProvidersCustomization(ParameterInfo parameter, int howMany) : base(parameter) + { + this.howMany = howMany; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/MockBookmarkProvidersSpecimenBuilder.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/MockBookmarkProvidersSpecimenBuilder.cs new file mode 100644 index 000000000..83bff4b37 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/MockBookmarkProvidersSpecimenBuilder.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using AutoFixture.Kernel; +using Elsa.Bookmarks; +using Moq; + +namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders +{ + public class MockBookmarkProvidersSpecimenBuilder : ISpecimenBuilder + { + readonly int howMany; + + public object Create(object request, ISpecimenContext context) + { + if (!request.IsAnAutofixtureRequestForType>()) + return new NoSpecimen(); + + return Enumerable.Range(0, howMany) + .Select(x => Mock.Of()) + .ToList(); + } + + public MockBookmarkProvidersSpecimenBuilder(int howMany) + { + this.howMany = howMany; + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs new file mode 100644 index 000000000..3617414de --- /dev/null +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoFixture.Xunit2; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Models; +using Elsa.Services; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Elsa.Triggers; +using Moq; +using Xunit; + +namespace Elsa.UnitTests.Triggers +{ + public class TriggersForBlueprintsProviderTests + { + [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for the compatible bookmarks of the start activities of the blueprints"), AutoMoqData] + public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks(IActivityTypeService activityTypeService, + IBookmarkHasher bookmarkHasher, + IBookmarkProvider provider1, + IBookmarkProvider provider2, + IBookmarkProvider provider3, + [AutofixtureServiceProvider] IServiceProvider serviceProvider, + IWorkflowFactory workflowFactory, + ActivityType activityType1, + ActivityType activityType2, + ActivityType activityType3, + IWorkflowBlueprint workflowBlueprint1, + IWorkflowBlueprint workflowBlueprint2, + IActivityBlueprint activityBlueprint1, + IActivityBlueprint activityBlueprint2, + IActivityBlueprint activityBlueprint3, + IActivityBlueprint activityBlueprint4, + IActivityBlueprint activityBlueprint5, + IActivityBlueprint activityBlueprint6, + string activityBlueprintId1, + string activityBlueprintId2, + string activityBlueprintId3, + string activityBlueprintId4, + string activityBlueprintId5, + string activityBlueprintId6, + IConnection connection1, + IConnection connection2, + IConnection connection3, + ITargetEndpoint endpoint1, + ITargetEndpoint endpoint2, + ITargetEndpoint endpoint3, + [OmitOnRecursion] WorkflowInstance workflow1, + [OmitOnRecursion] WorkflowInstance workflow2, + IBookmark bookmark1, + IBookmark bookmark2, + IBookmark bookmark3, + IBookmark bookmark4, + IBookmark bookmark5, + IBookmark bookmark6, + string bookmarkHash1, + string bookmarkHash2, + string bookmarkHash3, + string bookmarkHash4, + string bookmarkHash5, + string bookmarkHash6) + { + // Unfortunately the SUT can't be created by Autofixture because of the IEnumerable in the ctor + var sut = new TriggersForBlueprintsProvider(activityTypeService, bookmarkHasher, new [] { provider1, provider2, provider3 }, serviceProvider, workflowFactory); + + Mock.Get(activityTypeService) + .Setup(x => x.GetActivityTypesAsync(default)) + .Returns(ValueTask.FromResult>(new [] { activityType1, activityType2, activityType3 })); + Mock.Get(workflowBlueprint1).SetupGet(x => x.Connections).Returns(new [] { connection1 }); + Mock.Get(workflowBlueprint2).SetupGet(x => x.Connections).Returns(new [] { connection2, connection3 }); + Mock.Get(workflowBlueprint1).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint4 }); + Mock.Get(workflowBlueprint2).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint3, activityBlueprint5, activityBlueprint6 }); + Mock.Get(workflowFactory) + .Setup(x => x.InstantiateAsync(workflowBlueprint1, null, null, default)) + .Returns(Task.FromResult(workflow1)); + Mock.Get(workflowFactory) + .Setup(x => x.InstantiateAsync(workflowBlueprint2, null, null, default)) + .Returns(Task.FromResult(workflow2)); + Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); + Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); + Mock.Get(connection3).SetupGet(x => x.Target).Returns(endpoint3); + Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint4); + Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint5); + Mock.Get(endpoint3).SetupGet(x => x.Activity).Returns(activityBlueprint6); + Mock.Get(activityBlueprint1).SetupGet(x => x.Type).Returns(activityType1.TypeName); + Mock.Get(activityBlueprint2).SetupGet(x => x.Type).Returns(activityType2.TypeName); + Mock.Get(activityBlueprint3).SetupGet(x => x.Type).Returns(activityType3.TypeName); + Mock.Get(activityBlueprint1).SetupGet(x => x.Id).Returns(activityBlueprintId1); + Mock.Get(activityBlueprint2).SetupGet(x => x.Id).Returns(activityBlueprintId2); + Mock.Get(activityBlueprint3).SetupGet(x => x.Id).Returns(activityBlueprintId3); + Mock.Get(activityBlueprint4).SetupGet(x => x.Id).Returns(activityBlueprintId4); + Mock.Get(activityBlueprint5).SetupGet(x => x.Id).Returns(activityBlueprintId5); + Mock.Get(activityBlueprint6).SetupGet(x => x.Id).Returns(activityBlueprintId6); + SetupProviderToBeCompatibleWithActivityType(provider1, activityType1); + SetupProviderToBeCompatibleWithActivityType(provider2, activityType2); + SetupProviderToBeCompatibleWithActivityType(provider3, activityType3); + Mock.Get(provider1) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); + Mock.Get(provider2) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); + Mock.Get(provider3) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(ValueTask.FromResult>(new [] { bookmark5, bookmark6 })); + Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark1)).Returns(bookmarkHash1); + Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark2)).Returns(bookmarkHash2); + Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark3)).Returns(bookmarkHash3); + Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark4)).Returns(bookmarkHash4); + Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark5)).Returns(bookmarkHash5); + Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark6)).Returns(bookmarkHash6); + + var result = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 }); + + Assert.Equal(6, result.Count()); + Assert.True(ContainsTriggerWithHash(result, bookmarkHash1) + && ContainsTriggerWithHash(result, bookmarkHash2) + && ContainsTriggerWithHash(result, bookmarkHash3) + && ContainsTriggerWithHash(result, bookmarkHash4) + && ContainsTriggerWithHash(result, bookmarkHash5) + && ContainsTriggerWithHash(result, bookmarkHash6), + "The returned triggers should contain a trigger with every one of the six expected hashes"); + } + + void SetupProviderToBeCompatibleWithActivityType(IBookmarkProvider provider, ActivityType activityType) + { + Mock.Get(provider) + .Setup(x => x.SupportsActivityAsync(It.IsAny(), default)) + .Returns(ValueTask.FromResult(false)); + Mock.Get(provider) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(ValueTask.FromResult(true)); + } + + bool ContainsTriggerWithHash(IEnumerable triggers, string hash) + => triggers.Any(x => Equals(x.BookmarkHash, hash)); + } +} \ No newline at end of file From 654f3072d924997a8b4aa6731dacfc9c3e11e746 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 18 Mar 2021 19:35:05 +0000 Subject: [PATCH 03/10] WIP #738 - Refactor some logic into services This simplifies the unit a little and the test proves that it still does the same thing overall. Strictly speaking, this is now an integration test. --- ...ityExecutionContextForActivityBlueprint.cs | 23 ++++++ ...lowExecutionContextForWorkflowBlueprint.cs | 21 +++++ .../ElsaServiceCollectionExtensions.cs | 2 + ...utionContextForActivityBlueprintFactory.cs | 34 ++++++++ ...utionContextForWorkflowBlueprintFactory.cs | 34 ++++++++ .../Triggers/TriggersForBlueprintsProvider.cs | 79 ++++++++++++------- .../TriggersForBlueprintsProviderTests.cs | 9 ++- 7 files changed, 174 insertions(+), 28 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Services/ICreatesActivityExecutionContextForActivityBlueprint.cs create mode 100644 src/core/Elsa.Abstractions/Services/ICreatesWorkflowExecutionContextForWorkflowBlueprint.cs create mode 100644 src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs create mode 100644 src/core/Elsa.Core/Services/WorkflowExecutionContextForWorkflowBlueprintFactory.cs diff --git a/src/core/Elsa.Abstractions/Services/ICreatesActivityExecutionContextForActivityBlueprint.cs b/src/core/Elsa.Abstractions/Services/ICreatesActivityExecutionContextForActivityBlueprint.cs new file mode 100644 index 000000000..d7afab8bd --- /dev/null +++ b/src/core/Elsa.Abstractions/Services/ICreatesActivityExecutionContextForActivityBlueprint.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// An object which can create an activity execution context for a specified activity blueprint. + /// + public interface ICreatesActivityExecutionContextForActivityBlueprint + { + /// + /// Creates a activity execution context for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A cancellation token + /// An activity execution context + ActivityExecutionContext CreateActivityExecutionContext(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/ICreatesWorkflowExecutionContextForWorkflowBlueprint.cs b/src/core/Elsa.Abstractions/Services/ICreatesWorkflowExecutionContextForWorkflowBlueprint.cs new file mode 100644 index 000000000..c819a15c1 --- /dev/null +++ b/src/core/Elsa.Abstractions/Services/ICreatesWorkflowExecutionContextForWorkflowBlueprint.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// An object which can create a workflow execution context for a specified workflow blueprint. + /// + public interface ICreatesWorkflowExecutionContextForWorkflowBlueprint + { + /// + /// Creates a workflow execution context for the specified workflow blueprint. + /// + /// A workflow blueprint + /// An optional cancellation token + /// A task for a workflow execution context + Task CreateWorkflowExecutionContextAsync(IWorkflowBlueprint workflowBlueprint, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs index 82e89c63a..ee660fcb5 100644 --- a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs @@ -109,6 +109,8 @@ namespace Microsoft.Extensions.DependencyInjection .AddSingleton() .AddActivityTypeProvider() .AddScoped() + .AddTransient() + .AddTransient() ; // Serialization. diff --git a/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs new file mode 100644 index 000000000..794c9f91f --- /dev/null +++ b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// Default implementation of . + /// + public class ActivityExecutionContextForActivityBlueprintFactory : ICreatesActivityExecutionContextForActivityBlueprint + { + readonly IServiceProvider serviceProvider; + + public ActivityExecutionContextForActivityBlueprintFactory(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + + } + + /// + /// Creates a activity execution context for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A cancellation token + /// An activity execution context + public ActivityExecutionContext CreateActivityExecutionContext(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken) + { + return new ActivityExecutionContext(serviceProvider, workflowExecutionContext, activityBlueprint, null, false, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowExecutionContextForWorkflowBlueprintFactory.cs b/src/core/Elsa.Core/Services/WorkflowExecutionContextForWorkflowBlueprintFactory.cs new file mode 100644 index 000000000..e2f806c4a --- /dev/null +++ b/src/core/Elsa.Core/Services/WorkflowExecutionContextForWorkflowBlueprintFactory.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// Default implementation of . + /// + public class WorkflowExecutionContextForWorkflowBlueprintFactory : ICreatesWorkflowExecutionContextForWorkflowBlueprint + { + readonly IServiceProvider serviceProvider; + readonly IWorkflowFactory workflowFactory; + + public WorkflowExecutionContextForWorkflowBlueprintFactory(IServiceProvider serviceProvider, IWorkflowFactory workflowFactory) + { + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + this.workflowFactory = workflowFactory ?? throw new ArgumentNullException(nameof(workflowFactory)); + } + + /// + /// Creates a workflow execution context for the specified workflow blueprint. + /// + /// A workflow blueprint + /// An optional cancellation token + /// A task for a workflow execution context + public async Task CreateWorkflowExecutionContextAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken = default) + { + var workflowInstance = await workflowFactory.InstantiateAsync(workflowBlueprint, cancellationToken: cancellationToken); + return new WorkflowExecutionContext(serviceProvider, workflowBlueprint, workflowInstance); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs index 142baf372..0a4891621 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -16,23 +16,23 @@ namespace Elsa.Triggers /// public class TriggersForBlueprintsProvider : IGetsTriggersForWorkflowBlueprints { - private readonly IActivityTypeService _activityTypeService; - private readonly IBookmarkHasher _bookmarkHasher; - private readonly IEnumerable _providers; - private readonly IServiceProvider _serviceProvider; - private readonly IWorkflowFactory _workflowFactory; + readonly IActivityTypeService activityTypeService; + readonly IBookmarkHasher bookmarkHasher; + readonly IEnumerable bookmarkProviders; + readonly ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory; + readonly ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory; public TriggersForBlueprintsProvider(IActivityTypeService activityTypeService, IBookmarkHasher bookmarkHasher, - IEnumerable providers, - IServiceProvider serviceProvider, - IWorkflowFactory workflowFactory) + IEnumerable bookmarkProviders, + ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory, + ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory) { - _activityTypeService = activityTypeService; - _bookmarkHasher = bookmarkHasher; - _providers = providers; - _serviceProvider = serviceProvider; - _workflowFactory = workflowFactory; + this.activityTypeService = activityTypeService ?? throw new ArgumentNullException(nameof(activityTypeService)); + this.bookmarkHasher = bookmarkHasher ?? throw new ArgumentNullException(nameof(bookmarkHasher)); + this.bookmarkProviders = bookmarkProviders ?? throw new ArgumentNullException(nameof(bookmarkProviders)); + this.workflowExecutionContextFactory = workflowExecutionContextFactory ?? throw new ArgumentNullException(nameof(workflowExecutionContextFactory)); + this.activityExecutionContextFactory = activityExecutionContextFactory ?? throw new ArgumentNullException(nameof(activityExecutionContextFactory)); } /// @@ -45,25 +45,26 @@ namespace Elsa.Triggers CancellationToken cancellationToken = default) { var allTriggers = new List(); - var activityTypes = (await _activityTypeService.GetActivityTypesAsync(cancellationToken)).ToDictionary(x => x.TypeName); + var activityTypes = (await activityTypeService.GetActivityTypesAsync(cancellationToken)).ToDictionary(x => x.TypeName); foreach (var workflowBlueprint in workflowBlueprints) { - var startActivities = workflowBlueprint.GetStartActivities(); - var workflowInstance = await _workflowFactory.InstantiateAsync(workflowBlueprint, cancellationToken: cancellationToken); - var workflowExecutionContext = new WorkflowExecutionContext(_serviceProvider, workflowBlueprint, workflowInstance); + var startingActivityBlueprints = workflowBlueprint.GetStartActivities(); + var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, + cancellationToken); - foreach (var activity in startActivities) + foreach (var activityBlueprint in startingActivityBlueprints) { - var activityExecutionContext = new ActivityExecutionContext(_serviceProvider, workflowExecutionContext, activity, null, false, cancellationToken); - var activityType = activityTypes[activity.Type]; - var context = new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); - var providers = await FilterProvidersAsync(context).ToListAsync(cancellationToken); + var bookmarkProviderContext = GetBookmarkProviderContext(activityBlueprint, workflowExecutionContext, cancellationToken, activityTypes); + var supportedBookmarkProviders = await GetSupportedBookmarkProvidersForContextAsync(bookmarkProviderContext).ToListAsync(cancellationToken); - foreach (var provider in providers) + foreach (var bookmarkProvider in supportedBookmarkProviders) { - var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); - var triggers = bookmarks.Select(x => new WorkflowTrigger(workflowBlueprint, activity.Id, activity.Type, _bookmarkHasher.Hash(x), x)).ToList(); + var triggers = await GetTriggersForBookmarkProvider(bookmarkProvider, + bookmarkProviderContext, + activityBlueprint, + workflowBlueprint, + cancellationToken); allTriggers.AddRange(triggers); } } @@ -72,11 +73,35 @@ namespace Elsa.Triggers return allTriggers; } - async IAsyncEnumerable FilterProvidersAsync(BookmarkProviderContext context) + BookmarkProviderContext GetBookmarkProviderContext(IActivityBlueprint activity, + WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken, + IDictionary activityTypes) { - foreach (var provider in _providers) + var activityExecutionContext = activityExecutionContextFactory.CreateActivityExecutionContext(activity, + workflowExecutionContext, + cancellationToken); + var activityType = activityTypes[activity.Type]; + return new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); + } + + async IAsyncEnumerable GetSupportedBookmarkProvidersForContextAsync(BookmarkProviderContext context) + { + foreach (var provider in bookmarkProviders) if (await provider.SupportsActivityAsync(context)) yield return provider; } + + async Task> GetTriggersForBookmarkProvider(IBookmarkProvider provider, + BookmarkProviderContext context, + IActivityBlueprint activityBlueprint, + IWorkflowBlueprint workflowBlueprint, + CancellationToken cancellationToken = default) + { + var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); + return bookmarks + .Select(x => new WorkflowTrigger(workflowBlueprint, activityBlueprint.Id, activityBlueprint.Type, bookmarkHasher.Hash(x), x)) + .ToList(); + } } } \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs index 3617414de..77a193e11 100644 --- a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -63,8 +63,15 @@ namespace Elsa.UnitTests.Triggers string bookmarkHash5, string bookmarkHash6) { + var workflowExecutionContextFactory = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); + var activityExecutionContextFactory = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); + // Unfortunately the SUT can't be created by Autofixture because of the IEnumerable in the ctor - var sut = new TriggersForBlueprintsProvider(activityTypeService, bookmarkHasher, new [] { provider1, provider2, provider3 }, serviceProvider, workflowFactory); + var sut = new TriggersForBlueprintsProvider(activityTypeService, + bookmarkHasher, + new[] { provider1, provider2, provider3 }, + workflowExecutionContextFactory, + activityExecutionContextFactory); Mock.Get(activityTypeService) .Setup(x => x.GetActivityTypesAsync(default)) From d9724e5267c469c61de1809e92fbcb97482ea749 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 18 Mar 2021 20:29:06 +0000 Subject: [PATCH 04/10] WIP #738 - Refactor-out a further service The triggers-for-activity-blueprint functionality is quite complex just on its own, so I have moved this to a new service. Once again the integration test which is left behind proves that it still does the same job. --- ...TriggersForActivityBlueprintAndWorkflow.cs | 27 ++++++ .../ElsaServiceCollectionExtensions.cs | 1 + ...ForActivityBlueprintAndWorkflowProvider.cs | 89 +++++++++++++++++++ .../Triggers/TriggersForBlueprintsProvider.cs | 69 +++----------- .../TriggersForBlueprintsProviderTests.cs | 10 +-- 5 files changed, 134 insertions(+), 62 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Triggers/IGetsTriggersForActivityBlueprintAndWorkflow.cs create mode 100644 src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs diff --git a/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForActivityBlueprintAndWorkflow.cs b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForActivityBlueprintAndWorkflow.cs new file mode 100644 index 000000000..8a8e9bf25 --- /dev/null +++ b/src/core/Elsa.Abstractions/Triggers/IGetsTriggersForActivityBlueprintAndWorkflow.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// An object which can get a collection of the for a specified activity blueprint and workflow. + /// + public interface IGetsTriggersForActivityBlueprintAndWorkflow + { + /// + /// Gets a collection of the workflow triggers for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A dictionary of all of the activity types (by name) + /// An optional cancellation token + /// A task exposing a collection of workflow triggers for the activity and workflow. + Task> GetTriggersForActivityBlueprintAsync(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + IDictionary activityTypes, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs index ee660fcb5..ca7ec6115 100644 --- a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs @@ -146,6 +146,7 @@ namespace Microsoft.Extensions.DependencyInjection .AddScoped() .AddScoped() .AddScoped() + .AddTransient() .AddSingleton() .AddScoped() .AddBookmarkProvider() diff --git a/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs new file mode 100644 index 000000000..cab1f8bf9 --- /dev/null +++ b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Triggers +{ + /// + /// Default implementation of . + /// + public class TriggersForActivityBlueprintAndWorkflowProvider : IGetsTriggersForActivityBlueprintAndWorkflow + { + readonly IBookmarkHasher bookmarkHasher; + readonly IEnumerable bookmarkProviders; + readonly ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory; + + public TriggersForActivityBlueprintAndWorkflowProvider(IBookmarkHasher bookmarkHasher, + IEnumerable bookmarkProviders, + ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory) + { + this.bookmarkHasher = bookmarkHasher ?? throw new System.ArgumentNullException(nameof(bookmarkHasher)); + this.bookmarkProviders = bookmarkProviders ?? throw new System.ArgumentNullException(nameof(bookmarkProviders)); + this.activityExecutionContextFactory = activityExecutionContextFactory ?? throw new System.ArgumentNullException(nameof(activityExecutionContextFactory)); + } + + /// + /// Gets a collection of the workflow triggers for the specified activity blueprint. + /// + /// An activity blueprint + /// A workflow execution context + /// A dictionary of all of the activity types (by name) + /// An optional cancellation token + /// A task exposing a collection of workflow triggers for the activity and workflow. + public async Task> GetTriggersForActivityBlueprintAsync(IActivityBlueprint activityBlueprint, + WorkflowExecutionContext workflowExecutionContext, + IDictionary activityTypes, + CancellationToken cancellationToken = default) + { + var bookmarkProviderContext = GetBookmarkProviderContext(activityBlueprint, workflowExecutionContext, cancellationToken, activityTypes); + var supportedBookmarkProviders = await GetSupportedBookmarkProvidersForContextAsync(bookmarkProviderContext) + .ToListAsync(cancellationToken); + + var tasksOfListsOfTriggers = supportedBookmarkProviders + .Select(async bookmarkProvider => await GetTriggersForBookmarkProvider(bookmarkProvider, + bookmarkProviderContext, + activityBlueprint, + workflowExecutionContext.WorkflowBlueprint, + cancellationToken)); + return (await Task.WhenAll(tasksOfListsOfTriggers)) + .SelectMany(x => x) + .ToList(); + } + + BookmarkProviderContext GetBookmarkProviderContext(IActivityBlueprint activity, + WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken, + IDictionary activityTypes) + { + var activityExecutionContext = activityExecutionContextFactory.CreateActivityExecutionContext(activity, + workflowExecutionContext, + cancellationToken); + var activityType = activityTypes[activity.Type]; + return new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); + } + + async IAsyncEnumerable GetSupportedBookmarkProvidersForContextAsync(BookmarkProviderContext context) + { + foreach (var provider in bookmarkProviders) + if (await provider.SupportsActivityAsync(context)) + yield return provider; + } + + async Task> GetTriggersForBookmarkProvider(IBookmarkProvider provider, + BookmarkProviderContext context, + IActivityBlueprint activityBlueprint, + IWorkflowBlueprint workflowBlueprint, + CancellationToken cancellationToken = default) + { + var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); + return bookmarks + .Select(x => new WorkflowTrigger(workflowBlueprint, activityBlueprint.Id, activityBlueprint.Type, bookmarkHasher.Hash(x), x)) + .ToList(); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs index 0a4891621..74e1aa5b4 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Elsa.ActivityProviders; -using Elsa.Bookmarks; using Elsa.Services; using Elsa.Services.Models; @@ -17,22 +16,16 @@ namespace Elsa.Triggers public class TriggersForBlueprintsProvider : IGetsTriggersForWorkflowBlueprints { readonly IActivityTypeService activityTypeService; - readonly IBookmarkHasher bookmarkHasher; - readonly IEnumerable bookmarkProviders; readonly ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory; - readonly ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory; + readonly IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider; public TriggersForBlueprintsProvider(IActivityTypeService activityTypeService, - IBookmarkHasher bookmarkHasher, - IEnumerable bookmarkProviders, ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory, - ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory) + IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider) { this.activityTypeService = activityTypeService ?? throw new ArgumentNullException(nameof(activityTypeService)); - this.bookmarkHasher = bookmarkHasher ?? throw new ArgumentNullException(nameof(bookmarkHasher)); - this.bookmarkProviders = bookmarkProviders ?? throw new ArgumentNullException(nameof(bookmarkProviders)); this.workflowExecutionContextFactory = workflowExecutionContextFactory ?? throw new ArgumentNullException(nameof(workflowExecutionContextFactory)); - this.activityExecutionContextFactory = activityExecutionContextFactory ?? throw new ArgumentNullException(nameof(activityExecutionContextFactory)); + this.triggerProvider = triggerProvider ?? throw new ArgumentNullException(nameof(triggerProvider)); } /// @@ -52,56 +45,18 @@ namespace Elsa.Triggers var startingActivityBlueprints = workflowBlueprint.GetStartActivities(); var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, cancellationToken); - - foreach (var activityBlueprint in startingActivityBlueprints) - { - var bookmarkProviderContext = GetBookmarkProviderContext(activityBlueprint, workflowExecutionContext, cancellationToken, activityTypes); - var supportedBookmarkProviders = await GetSupportedBookmarkProvidersForContextAsync(bookmarkProviderContext).ToListAsync(cancellationToken); - - foreach (var bookmarkProvider in supportedBookmarkProviders) - { - var triggers = await GetTriggersForBookmarkProvider(bookmarkProvider, - bookmarkProviderContext, - activityBlueprint, - workflowBlueprint, - cancellationToken); - allTriggers.AddRange(triggers); - } - } + var tasksOfCollectionsOfTriggers = startingActivityBlueprints + .Select(async activityBlueprint => await triggerProvider.GetTriggersForActivityBlueprintAsync(activityBlueprint, + workflowExecutionContext, + activityTypes, + cancellationToken)); + var triggers = (await Task.WhenAll(tasksOfCollectionsOfTriggers)) + .SelectMany(x => x) + .ToList(); + allTriggers.AddRange(triggers); } return allTriggers; } - - BookmarkProviderContext GetBookmarkProviderContext(IActivityBlueprint activity, - WorkflowExecutionContext workflowExecutionContext, - CancellationToken cancellationToken, - IDictionary activityTypes) - { - var activityExecutionContext = activityExecutionContextFactory.CreateActivityExecutionContext(activity, - workflowExecutionContext, - cancellationToken); - var activityType = activityTypes[activity.Type]; - return new BookmarkProviderContext(activityExecutionContext, activityType, BookmarkIndexingMode.WorkflowBlueprint); - } - - async IAsyncEnumerable GetSupportedBookmarkProvidersForContextAsync(BookmarkProviderContext context) - { - foreach (var provider in bookmarkProviders) - if (await provider.SupportsActivityAsync(context)) - yield return provider; - } - - async Task> GetTriggersForBookmarkProvider(IBookmarkProvider provider, - BookmarkProviderContext context, - IActivityBlueprint activityBlueprint, - IWorkflowBlueprint workflowBlueprint, - CancellationToken cancellationToken = default) - { - var bookmarks = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); - return bookmarks - .Select(x => new WorkflowTrigger(workflowBlueprint, activityBlueprint.Id, activityBlueprint.Type, bookmarkHasher.Hash(x), x)) - .ToList(); - } } } \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs index 77a193e11..f6d2fccaf 100644 --- a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -17,7 +17,7 @@ namespace Elsa.UnitTests.Triggers { public class TriggersForBlueprintsProviderTests { - [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for the compatible bookmarks of the start activities of the blueprints"), AutoMoqData] + [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for the compatible bookmarks of the start activities of the blueprints. This is actually an integration test."), AutoMoqData] public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks(IActivityTypeService activityTypeService, IBookmarkHasher bookmarkHasher, IBookmarkProvider provider1, @@ -65,13 +65,13 @@ namespace Elsa.UnitTests.Triggers { var workflowExecutionContextFactory = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); var activityExecutionContextFactory = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); + var triggersForActivityProvider = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, + new[] { provider1, provider2, provider3 }, + activityExecutionContextFactory); - // Unfortunately the SUT can't be created by Autofixture because of the IEnumerable in the ctor var sut = new TriggersForBlueprintsProvider(activityTypeService, - bookmarkHasher, - new[] { provider1, provider2, provider3 }, workflowExecutionContextFactory, - activityExecutionContextFactory); + triggersForActivityProvider); Mock.Get(activityTypeService) .Setup(x => x.GetActivityTypesAsync(default)) From f00f02041a2a5a9cbaf123de7db313a5a4dd862c Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 18 Mar 2021 20:40:09 +0000 Subject: [PATCH 05/10] WIP #738 - Further refactor (remove loop) This is pretty trivial really, it just cuts down on indentation and moves the looping into a Linq map (Select) which builds a collection and then reduces it (SelectMany) to a flattened collection. Also remove some needless await'ing. --- ...ForActivityBlueprintAndWorkflowProvider.cs | 10 ++--- .../Triggers/TriggersForBlueprintsProvider.cs | 42 +++++++++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs index cab1f8bf9..6769d3136 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs @@ -45,11 +45,11 @@ namespace Elsa.Triggers .ToListAsync(cancellationToken); var tasksOfListsOfTriggers = supportedBookmarkProviders - .Select(async bookmarkProvider => await GetTriggersForBookmarkProvider(bookmarkProvider, - bookmarkProviderContext, - activityBlueprint, - workflowExecutionContext.WorkflowBlueprint, - cancellationToken)); + .Select(bookmarkProvider => GetTriggersForBookmarkProvider(bookmarkProvider, + bookmarkProviderContext, + activityBlueprint, + workflowExecutionContext.WorkflowBlueprint, + cancellationToken)); return (await Task.WhenAll(tasksOfListsOfTriggers)) .SelectMany(x => x) .ToList(); diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs index 74e1aa5b4..8fe2d642e 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -37,26 +37,32 @@ namespace Elsa.Triggers public async Task> GetTriggersAsync(IEnumerable workflowBlueprints, CancellationToken cancellationToken = default) { - var allTriggers = new List(); - var activityTypes = (await activityTypeService.GetActivityTypesAsync(cancellationToken)).ToDictionary(x => x.TypeName); + var activityTypes = (await activityTypeService.GetActivityTypesAsync(cancellationToken)) + .ToDictionary(x => x.TypeName); - foreach (var workflowBlueprint in workflowBlueprints) - { - var startingActivityBlueprints = workflowBlueprint.GetStartActivities(); - var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, - cancellationToken); - var tasksOfCollectionsOfTriggers = startingActivityBlueprints - .Select(async activityBlueprint => await triggerProvider.GetTriggersForActivityBlueprintAsync(activityBlueprint, - workflowExecutionContext, - activityTypes, - cancellationToken)); - var triggers = (await Task.WhenAll(tasksOfCollectionsOfTriggers)) - .SelectMany(x => x) - .ToList(); - allTriggers.AddRange(triggers); - } + var tasksOfListsOfTriggers = workflowBlueprints + .Select(workflowBlueprint => GetWorkflowTriggersForWorkflowBlueprintAsync(workflowBlueprint, activityTypes, cancellationToken)); + + return (await Task.WhenAll(tasksOfListsOfTriggers)) + .SelectMany(x => x) + .ToList(); + } - return allTriggers; + async Task> GetWorkflowTriggersForWorkflowBlueprintAsync(IWorkflowBlueprint workflowBlueprint, + IDictionary activityTypes, + CancellationToken cancellationToken) + { + var startingActivityBlueprints = workflowBlueprint.GetStartActivities(); + var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, + cancellationToken); + var tasksOfCollectionsOfTriggers = startingActivityBlueprints + .Select(async activityBlueprint => await triggerProvider.GetTriggersForActivityBlueprintAsync(activityBlueprint, + workflowExecutionContext, + activityTypes, + cancellationToken)); + return (await Task.WhenAll(tasksOfCollectionsOfTriggers)) + .SelectMany(x => x) + .ToList(); } } } \ No newline at end of file From 9ec698c7bc38dad7b53a2d77d4ca9149b9e57c91 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 18 Mar 2021 21:09:04 +0000 Subject: [PATCH 06/10] WIP #738 - Refactor a troublesome extension method This extension method is quite complex and is frustrating when a unit test passes-through it. That's because extension methods can't be mocked in tests. --- .../CompositeActivityBlueprintExtensions.cs | 15 --------- ...siteActivityBlueprintProviderExtensions.cs | 25 +++++++++++++++ ...ActivitiesForCompositeActivityBlueprint.cs | 18 +++++++++++ .../Builders/CompositeActivityBuilder.cs | 11 ++++--- .../Elsa.Core/Builders/WorkflowBuilder.cs | 2 +- .../ElsaServiceCollectionExtensions.cs | 1 + ...esForCompositeActivityBlueprintProvider.cs | 31 +++++++++++++++++++ .../Services/WorkflowBlueprintMaterializer.cs | 30 ++++++++++-------- .../Elsa.Core/Services/WorkflowReviver.cs | 10 ++++-- src/core/Elsa.Core/Services/WorkflowRunner.cs | 7 +++-- .../Triggers/TriggersForBlueprintsProvider.cs | 7 +++-- .../TriggersForBlueprintsProviderTests.cs | 4 ++- 12 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Extensions/StartActivitiesForCompositeActivityBlueprintProviderExtensions.cs create mode 100644 src/core/Elsa.Abstractions/Services/IGetsStartActivitiesForCompositeActivityBlueprint.cs create mode 100644 src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs diff --git a/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs b/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs index 95c4cfa1d..280115edb 100644 --- a/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/CompositeActivityBlueprintExtensions.cs @@ -9,21 +9,6 @@ namespace Elsa { public static class CompositeActivityBlueprintExtensions { - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint) - { - var targetActivityIds = workflowBlueprint.Connections.Select(x => x.Target.Activity?.Id).Distinct().ToLookup(x => x); - - var query = - from activity in workflowBlueprint.Activities - where !targetActivityIds.Contains(activity.Id) - select activity; - - return query; - } - - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint, string activityType) => workflowBlueprint.GetStartActivities().Where(x => x.Type == activityType); - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint, Type activityType) => workflowBlueprint.GetStartActivities(activityType.Name); - public static IEnumerable GetStartActivities(this ICompositeActivityBlueprint workflowBlueprint) where T : IActivity => workflowBlueprint.GetStartActivities(typeof(T)); public static IEnumerable GetEndActivities(this ICompositeActivityBlueprint workflowBlueprint) => workflowBlueprint.Activities.Where(x => !workflowBlueprint.GetOutboundConnections(x.Id).Any()); public static IActivityBlueprint? GetActivity(this ICompositeActivityBlueprint workflowBlueprint, string id) => workflowBlueprint.Activities.FirstOrDefault(x => x.Id == id); public static IEnumerable GetActivities(this ICompositeActivityBlueprint workflowBlueprint, IEnumerable ids) => workflowBlueprint.Activities.Where(x => ids.Contains(x.Id)); diff --git a/src/core/Elsa.Abstractions/Extensions/StartActivitiesForCompositeActivityBlueprintProviderExtensions.cs b/src/core/Elsa.Abstractions/Extensions/StartActivitiesForCompositeActivityBlueprintProviderExtensions.cs new file mode 100644 index 000000000..90a7ecad4 --- /dev/null +++ b/src/core/Elsa.Abstractions/Extensions/StartActivitiesForCompositeActivityBlueprintProviderExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Extensions +{ + public static class StartActivitiesForCompositeActivityBlueprintProviderExtensions + { + public static IEnumerable GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider, + ICompositeActivityBlueprint workflowBlueprint, + string activityType) + => startActivitiesProvider.GetStartActivities(workflowBlueprint).Where(x => x.Type == activityType); + + public static IEnumerable GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider, + ICompositeActivityBlueprint workflowBlueprint, + Type activityType) + => startActivitiesProvider.GetStartActivities(workflowBlueprint, activityType.Name); + + public static IEnumerable GetStartActivities(this IGetsStartActivitiesForCompositeActivityBlueprint startActivitiesProvider, + ICompositeActivityBlueprint workflowBlueprint) where T : IActivity + => startActivitiesProvider.GetStartActivities(workflowBlueprint, typeof(T)); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/IGetsStartActivitiesForCompositeActivityBlueprint.cs b/src/core/Elsa.Abstractions/Services/IGetsStartActivitiesForCompositeActivityBlueprint.cs new file mode 100644 index 000000000..0c6f91c64 --- /dev/null +++ b/src/core/Elsa.Abstractions/Services/IGetsStartActivitiesForCompositeActivityBlueprint.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// An object which gets the starting activities for a specified . + /// + public interface IGetsStartActivitiesForCompositeActivityBlueprint + { + /// + /// Gets a collection of the starting activities for the specified composite activity blueprint. + /// + /// A composite activity blueprint + /// A collection of the blueprint's starting activities + IEnumerable GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs index 0bd2bbf6a..6b76b0444 100644 --- a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs +++ b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs @@ -14,9 +14,12 @@ namespace Elsa.Builders public class CompositeActivityBuilder : ActivityBuilder, ICompositeActivityBuilder { private readonly Func _compositeActivityBuilderFactory; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; - public CompositeActivityBuilder(IServiceProvider serviceProvider) + public CompositeActivityBuilder(IServiceProvider serviceProvider, IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); + ServiceProvider = serviceProvider; ActivityBuilders = new List(); ConnectionBuilders = new List(); @@ -58,7 +61,7 @@ namespace Elsa.Builders var valueProviders = propertyValuesBuilder.ValueProviders.ToDictionary( x => x.Key, - x => (IActivityPropertyValueProvider) new DelegateActivityPropertyValueProvider(x.Value)); + x => (IActivityPropertyValueProvider)new DelegateActivityPropertyValueProvider(x.Value)); return New(valueProviders, lineNumber, sourceFile); } @@ -181,7 +184,7 @@ namespace Elsa.Builders using var scope = ServiceProvider.CreateScope(); foreach (var activityBuilder in compositeActivityBuilders) { - var compositeActivity = (CompositeActivity) ActivatorUtilities.CreateInstance(scope.ServiceProvider, activityBuilder.ActivityType); + var compositeActivity = (CompositeActivity)ActivatorUtilities.CreateInstance(scope.ServiceProvider, activityBuilder.ActivityType); var compositeActivityBuilder = _compositeActivityBuilderFactory(); compositeActivityBuilder.ActivityId = activityBuilder.ActivityId; compositeActivity.Build(compositeActivityBuilder); @@ -198,7 +201,7 @@ namespace Elsa.Builders compositeActivityBlueprint.ActivityPropertyProviders = compositeActivityBlueprint.ActivityPropertyProviders; // Connect the composite activity to its starting activities. - var startActivities = compositeActivityBlueprint.GetStartActivities().ToList(); + var startActivities = startingActivitiesProvider.GetStartActivities(compositeActivityBlueprint).ToList(); connections.AddRange(startActivities.Select(x => new Connection(compositeActivityBlueprint, x, CompositeActivity.Enter))); } } diff --git a/src/core/Elsa.Core/Builders/WorkflowBuilder.cs b/src/core/Elsa.Core/Builders/WorkflowBuilder.cs index 1aadcf86d..ad425f07b 100644 --- a/src/core/Elsa.Core/Builders/WorkflowBuilder.cs +++ b/src/core/Elsa.Core/Builders/WorkflowBuilder.cs @@ -10,7 +10,7 @@ namespace Elsa.Builders { public class WorkflowBuilder : CompositeActivityBuilder, IWorkflowBuilder { - public WorkflowBuilder(IIdGenerator idGenerator, IServiceProvider serviceProvider) : base(serviceProvider) + public WorkflowBuilder(IIdGenerator idGenerator, IServiceProvider serviceProvider, IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) : base(serviceProvider, startingActivitiesProvider) { Version = 1; Variables = new Variables(); diff --git a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs index ca7ec6115..70d7636bb 100644 --- a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs @@ -111,6 +111,7 @@ namespace Microsoft.Extensions.DependencyInjection .AddScoped() .AddTransient() .AddTransient() + .AddTransient() ; // Serialization. diff --git a/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs new file mode 100644 index 000000000..d397f2775 --- /dev/null +++ b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Elsa.Services.Models; + +namespace Elsa.Services +{ + /// + /// Default implementation of . + /// + public class StartActivitiesForCompositeActivityBlueprintProvider : IGetsStartActivitiesForCompositeActivityBlueprint + { + /// + /// Gets a collection of the starting activities for the specified composite activity blueprint. + /// + /// A composite activity blueprint + /// A collection of the blueprint's starting activities + public IEnumerable GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint) + { + var targetActivityIds = compositeActivityBlueprint.Connections + .Select(x => x.Target.Activity?.Id) + .Distinct() + .ToLookup(x => x); + + var query = from activity in compositeActivityBlueprint.Activities + where !targetActivityIds.Contains(activity.Id) + select activity; + + return query; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs b/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs index 6de34dfaa..609c56f83 100644 --- a/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs +++ b/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs @@ -14,21 +14,25 @@ namespace Elsa.Services { private readonly IActivityTypeService _activityTypeService; private readonly ILogger _logger; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; - public WorkflowBlueprintMaterializer(IActivityTypeService activityTypeService, ILogger logger) + public WorkflowBlueprintMaterializer(IActivityTypeService activityTypeService, + ILogger logger, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new System.ArgumentNullException(nameof(startingActivitiesProvider)); _activityTypeService = activityTypeService; _logger = logger; } - + public async Task CreateWorkflowBlueprintAsync(WorkflowDefinition workflowDefinition, CancellationToken cancellationToken) { var manyActivityBlueprints = await Task.WhenAll(workflowDefinition.Activities.Select(async x => await CreateBlueprintsAsync(x, cancellationToken))); var activityBlueprints = manyActivityBlueprints.SelectMany(x => x).Distinct().ToDictionary(x => x.Id); - var compositeActivityBlueprints = activityBlueprints.Values.Where(x => x is ICompositeActivityBlueprint).Cast().ToList(); + var compositeActivityBlueprints = activityBlueprints.Values.Where(x => x is ICompositeActivityBlueprint).Cast().ToList(); var connections = compositeActivityBlueprints.SelectMany(x => x.Connections).Distinct().ToList(); var propertyProviders = compositeActivityBlueprints.SelectMany(x => x.ActivityPropertyProviders).ToList(); - + connections.AddRange(workflowDefinition.Connections.Select(x => ResolveConnection(x, activityBlueprints)).Where(x => x != null).Select(x => x!)); propertyProviders.AddRange(await CreatePropertyProviders(workflowDefinition, cancellationToken)); @@ -63,7 +67,7 @@ namespace Elsa.Services var activityType = await _activityTypeService.GetActivityTypeAsync(activityDefinition.Type, cancellationToken); var type = activityType.Type; var props = type.GetProperties(); - + foreach (var property in activityDefinition.Properties) { var prop = props.FirstOrDefault(x => x.Name == property.Name); @@ -73,7 +77,7 @@ namespace Elsa.Services _logger.LogWarning("Could not find the specified property '{PropertyName}' for activity type {ActivityTypeName}. Was the activity property renamed/removed/refactored after the workflow definition was created?", property.Name, activityType.Type.Name); continue; } - + var provider = new ExpressionActivityPropertyValueProvider(property.Expression, property.Syntax, prop.PropertyType); propertyProviders.AddProvider(activityDefinition.ActivityId, property.Name, provider); } @@ -101,12 +105,12 @@ namespace Elsa.Services private async Task> CreateBlueprintsAsync(ActivityDefinition activityDefinition, CancellationToken cancellationToken) { var list = new List(); - + if (activityDefinition is CompositeActivityDefinition compositeActivityDefinition) { var manyActivityBlueprints = await Task.WhenAll(compositeActivityDefinition.Activities.Select(async x => await CreateBlueprintsAsync(x, cancellationToken))); var activityBlueprints = manyActivityBlueprints.SelectMany(x => x).ToDictionary(x => x.Id); - + list.AddRange(activityBlueprints.Values); var compositeActivityBlueprint = new CompositeActivityBlueprint @@ -121,12 +125,12 @@ namespace Elsa.Services LoadWorkflowContext = activityDefinition.LoadWorkflowContext, SaveWorkflowContext = activityDefinition.SaveWorkflowContext, ActivityPropertyProviders = await CreatePropertyProviders(compositeActivityDefinition, cancellationToken) - }; - + }; + list.Add(compositeActivityBlueprint); - + // Connect the composite activity to its starting activities. - var startActivities = compositeActivityBlueprint.GetStartActivities().ToList(); + var startActivities = startingActivitiesProvider.GetStartActivities(compositeActivityBlueprint).ToList(); compositeActivityBlueprint.Connections.AddRange(startActivities.Select(x => new Connection(compositeActivityBlueprint, x, CompositeActivity.Enter))); } else @@ -140,7 +144,7 @@ namespace Elsa.Services PersistWorkflow = activityDefinition.PersistWorkflow, LoadWorkflowContext = activityDefinition.LoadWorkflowContext, SaveWorkflowContext = activityDefinition.SaveWorkflowContext, - }); + }); } return list; diff --git a/src/core/Elsa.Core/Services/WorkflowReviver.cs b/src/core/Elsa.Core/Services/WorkflowReviver.cs index 24dbb5d15..01b2ee519 100644 --- a/src/core/Elsa.Core/Services/WorkflowReviver.cs +++ b/src/core/Elsa.Core/Services/WorkflowReviver.cs @@ -14,13 +14,19 @@ namespace Elsa.Services private readonly IWorkflowQueue _workflowQueue; private readonly IWorkflowRegistry _workflowRegistry; private readonly IWorkflowInstanceStore _workflowInstanceStore; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; - public WorkflowReviver(IWorkflowRunner workflowRunner, IWorkflowQueue workflowQueue, IWorkflowRegistry workflowRegistry, IWorkflowInstanceStore workflowInstanceStore) + public WorkflowReviver(IWorkflowRunner workflowRunner, + IWorkflowQueue workflowQueue, + IWorkflowRegistry workflowRegistry, + IWorkflowInstanceStore workflowInstanceStore, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { _workflowRunner = workflowRunner; _workflowQueue = workflowQueue; _workflowRegistry = workflowRegistry; _workflowInstanceStore = workflowInstanceStore; + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); } public async Task ReviveAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) @@ -81,7 +87,7 @@ namespace Elsa.Services if (workflowBlueprint == null) throw new WorkflowException($"Could not find associated workflow definition {workflowInstance.DefinitionId} with version {workflowInstance.Version}"); - var startActivity = workflowBlueprint.GetStartActivities().FirstOrDefault(); + var startActivity = startingActivitiesProvider.GetStartActivities(workflowBlueprint).FirstOrDefault(); if (startActivity == null) throw new WorkflowException($"Cannot revive workflow {workflowInstance.Id} because it has no start activities"); diff --git a/src/core/Elsa.Core/Services/WorkflowRunner.cs b/src/core/Elsa.Core/Services/WorkflowRunner.cs index ca3ea1382..0d1cd4c9e 100644 --- a/src/core/Elsa.Core/Services/WorkflowRunner.cs +++ b/src/core/Elsa.Core/Services/WorkflowRunner.cs @@ -37,6 +37,7 @@ namespace Elsa.Services private readonly IMediator _mediator; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger _logger; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; public WorkflowRunner( IWorkflowRegistry workflowRegistry, @@ -48,7 +49,8 @@ namespace Elsa.Services Func workflowBuilderFactory, IMediator mediator, IServiceScopeFactory serviceScopeFactory, - ILogger logger) + ILogger logger, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { _workflowRegistry = workflowRegistry; _workflowFactory = workflowFactory; @@ -56,6 +58,7 @@ namespace Elsa.Services _mediator = mediator; _serviceScopeFactory = serviceScopeFactory; _logger = logger; + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); _workflowInstanceManager = workflowInstanceStore; _workflowContextManager = workflowContextManager; _bookmarkFinder = bookmarkFinder; @@ -284,7 +287,7 @@ namespace Elsa.Services private async Task BeginWorkflow(WorkflowExecutionContext workflowExecutionContext, IActivityBlueprint? activity, object? input, CancellationToken cancellationToken) { if (activity == null) - activity = workflowExecutionContext.WorkflowBlueprint.GetStartActivities().FirstOrDefault() ?? workflowExecutionContext.WorkflowBlueprint.Activities.First(); + activity = startingActivitiesProvider.GetStartActivities(workflowExecutionContext.WorkflowBlueprint).FirstOrDefault() ?? workflowExecutionContext.WorkflowBlueprint.Activities.First(); if (!await CanExecuteAsync(workflowExecutionContext, activity, input, false, cancellationToken)) return false; diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs index 8fe2d642e..f785bfc25 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -18,14 +18,17 @@ namespace Elsa.Triggers readonly IActivityTypeService activityTypeService; readonly ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory; readonly IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider; + readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; public TriggersForBlueprintsProvider(IActivityTypeService activityTypeService, ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory, - IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider) + IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { this.activityTypeService = activityTypeService ?? throw new ArgumentNullException(nameof(activityTypeService)); this.workflowExecutionContextFactory = workflowExecutionContextFactory ?? throw new ArgumentNullException(nameof(workflowExecutionContextFactory)); this.triggerProvider = triggerProvider ?? throw new ArgumentNullException(nameof(triggerProvider)); + this.startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); } /// @@ -52,7 +55,7 @@ namespace Elsa.Triggers IDictionary activityTypes, CancellationToken cancellationToken) { - var startingActivityBlueprints = workflowBlueprint.GetStartActivities(); + var startingActivityBlueprints = startingActivitiesProvider.GetStartActivities(workflowBlueprint); var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, cancellationToken); var tasksOfCollectionsOfTriggers = startingActivityBlueprints diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs index f6d2fccaf..0a37049ce 100644 --- a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -68,10 +68,12 @@ namespace Elsa.UnitTests.Triggers var triggersForActivityProvider = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, new[] { provider1, provider2, provider3 }, activityExecutionContextFactory); + var startingActivitiesProvider = new StartActivitiesForCompositeActivityBlueprintProvider(); var sut = new TriggersForBlueprintsProvider(activityTypeService, workflowExecutionContextFactory, - triggersForActivityProvider); + triggersForActivityProvider, + startingActivitiesProvider); Mock.Get(activityTypeService) .Setup(x => x.GetActivityTypesAsync(default)) From 6700ac2402ffc074c148c0dba70e16e1218c1e1b Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 18 Mar 2021 21:22:19 +0000 Subject: [PATCH 07/10] WIP #738 - Simplify unit test Now that the class is a little less complex, this simplified unit test covers just what remains. --- .../Triggers/TriggersForBlueprintsProvider.cs | 3 +- .../TriggersForBlueprintsProviderTests.cs | 282 +++++++++++------- 2 files changed, 167 insertions(+), 118 deletions(-) diff --git a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs index f785bfc25..0b57b66c3 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForBlueprintsProvider.cs @@ -56,8 +56,7 @@ namespace Elsa.Triggers CancellationToken cancellationToken) { var startingActivityBlueprints = startingActivitiesProvider.GetStartActivities(workflowBlueprint); - var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, - cancellationToken); + var workflowExecutionContext = await workflowExecutionContextFactory.CreateWorkflowExecutionContextAsync(workflowBlueprint, cancellationToken); var tasksOfCollectionsOfTriggers = startingActivityBlueprints .Select(async activityBlueprint => await triggerProvider.GetTriggersForActivityBlueprintAsync(activityBlueprint, workflowExecutionContext, diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs index 0a37049ce..b117cf20e 100644 --- a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using AutoFixture.Xunit2; using Elsa.ActivityProviders; @@ -17,134 +18,183 @@ namespace Elsa.UnitTests.Triggers { public class TriggersForBlueprintsProviderTests { - [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for the compatible bookmarks of the start activities of the blueprints. This is actually an integration test."), AutoMoqData] - public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks(IActivityTypeService activityTypeService, - IBookmarkHasher bookmarkHasher, - IBookmarkProvider provider1, - IBookmarkProvider provider2, - IBookmarkProvider provider3, - [AutofixtureServiceProvider] IServiceProvider serviceProvider, - IWorkflowFactory workflowFactory, + // [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for the compatible bookmarks of the start activities of the blueprints. This is actually an integration test."), AutoMoqData] + // public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks(IActivityTypeService activityTypeService, + // IBookmarkHasher bookmarkHasher, + // IBookmarkProvider provider1, + // IBookmarkProvider provider2, + // IBookmarkProvider provider3, + // [AutofixtureServiceProvider] IServiceProvider serviceProvider, + // IWorkflowFactory workflowFactory, + // ActivityType activityType1, + // ActivityType activityType2, + // ActivityType activityType3, + // IWorkflowBlueprint workflowBlueprint1, + // IWorkflowBlueprint workflowBlueprint2, + // IActivityBlueprint activityBlueprint1, + // IActivityBlueprint activityBlueprint2, + // IActivityBlueprint activityBlueprint3, + // IActivityBlueprint activityBlueprint4, + // IActivityBlueprint activityBlueprint5, + // IActivityBlueprint activityBlueprint6, + // string activityBlueprintId1, + // string activityBlueprintId2, + // string activityBlueprintId3, + // string activityBlueprintId4, + // string activityBlueprintId5, + // string activityBlueprintId6, + // IConnection connection1, + // IConnection connection2, + // IConnection connection3, + // ITargetEndpoint endpoint1, + // ITargetEndpoint endpoint2, + // ITargetEndpoint endpoint3, + // [OmitOnRecursion] WorkflowInstance workflow1, + // [OmitOnRecursion] WorkflowInstance workflow2, + // IBookmark bookmark1, + // IBookmark bookmark2, + // IBookmark bookmark3, + // IBookmark bookmark4, + // IBookmark bookmark5, + // IBookmark bookmark6, + // string bookmarkHash1, + // string bookmarkHash2, + // string bookmarkHash3, + // string bookmarkHash4, + // string bookmarkHash5, + // string bookmarkHash6) + // { + // var workflowExecutionContextFactory = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); + // var activityExecutionContextFactory = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); + // var triggersForActivityProvider = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, + // new[] { provider1, provider2, provider3 }, + // activityExecutionContextFactory); + // var startingActivitiesProvider = new StartActivitiesForCompositeActivityBlueprintProvider(); + + // var sut = new TriggersForBlueprintsProvider(activityTypeService, + // workflowExecutionContextFactory, + // triggersForActivityProvider, + // startingActivitiesProvider); + + // Mock.Get(workflowBlueprint1).SetupGet(x => x.Connections).Returns(new [] { connection1 }); + // Mock.Get(workflowBlueprint2).SetupGet(x => x.Connections).Returns(new [] { connection2, connection3 }); + // Mock.Get(workflowBlueprint1).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint4 }); + // Mock.Get(workflowBlueprint2).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint3, activityBlueprint5, activityBlueprint6 }); + // Mock.Get(workflowFactory) + // .Setup(x => x.InstantiateAsync(workflowBlueprint1, null, null, default)) + // .Returns(Task.FromResult(workflow1)); + // Mock.Get(workflowFactory) + // .Setup(x => x.InstantiateAsync(workflowBlueprint2, null, null, default)) + // .Returns(Task.FromResult(workflow2)); + // Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); + // Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); + // Mock.Get(connection3).SetupGet(x => x.Target).Returns(endpoint3); + // Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint4); + // Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint5); + // Mock.Get(endpoint3).SetupGet(x => x.Activity).Returns(activityBlueprint6); + // Mock.Get(activityBlueprint1).SetupGet(x => x.Type).Returns(activityType1.TypeName); + // Mock.Get(activityBlueprint2).SetupGet(x => x.Type).Returns(activityType2.TypeName); + // Mock.Get(activityBlueprint3).SetupGet(x => x.Type).Returns(activityType3.TypeName); + // Mock.Get(activityBlueprint1).SetupGet(x => x.Id).Returns(activityBlueprintId1); + // Mock.Get(activityBlueprint2).SetupGet(x => x.Id).Returns(activityBlueprintId2); + // Mock.Get(activityBlueprint3).SetupGet(x => x.Id).Returns(activityBlueprintId3); + // Mock.Get(activityBlueprint4).SetupGet(x => x.Id).Returns(activityBlueprintId4); + // Mock.Get(activityBlueprint5).SetupGet(x => x.Id).Returns(activityBlueprintId5); + // Mock.Get(activityBlueprint6).SetupGet(x => x.Id).Returns(activityBlueprintId6); + // SetupProviderToBeCompatibleWithActivityType(provider1, activityType1); + // SetupProviderToBeCompatibleWithActivityType(provider2, activityType2); + // SetupProviderToBeCompatibleWithActivityType(provider3, activityType3); + // Mock.Get(provider1) + // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + // .Returns(ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); + // Mock.Get(provider2) + // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + // .Returns(ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); + // Mock.Get(provider3) + // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + // .Returns(ValueTask.FromResult>(new [] { bookmark5, bookmark6 })); + // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark1)).Returns(bookmarkHash1); + // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark2)).Returns(bookmarkHash2); + // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark3)).Returns(bookmarkHash3); + // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark4)).Returns(bookmarkHash4); + // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark5)).Returns(bookmarkHash5); + // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark6)).Returns(bookmarkHash6); + + // var result = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 }); + + // Assert.Equal(6, result.Count()); + // Assert.True(ContainsTriggerWithHash(result, bookmarkHash1) + // && ContainsTriggerWithHash(result, bookmarkHash2) + // && ContainsTriggerWithHash(result, bookmarkHash3) + // && ContainsTriggerWithHash(result, bookmarkHash4) + // && ContainsTriggerWithHash(result, bookmarkHash5) + // && ContainsTriggerWithHash(result, bookmarkHash6), + // "The returned triggers should contain a trigger with every one of the six expected hashes"); + // } + + // void SetupProviderToBeCompatibleWithActivityType(IBookmarkProvider provider, ActivityType activityType) + // { + // Mock.Get(provider) + // .Setup(x => x.SupportsActivityAsync(It.IsAny(), default)) + // .Returns(ValueTask.FromResult(false)); + // Mock.Get(provider) + // .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + // .Returns(ValueTask.FromResult(true)); + // } + + // bool ContainsTriggerWithHash(IEnumerable triggers, string hash) + // => triggers.Any(x => Equals(x.BookmarkHash, hash)); + + + [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for start activities of the workflow blueprints"), AutoMoqData] + public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks([Frozen] IActivityTypeService activityTypeService, + [Frozen] ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory, + [Frozen] IGetsTriggersForActivityBlueprintAndWorkflow triggerProvider, + [Frozen] IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider, + TriggersForBlueprintsProvider sut, + IWorkflowBlueprint workflowBlueprint1, + IWorkflowBlueprint workflowBlueprint2, ActivityType activityType1, ActivityType activityType2, ActivityType activityType3, - IWorkflowBlueprint workflowBlueprint1, - IWorkflowBlueprint workflowBlueprint2, IActivityBlueprint activityBlueprint1, IActivityBlueprint activityBlueprint2, IActivityBlueprint activityBlueprint3, - IActivityBlueprint activityBlueprint4, - IActivityBlueprint activityBlueprint5, - IActivityBlueprint activityBlueprint6, - string activityBlueprintId1, - string activityBlueprintId2, - string activityBlueprintId3, - string activityBlueprintId4, - string activityBlueprintId5, - string activityBlueprintId6, - IConnection connection1, - IConnection connection2, - IConnection connection3, - ITargetEndpoint endpoint1, - ITargetEndpoint endpoint2, - ITargetEndpoint endpoint3, - [OmitOnRecursion] WorkflowInstance workflow1, - [OmitOnRecursion] WorkflowInstance workflow2, - IBookmark bookmark1, - IBookmark bookmark2, - IBookmark bookmark3, - IBookmark bookmark4, - IBookmark bookmark5, - IBookmark bookmark6, - string bookmarkHash1, - string bookmarkHash2, - string bookmarkHash3, - string bookmarkHash4, - string bookmarkHash5, - string bookmarkHash6) + WorkflowTrigger trigger1, + WorkflowTrigger trigger2, + WorkflowTrigger trigger3, + WorkflowTrigger trigger4, + WorkflowTrigger trigger5, + WorkflowTrigger trigger6, + [AutofixtureServiceProvider] IServiceProvider serviceProvider, + [NoAutoProperties] WorkflowInstance workflowInstance) { - var workflowExecutionContextFactory = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); - var activityExecutionContextFactory = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); - var triggersForActivityProvider = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, - new[] { provider1, provider2, provider3 }, - activityExecutionContextFactory); - var startingActivitiesProvider = new StartActivitiesForCompositeActivityBlueprintProvider(); - - var sut = new TriggersForBlueprintsProvider(activityTypeService, - workflowExecutionContextFactory, - triggersForActivityProvider, - startingActivitiesProvider); - Mock.Get(activityTypeService) .Setup(x => x.GetActivityTypesAsync(default)) .Returns(ValueTask.FromResult>(new [] { activityType1, activityType2, activityType3 })); - Mock.Get(workflowBlueprint1).SetupGet(x => x.Connections).Returns(new [] { connection1 }); - Mock.Get(workflowBlueprint2).SetupGet(x => x.Connections).Returns(new [] { connection2, connection3 }); - Mock.Get(workflowBlueprint1).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint4 }); - Mock.Get(workflowBlueprint2).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint3, activityBlueprint5, activityBlueprint6 }); - Mock.Get(workflowFactory) - .Setup(x => x.InstantiateAsync(workflowBlueprint1, null, null, default)) - .Returns(Task.FromResult(workflow1)); - Mock.Get(workflowFactory) - .Setup(x => x.InstantiateAsync(workflowBlueprint2, null, null, default)) - .Returns(Task.FromResult(workflow2)); - Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); - Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); - Mock.Get(connection3).SetupGet(x => x.Target).Returns(endpoint3); - Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint4); - Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint5); - Mock.Get(endpoint3).SetupGet(x => x.Activity).Returns(activityBlueprint6); - Mock.Get(activityBlueprint1).SetupGet(x => x.Type).Returns(activityType1.TypeName); - Mock.Get(activityBlueprint2).SetupGet(x => x.Type).Returns(activityType2.TypeName); - Mock.Get(activityBlueprint3).SetupGet(x => x.Type).Returns(activityType3.TypeName); - Mock.Get(activityBlueprint1).SetupGet(x => x.Id).Returns(activityBlueprintId1); - Mock.Get(activityBlueprint2).SetupGet(x => x.Id).Returns(activityBlueprintId2); - Mock.Get(activityBlueprint3).SetupGet(x => x.Id).Returns(activityBlueprintId3); - Mock.Get(activityBlueprint4).SetupGet(x => x.Id).Returns(activityBlueprintId4); - Mock.Get(activityBlueprint5).SetupGet(x => x.Id).Returns(activityBlueprintId5); - Mock.Get(activityBlueprint6).SetupGet(x => x.Id).Returns(activityBlueprintId6); - SetupProviderToBeCompatibleWithActivityType(provider1, activityType1); - SetupProviderToBeCompatibleWithActivityType(provider2, activityType2); - SetupProviderToBeCompatibleWithActivityType(provider3, activityType3); - Mock.Get(provider1) - .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - .Returns(ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); - Mock.Get(provider2) - .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - .Returns(ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); - Mock.Get(provider3) - .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - .Returns(ValueTask.FromResult>(new [] { bookmark5, bookmark6 })); - Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark1)).Returns(bookmarkHash1); - Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark2)).Returns(bookmarkHash2); - Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark3)).Returns(bookmarkHash3); - Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark4)).Returns(bookmarkHash4); - Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark5)).Returns(bookmarkHash5); - Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark6)).Returns(bookmarkHash6); + Mock.Get(startingActivitiesProvider) + .Setup(x => x.GetStartActivities(workflowBlueprint1)) + .Returns(() => new [] { activityBlueprint1, activityBlueprint2 }); + Mock.Get(startingActivitiesProvider) + .Setup(x => x.GetStartActivities(workflowBlueprint2)) + .Returns(() => new [] { activityBlueprint3 }); + Mock.Get(workflowExecutionContextFactory) + .Setup(x => x.CreateWorkflowExecutionContextAsync(It.IsAny(), default)) + .Returns((IWorkflowBlueprint bp, CancellationToken c) => Task.FromResult(new WorkflowExecutionContext(serviceProvider, bp, workflowInstance, default))); + Mock.Get(triggerProvider) + .Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint1, It.IsAny(), It.IsAny>(), default)) + .Returns(() => Task.FromResult>(new [] { trigger1, trigger2 })); + Mock.Get(triggerProvider) + .Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint2, It.IsAny(), It.IsAny>(), default)) + .Returns(() => Task.FromResult>(new [] { trigger3, trigger4 })); + Mock.Get(triggerProvider) + .Setup(x => x.GetTriggersForActivityBlueprintAsync(activityBlueprint3, It.IsAny(), It.IsAny>(), default)) + .Returns(() => Task.FromResult>(new [] { trigger5, trigger6 })); - var result = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 }); + var results = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 }); - Assert.Equal(6, result.Count()); - Assert.True(ContainsTriggerWithHash(result, bookmarkHash1) - && ContainsTriggerWithHash(result, bookmarkHash2) - && ContainsTriggerWithHash(result, bookmarkHash3) - && ContainsTriggerWithHash(result, bookmarkHash4) - && ContainsTriggerWithHash(result, bookmarkHash5) - && ContainsTriggerWithHash(result, bookmarkHash6), - "The returned triggers should contain a trigger with every one of the six expected hashes"); + Assert.Equal(new [] { trigger1, trigger2, trigger3, trigger4, trigger5, trigger6 }, + results); } - - void SetupProviderToBeCompatibleWithActivityType(IBookmarkProvider provider, ActivityType activityType) - { - Mock.Get(provider) - .Setup(x => x.SupportsActivityAsync(It.IsAny(), default)) - .Returns(ValueTask.FromResult(false)); - Mock.Get(provider) - .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) - .Returns(ValueTask.FromResult(true)); - } - - bool ContainsTriggerWithHash(IEnumerable triggers, string hash) - => triggers.Any(x => Equals(x.BookmarkHash, hash)); } } \ No newline at end of file From 3943e64c702fa8cf37cdbe3d3ac0aa4897030096 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Fri, 19 Mar 2021 19:45:16 +0000 Subject: [PATCH 08/10] WIP #738 - Add test coverage to replace big test The large/complex test is now removed, replaced by smaller/simpler unit tests for the individual units in the overall process of getting triggers. --- ...utionContextForActivityBlueprintFactory.cs | 1 - ...esForCompositeActivityBlueprintProvider.cs | 26 +++- ...ContextForActivityBlueprintFactoryTests.cs | 27 ++++ ...CompositeActivityBlueprintProviderTests.cs | 51 +++++++ ...ContextForWorkflowBlueprintFactoryTests.cs | 31 +++++ ...tivityBlueprintAndWorkflowProviderTests.cs | 71 ++++++++++ .../TriggersForBlueprintsProviderTests.cs | 128 ------------------ 7 files changed, 201 insertions(+), 134 deletions(-) create mode 100644 test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs create mode 100644 test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs create mode 100644 test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs create mode 100644 test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs diff --git a/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs index 794c9f91f..02c80c835 100644 --- a/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs +++ b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs @@ -14,7 +14,6 @@ namespace Elsa.Services public ActivityExecutionContextForActivityBlueprintFactory(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - } /// diff --git a/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs index d397f2775..ab59f9abb 100644 --- a/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs +++ b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs @@ -16,16 +16,32 @@ namespace Elsa.Services /// A collection of the blueprint's starting activities public IEnumerable GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint) { - var targetActivityIds = compositeActivityBlueprint.Connections - .Select(x => x.Target.Activity?.Id) - .Distinct() - .ToLookup(x => x); + var activityIdsThatAreNotStartingActivities = GetAllActivityIdsWhichHaveInboundConnections(compositeActivityBlueprint); var query = from activity in compositeActivityBlueprint.Activities - where !targetActivityIds.Contains(activity.Id) + where !activityIdsThatAreNotStartingActivities.Contains(activity.Id) select activity; return query; } + + /// + /// This method gets activities that have inbound connections. + /// + /// + /// + /// "Start activities" are those with no inbound connections; IE no workflow connection-target will point to a start activity. + /// What this method returns is essentially a blacklist of activity IDs which are not starting activities. + /// + /// + /// A composite activity blueprint + /// A lookup of activity IDs which are not starting activities + ILookup GetAllActivityIdsWhichHaveInboundConnections(ICompositeActivityBlueprint compositeActivityBlueprint) + { + return compositeActivityBlueprint.Connections + .Select(x => x.Target.Activity?.Id) + .Distinct() + .ToLookup(x => x); + } } } \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs b/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs new file mode 100644 index 000000000..8949c53a0 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Xunit; + +namespace Elsa.Services +{ + public class ActivityExecutionContextForActivityBlueprintFactoryTests + { + [Theory(DisplayName = "The CreateActivityExecutionContext method should create a context using the activity blueprint, the workflow execution context, cancellation token and injected service provider."), AutoMoqData] + public void CreateActivityExecutionContextCreatesContextUsingBlueprintExecutionContextCancellationTokenAndServiceProvider([AutofixtureServiceProvider] IServiceProvider serviceProvider, + IActivityBlueprint activityBlueprint, + [OmitOnRecursion] WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken) + { + var sut = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); + + var result = sut.CreateActivityExecutionContext(activityBlueprint, workflowExecutionContext, cancellationToken); + + Assert.True(ReferenceEquals(activityBlueprint, result.ActivityBlueprint), "The activity blueprint should be the same"); + Assert.True(ReferenceEquals(workflowExecutionContext, result.WorkflowExecutionContext), "The workflow execution context should be the same"); + Assert.True(Equals(cancellationToken, result.CancellationToken), "The cancellation token should be equal"); + Assert.True(ReferenceEquals(serviceProvider, result.ServiceProvider), "The service provider should be the same"); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs b/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs new file mode 100644 index 000000000..eb421affa --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using Elsa.Services.Models; +using Moq; +using Xunit; + +namespace Elsa.Services +{ + public class StartActivitiesForCompositeActivityBlueprintProviderTests + { + [Theory(DisplayName = "The GetStartActivities returns only activities that have no inbound workflow connections"), AutoMoqData] + public void GetStartActivitiesReturnsAllActivitiesWhichHaveNoInboundConnections(StartActivitiesForCompositeActivityBlueprintProvider sut, + IWorkflowBlueprint workflowBlueprint, + IActivityBlueprint activityBlueprint1, + IActivityBlueprint activityBlueprint2, + IActivityBlueprint activityBlueprint3, + IActivityBlueprint activityBlueprint4, + string activityBlueprintId1, + string activityBlueprintId2, + string activityBlueprintId3, + string activityBlueprintId4, + IConnection connection1, + IConnection connection2, + ITargetEndpoint endpoint1, + ITargetEndpoint endpoint2) + { + SetupActivitiesWithIds(new () { + {activityBlueprintId1, activityBlueprint1}, + {activityBlueprintId2, activityBlueprint2}, + {activityBlueprintId3, activityBlueprint3}, + {activityBlueprintId4, activityBlueprint4}, + }); + Mock.Get(workflowBlueprint).SetupGet(x => x.Connections).Returns(new [] { connection1, connection2 }); + Mock.Get(workflowBlueprint).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint3, activityBlueprint4 }); + Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); + Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); + Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint2); + Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint4); + + var result = sut.GetStartActivities(workflowBlueprint).ToArray(); + + Assert.Equal(new [] { activityBlueprint1, activityBlueprint3 }, result); + } + + void SetupActivitiesWithIds(Dictionary idsToBlueprints) + { + foreach(var kvp in idsToBlueprints) + Mock.Get(kvp.Value).SetupGet(x => x.Id).Returns(kvp.Key); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs b/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs new file mode 100644 index 000000000..20ba00b63 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using Elsa.Models; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Moq; +using Xunit; + +namespace Elsa.Services +{ + public class WorkflowExecutionContextForWorkflowBlueprintFactoryTests + { + [Theory(DisplayName = "The CreateWorkflowExecutionContextAsync method returns an execution context using the blueprint, an instance and the service provider"), AutoMoqData] + public async Task CreateWorkflowExecutionContextAsyncReturnsContextWithBlueprintInstanceAndServiceProvider([AutofixtureServiceProvider] IServiceProvider serviceProvider, + IWorkflowFactory workflowFactory, + IWorkflowBlueprint workflowBlueprint, + [OmitOnRecursion] WorkflowInstance instance) + { + var sut = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); + Mock.Get(workflowFactory) + .Setup(x => x.InstantiateAsync(workflowBlueprint, default, default, default)) + .Returns(() => Task.FromResult(instance)); + + var result = await sut.CreateWorkflowExecutionContextAsync(workflowBlueprint); + + Assert.Same(serviceProvider, result.ServiceProvider); + Assert.Same(workflowBlueprint, result.WorkflowBlueprint); + Assert.Same(instance, result.WorkflowInstance); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs new file mode 100644 index 000000000..22a0c1d69 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Models; +using Elsa.Services; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Moq; +using Xunit; + +namespace Elsa.Triggers +{ + public class TriggersForActivityBlueprintAndWorkflowProviderTests + { + [Theory(DisplayName = "The GetTriggersForActivityBlueprintAsync returns a trigger for every bookmark in the bookmark providers that support the activity"), AutoMoqData] + public async Task GetTriggersForActivityBlueprintAsyncReturnsTriggersForEachBookmarkInSupportedBookmarkProviders(IBookmarkHasher bookmarkHasher, + IBookmarkProvider bookmarkProvider1, + IBookmarkProvider unsupportedBookmarkProvider, + IBookmarkProvider bookmarkProvider2, + ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory, + IActivityBlueprint activityBlueprint, + [AutofixtureServiceProvider] IServiceProvider serviceProvider, + IWorkflowBlueprint workflowBlueprint, + [OmitOnRecursion] WorkflowInstance workflowInstance, + ActivityType activityType, + IBookmark bookmark1, + IBookmark bookmark2, + IBookmark bookmark3, + IBookmark bookmark4) + { + var sut = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, + new[] { bookmarkProvider1, unsupportedBookmarkProvider, bookmarkProvider2 }, + activityExecutionContextFactory); + var workflowExecutionContext = new WorkflowExecutionContext(serviceProvider, workflowBlueprint, workflowInstance); + var activityExecutionContext = new ActivityExecutionContext(serviceProvider, workflowExecutionContext, activityBlueprint, default, default, default); + + Mock.Get(activityExecutionContextFactory) + .Setup(x => x.CreateActivityExecutionContext(activityBlueprint, workflowExecutionContext, default)) + .Returns(activityExecutionContext); + Mock.Get(activityBlueprint).SetupGet(x => x.Type).Returns(activityType.TypeName); + Mock.Get(bookmarkProvider1) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(true)); + Mock.Get(unsupportedBookmarkProvider) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(false)); + Mock.Get(bookmarkProvider2) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(true)); + Mock.Get(bookmarkProvider1) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(() => ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); + Mock.Get(bookmarkProvider2) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(() => ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); + + var result = await sut.GetTriggersForActivityBlueprintAsync(activityBlueprint, + workflowExecutionContext, + new Dictionary { { activityType.TypeName, activityType } }); + + Assert.True(result.Any(x => x.Bookmark == bookmark1), "Result contains a trigger for bookmark 1"); + Assert.True(result.Any(x => x.Bookmark == bookmark2), "Result contains a trigger for bookmark 2"); + Assert.True(result.Any(x => x.Bookmark == bookmark3), "Result contains a trigger for bookmark 3"); + Assert.True(result.Any(x => x.Bookmark == bookmark4), "Result contains a trigger for bookmark 4"); + Assert.True(result.Count() == 4, "Result has 4 items"); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs index b117cf20e..4ed32364e 100644 --- a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -18,134 +18,6 @@ namespace Elsa.UnitTests.Triggers { public class TriggersForBlueprintsProviderTests { - // [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for the compatible bookmarks of the start activities of the blueprints. This is actually an integration test."), AutoMoqData] - // public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks(IActivityTypeService activityTypeService, - // IBookmarkHasher bookmarkHasher, - // IBookmarkProvider provider1, - // IBookmarkProvider provider2, - // IBookmarkProvider provider3, - // [AutofixtureServiceProvider] IServiceProvider serviceProvider, - // IWorkflowFactory workflowFactory, - // ActivityType activityType1, - // ActivityType activityType2, - // ActivityType activityType3, - // IWorkflowBlueprint workflowBlueprint1, - // IWorkflowBlueprint workflowBlueprint2, - // IActivityBlueprint activityBlueprint1, - // IActivityBlueprint activityBlueprint2, - // IActivityBlueprint activityBlueprint3, - // IActivityBlueprint activityBlueprint4, - // IActivityBlueprint activityBlueprint5, - // IActivityBlueprint activityBlueprint6, - // string activityBlueprintId1, - // string activityBlueprintId2, - // string activityBlueprintId3, - // string activityBlueprintId4, - // string activityBlueprintId5, - // string activityBlueprintId6, - // IConnection connection1, - // IConnection connection2, - // IConnection connection3, - // ITargetEndpoint endpoint1, - // ITargetEndpoint endpoint2, - // ITargetEndpoint endpoint3, - // [OmitOnRecursion] WorkflowInstance workflow1, - // [OmitOnRecursion] WorkflowInstance workflow2, - // IBookmark bookmark1, - // IBookmark bookmark2, - // IBookmark bookmark3, - // IBookmark bookmark4, - // IBookmark bookmark5, - // IBookmark bookmark6, - // string bookmarkHash1, - // string bookmarkHash2, - // string bookmarkHash3, - // string bookmarkHash4, - // string bookmarkHash5, - // string bookmarkHash6) - // { - // var workflowExecutionContextFactory = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); - // var activityExecutionContextFactory = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); - // var triggersForActivityProvider = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, - // new[] { provider1, provider2, provider3 }, - // activityExecutionContextFactory); - // var startingActivitiesProvider = new StartActivitiesForCompositeActivityBlueprintProvider(); - - // var sut = new TriggersForBlueprintsProvider(activityTypeService, - // workflowExecutionContextFactory, - // triggersForActivityProvider, - // startingActivitiesProvider); - - // Mock.Get(workflowBlueprint1).SetupGet(x => x.Connections).Returns(new [] { connection1 }); - // Mock.Get(workflowBlueprint2).SetupGet(x => x.Connections).Returns(new [] { connection2, connection3 }); - // Mock.Get(workflowBlueprint1).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint4 }); - // Mock.Get(workflowBlueprint2).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint3, activityBlueprint5, activityBlueprint6 }); - // Mock.Get(workflowFactory) - // .Setup(x => x.InstantiateAsync(workflowBlueprint1, null, null, default)) - // .Returns(Task.FromResult(workflow1)); - // Mock.Get(workflowFactory) - // .Setup(x => x.InstantiateAsync(workflowBlueprint2, null, null, default)) - // .Returns(Task.FromResult(workflow2)); - // Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); - // Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); - // Mock.Get(connection3).SetupGet(x => x.Target).Returns(endpoint3); - // Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint4); - // Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint5); - // Mock.Get(endpoint3).SetupGet(x => x.Activity).Returns(activityBlueprint6); - // Mock.Get(activityBlueprint1).SetupGet(x => x.Type).Returns(activityType1.TypeName); - // Mock.Get(activityBlueprint2).SetupGet(x => x.Type).Returns(activityType2.TypeName); - // Mock.Get(activityBlueprint3).SetupGet(x => x.Type).Returns(activityType3.TypeName); - // Mock.Get(activityBlueprint1).SetupGet(x => x.Id).Returns(activityBlueprintId1); - // Mock.Get(activityBlueprint2).SetupGet(x => x.Id).Returns(activityBlueprintId2); - // Mock.Get(activityBlueprint3).SetupGet(x => x.Id).Returns(activityBlueprintId3); - // Mock.Get(activityBlueprint4).SetupGet(x => x.Id).Returns(activityBlueprintId4); - // Mock.Get(activityBlueprint5).SetupGet(x => x.Id).Returns(activityBlueprintId5); - // Mock.Get(activityBlueprint6).SetupGet(x => x.Id).Returns(activityBlueprintId6); - // SetupProviderToBeCompatibleWithActivityType(provider1, activityType1); - // SetupProviderToBeCompatibleWithActivityType(provider2, activityType2); - // SetupProviderToBeCompatibleWithActivityType(provider3, activityType3); - // Mock.Get(provider1) - // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); - // Mock.Get(provider2) - // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); - // Mock.Get(provider3) - // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult>(new [] { bookmark5, bookmark6 })); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark1)).Returns(bookmarkHash1); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark2)).Returns(bookmarkHash2); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark3)).Returns(bookmarkHash3); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark4)).Returns(bookmarkHash4); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark5)).Returns(bookmarkHash5); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark6)).Returns(bookmarkHash6); - - // var result = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 }); - - // Assert.Equal(6, result.Count()); - // Assert.True(ContainsTriggerWithHash(result, bookmarkHash1) - // && ContainsTriggerWithHash(result, bookmarkHash2) - // && ContainsTriggerWithHash(result, bookmarkHash3) - // && ContainsTriggerWithHash(result, bookmarkHash4) - // && ContainsTriggerWithHash(result, bookmarkHash5) - // && ContainsTriggerWithHash(result, bookmarkHash6), - // "The returned triggers should contain a trigger with every one of the six expected hashes"); - // } - - // void SetupProviderToBeCompatibleWithActivityType(IBookmarkProvider provider, ActivityType activityType) - // { - // Mock.Get(provider) - // .Setup(x => x.SupportsActivityAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult(false)); - // Mock.Get(provider) - // .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) - // .Returns(ValueTask.FromResult(true)); - // } - - // bool ContainsTriggerWithHash(IEnumerable triggers, string hash) - // => triggers.Any(x => Equals(x.BookmarkHash, hash)); - - [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for start activities of the workflow blueprints"), AutoMoqData] public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks([Frozen] IActivityTypeService activityTypeService, [Frozen] ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory, From 2539f6425a5ef13ac068a4b8d878dee1288e10c6 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 20 Mar 2021 09:44:35 +0000 Subject: [PATCH 09/10] WIP #738 - Integration tests for Trigger Indexer One of these tests is failing and this is the one which demonstrates issue #738. The composite activity which begins with ReceiveSignal doesn't generate a trigger. --- .../WorkflowDefinitionExtensions.cs | 2 + .../TriggerIndexerIntegrationTests.cs | 203 ++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 test/integration/Elsa.Core.IntegrationTests/Triggers/TriggerIndexerIntegrationTests.cs diff --git a/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs b/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs index 7867b18e2..f43b6cad8 100644 --- a/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/WorkflowDefinitionExtensions.cs @@ -10,6 +10,8 @@ namespace Elsa GetActivityById(this WorkflowDefinition workflowDefinition, string activityId) => workflowDefinition.Activities.First(x => x.ActivityId == activityId); + // TODO: Consider adding this as an overload for IGetsStartActivitiesForCompositeActivityBlueprint + // The intent is the same and the logic is near-identical. public static IEnumerable GetStartActivities(this WorkflowDefinition workflowDefinition) { var targetActivities = workflowDefinition.Connections diff --git a/test/integration/Elsa.Core.IntegrationTests/Triggers/TriggerIndexerIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Triggers/TriggerIndexerIntegrationTests.cs new file mode 100644 index 000000000..d782f0b91 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Triggers/TriggerIndexerIntegrationTests.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elsa.Activities.ControlFlow; +using Elsa.Activities.Primitives; +using Elsa.Activities.Signaling; +using Elsa.Activities.UserTask.Activities; +using Elsa.Builders; +using Elsa.Models; +using Elsa.Persistence; +using Elsa.Triggers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Elsa.Core.IntegrationTests.Triggers +{ + public class TriggerIndexerIntegrationTests + { + [Fact(DisplayName = "The IndexTriggersAsync method should return a trigger for a blocking start activity")] + public async Task IndexTriggersAsyncShouldIncludeBlockingStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.True(allTriggers.Any(x => x.ActivityType == nameof(SignalReceived) + && x.WorkflowBlueprint.Name == nameof(WorkflowWithBlockingStartActivity)), + "A trigger exists for the expected blocking activity"); + } + + [Fact(DisplayName = "The IndexTriggersAsync method should not return a trigger for a non-blocking start activity")] + public async Task IndexTriggersAsyncShouldNotIncludeNonBlockingStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.False(allTriggers.Any(x => x.WorkflowBlueprint.Name == nameof(WorkflowWithNonBlockingStartActivity)), + "No triggers exist for the workflow which starts with a non-blocking activity"); + } + + [Fact(DisplayName = "The IndexTriggersAsync method should return a trigger for a blocking start activity within a composite activity", + Skip = "This test is the repro case for #738, which is not yet implemented")] + public async Task IndexTriggersAsyncShouldIncludeBlockingCompositeStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.True(allTriggers.Any(x => x.WorkflowBlueprint.Name == "WorkflowWithBlockingCompositeStartActivity"), + "A trigger exists for the expected blocking composite activity"); + } + + [Fact(DisplayName = "The IndexTriggersAsync method should not return a trigger for a non-blocking start activity within a composite activity")] + public async Task IndexTriggersAsyncShouldNotIncludeNonBlockingCompositeStartActivities() + { + var allTriggers = await IndexThenGetAllTriggersAsync(); + Assert.False(allTriggers.Any(x => x.WorkflowBlueprint.Name == "WorkflowWithNonBlockingCompositeStartActivity"), + "No triggers exist for the workflow which starts with a non-blocking composite activity"); + } + + async Task> IndexThenGetAllTriggersAsync() + { + var serviceProvider = await GetServiceProvider(); + + var sut = serviceProvider.GetRequiredService(); + await sut.IndexTriggersAsync(); + + var triggerStore = serviceProvider.GetRequiredService(); + return await triggerStore.GetAsync(); + } + + async Task GetServiceProvider() + { + var services = new ServiceCollection(); + services.AddElsa(elsa => { + elsa + .AddWorkflow() + .AddWorkflow() + .AddActivity() + .AddActivity() + ; + }); + + var serviceProvider = services.BuildServiceProvider(); + + var definitionStore = serviceProvider.GetRequiredService(); + await definitionStore.AddAsync(new WorkflowDefinition + { + Id = "1", + DefinitionId = "WorkflowWithNonBlockingCompositeStartActivity", + Name = "WorkflowWithNonBlockingCompositeStartActivity", + Version = 1, + IsPublished = true, + IsLatest = true, + PersistenceBehavior = WorkflowPersistenceBehavior.Suspended, + Activities = new[] + { + GetNonBlockingCompositeActivityDefinition("nonBlockingComposite1"), + GetNonBlockingActivityDefinition("nonBlocking1"), + }, + Connections = new[] + { + new ConnectionDefinition("nonBlockingComposite1", "nonBlocking1", OutcomeNames.Done), + } + }); + await definitionStore.AddAsync(new WorkflowDefinition + { + Id = "2", + DefinitionId = "WorkflowWithBlockingCompositeStartActivity", + Name = "WorkflowWithBlockingCompositeStartActivity", + Version = 1, + IsPublished = true, + IsLatest = true, + PersistenceBehavior = WorkflowPersistenceBehavior.Suspended, + Activities = new[] + { + GetBlockingCompositeActivityDefinition("blockingComposite1"), + GetNonBlockingActivityDefinition("nonBlocking2"), + }, + Connections = new[] + { + new ConnectionDefinition("blockingComposite1", "nonBlocking2", OutcomeNames.Done), + } + }); + + return serviceProvider; + } + + class WorkflowWithBlockingStartActivity : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(a => a.Set(x => x.Signal, "MySignal").Set(x => x.Id, "SignalReceived1")) + .Then(a => a.Set(x => x.Id, "Finish1")); + } + } + + class WorkflowWithNonBlockingStartActivity : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(t => t.Set(x => x.VariableName, "Unused").Set(x => x.Value, "Unused").Set(x => x.Id, "SetVariable1")) + .Then(a => a.Set(x => x.Id, "Finish2")); + } + } + + ActivityDefinition GetBlockingActivityDefinition(string id) + { + return new() + { + ActivityId = id, + Type = nameof(SignalReceived), + Properties = new [] + { + ActivityDefinitionProperty.Literal(nameof(SignalReceived.Signal), "MySignal"), + } + }; + } + + ActivityDefinition GetNonBlockingActivityDefinition(string id) + { + return new() + { + ActivityId = id, + Type = nameof(SetVariable), + Properties = new [] + { + ActivityDefinitionProperty.Literal(nameof(SetVariable.VariableName), "Unused"), + ActivityDefinitionProperty.Literal(nameof(SetVariable.Value), "Unused"), + } + }; + } + + ActivityDefinition GetBlockingCompositeActivityDefinition(string id) + { + return new CompositeActivityDefinition + { + ActivityId = id, + Activities = new [] + { + GetBlockingActivityDefinition("SignalReceived2"), + GetNonBlockingActivityDefinition("SetVariable2"), + }, + Connections = new [] + { + new ConnectionDefinition("UserTask2", "SetVariable2", OutcomeNames.Done), + } + }; + } + + ActivityDefinition GetNonBlockingCompositeActivityDefinition(string id) + { + return new CompositeActivityDefinition + { + ActivityId = id, + Activities = new [] + { + GetNonBlockingActivityDefinition("SetVariable3"), + GetNonBlockingActivityDefinition("SetVariable4"), + }, + Connections = new [] + { + new ConnectionDefinition("SetVariable3", "SetVariable4", OutcomeNames.Done), + } + }; + } + } +} \ No newline at end of file From f91e224314029d5cdfc6435f9c5e666d36654184 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 20 Mar 2021 11:14:35 +0000 Subject: [PATCH 10/10] Fix intermittent failing tests The issue here is that the elsa.db for sqlite is going in the same path every time. Depending on the order in which tests are run, sometimes it means that migrations are run on a DB which is expected to be empty but which already exists. That causes an error & test failure. Here I've just used temp folders to ensure that the DB file is in a different path every time. --- .../HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs | 5 ++++- ...HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs index 48ef3dddc..c0aa02255 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithDuplicateActivitiesWorkflowAttributes.cs @@ -11,6 +11,7 @@ using Elsa.Persistence.EntityFramework.Sqlite; using Elsa.Persistence.YesSql; using YesSql.Provider.Sqlite; using System.Data; +using Elsa.Testing.Shared.Helpers; namespace Elsa.Core.IntegrationTests.Autofixture { @@ -48,13 +49,15 @@ namespace Elsa.Core.IntegrationTests.Autofixture { public override ICustomization GetCustomization(ParameterInfo parameter) { + var tempFolder = new TemporaryFolder(); + return new HostBubilderUsingServicesCustomization(services => { services .AddElsa(elsa => { elsa .AddWorkflow() .UseEntityFrameworkPersistence(opts => { - opts.UseSqlite("Data Source=elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); + opts.UseSqlite($"Data Source={tempFolder.Folder}elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); }); }); }, parameter); diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs index 4fd62d32a..ebf0974c7 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithPersistableWorkflowAndEfSqliteAttribute.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Elsa.Persistence.EntityFramework.Core.Extensions; using Microsoft.EntityFrameworkCore; using Elsa.Persistence.EntityFramework.Sqlite; +using Elsa.Testing.Shared.Helpers; namespace Elsa.Core.IntegrationTests.Autofixture { @@ -13,12 +14,14 @@ namespace Elsa.Core.IntegrationTests.Autofixture { public override ICustomization GetCustomization(ParameterInfo parameter) { + var tempFolder = new TemporaryFolder(); + return new HostBubilderUsingServicesCustomization(services => { services .AddElsa(elsa => { elsa .UseEntityFrameworkPersistence(opts => { - opts.UseSqlite("Data Source=elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); + opts.UseSqlite($"Data Source={tempFolder.Folder}elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name)); }) .AddPersistableWorkflow(); });