From bb02e16a2e31f64b1fb350729ab4f697440ebefd Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:57:30 +0100 Subject: [PATCH 1/2] Refactor `ProcessExecutorInfo` and builders to simplify argument handling, add support for `ThrowOnError` and `ConfigureStartInfo`, and enhance error handling with `ProcessExecutionFailedException`. --- .../Execution/IProcessExecutorBuilder.cs | 11 ++++- .../ProcessExecutionFailedException.cs | 9 ++++ .../Execution/ProcessExecutor.cs | 8 ++++ .../Execution/ProcessExecutorBase.cs | 33 ++++++++++++++ .../Execution/ProcessExecutorBuilder.cs | 45 ++++++++++++++++--- .../Execution/ProcessExecutorBuilderBase.cs | 8 +++- .../Execution/ProcessExecutorInfo.cs | 13 ++++-- .../Execution/ProcessExecutorGenericTests.cs | 24 +++++----- .../Execution/ProcessExecutorTests.cs | 36 +++++++-------- 9 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutionFailedException.cs diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/IProcessExecutorBuilder.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/IProcessExecutorBuilder.cs index cd9f1014..e7a0e0e2 100644 --- a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/IProcessExecutorBuilder.cs +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/IProcessExecutorBuilder.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System.Diagnostics; +using JetBrains.Annotations; namespace CreativeCoders.ProcessUtils.Execution; @@ -14,6 +15,10 @@ public interface IProcessExecutorBuilder IProcessExecutorBuilder SetOutputParser(Action? configure = null) where TParser : IProcessOutputParser, new(); + IProcessExecutorBuilder SetupStartInfo(Action configure); + + IProcessExecutorBuilder ShouldThrowOnError(bool throwOnError = true); + IProcessExecutor Build(); } @@ -24,5 +29,9 @@ public interface IProcessExecutorBuilder IProcessExecutorBuilder SetArguments(string[] arguments); + IProcessExecutorBuilder SetupStartInfo(Action configure); + + IProcessExecutorBuilder ShouldThrowOnError(bool throwOnError = true); + IProcessExecutor Build(); } diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutionFailedException.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutionFailedException.cs new file mode 100644 index 00000000..9984d774 --- /dev/null +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutionFailedException.cs @@ -0,0 +1,9 @@ +namespace CreativeCoders.ProcessUtils.Execution; + +public class ProcessExecutionFailedException(int exitCode, string? errorOutput, string? message = null) + : Exception(message ?? $"Process execution failed with exit code {exitCode}. Error output: {errorOutput}") +{ + public int ExitCode { get; } = exitCode; + + public string ErrorOutput { get; } = errorOutput ?? string.Empty; +} diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutor.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutor.cs index 8a7e575d..b3206299 100644 --- a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutor.cs +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutor.cs @@ -50,6 +50,8 @@ private IProcess ExecuteEx(string[]? args, IDictionary? placeho process.WaitForExit(); + CheckThrowOnError(process, true); + return process; } @@ -68,6 +70,8 @@ private async Task ExecuteExAsync(string[]? args, IDictionary(ProcessExecutorInfo processExecutorInfo, IPro process.WaitForExit(); + CheckThrowOnError(process, true); + return new ProcessExecutionResult(process, _outputParser.ParseOutput(output)); } @@ -189,6 +195,8 @@ public class ProcessExecutor(ProcessExecutorInfo processExecutorInfo, IPro await process.WaitForExitAsync().ConfigureAwait(false); + await CheckThrowOnErrorAsync(process, true).ConfigureAwait(false); + return new ProcessExecutionResult(process, _outputParser.ParseOutput(output)); } } diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs index 50e26cc2..8ca68d6f 100644 --- a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs @@ -26,6 +26,8 @@ private ProcessStartInfo CreateProcessStartInfo(string[]? args, CreateNoWindow = true }; + _processExecutorInfo.ConfigureStartInfo?.Invoke(startupInfo); + return startupInfo; } @@ -53,4 +55,35 @@ protected IProcess StartProcess(string[]? args = null, return process ?? throw new InvalidOperationException("Failed to start process."); } + + protected void CheckThrowOnError(IProcess process, bool disposeProcessOnThrow) + { + if (!_processExecutorInfo.ThrowOnError || process.ExitCode == 0) + { + return; + } + + if (disposeProcessOnThrow) + { + process.Dispose(); + } + + throw new ProcessExecutionFailedException(process.ExitCode, process.StandardError.ReadToEnd()); + } + + protected async Task CheckThrowOnErrorAsync(IProcess process, bool disposeProcessOnThrow) + { + if (!_processExecutorInfo.ThrowOnError || process.ExitCode == 0) + { + return; + } + + if (disposeProcessOnThrow) + { + process.Dispose(); + } + + throw new ProcessExecutionFailedException(process.ExitCode, + await process.StandardError.ReadToEndAsync().ConfigureAwait(false)); + } } diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilder.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilder.cs index f5bd9fc3..df5eae4c 100644 --- a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilder.cs +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilder.cs @@ -1,4 +1,5 @@ -using CreativeCoders.Core; +using System.Diagnostics; +using CreativeCoders.Core; using CreativeCoders.ProcessUtils.Execution.Parsers; namespace CreativeCoders.ProcessUtils.Execution; @@ -22,6 +23,20 @@ public IProcessExecutorBuilder SetArguments(string[] arguments) return this; } + public IProcessExecutorBuilder SetupStartInfo(Action configure) + { + ConfigureStartInfo = configure; + + return this; + } + + public IProcessExecutorBuilder ShouldThrowOnError(bool throwOnError = true) + { + ThrowOnError = throwOnError; + + return this; + } + public IProcessExecutor Build() { if (string.IsNullOrWhiteSpace(FileName)) @@ -31,8 +46,11 @@ public IProcessExecutor Build() var executorInfo = new ProcessExecutorInfo( FileName, - Arguments ?? [], - UsePlaceholderVars); + Arguments ?? []) + { + ConfigureStartInfo = ConfigureStartInfo, + ThrowOnError = ThrowOnError + }; return new ProcessExecutor(executorInfo, _processFactory); } @@ -78,6 +96,20 @@ public IProcessExecutorBuilder SetOutputParser(Action? conf return this; } + public IProcessExecutorBuilder SetupStartInfo(Action configure) + { + ConfigureStartInfo = configure; + + return this; + } + + public IProcessExecutorBuilder ShouldThrowOnError(bool throwOnError = true) + { + ThrowOnError = throwOnError; + + return this; + } + private void ReturnOutputAsText() { if (typeof(T) != typeof(string)) @@ -108,9 +140,12 @@ public IProcessExecutor Build() var executorInfo = new ProcessExecutorInfo( FileName, Arguments ?? [], - UsePlaceholderVars, _outputParser ?? - throw new InvalidOperationException("OutputParser must be set before building the executor.")); + throw new InvalidOperationException("OutputParser must be set before building the executor.")) + { + ConfigureStartInfo = ConfigureStartInfo, + ThrowOnError = ThrowOnError + }; return new ProcessExecutor(executorInfo, _processFactory); } diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilderBase.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilderBase.cs index e00cc3a6..ef64aa54 100644 --- a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilderBase.cs +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBuilderBase.cs @@ -1,4 +1,6 @@ -namespace CreativeCoders.ProcessUtils.Execution; +using System.Diagnostics; + +namespace CreativeCoders.ProcessUtils.Execution; public abstract class ProcessExecutorBuilderBase { @@ -6,5 +8,7 @@ public abstract class ProcessExecutorBuilderBase protected string[]? Arguments { get; set; } - protected bool UsePlaceholderVars { get; set; } + protected bool ThrowOnError { get; set; } + + protected Action? ConfigureStartInfo { get; set; } } diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorInfo.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorInfo.cs index 16bde167..00b9d5a4 100644 --- a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorInfo.cs +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorInfo.cs @@ -1,21 +1,26 @@ -namespace CreativeCoders.ProcessUtils.Execution; +using System.Diagnostics; + +namespace CreativeCoders.ProcessUtils.Execution; public class ProcessExecutorInfo( string fileName, string[] arguments, - bool usePlaceholderVars, IProcessOutputParser outputParser) - : ProcessExecutorInfo(fileName, arguments, usePlaceholderVars) + : ProcessExecutorInfo(fileName, arguments) { public IProcessOutputParser OutputParser { get; set; } = outputParser; } -public class ProcessExecutorInfo(string fileName, string[] arguments, bool usePlaceholderVars) +public class ProcessExecutorInfo(string fileName, string[] arguments) { public string FileName { get; } = fileName; public string[] Arguments { get; } = arguments; + public Action? ConfigureStartInfo { get; set; } + + public bool ThrowOnError { get; set; } + public bool RedirectStandardOutput { get; set; } = true; public bool RedirectStandardError { get; set; } = true; diff --git a/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorGenericTests.cs b/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorGenericTests.cs index a54dc986..80fb41e5 100644 --- a/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorGenericTests.cs +++ b/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorGenericTests.cs @@ -29,7 +29,7 @@ public void Execute_ReadsOutput_AndParsesResult() A.CallTo(() => parser.ParseOutput("42\n")) .Returns(42); - var info = new ProcessExecutorInfo(fileName, args, false, parser); + var info = new ProcessExecutorInfo(fileName, args, parser); // Fake process with output var fakeProcess = A.Fake(); @@ -69,7 +69,7 @@ public void ExecuteEx_ReadsOutput_AndParsesResult() A.CallTo(() => parser.ParseOutput("42\n")) .Returns(42); - var info = new ProcessExecutorInfo(fileName, args, false, parser); + var info = new ProcessExecutorInfo(fileName, args, parser); // Fake process with output var fakeProcess = A.Fake(); @@ -115,7 +115,7 @@ public async Task ExecuteAsync_ReadsOutputAsync_AndParsesResult() A.CallTo(() => parser.ParseOutput("hello world")) .Returns("hello world"); - var info = new ProcessExecutorInfo(fileName, args, false, parser); + var info = new ProcessExecutorInfo(fileName, args, parser); var fakeProcess = A.Fake(); @@ -157,7 +157,7 @@ public async Task ExecuteExAsync_ReadsOutput_AndParsesResult() A.CallTo(() => parser.ParseOutput("42\n")) .Returns(42); - var info = new ProcessExecutorInfo(fileName, args, false, parser); + var info = new ProcessExecutorInfo(fileName, args, parser); // Fake process with output var fakeProcess = A.Fake(); @@ -204,7 +204,7 @@ public void Execute_WithArgs_ReadsOutput_ParsesResult_AndUsesProvidedArgs() A.CallTo(() => parser.ParseOutput("13")) .Returns(13); - var info = new ProcessExecutorInfo(fileName, defaultArgs, false, parser); + var info = new ProcessExecutorInfo(fileName, defaultArgs, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("13"u8.ToArray()); @@ -248,7 +248,7 @@ public void Execute_WithPlaceholderVars_ReadsOutput_ParsesResult_AndReplacesPlac A.CallTo(() => parser.ParseOutput("11")) .Returns(11); - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false, parser); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("11"u8.ToArray()); @@ -293,7 +293,7 @@ public void ExecuteEx_WithArgs_ReadsOutput_ParsesResult_UsesProvidedArgs_AndRetu A.CallTo(() => parser.ParseOutput("ok")) .Returns("ok"); - var info = new ProcessExecutorInfo(fileName, defaultArgs, false, parser); + var info = new ProcessExecutorInfo(fileName, defaultArgs, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("ok"u8.ToArray()); @@ -344,7 +344,7 @@ public void A.CallTo(() => parser.ParseOutput("done")) .Returns("done"); - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false, parser); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("done"u8.ToArray()); @@ -395,7 +395,7 @@ public async Task ExecuteAsync_WithArgs_ReadsOutput_ParsesResult_AndUsesProvided A.CallTo(() => parser.ParseOutput("res")) .Returns("res"); - var info = new ProcessExecutorInfo(fileName, defaultArgs, false, parser); + var info = new ProcessExecutorInfo(fileName, defaultArgs, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("res"u8.ToArray()); @@ -441,7 +441,7 @@ public async Task ExecuteAsync_WithPlaceholderVars_ReadsOutput_ParsesResult_AndR A.CallTo(() => parser.ParseOutput("ok")) .Returns("ok"); - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false, parser); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("ok"u8.ToArray()); @@ -489,7 +489,7 @@ public async Task ExecuteExAsync_WithArgs_ReadsOutput_ParsesResult_UsesProvidedA A.CallTo(() => parser.ParseOutput("value")) .Returns("value"); - var info = new ProcessExecutorInfo(fileName, defaultArgs, false, parser); + var info = new ProcessExecutorInfo(fileName, defaultArgs, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("value"u8.ToArray()); @@ -540,7 +540,7 @@ public async Task A.CallTo(() => parser.ParseOutput("rdy")) .Returns("rdy"); - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false, parser); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, parser); var fakeProcess = A.Fake(); var outputStream = new MemoryStream("rdy"u8.ToArray()); diff --git a/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorTests.cs b/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorTests.cs index 0d0a95b4..c0ebee57 100644 --- a/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorTests.cs +++ b/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorTests.cs @@ -23,7 +23,7 @@ public void Execute_StartsProcess_WithConfiguredStartInfo_AndWaitsForExit() const string fileName = "my-tool"; var args = new[] { "arg1", "arg2" }; - var info = new ProcessExecutorInfo(fileName, args, false); + var info = new ProcessExecutorInfo(fileName, args); var fakeProcess = A.Fake(); @@ -84,7 +84,7 @@ public void Execute_WithArgs_StartsProcess_UsesProvidedArgs_AndWaitsForExit() var defaultArgs = new[] { "x", "y" }; var providedArgs = new[] { "argA", "argB", "argC" }; - var info = new ProcessExecutorInfo(fileName, defaultArgs, false); + var info = new ProcessExecutorInfo(fileName, defaultArgs); var fakeProcess = A.Fake(); @@ -123,7 +123,7 @@ public void Execute_WithPlaceholderVars_StartsProcess_ReplacesPlaceholders_AndWa const string fileName = "my-tool"; var argsWithPlaceholders = new[] { "run", "--value", "{{val}}" }; - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders); var fakeProcess = A.Fake(); @@ -162,7 +162,7 @@ public void ExecuteEx_StartsProcess_WithConfiguredStartInfo_AndWaitsForExit() const string fileName = "my-tool"; var args = new[] { "arg1", "arg2" }; - var info = new ProcessExecutorInfo(fileName, args, false); + var info = new ProcessExecutorInfo(fileName, args); var fakeProcess = A.Fake(); @@ -227,7 +227,7 @@ public void ExecuteEx_WithArgs_StartsProcess_UsesProvidedArgs_AndReturnsProcess( var defaultArgs = new[] { "d1" }; var providedArgs = new[] { "n1", "n2" }; - var info = new ProcessExecutorInfo(fileName, defaultArgs, false); + var info = new ProcessExecutorInfo(fileName, defaultArgs); var fakeProcess = A.Fake(); @@ -266,7 +266,7 @@ public void ExecuteEx_WithPlaceholderVars_StartsProcess_ReplacesPlaceholders_And const string fileName = "tool-ex"; var argsWithPlaceholders = new[] { "run", "{{name}}" }; - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders); var fakeProcess = A.Fake(); @@ -295,7 +295,7 @@ public async Task ExecuteAsync_StartsProcess_WithConfiguredStartInfo_AndWaitsFor var fileName = "my-tool-async"; var args = new[] { "a", "b" }; - var info = new ProcessExecutorInfo(fileName, args, false); + var info = new ProcessExecutorInfo(fileName, args); var fakeProcess = A.Fake(); @@ -359,7 +359,7 @@ public async Task ExecuteExAsync_StartsProcess_WithConfiguredStartInfo_AndWaitsF var fileName = "my-tool-async"; var args = new[] { "a", "b" }; - var info = new ProcessExecutorInfo(fileName, args, false); + var info = new ProcessExecutorInfo(fileName, args); var fakeProcess = A.Fake(); @@ -428,7 +428,7 @@ public async Task ExecuteAsync_WithArgs_StartsProcess_UsesProvidedArgs_AndWaitsF var defaultArgs = new[] { "d" }; var providedArgs = new[] { "x", "y" }; - var info = new ProcessExecutorInfo(fileName, defaultArgs, false); + var info = new ProcessExecutorInfo(fileName, defaultArgs); var fakeProcess = A.Fake(); @@ -462,7 +462,7 @@ public async Task var fileName = "my-tool-async"; var argsWithPlaceholders = new[] { "go", "--n", "{{num}}" }; - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders); var fakeProcess = A.Fake(); @@ -496,7 +496,7 @@ public async Task ExecuteExAsync_WithArgs_StartsProcess_UsesProvidedArgs_AndRetu var defaultArgs = new[] { "d" }; var providedArgs = new[] { "x", "y" }; - var info = new ProcessExecutorInfo(fileName, defaultArgs, false); + var info = new ProcessExecutorInfo(fileName, defaultArgs); var fakeProcess = A.Fake(); @@ -533,7 +533,7 @@ public async Task var fileName = "my-tool-async"; var argsWithPlaceholders = new[] { "go", "{{word}}" }; - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders); var fakeProcess = A.Fake(); @@ -571,7 +571,7 @@ public void ExecuteAndReturnExitCode_StartsProcess_WithConfiguredStartInfo_Retur var args = new[] { "arg1", "arg2" }; const int expectedExitCode = 1235; - var info = new ProcessExecutorInfo(fileName, args, false); + var info = new ProcessExecutorInfo(fileName, args); var fakeProcess = A.Fake(); @@ -604,7 +604,7 @@ public async Task ExecuteAndReturnExitCodeAsync_StartsProcess_WithConfiguredStar var args = new[] { "a", "b" }; const int expectedExitCode = 1235; - var info = new ProcessExecutorInfo(fileName, args, false); + var info = new ProcessExecutorInfo(fileName, args); var fakeProcess = A.Fake(); @@ -642,7 +642,7 @@ public void ExecuteAndReturnExitCode_WithArgs_UsesProvidedArgs_AndReturnsExitCod var providedArgs = new[] { "a1", "a2" }; const int expectedExitCode = 55; - var info = new ProcessExecutorInfo(fileName, defaultArgs, false); + var info = new ProcessExecutorInfo(fileName, defaultArgs); var fakeProcess = A.Fake(); A.CallTo(() => fakeProcess.ExitCode).Returns(expectedExitCode); @@ -673,7 +673,7 @@ public void ExecuteAndReturnExitCode_WithPlaceholderVars_ReplacesPlaceholders_An var argsWithPlaceholders = new[] { "do", "{{n}}" }; const int expectedExitCode = 77; - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders); var fakeProcess = A.Fake(); A.CallTo(() => fakeProcess.ExitCode).Returns(expectedExitCode); @@ -705,7 +705,7 @@ public async Task ExecuteAndReturnExitCodeAsync_WithArgs_UsesProvidedArgs_AndRet var providedArgs = new[] { "a1", "a2" }; const int expectedExitCode = 101; - var info = new ProcessExecutorInfo(fileName, defaultArgs, false); + var info = new ProcessExecutorInfo(fileName, defaultArgs); var fakeProcess = A.Fake(); A.CallTo(() => fakeProcess.ExitCode).Returns(expectedExitCode); @@ -740,7 +740,7 @@ public async Task var argsWithPlaceholders = new[] { "do", "{{n}}" }; const int expectedExitCode = 202; - var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders, false); + var info = new ProcessExecutorInfo(fileName, argsWithPlaceholders); var fakeProcess = A.Fake(); A.CallTo(() => fakeProcess.ExitCode).Returns(expectedExitCode); From 7af19fadc562f87f51ab37db82187f619f195877 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:27:05 +0100 Subject: [PATCH 2/2] Add unit tests for `ProcessExecutorBuilder` and enhance error handling in `ProcessExecutorBase` - Introduced new unit tests covering various scenarios such as synchronous/asynchronous execution, generic/non-generic executors, and start info configuration in `ProcessExecutorBuilder`. - Improved exception handling in `ProcessExecutorBase` by caching `ExitCode` and error output before disposing of the process. --- .../Execution/ProcessExecutorBase.cs | 11 +- .../Execution/ProcessExecutorBuilderTests.cs | 193 ++++++++++++++++++ 2 files changed, 201 insertions(+), 3 deletions(-) diff --git a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs index 8ca68d6f..176b8f76 100644 --- a/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs +++ b/source/ProcessUtils/CreativeCoders.ProcessUtils/Execution/ProcessExecutorBase.cs @@ -63,12 +63,15 @@ protected void CheckThrowOnError(IProcess process, bool disposeProcessOnThrow) return; } + var exitCode = process.ExitCode; + var standardErrorOutput = process.StandardError.ReadToEnd(); + if (disposeProcessOnThrow) { process.Dispose(); } - throw new ProcessExecutionFailedException(process.ExitCode, process.StandardError.ReadToEnd()); + throw new ProcessExecutionFailedException(exitCode, standardErrorOutput); } protected async Task CheckThrowOnErrorAsync(IProcess process, bool disposeProcessOnThrow) @@ -78,12 +81,14 @@ protected async Task CheckThrowOnErrorAsync(IProcess process, bool disposeProces return; } + var exitCode = process.ExitCode; + var standardErrorOutput = await process.StandardError.ReadToEndAsync().ConfigureAwait(false); + if (disposeProcessOnThrow) { process.Dispose(); } - throw new ProcessExecutionFailedException(process.ExitCode, - await process.StandardError.ReadToEndAsync().ConfigureAwait(false)); + throw new ProcessExecutionFailedException(exitCode, standardErrorOutput); } } diff --git a/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorBuilderTests.cs b/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorBuilderTests.cs index ac17a395..547d72d3 100644 --- a/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorBuilderTests.cs +++ b/tests/CreativeCoders.Core.UnitTests/ProcessUtils/Execution/ProcessExecutorBuilderTests.cs @@ -257,6 +257,199 @@ public void Build_WhenTIsStringAndNoOutputParser_DoesNotThrowAndReturnsStringExe .Be(processOutput); } + [Fact] + public void BuildAndUseExecutor_ShouldThrowOnErrorSet_ThrowsExitCodeNotZero() + { + // Arrange + var fakeProcessFactory = A.Fake(); + var fakeProcess = A.Fake(); + + A.CallTo(() => fakeProcess.ExitCode).Returns(1); + A.CallTo(() => fakeProcessFactory.StartProcess(A._)) + .Returns(fakeProcess); + + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("Process failed")); + var reader = new StreamReader(outputStream); + A.CallTo(() => fakeProcess.StandardError).Returns(reader); + + var executor = new ProcessExecutorBuilder(fakeProcessFactory) + .SetFileName("dummy") + .ShouldThrowOnError() + .Build(); + + // Act + var act = () => executor.Execute(); + + // Assert + act + .Should().Throw() + .Which + .Should().Match(x => + x.ExitCode == 1 && x.ErrorOutput == "Process failed"); + } + + [Fact] + public async Task BuildAndUseExecutorAsync_ShouldThrowOnErrorSet_ThrowsExitCodeNotZero() + { + // Arrange + var fakeProcessFactory = A.Fake(); + var fakeProcess = A.Fake(); + + A.CallTo(() => fakeProcess.ExitCode).Returns(1); + A.CallTo(() => fakeProcessFactory.StartProcess(A._)) + .Returns(fakeProcess); + + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("Process failed")); + var reader = new StreamReader(outputStream); + A.CallTo(() => fakeProcess.StandardError).Returns(reader); + + var executor = new ProcessExecutorBuilder(fakeProcessFactory) + .SetFileName("dummy") + .ShouldThrowOnError() + .Build(); + + // Act + var act = async () => await executor.ExecuteAsync(); + + // Assert + (await act + .Should().ThrowAsync()) + .Which + .Should().Match(x => + x.ExitCode == 1 && x.ErrorOutput == "Process failed"); + } + + [Fact] + public async Task BuildAndUseExecutorGenericAsync_ShouldThrowOnErrorSet_ThrowsExitCodeNotZero() + { + // Arrange + var fakeProcessFactory = A.Fake(); + var fakeProcess = A.Fake(); + + A.CallTo(() => fakeProcess.ExitCode).Returns(1); + A.CallTo(() => fakeProcessFactory.StartProcess(A._)) + .Returns(fakeProcess); + + var outputErrorStream = new MemoryStream(Encoding.UTF8.GetBytes("Process failed")); + var errorReader = new StreamReader(outputErrorStream); + A.CallTo(() => fakeProcess.StandardError).Returns(errorReader); + + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("Process failed")); + var reader = new StreamReader(outputStream); + A.CallTo(() => fakeProcess.StandardOutput).Returns(reader); + + var executor = new ProcessExecutorBuilder(fakeProcessFactory) + .SetFileName("dummy") + .ShouldThrowOnError() + .Build(); + + // Act + var act = async () => await executor.ExecuteAsync(); + + // Assert + (await act + .Should().ThrowAsync()) + .Which + .Should().Match(x => + x.ExitCode == 1 && x.ErrorOutput == "Process failed"); + } + + [Fact] + public void BuildAndUseExecutor_SetupStartInfo_StartInfoIsConfigured() + { + // Arrange + var fakeProcessFactory = A.Fake(); + var fakeProcess = A.Fake(); + + ProcessStartInfo startInfo = null; + A.CallTo(() => fakeProcess.ExitCode).Returns(1); + A.CallTo(() => fakeProcessFactory.StartProcess(A._)) + .Invokes((ProcessStartInfo si) => startInfo = si) + .Returns(fakeProcess); + + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("Process failed")); + var reader = new StreamReader(outputStream); + A.CallTo(() => fakeProcess.StandardError).Returns(reader); + + var executor = new ProcessExecutorBuilder(fakeProcessFactory) + .SetFileName("dummy") + .SetupStartInfo(x => + { + x.CreateNoWindow = false; + x.RedirectStandardError = false; + x.UseShellExecute = true; + }) + .Build(); + + // Act + executor.Execute(); + + // Assert + startInfo + .Should() + .NotBeNull(); + + startInfo.CreateNoWindow + .Should() + .BeFalse(); + + startInfo.RedirectStandardError + .Should() + .BeFalse(); + + startInfo.UseShellExecute + .Should() + .BeTrue(); + } + + [Fact] + public void BuildAndUseGenericExecutor_SetupStartInfo_StartInfoIsConfigured() + { + // Arrange + var fakeProcessFactory = A.Fake(); + var fakeProcess = A.Fake(); + + ProcessStartInfo startInfo = null; + A.CallTo(() => fakeProcess.ExitCode).Returns(1); + A.CallTo(() => fakeProcessFactory.StartProcess(A._)) + .Invokes((ProcessStartInfo si) => startInfo = si) + .Returns(fakeProcess); + + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("Process running")); + var reader = new StreamReader(outputStream); + A.CallTo(() => fakeProcess.StandardOutput).Returns(reader); + + var executor = new ProcessExecutorBuilder(fakeProcessFactory) + .SetFileName("dummy") + .SetupStartInfo(x => + { + x.CreateNoWindow = false; + x.RedirectStandardError = false; + x.UseShellExecute = true; + }) + .Build(); + + // Act + executor.Execute(); + + // Assert + startInfo + .Should() + .NotBeNull(); + + startInfo.CreateNoWindow + .Should() + .BeFalse(); + + startInfo.RedirectStandardError + .Should() + .BeFalse(); + + startInfo.UseShellExecute + .Should() + .BeTrue(); + } + /// /// Simple test parser for integers; parses text to int and multiplies by . ///