From ed749567999cc172b56f425d0a2dd88bc734b681 Mon Sep 17 00:00:00 2001 From: Thyge Skoedt Steffensen <31892312+thygesteffensen@users.noreply.github.com> Date: Mon, 1 Mar 2021 10:17:13 +0100 Subject: [PATCH] feat!: Added flow result When triggering a flow, the function returns and FlowResult instead of just a Task. It is now supposed to be easier to write general action executors and asserting a flow inside the unit test, instead of in the action executors. #43 --- PowerAutomateMockUp/FlowParser/ActionState.cs | 45 ++++++ PowerAutomateMockUp/FlowParser/FlowRunner.cs | 38 ++++- .../FlowParser/FlowSettings.cs | 2 + Test/FullFlowTestV2.cs | 137 ++++++++++++++++++ 4 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 PowerAutomateMockUp/FlowParser/ActionState.cs create mode 100644 Test/FullFlowTestV2.cs diff --git a/PowerAutomateMockUp/FlowParser/ActionState.cs b/PowerAutomateMockUp/FlowParser/ActionState.cs new file mode 100644 index 0000000..2a4bb73 --- /dev/null +++ b/PowerAutomateMockUp/FlowParser/ActionState.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.Collections.Generic; +using Parser.ExpressionParser; +using Parser.FlowParser.ActionExecutors; + +namespace Parser.FlowParser +{ + public class FlowResult + { + public FlowResult() + { + ActionStates = new Dictionary(); + } + + public Dictionary ActionStates { get; set; } + public int NumberOfExecutedActions { get; set; } + } + + public class ActionState + { + private ValueContainer? _actionInput; +#nullable enable + public ValueContainer? ActionInput + { + get => _actionInput; + set + { + _actionInput = value; + if (_actionInput == null || _actionInput.Type() != ValueContainer.ValueType.Object) return; + if (_actionInput.AsDict().ContainsKey("parameters")) + { + ActionParameters = _actionInput.AsDict()["parameters"]; + } + } + } + + public ValueContainer? ActionParameters { set; get; } + + public ActionResult? ActionOutput { get; set; } +#nullable disable + public string ActionName { get; set; } + public string ActionType { get; set; } + public int ActionOrder { get; set; } + } +} \ No newline at end of file diff --git a/PowerAutomateMockUp/FlowParser/FlowRunner.cs b/PowerAutomateMockUp/FlowParser/FlowRunner.cs index d5d9fca..2efe206 100644 --- a/PowerAutomateMockUp/FlowParser/FlowRunner.cs +++ b/PowerAutomateMockUp/FlowParser/FlowRunner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -7,7 +8,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Parser.ExpressionParser; -using Parser.ExpressionParser.Functions.Base; using Parser.FlowParser.ActionExecutors; namespace Parser.FlowParser @@ -15,8 +15,8 @@ namespace Parser.FlowParser public interface IFlowRunner { void InitializeFlowRunner(in string path); - Task Trigger(); - Task Trigger(ValueContainer triggerOutput); + Task Trigger(); + Task Trigger(ValueContainer triggerOutput); } public class FlowRunner : IFlowRunner @@ -26,6 +26,8 @@ public class FlowRunner : IFlowRunner private readonly IScopeDepthManager _scopeManager; private readonly IActionExecutorFactory _actionExecutorFactory; private readonly ILogger _logger; + private readonly Dictionary _actionSates; + private int _actionsExecuted; private JProperty _trigger; public FlowRunner( @@ -41,6 +43,8 @@ public FlowRunner( _actionExecutorFactory = actionExecutorFactory ?? throw new ArgumentNullException(nameof(actionExecutorFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _actionSates = new Dictionary(); + _actionsExecuted = 0; } public void InitializeFlowRunner(in string path) @@ -54,7 +58,7 @@ public void InitializeFlowRunner(in string path) _scopeManager.Push("root", flowDefinition.SelectToken("$.actions").OfType(), null); } - public async Task Trigger() + public async Task Trigger() { var trigger = GetActionExecutor(_trigger); @@ -67,13 +71,25 @@ public async Task Trigger() } await RunFlow(); + + return new FlowResult + { + ActionStates = _actionSates, + NumberOfExecutedActions = _actionsExecuted + }; } - public async Task Trigger(ValueContainer triggerOutput) + public async Task Trigger(ValueContainer triggerOutput) { _state.AddTriggerOutputs(triggerOutput); await RunFlow(); + + return new FlowResult + { + ActionStates = _actionSates, + NumberOfExecutedActions = _actionsExecuted + }; } private async Task RunFlow() @@ -95,6 +111,18 @@ private async Task RunFlow() var actionExecutor = GetActionExecutor(currentAd); var actionResult = await ExecuteAction(actionExecutor, currentAd); + + if (_flowRunnerSettings.LogActionsStates) + { + _actionSates[currentAd.Name] = new ActionState + { + ActionInput = actionExecutor?.Inputs, + ActionOutput = actionResult, + ActionOrder = _actionsExecuted++, + ActionName = actionExecutor?.ActionName + }; + } + if (!(actionResult?.ContinueExecution ?? true)) { break; diff --git a/PowerAutomateMockUp/FlowParser/FlowSettings.cs b/PowerAutomateMockUp/FlowParser/FlowSettings.cs index 63aaf5d..73c4d69 100644 --- a/PowerAutomateMockUp/FlowParser/FlowSettings.cs +++ b/PowerAutomateMockUp/FlowParser/FlowSettings.cs @@ -7,5 +7,7 @@ public class FlowSettings public bool FailOnUnknownAction { get; set; } = true; public List IgnoreActions { get; set; } = new List(); + + public bool LogActionsStates { get; set; } = true; } } \ No newline at end of file diff --git a/Test/FullFlowTestV2.cs b/Test/FullFlowTestV2.cs new file mode 100644 index 0000000..ee04a16 --- /dev/null +++ b/Test/FullFlowTestV2.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Parser; +using Parser.ExpressionParser; +using Parser.FlowParser; +using Parser.FlowParser.ActionExecutors; + +namespace Test +{ + [TestFixture] + public class FullFlowTestV2 + { + private static readonly string TestFlowPath = System.IO.Path.GetFullPath(@"FlowSamples"); + + [Test] + public async Task TestFlowFalse() + { + var path = @$"{TestFlowPath}\PowerAutomateMockUpSampleFlow.json"; + + var services = new ServiceCollection(); + services.AddFlowRunner(); + + services.AddFlowActionByName(UpdateAccountInvalidId.FlowActionName); + services.AddFlowActionByApiIdAndOperationsName(SendEmailNotification.ApiId, + SendEmailNotification.SupportedOperations); + services.AddFlowActionByName(GetRecordValidId.FlowActionName); + services.AddFlowActionByName(UpdateAccountValidId.FlowActionName); + services.AddFlowActionByName(SendOutWarning.FlowActionName); + + + var sp = services.BuildServiceProvider(); + var flowRunner = sp.GetRequiredService(); + + flowRunner.InitializeFlowRunner(path); + + var flowResult = await flowRunner.Trigger(new ValueContainer( + new Dictionary + { + {"body/name", new ValueContainer("Alice Bob")}, + {"body/accountid", new ValueContainer(Guid.NewGuid().ToString())} + })); + + Assert.AreEqual(7, flowResult.NumberOfExecutedActions); + + const string actionName = "Send_me_an_email_notification"; + Assert.IsTrue(flowResult.ActionStates.ContainsKey(actionName), "Action is expected to be triggered."); + Assert.NotNull(flowResult.ActionStates[actionName].ActionParameters, "Action input is expected."); + var actionInput = flowResult.ActionStates[actionName].ActionParameters.AsDict(); + Assert.IsTrue(actionInput.ContainsKey("NotificationEmailDefinition"), "Action input should contain this object."); + var notification = actionInput["NotificationEmailDefinition"].AsDict(); + Assert.AreEqual("A new Account have been added", notification["notificationSubject"].GetValue(), "Asserting the input"); + + Assert.IsFalse(flowResult.ActionStates.ContainsKey(SendOutWarning.FlowActionName), "Action is not expected to be triggered."); + } + + private class UpdateAccountInvalidId : OpenApiConnectionActionExecutorBase + { + public const string FlowActionName = "Update_Account_-_Invalid_Id"; + + public UpdateAccountInvalidId(IExpressionEngine expressionEngine) : base(expressionEngine) + { + } + + public override Task Execute() + { + return Task.FromResult(new ActionResult + { + ActionStatus = ActionStatus.Failed, + ActionOutput = new ValueContainer(true) + }); + } + } + + private class SendEmailNotification : OpenApiConnectionActionExecutorBase + { + public const string ApiId = "/providers/Microsoft.PowerApps/apis/shared_flowpush"; + public static readonly string[] SupportedOperations = {"SendEmailNotification"}; + + public SendEmailNotification(IExpressionEngine expressionEngine) : base(expressionEngine) + { + } + + public override Task Execute() + { + Console.WriteLine($"Email Title: {Parameters["NotificationEmailDefinition/notificationSubject"]}"); + Console.WriteLine($"Email Content: {Parameters["NotificationEmailDefinition/notificationBody"]}"); + + return Task.FromResult(new ActionResult {ActionOutput = new ValueContainer(true)}); + } + } + + private class GetRecordValidId : OpenApiConnectionActionExecutorBase + { + public const string FlowActionName = "Get_a_record_-_Valid_Id"; + + public GetRecordValidId(IExpressionEngine expressionEngine) : base(expressionEngine) + { + } + + public override Task Execute() + { + return Task.FromResult(new ActionResult {ActionOutput = new ValueContainer(true)}); + } + } + + private class UpdateAccountValidId : OpenApiConnectionActionExecutorBase + { + public const string FlowActionName = "Update_Account_-_Valid_Id"; + + public override Task Execute() + { + return Task.FromResult(new ActionResult()); + } + + public UpdateAccountValidId(IExpressionEngine expressionEngine) : base(expressionEngine) + { + } + } + + private class SendOutWarning : OpenApiConnectionActionExecutorBase + { + public const string FlowActionName = "Send_an_error_message_to_owner"; + + public SendOutWarning(IExpressionEngine expressionEngine) : base(expressionEngine) + { + } + + public override Task Execute() + { + return Task.FromResult(new ActionResult()); + } + } + } +} \ No newline at end of file