From c8fdca0103b14c05ad8e584af2eb6d25be5fed26 Mon Sep 17 00:00:00 2001 From: Nathan Willoughby Date: Tue, 5 Dec 2023 15:56:10 +1000 Subject: [PATCH] . --- .../Scripts/ScriptOrchestratorFactory.cs | 10 ++- ...ionCanBeCancelledWhenRetriesAreDisabled.cs | 25 +++++-- ...tionCanBeCancelledWhenRetriesAreEnabled.cs | 25 +++++-- ...ntractAssertionBuilderExtensionMethods.cs} | 71 +++++++++++++------ 4 files changed, 95 insertions(+), 36 deletions(-) rename source/Octopus.Tentacle.Tests.Integration/{ExceptionContractFixture.cs => Support/ExceptionContractAssertionBuilderExtensionMethods.cs} (73%) diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptOrchestratorFactory.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptOrchestratorFactory.cs index be24ba291..994dc372a 100644 --- a/source/Octopus.Tentacle.Client/Scripts/ScriptOrchestratorFactory.cs +++ b/source/Octopus.Tentacle.Client/Scripts/ScriptOrchestratorFactory.cs @@ -58,7 +58,15 @@ public ScriptOrchestratorFactory( public async Task CreateOrchestrator(CancellationToken cancellationToken) { - var scriptServiceToUse = await DetermineScriptServiceVersionToUse(cancellationToken); + ScriptServiceVersion scriptServiceToUse; + try + { + scriptServiceToUse = await DetermineScriptServiceVersionToUse(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Script execution was cancelled"); + } return scriptServiceToUse switch { diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs index 42a1a7231..c0fbc97ad 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreDisabled.cs @@ -85,8 +85,10 @@ public async Task DuringGetCapabilities_ScriptExecutionCanBeCancelled(TentacleCo var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.GetCapabilities).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = capabilitiesMethodUsages.For(nameof(IAsyncClientCapabilitiesServiceV2.GetCapabilitiesAsync)).LastException; @@ -185,8 +187,10 @@ public async Task DuringStartScript_ScriptExecutionCanBeCancelled(TentacleConfig var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.StartScript).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).LastException; @@ -303,8 +307,10 @@ public async Task DuringGetStatus_ScriptExecutionCanBeCancelled(TentacleConfigur var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.GetStatus).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.GetStatusAsync)).LastException; @@ -384,9 +390,14 @@ public async Task DuringCompleteScript_ScriptExecutionCanBeCancelled(TentacleCon .Build(); // ACT - var (responseAndLogs, _, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(Int32.MaxValue, Int32.MaxValue)); + var (responseAndLogs, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(int.MaxValue, Int32.MaxValue)); // ASSERT + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.CompleteScript).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); + // Halibut Errors were recorded on CompleteScript recordedUsages.For(nameof(IAsyncClientScriptServiceV2.CompleteScriptAsync)).LastException?.Should().Match(x => x is HalibutClientException || x is OperationCanceledException || x is TaskCanceledException); // Complete Script was cancelled quickly cancellationDuration.Should().BeLessOrEqualTo(TimeSpan.FromSeconds(30)); diff --git a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs index 93a5b3775..60082872c 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs +++ b/source/Octopus.Tentacle.Tests.Integration/ClientScriptExecutionCanBeCancelledWhenRetriesAreEnabled.cs @@ -98,8 +98,10 @@ public async Task DuringGetCapabilities_ScriptExecutionCanBeCancelled(TentacleCo var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.GetCapabilities).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = capabilitiesMethodUsages.For(nameof(IAsyncClientCapabilitiesServiceV2.GetCapabilitiesAsync)).LastException; @@ -235,8 +237,10 @@ public async Task DuringStartScript_ScriptExecutionCanBeCancelled(TentacleConfig var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.StartScript).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.StartScriptAsync)).LastException; @@ -390,8 +394,10 @@ public async Task DuringGetStatus_ScriptExecutionCanBeCancelled(TentacleConfigur var (_, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, ensureCancellationOccursDuringAnRpcCall); // ASSERT - // The ExecuteScript operation threw an OperationCancelledException - actualException.Should().BeTaskOrOperationCancelledException(); + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.GetStatus).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); // If the rpc call could be cancelled then the correct error was recorded var latestException = recordedUsages.For(nameof(IAsyncClientScriptServiceV2.GetStatusAsync)).LastException; @@ -483,9 +489,14 @@ public async Task DuringCompleteScript_ScriptExecutionCanBeCancelled(TentacleCon .Build(); // ACT - var (responseAndLogs, _, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(int.MaxValue, int.MaxValue)); + var (responseAndLogs, actualException, cancellationDuration) = await ExecuteScriptThenCancelExecutionWhenRpcCallHasStarted(clientAndTentacle, startScriptCommand, rpcCallHasStarted, new SemaphoreSlim(int.MaxValue, int.MaxValue)); // ASSERT + var expectedException = new ExceptionContractAssertionBuilder(FailureScenario.ScriptExecutionCancelled, tentacleConfigurationTestCase, clientAndTentacle) + .ForScriptService(ScriptServiceOperation.CompleteScript).Build(); + + actualException.ShouldMatchExceptionContract(expectedException); + // Halibut Errors were recorded on CompleteScript recordedUsages.For(nameof(IAsyncClientScriptServiceV2.CompleteScriptAsync)).LastException?.Should().Match(x => x is HalibutClientException || x is OperationCanceledException || x is TaskCanceledException); diff --git a/source/Octopus.Tentacle.Tests.Integration/ExceptionContractFixture.cs b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilderExtensionMethods.cs similarity index 73% rename from source/Octopus.Tentacle.Tests.Integration/ExceptionContractFixture.cs rename to source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilderExtensionMethods.cs index 01a4ba01a..6a956b231 100644 --- a/source/Octopus.Tentacle.Tests.Integration/ExceptionContractFixture.cs +++ b/source/Octopus.Tentacle.Tests.Integration/Support/ExceptionContractAssertionBuilderExtensionMethods.cs @@ -1,43 +1,51 @@ using System; +using System.Linq; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Specialized; using Halibut; -using Octopus.Tentacle.Tests.Integration.Support; -namespace Octopus.Tentacle.Tests.Integration +namespace Octopus.Tentacle.Tests.Integration.Support { public static class ExceptionContractAssertionBuilderExtensionMethods { public static async Task> ThrowExceptionContractAsync( - this NonGenericAsyncFunctionAssertions should, - ExceptionContract expected, + this NonGenericAsyncFunctionAssertions should, + ExceptionContract expected, string because = "", params object[] becauseArgs) { var exceptionAssertions = await should.ThrowAsync(); var exception = exceptionAssertions.And; - exception.Should().BeOfType(expected.ExceptionType, because, becauseArgs); - exception.Message.Should().ContainAny(expected.ExceptionMessageShouldContainAny, because, becauseArgs); + exception.ShouldMatchExceptionContract(expected, because, becauseArgs); return exceptionAssertions; } public static async Task> ThrowExceptionContractAsync( - this GenericAsyncFunctionAssertions should, - ExceptionContract expected, + this GenericAsyncFunctionAssertions should, + ExceptionContract expected, string because = "", params object[] becauseArgs) { var exceptionAssertions = await should.ThrowAsync(); var exception = exceptionAssertions.And; - exception.Should().BeOfType(expected.ExceptionType, because, becauseArgs); - exception.Message.Should().ContainAny(expected.ExceptionMessageShouldContainAny, because, becauseArgs); + exception.ShouldMatchExceptionContract(expected, because, becauseArgs); return exceptionAssertions; } + + public static void ShouldMatchExceptionContract( + this Exception exception, + ExceptionContract expected, + string because = "", + params object[] becauseArgs) + { + exception.Should().Match(x => expected.ExceptionTypes.Contains(x.GetType()), because, becauseArgs); + exception.Message.Should().ContainAny(expected.ExceptionMessageShouldContainAny, because, becauseArgs); + } } public class ExceptionContractAssertionBuilder @@ -96,10 +104,10 @@ public ExceptionContractAssertionBuilder ForScriptService(ScriptServiceOperation return this; } - + public ExceptionContractAssertionBuilder ForFileTransferService(FileTransferServiceOperation fileTransferSercviceOperation) { - this.fileTransferServiceOperation = fileTransferSercviceOperation; + fileTransferServiceOperation = fileTransferSercviceOperation; return this; } @@ -120,7 +128,7 @@ public ExceptionContract Build() { throw new InvalidOperationException("Script Service Version not specified in the TentacleConfigurationTestCase"); } - + if (fileTransferServiceOperation != null) { if (failureScenario == FailureScenario.ConnectionFaulted) @@ -133,6 +141,7 @@ public ExceptionContract Build() $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Attempted to read past the end of the stream.", $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to write data to the transport connection: An established connection was aborted by the software in your host machine", $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to read data from the transport connection: An established connection was aborted by the software in your host machine", + $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', after the request began: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host", $"An error occurred when sending a request to '{clientAndTentacle.ServiceEndPoint}/', before the request could begin: Connection refused" }); case TentacleType.Polling: @@ -142,16 +151,29 @@ public ExceptionContract Build() "Unable to write data to the transport connection: An established connection was aborted by the software in your host machine", "Unable to read data from the transport connection: An established connection was aborted by the software in your host machine", "Connection refused", - "Unable to write data to the transport connection: Broken pipe" + "Unable to write data to the transport connection: Broken pipe", + "Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host" }); default: throw new ArgumentOutOfRangeException(); } } } - else + else if (scriptServiceOperation != null) { - + if (failureScenario == FailureScenario.ScriptExecutionCancelled) + { + return new ExceptionContract( + new[]{ + typeof(OperationCanceledException), + typeof(TaskCanceledException) + }, + new[] + { + "Script execution was cancelled", + "A task was canceled." // Cancellation during StartScript while connecting throws the wrong error + }); + } } throw new NotImplementedException(); @@ -160,12 +182,18 @@ public ExceptionContract Build() public class ExceptionContract { - public Type ExceptionType { get; } + public Type[] ExceptionTypes { get; } public string[] ExceptionMessageShouldContainAny { get; } - public ExceptionContract(Type exceptionType, string[] exceptionMessageShouldContainAny) + public ExceptionContract(Type exceptionTypes, string[] exceptionMessageShouldContainAny) + { + ExceptionTypes = new[] { exceptionTypes }; + ExceptionMessageShouldContainAny = exceptionMessageShouldContainAny; + } + + public ExceptionContract(Type[] exceptionTypes, string[] exceptionMessageShouldContainAny) { - ExceptionType = exceptionType; + ExceptionTypes = exceptionTypes; ExceptionMessageShouldContainAny = exceptionMessageShouldContainAny; } } @@ -186,7 +214,8 @@ public enum FileTransferServiceOperation } public enum FailureScenario - { - ConnectionFaulted + { + ConnectionFaulted, + ScriptExecutionCancelled } }