From 4eccdefd294d6373edb3ef55958ad0b1a69f64e5 Mon Sep 17 00:00:00 2001 From: Adam Ralph Date: Mon, 25 Dec 2023 11:49:52 +0000 Subject: [PATCH] minor refactoring --- .editorconfig | 1 + SimpleExec/Command.cs | 845 +++++++++--------- SimpleExec/ExitCodeException.cs | 55 +- SimpleExec/ExitCodeReadException.cs | 53 +- SimpleExec/ProcessExtensions.cs | 201 +++-- SimpleExec/ProcessStartInfo.cs | 61 +- SimpleExecTester/Program.cs | 71 +- SimpleExecTests/CancellingCommands.cs | 131 ++- SimpleExecTests/ConfiguringEnvironments.cs | 19 +- SimpleExecTests/EchoingCommands.cs | 155 ++-- SimpleExecTests/ExitCodes.cs | 105 ++- SimpleExecTests/Infra/Capture.cs | 15 +- SimpleExecTests/Infra/Tester.cs | 11 +- SimpleExecTests/Infra/WindowsFactAttribute.cs | 13 +- .../Infra/WindowsTheoryAttribute.cs | 13 +- SimpleExecTests/ReadingCommands.cs | 183 ++-- SimpleExecTests/RunningCommands.cs | 331 ++++--- 17 files changed, 1120 insertions(+), 1143 deletions(-) diff --git a/.editorconfig b/.editorconfig index e3f6821d..6c6bd4e5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,6 +17,7 @@ csharp_style_expression_bodied_local_functions = true csharp_style_expression_bodied_methods = true csharp_style_expression_bodied_operators = true csharp_style_expression_bodied_properties = true +csharp_style_namespace_declarations = file_scoped csharp_style_var_elsewhere = true csharp_style_var_for_built_in_types = true csharp_style_var_when_type_is_apparent = true diff --git a/SimpleExec/Command.cs b/SimpleExec/Command.cs index 0cc8b982..b0a35c0b 100644 --- a/SimpleExec/Command.cs +++ b/SimpleExec/Command.cs @@ -9,475 +9,470 @@ using System.Threading; using System.Threading.Tasks; -namespace SimpleExec +namespace SimpleExec; + +/// +/// Contains methods for running commands and reading standard output (stdout). +/// +public static class Command { + private static readonly Action> defaultAction = _ => { }; + private static readonly string defaultEchoPrefix = Assembly.GetEntryAssembly()?.GetName().Name ?? "SimpleExec"; + + /// + /// Runs a command without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). + /// By default, the command line is echoed to standard output (stdout). + /// + /// The name of the command. This can be a path to an executable file. + /// The arguments to pass to the command. + /// The working directory in which to run the command. + /// Whether or not to echo the resulting command line and working directory (if specified) to standard output (stdout). + /// The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout). + /// An action which configures environment variables for the command. + /// Whether to run the command in a new window. + /// + /// A delegate which accepts an representing exit code of the command and + /// returns when it has handled the exit code and default exit code handling should be suppressed, and + /// returns otherwise. + /// + /// + /// Whether to ignore the process tree when cancelling the command. + /// If set to true, when the command is cancelled, any child processes created by the command + /// are left running after the command is cancelled. + /// + /// A to observe while waiting for the command to exit. + /// The command exited with non-zero exit code. + /// + /// By default, the resulting command line and the working directory (if specified) are echoed to standard output (stdout). + /// To suppress this behavior, provide the parameter with a value of true. + /// + public static void Run( + string name, + string args = "", + string workingDirectory = "", + bool noEcho = false, + string? echoPrefix = null, + Action>? configureEnvironment = null, + bool createNoWindow = false, + Func? handleExitCode = null, + bool cancellationIgnoresProcessTree = false, + CancellationToken cancellationToken = default) => + ProcessStartInfo + .Create( + Resolve(Validate(name)), + args, + Enumerable.Empty(), + workingDirectory, + false, + configureEnvironment ?? defaultAction, + createNoWindow) + .Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken); + /// - /// Contains methods for running commands and reading standard output (stdout). + /// Runs a command without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). + /// By default, the command line is echoed to standard output (stdout). /// - public static class Command + /// The name of the command. This can be a path to an executable file. + /// + /// The arguments to pass to the command. + /// As with , the strings don't need to be escaped. + /// + /// The working directory in which to run the command. + /// Whether or not to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout). + /// The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout). + /// An action which configures environment variables for the command. + /// Whether to run the command in a new window. + /// + /// A delegate which accepts an representing exit code of the command and + /// returns when it has handled the exit code and default exit code handling should be suppressed, and + /// returns otherwise. + /// + /// + /// Whether to ignore the process tree when cancelling the command. + /// If set to true, when the command is cancelled, any child processes created by the command + /// are left running after the command is cancelled. + /// + /// A to observe while waiting for the command to exit. + /// The command exited with non-zero exit code. + public static void Run( + string name, + IEnumerable args, + string workingDirectory = "", + bool noEcho = false, + string? echoPrefix = null, + Action>? configureEnvironment = null, + bool createNoWindow = false, + Func? handleExitCode = null, + bool cancellationIgnoresProcessTree = false, + CancellationToken cancellationToken = default) => + ProcessStartInfo + .Create( + Resolve(Validate(name)), + "", + args ?? throw new ArgumentNullException(nameof(args)), + workingDirectory, + false, + configureEnvironment ?? defaultAction, + createNoWindow) + .Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken); + + private static void Run( + this System.Diagnostics.ProcessStartInfo startInfo, + bool noEcho, + string echoPrefix, + Func? handleExitCode, + bool cancellationIgnoresProcessTree, + CancellationToken cancellationToken) { - private static readonly Action> defaultAction = _ => { }; - private static readonly string defaultEchoPrefix = Assembly.GetEntryAssembly()?.GetName().Name ?? "SimpleExec"; - - /// - /// Runs a command without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). - /// By default, the command line is echoed to standard output (stdout). - /// - /// The name of the command. This can be a path to an executable file. - /// The arguments to pass to the command. - /// The working directory in which to run the command. - /// Whether or not to echo the resulting command line and working directory (if specified) to standard output (stdout). - /// The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout). - /// An action which configures environment variables for the command. - /// Whether to run the command in a new window. - /// - /// A delegate which accepts an representing exit code of the command and - /// returns when it has handled the exit code and default exit code handling should be suppressed, and - /// returns otherwise. - /// - /// - /// Whether to ignore the process tree when cancelling the command. - /// If set to true, when the command is cancelled, any child processes created by the command - /// are left running after the command is cancelled. - /// - /// A to observe while waiting for the command to exit. - /// The command exited with non-zero exit code. - /// - /// By default, the resulting command line and the working directory (if specified) are echoed to standard output (stdout). - /// To suppress this behavior, provide the parameter with a value of true. - /// - public static void Run( - string name, - string args = "", - string workingDirectory = "", - bool noEcho = false, - string? echoPrefix = null, - Action>? configureEnvironment = null, - bool createNoWindow = false, - Func? handleExitCode = null, - bool cancellationIgnoresProcessTree = false, - CancellationToken cancellationToken = default) => - ProcessStartInfo - .Create( - Resolve(Validate(name)), - args, - Enumerable.Empty(), - workingDirectory, - false, - configureEnvironment ?? defaultAction, - createNoWindow) - .Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken); - - /// - /// Runs a command without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). - /// By default, the command line is echoed to standard output (stdout). - /// - /// The name of the command. This can be a path to an executable file. - /// - /// The arguments to pass to the command. - /// As with , the strings don't need to be escaped. - /// - /// The working directory in which to run the command. - /// Whether or not to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout). - /// The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout). - /// An action which configures environment variables for the command. - /// Whether to run the command in a new window. - /// - /// A delegate which accepts an representing exit code of the command and - /// returns when it has handled the exit code and default exit code handling should be suppressed, and - /// returns otherwise. - /// - /// - /// Whether to ignore the process tree when cancelling the command. - /// If set to true, when the command is cancelled, any child processes created by the command - /// are left running after the command is cancelled. - /// - /// A to observe while waiting for the command to exit. - /// The command exited with non-zero exit code. - public static void Run( - string name, - IEnumerable args, - string workingDirectory = "", - bool noEcho = false, - string? echoPrefix = null, - Action>? configureEnvironment = null, - bool createNoWindow = false, - Func? handleExitCode = null, - bool cancellationIgnoresProcessTree = false, - CancellationToken cancellationToken = default) => - ProcessStartInfo - .Create( - Resolve(Validate(name)), - "", - args ?? throw new ArgumentNullException(nameof(args)), - workingDirectory, - false, - configureEnvironment ?? defaultAction, - createNoWindow) - .Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken); - - private static void Run( - this System.Diagnostics.ProcessStartInfo startInfo, - bool noEcho, - string echoPrefix, - Func? handleExitCode, - bool cancellationIgnoresProcessTree, - CancellationToken cancellationToken) - { - using var process = new Process(); - process.StartInfo = startInfo; + using var process = new Process(); + process.StartInfo = startInfo; - process.Run(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken); + process.Run(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken); - if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0) - { - throw new ExitCodeException(process.ExitCode); - } + if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0) + { + throw new ExitCodeException(process.ExitCode); } + } - /// - /// Runs a command asynchronously without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). - /// By default, the command line is echoed to standard output (stdout). - /// - /// The name of the command. This can be a path to an executable file. - /// The arguments to pass to the command. - /// The working directory in which to run the command. - /// Whether or not to echo the resulting command line and working directory (if specified) to standard output (stdout). - /// The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout). - /// An action which configures environment variables for the command. - /// Whether to run the command in a new window. - /// - /// A delegate which accepts an representing exit code of the command and - /// returns when it has handled the exit code and default exit code handling should be suppressed, and - /// returns otherwise. - /// - /// - /// Whether to ignore the process tree when cancelling the command. - /// If set to true, when the command is cancelled, any child processes created by the command - /// are left running after the command is cancelled. - /// - /// A to observe while waiting for the command to exit. - /// A that represents the asynchronous running of the command. - /// The command exited with non-zero exit code. - /// - /// By default, the resulting command line and the working directory (if specified) are echoed to standard output (stdout). - /// To suppress this behavior, provide the parameter with a value of true. - /// - public static async Task RunAsync( - string name, - string args = "", - string workingDirectory = "", - bool noEcho = false, - string? echoPrefix = null, - Action>? configureEnvironment = null, - bool createNoWindow = false, - Func? handleExitCode = null, - bool cancellationIgnoresProcessTree = false, - CancellationToken cancellationToken = default) => - await ProcessStartInfo - .Create( - Resolve(Validate(name)), - args, - Enumerable.Empty(), - workingDirectory, - false, - configureEnvironment ?? defaultAction, - createNoWindow) - .RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken) - .ConfigureAwait(false); - - /// - /// Runs a command asynchronously without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). - /// By default, the command line is echoed to standard output (stdout). - /// - /// The name of the command. This can be a path to an executable file. - /// - /// The arguments to pass to the command. - /// As with , the strings don't need to be escaped. - /// - /// The working directory in which to run the command. - /// Whether or not to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout). - /// The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout). - /// An action which configures environment variables for the command. - /// Whether to run the command in a new window. - /// - /// A delegate which accepts an representing exit code of the command and - /// returns when it has handled the exit code and default exit code handling should be suppressed, and - /// returns otherwise. - /// - /// - /// Whether to ignore the process tree when cancelling the command. - /// If set to true, when the command is cancelled, any child processes created by the command - /// are left running after the command is cancelled. - /// - /// A to observe while waiting for the command to exit. - /// A that represents the asynchronous running of the command. - /// The command exited with non-zero exit code. - public static async Task RunAsync( - string name, - IEnumerable args, - string workingDirectory = "", - bool noEcho = false, - string? echoPrefix = null, - Action>? configureEnvironment = null, - bool createNoWindow = false, - Func? handleExitCode = null, - bool cancellationIgnoresProcessTree = false, - CancellationToken cancellationToken = default) => - await ProcessStartInfo - .Create( - Resolve(Validate(name)), - "", - args ?? throw new ArgumentNullException(nameof(args)), - workingDirectory, - false, - configureEnvironment ?? defaultAction, - createNoWindow) - .RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken) - .ConfigureAwait(false); - - private static async Task RunAsync( - this System.Diagnostics.ProcessStartInfo startInfo, - bool noEcho, - string echoPrefix, - Func? handleExitCode, - bool cancellationIgnoresProcessTree, - CancellationToken cancellationToken) - { - using var process = new Process(); - process.StartInfo = startInfo; + /// + /// Runs a command asynchronously without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). + /// By default, the command line is echoed to standard output (stdout). + /// + /// The name of the command. This can be a path to an executable file. + /// The arguments to pass to the command. + /// The working directory in which to run the command. + /// Whether or not to echo the resulting command line and working directory (if specified) to standard output (stdout). + /// The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout). + /// An action which configures environment variables for the command. + /// Whether to run the command in a new window. + /// + /// A delegate which accepts an representing exit code of the command and + /// returns when it has handled the exit code and default exit code handling should be suppressed, and + /// returns otherwise. + /// + /// + /// Whether to ignore the process tree when cancelling the command. + /// If set to true, when the command is cancelled, any child processes created by the command + /// are left running after the command is cancelled. + /// + /// A to observe while waiting for the command to exit. + /// A that represents the asynchronous running of the command. + /// The command exited with non-zero exit code. + /// + /// By default, the resulting command line and the working directory (if specified) are echoed to standard output (stdout). + /// To suppress this behavior, provide the parameter with a value of true. + /// + public static Task RunAsync( + string name, + string args = "", + string workingDirectory = "", + bool noEcho = false, + string? echoPrefix = null, + Action>? configureEnvironment = null, + bool createNoWindow = false, + Func? handleExitCode = null, + bool cancellationIgnoresProcessTree = false, + CancellationToken cancellationToken = default) => + ProcessStartInfo + .Create( + Resolve(Validate(name)), + args, + Enumerable.Empty(), + workingDirectory, + false, + configureEnvironment ?? defaultAction, + createNoWindow) + .RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken); - await process.RunAsync(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken).ConfigureAwait(false); + /// + /// Runs a command asynchronously without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin). + /// By default, the command line is echoed to standard output (stdout). + /// + /// The name of the command. This can be a path to an executable file. + /// + /// The arguments to pass to the command. + /// As with , the strings don't need to be escaped. + /// + /// The working directory in which to run the command. + /// Whether or not to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout). + /// The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout). + /// An action which configures environment variables for the command. + /// Whether to run the command in a new window. + /// + /// A delegate which accepts an representing exit code of the command and + /// returns when it has handled the exit code and default exit code handling should be suppressed, and + /// returns otherwise. + /// + /// + /// Whether to ignore the process tree when cancelling the command. + /// If set to true, when the command is cancelled, any child processes created by the command + /// are left running after the command is cancelled. + /// + /// A to observe while waiting for the command to exit. + /// A that represents the asynchronous running of the command. + /// The command exited with non-zero exit code. + public static Task RunAsync( + string name, + IEnumerable args, + string workingDirectory = "", + bool noEcho = false, + string? echoPrefix = null, + Action>? configureEnvironment = null, + bool createNoWindow = false, + Func? handleExitCode = null, + bool cancellationIgnoresProcessTree = false, + CancellationToken cancellationToken = default) => + ProcessStartInfo + .Create( + Resolve(Validate(name)), + "", + args ?? throw new ArgumentNullException(nameof(args)), + workingDirectory, + false, + configureEnvironment ?? defaultAction, + createNoWindow) + .RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken); + + private static async Task RunAsync( + this System.Diagnostics.ProcessStartInfo startInfo, + bool noEcho, + string echoPrefix, + Func? handleExitCode, + bool cancellationIgnoresProcessTree, + CancellationToken cancellationToken) + { + using var process = new Process(); + process.StartInfo = startInfo; - if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0) - { - throw new ExitCodeException(process.ExitCode); - } - } + await process.RunAsync(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken).ConfigureAwait(false); - /// - /// Runs a command and reads standard output (stdout) and standard error (stderr) and optionally writes to standard input (stdin). - /// - /// The name of the command. This can be a path to an executable file. - /// The arguments to pass to the command. - /// The working directory in which to run the command. - /// An action which configures environment variables for the command. - /// The preferred for standard output (stdout) and standard output (stdout). - /// - /// A delegate which accepts an representing exit code of the command and - /// returns when it has handled the exit code and default exit code handling should be suppressed, and - /// returns otherwise. - /// - /// The contents of standard input (stdin). - /// - /// Whether to ignore the process tree when cancelling the command. - /// If set to true, when the command is cancelled, any child processes created by the command - /// are left running after the command is cancelled. - /// - /// A to observe while waiting for the command to exit. - /// - /// A representing the asynchronous running of the command and reading of standard output (stdout) and standard error (stderr). - /// The task result is a representing the contents of standard output (stdout) and standard error (stderr). - /// - /// - /// The command exited with non-zero exit code. The exception contains the contents of standard output (stdout) and standard error (stderr). - /// - public static async Task<(string StandardOutput, string StandardError)> ReadAsync( - string name, - string args = "", - string workingDirectory = "", - Action>? configureEnvironment = null, - Encoding? encoding = null, - Func? handleExitCode = null, - string? standardInput = null, - bool cancellationIgnoresProcessTree = false, - CancellationToken cancellationToken = default) => - await ProcessStartInfo - .Create( - Resolve(Validate(name)), - args, - Enumerable.Empty(), - workingDirectory, - true, - configureEnvironment ?? defaultAction, - true, - encoding) - .ReadAsync( - handleExitCode, - standardInput, - cancellationIgnoresProcessTree, - cancellationToken) - .ConfigureAwait(false); - - /// - /// Runs a command and reads standard output (stdout) and standard error (stderr) and optionally writes to standard input (stdin). - /// - /// The name of the command. This can be a path to an executable file. - /// - /// The arguments to pass to the command. - /// As with , the strings don't need to be escaped. - /// - /// The working directory in which to run the command. - /// An action which configures environment variables for the command. - /// The preferred for standard output (stdout) and standard error (stderr). - /// - /// A delegate which accepts an representing exit code of the command and - /// returns when it has handled the exit code and default exit code handling should be suppressed, and - /// returns otherwise. - /// - /// The contents of standard input (stdin). - /// - /// Whether to ignore the process tree when cancelling the command. - /// If set to true, when the command is cancelled, any child processes created by the command - /// are left running after the command is cancelled. - /// - /// A to observe while waiting for the command to exit. - /// - /// A representing the asynchronous running of the command and reading of standard output (stdout) and standard error (stderr). - /// The task result is a representing the contents of standard output (stdout) and standard error (stderr). - /// - /// - /// The command exited with non-zero exit code. The exception contains the contents of standard output (stdout) and standard error (stderr). - /// - public static async Task<(string StandardOutput, string StandardError)> ReadAsync( - string name, - IEnumerable args, - string workingDirectory = "", - Action>? configureEnvironment = null, - Encoding? encoding = null, - Func? handleExitCode = null, - string? standardInput = null, - bool cancellationIgnoresProcessTree = false, - CancellationToken cancellationToken = default) => - await ProcessStartInfo - .Create( - Resolve(Validate(name)), - "", - args ?? throw new ArgumentNullException(nameof(args)), - workingDirectory, - true, - configureEnvironment ?? defaultAction, - true, - encoding) - .ReadAsync( - handleExitCode, - standardInput, - cancellationIgnoresProcessTree, - cancellationToken) - .ConfigureAwait(false); - - private static async Task<(string StandardOutput, string StandardError)> ReadAsync( - this System.Diagnostics.ProcessStartInfo startInfo, - Func? handleExitCode, - string? standardInput, - bool cancellationIgnoresProcessTree, - CancellationToken cancellationToken) + if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0) { - using var process = new Process(); - process.StartInfo = startInfo; + throw new ExitCodeException(process.ExitCode); + } + } + + /// + /// Runs a command and reads standard output (stdout) and standard error (stderr) and optionally writes to standard input (stdin). + /// + /// The name of the command. This can be a path to an executable file. + /// The arguments to pass to the command. + /// The working directory in which to run the command. + /// An action which configures environment variables for the command. + /// The preferred for standard output (stdout) and standard output (stdout). + /// + /// A delegate which accepts an representing exit code of the command and + /// returns when it has handled the exit code and default exit code handling should be suppressed, and + /// returns otherwise. + /// + /// The contents of standard input (stdin). + /// + /// Whether to ignore the process tree when cancelling the command. + /// If set to true, when the command is cancelled, any child processes created by the command + /// are left running after the command is cancelled. + /// + /// A to observe while waiting for the command to exit. + /// + /// A representing the asynchronous running of the command and reading of standard output (stdout) and standard error (stderr). + /// The task result is a representing the contents of standard output (stdout) and standard error (stderr). + /// + /// + /// The command exited with non-zero exit code. The exception contains the contents of standard output (stdout) and standard error (stderr). + /// + public static Task<(string StandardOutput, string StandardError)> ReadAsync( + string name, + string args = "", + string workingDirectory = "", + Action>? configureEnvironment = null, + Encoding? encoding = null, + Func? handleExitCode = null, + string? standardInput = null, + bool cancellationIgnoresProcessTree = false, + CancellationToken cancellationToken = default) => + ProcessStartInfo + .Create( + Resolve(Validate(name)), + args, + Enumerable.Empty(), + workingDirectory, + true, + configureEnvironment ?? defaultAction, + true, + encoding) + .ReadAsync( + handleExitCode, + standardInput, + cancellationIgnoresProcessTree, + cancellationToken); - var runProcess = process.RunAsync(true, "", cancellationIgnoresProcessTree, cancellationToken); + /// + /// Runs a command and reads standard output (stdout) and standard error (stderr) and optionally writes to standard input (stdin). + /// + /// The name of the command. This can be a path to an executable file. + /// + /// The arguments to pass to the command. + /// As with , the strings don't need to be escaped. + /// + /// The working directory in which to run the command. + /// An action which configures environment variables for the command. + /// The preferred for standard output (stdout) and standard error (stderr). + /// + /// A delegate which accepts an representing exit code of the command and + /// returns when it has handled the exit code and default exit code handling should be suppressed, and + /// returns otherwise. + /// + /// The contents of standard input (stdin). + /// + /// Whether to ignore the process tree when cancelling the command. + /// If set to true, when the command is cancelled, any child processes created by the command + /// are left running after the command is cancelled. + /// + /// A to observe while waiting for the command to exit. + /// + /// A representing the asynchronous running of the command and reading of standard output (stdout) and standard error (stderr). + /// The task result is a representing the contents of standard output (stdout) and standard error (stderr). + /// + /// + /// The command exited with non-zero exit code. The exception contains the contents of standard output (stdout) and standard error (stderr). + /// + public static Task<(string StandardOutput, string StandardError)> ReadAsync( + string name, + IEnumerable args, + string workingDirectory = "", + Action>? configureEnvironment = null, + Encoding? encoding = null, + Func? handleExitCode = null, + string? standardInput = null, + bool cancellationIgnoresProcessTree = false, + CancellationToken cancellationToken = default) => + ProcessStartInfo + .Create( + Resolve(Validate(name)), + "", + args ?? throw new ArgumentNullException(nameof(args)), + workingDirectory, + true, + configureEnvironment ?? defaultAction, + true, + encoding) + .ReadAsync( + handleExitCode, + standardInput, + cancellationIgnoresProcessTree, + cancellationToken); + + private static async Task<(string StandardOutput, string StandardError)> ReadAsync( + this System.Diagnostics.ProcessStartInfo startInfo, + Func? handleExitCode, + string? standardInput, + bool cancellationIgnoresProcessTree, + CancellationToken cancellationToken) + { + using var process = new Process(); + process.StartInfo = startInfo; - Task readOutput; - Task readError; + var runProcess = process.RunAsync(true, "", cancellationIgnoresProcessTree, cancellationToken); - try - { - await process.StandardInput.WriteAsync(standardInput).ConfigureAwait(false); - process.StandardInput.Close(); + Task readOutput; + Task readError; + + try + { + await process.StandardInput.WriteAsync(standardInput).ConfigureAwait(false); + process.StandardInput.Close(); #if NET7_0_OR_GREATER - readOutput = process.StandardOutput.ReadToEndAsync(cancellationToken); - readError = process.StandardError.ReadToEndAsync(cancellationToken); + readOutput = process.StandardOutput.ReadToEndAsync(cancellationToken); + readError = process.StandardError.ReadToEndAsync(cancellationToken); #else - readOutput = process.StandardOutput.ReadToEndAsync(); - readError = process.StandardError.ReadToEndAsync(); + readOutput = process.StandardOutput.ReadToEndAsync(); + readError = process.StandardError.ReadToEndAsync(); #endif - } - catch (Exception) - { - await runProcess.ConfigureAwait(false); - throw; - } + } + catch (Exception) + { + await runProcess.ConfigureAwait(false); + throw; + } - await Task.WhenAll(runProcess, readOutput, readError).ConfigureAwait(false); + await Task.WhenAll(runProcess, readOutput, readError).ConfigureAwait(false); #pragma warning disable CA1849 // Call async methods when in an async method - var output = readOutput.Result; - var error = readError.Result; + var output = readOutput.Result; + var error = readError.Result; #pragma warning restore CA1849 // Call async methods when in an async method - return (handleExitCode?.Invoke(process.ExitCode) ?? false) || process.ExitCode == 0 - ? (output, error) - : throw new ExitCodeReadException(process.ExitCode, output, error); - } + return (handleExitCode?.Invoke(process.ExitCode) ?? false) || process.ExitCode == 0 + ? (output, error) + : throw new ExitCodeReadException(process.ExitCode, output, error); + } - private static string Validate(string name) => - string.IsNullOrWhiteSpace(name) - ? throw new ArgumentException("The command name is missing.", nameof(name)) - : name; + private static string Validate(string name) => + string.IsNullOrWhiteSpace(name) + ? throw new ArgumentException("The command name is missing.", nameof(name)) + : name; - private static string Resolve(string name) + private static string Resolve(string name) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Path.IsPathRooted(name)) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Path.IsPathRooted(name)) - { - return name; - } + return name; + } - var extension = Path.GetExtension(name); - if (!string.IsNullOrEmpty(extension) && extension != ".cmd" && extension != ".bat") - { - return name; - } + var extension = Path.GetExtension(name); + if (!string.IsNullOrEmpty(extension) && extension != ".cmd" && extension != ".bat") + { + return name; + } - var pathExt = Environment.GetEnvironmentVariable("PATHEXT") ?? ".EXE;.BAT;.CMD"; + var pathExt = Environment.GetEnvironmentVariable("PATHEXT") ?? ".EXE;.BAT;.CMD"; - var windowsExecutableExtensions = pathExt.Split(';') - .Select(ext => ext.TrimStart('.')) - .Where(ext => - string.Equals(ext, "exe", StringComparison.OrdinalIgnoreCase) || - string.Equals(ext, "bat", StringComparison.OrdinalIgnoreCase) || - string.Equals(ext, "cmd", StringComparison.OrdinalIgnoreCase)); + var windowsExecutableExtensions = pathExt.Split(';') + .Select(ext => ext.TrimStart('.')) + .Where(ext => + string.Equals(ext, "exe", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, "bat", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, "cmd", StringComparison.OrdinalIgnoreCase)); - var searchFileNames = string.IsNullOrEmpty(extension) - ? windowsExecutableExtensions.Select(ex => Path.ChangeExtension(name, ex)).ToList() + var searchFileNames = string.IsNullOrEmpty(extension) + ? windowsExecutableExtensions.Select(ex => Path.ChangeExtension(name, ex)).ToList() #if NET8_0_OR_GREATER - : [name]; + : [name]; #else : new List { name, }; #endif - var path = GetSearchDirectories().SelectMany(_ => searchFileNames, Path.Combine) - .FirstOrDefault(File.Exists); + var path = GetSearchDirectories().SelectMany(_ => searchFileNames, Path.Combine) + .FirstOrDefault(File.Exists); - return path == null || Path.GetExtension(path) == ".exe" ? name : path; - } + return path == null || Path.GetExtension(path) == ".exe" ? name : path; + } - // see https://github.com/dotnet/runtime/blob/14304eb31eea134db58870a6d87312231b1e02b6/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs#L703-L726 - private static IEnumerable GetSearchDirectories() + // see https://github.com/dotnet/runtime/blob/14304eb31eea134db58870a6d87312231b1e02b6/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs#L703-L726 + private static IEnumerable GetSearchDirectories() + { + var currentProcessPath = Process.GetCurrentProcess().MainModule?.FileName; + if (!string.IsNullOrEmpty(currentProcessPath)) { - var currentProcessPath = Process.GetCurrentProcess().MainModule?.FileName; - if (!string.IsNullOrEmpty(currentProcessPath)) + var currentProcessDirectory = Path.GetDirectoryName(currentProcessPath); + if (!string.IsNullOrEmpty(currentProcessDirectory)) { - var currentProcessDirectory = Path.GetDirectoryName(currentProcessPath); - if (!string.IsNullOrEmpty(currentProcessDirectory)) - { - yield return currentProcessDirectory; - } + yield return currentProcessDirectory; } + } - yield return Directory.GetCurrentDirectory(); + yield return Directory.GetCurrentDirectory(); - var path = Environment.GetEnvironmentVariable("PATH"); - if (string.IsNullOrEmpty(path)) - { - yield break; - } + var path = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(path)) + { + yield break; + } - foreach (var directory in path.Split(Path.PathSeparator)) - { - yield return directory; - } + foreach (var directory in path.Split(Path.PathSeparator)) + { + yield return directory; } } } diff --git a/SimpleExec/ExitCodeException.cs b/SimpleExec/ExitCodeException.cs index c7c8c0b8..6befc2f4 100644 --- a/SimpleExec/ExitCodeException.cs +++ b/SimpleExec/ExitCodeException.cs @@ -1,41 +1,40 @@ using System; -namespace SimpleExec -{ +namespace SimpleExec; + #if NET8_0_OR_GREATER - /// - /// The command exited with an unexpected exit code. - /// - /// The exit code of the command. +/// +/// The command exited with an unexpected exit code. +/// +/// The exit code of the command. #pragma warning disable CA1032 // Implement standard exception constructors - public class ExitCodeException(int exitCode) : Exception +public class ExitCodeException(int exitCode) : Exception #pragma warning restore CA1032 // Implement standard exception constructors - { - /// - /// Gets the exit code of the command. - /// - public int ExitCode { get; } = exitCode; -#else +{ /// - /// The command exited with an unexpected exit code. + /// Gets the exit code of the command. /// + public int ExitCode { get; } = exitCode; +#else +/// +/// The command exited with an unexpected exit code. +/// #pragma warning disable CA1032 // Implement standard exception constructors - public class ExitCodeException : Exception +public class ExitCodeException : Exception #pragma warning restore CA1032 // Implement standard exception constructors - { - /// - /// Constructs an instance of a . - /// - /// The exit code of the command. - public ExitCodeException(int exitCode) => this.ExitCode = exitCode; +{ + /// + /// Constructs an instance of a . + /// + /// The exit code of the command. + public ExitCodeException(int exitCode) => this.ExitCode = exitCode; - /// - /// Gets the exit code of the command. - /// - public int ExitCode { get; } + /// + /// Gets the exit code of the command. + /// + public int ExitCode { get; } #endif - /// - public override string Message => $"The command exited with code {this.ExitCode}."; - } + /// + public override string Message => $"The command exited with code {this.ExitCode}."; } diff --git a/SimpleExec/ExitCodeReadException.cs b/SimpleExec/ExitCodeReadException.cs index 6d65caa5..83a01a78 100644 --- a/SimpleExec/ExitCodeReadException.cs +++ b/SimpleExec/ExitCodeReadException.cs @@ -1,36 +1,35 @@ using System; -namespace SimpleExec -{ - /// - /// The command being read exited with an unexpected exit code. - /// +namespace SimpleExec; + +/// +/// The command being read exited with an unexpected exit code. +/// #pragma warning disable CA1032 // Implement standard exception constructors - public class ExitCodeReadException : ExitCodeException +public class ExitCodeReadException : ExitCodeException #pragma warning restore CA1032 // Implement standard exception constructors - { - private static readonly string twoNewLines = $"{Environment.NewLine}{Environment.NewLine}"; +{ + private static readonly string twoNewLines = $"{Environment.NewLine}{Environment.NewLine}"; - /// - /// Constructs an instance of a . - /// - /// The exit code of the command. - /// The contents of standard output (stdout). - /// The contents of standard error (stderr). - public ExitCodeReadException(int exitCode, string standardOutput, string standardError) : base(exitCode) => (this.StandardOutput, this.StandardError) = (standardOutput, standardError); + /// + /// Constructs an instance of a . + /// + /// The exit code of the command. + /// The contents of standard output (stdout). + /// The contents of standard error (stderr). + public ExitCodeReadException(int exitCode, string standardOutput, string standardError) : base(exitCode) => (this.StandardOutput, this.StandardError) = (standardOutput, standardError); - /// - /// Gets the contents of standard output (stdout). - /// - public string StandardOutput { get; } + /// + /// Gets the contents of standard output (stdout). + /// + public string StandardOutput { get; } - /// - /// Gets the contents of standard error (stderr). - /// - public string StandardError { get; } + /// + /// Gets the contents of standard error (stderr). + /// + public string StandardError { get; } - /// - public override string Message => - $"{base.Message}{twoNewLines}Standard output (stdout):{twoNewLines}{this.StandardOutput}{twoNewLines}Standard error (stderr):{twoNewLines}{this.StandardError}"; - } + /// + public override string Message => + $"{base.Message}{twoNewLines}Standard output (stdout):{twoNewLines}{this.StandardOutput}{twoNewLines}Standard error (stderr):{twoNewLines}{this.StandardError}"; } diff --git a/SimpleExec/ProcessExtensions.cs b/SimpleExec/ProcessExtensions.cs index ac4396b7..c45bbe31 100644 --- a/SimpleExec/ProcessExtensions.cs +++ b/SimpleExec/ProcessExtensions.cs @@ -5,146 +5,145 @@ using System.Threading; using System.Threading.Tasks; -namespace SimpleExec +namespace SimpleExec; + +internal static class ProcessExtensions { - internal static class ProcessExtensions + public static void Run(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken) { - public static void Run(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken) + var cancelled = 0L; + + if (!noEcho) { - var cancelled = 0L; + Console.Out.Write(process.StartInfo.GetEchoLines(echoPrefix)); + } - if (!noEcho) + _ = process.Start(); + + using var register = cancellationToken.Register( + () => { - Console.Out.Write(process.StartInfo.GetEchoLines(echoPrefix)); - } + if (process.TryKill(cancellationIgnoresProcessTree)) + { + _ = Interlocked.Increment(ref cancelled); + } + }, + useSynchronizationContext: false); - _ = process.Start(); + process.WaitForExit(); - using var register = cancellationToken.Register( + if (Interlocked.Read(ref cancelled) == 1) + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + + public static async Task RunAsync(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken) + { + using var sync = new SemaphoreSlim(1, 1); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + process.EnableRaisingEvents = true; + process.Exited += (_, _) => sync.Run(() => tcs.Task.Status != TaskStatus.Canceled, () => _ = tcs.TrySetResult()); + + if (!noEcho) + { + await Console.Out.WriteAsync(process.StartInfo.GetEchoLines(echoPrefix)).ConfigureAwait(false); + } + + _ = process.Start(); + + await using var register = cancellationToken.Register( + () => sync.Run( + () => tcs.Task.Status != TaskStatus.RanToCompletion, () => { if (process.TryKill(cancellationIgnoresProcessTree)) { - _ = Interlocked.Increment(ref cancelled); + _ = tcs.TrySetCanceled(cancellationToken); } - }, - useSynchronizationContext: false); + }), + useSynchronizationContext: false).ConfigureAwait(false); - process.WaitForExit(); + await tcs.Task.ConfigureAwait(false); + } - if (Interlocked.Read(ref cancelled) == 1) - { - cancellationToken.ThrowIfCancellationRequested(); - } - } + private static string GetEchoLines(this System.Diagnostics.ProcessStartInfo info, string echoPrefix) + { + var builder = new StringBuilder(); - public static async Task RunAsync(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken) + if (!string.IsNullOrEmpty(info.WorkingDirectory)) { - using var sync = new SemaphoreSlim(1, 1); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: Working directory: {info.WorkingDirectory}"); + } - process.EnableRaisingEvents = true; - process.Exited += (_, _) => sync.Run(() => tcs.Task.Status != TaskStatus.Canceled, () => _ = tcs.TrySetResult()); + if (info.ArgumentList.Count > 0) + { + _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: {info.FileName}"); - if (!noEcho) + foreach (var arg in info.ArgumentList) { - await Console.Out.WriteAsync(process.StartInfo.GetEchoLines(echoPrefix)).ConfigureAwait(false); + _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: {arg}"); } - - _ = process.Start(); - - await using var register = cancellationToken.Register( - () => sync.Run( - () => tcs.Task.Status != TaskStatus.RanToCompletion, - () => - { - if (process.TryKill(cancellationIgnoresProcessTree)) - { - _ = tcs.TrySetCanceled(cancellationToken); - } - }), - useSynchronizationContext: false).ConfigureAwait(false); - - await tcs.Task.ConfigureAwait(false); } - - private static string GetEchoLines(this System.Diagnostics.ProcessStartInfo info, string echoPrefix) + else { - var builder = new StringBuilder(); + _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: {info.FileName}{(string.IsNullOrEmpty(info.Arguments) ? "" : $" {info.Arguments}")}"); + } - if (!string.IsNullOrEmpty(info.WorkingDirectory)) - { - _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: Working directory: {info.WorkingDirectory}"); - } + return builder.ToString(); + } - if (info.ArgumentList.Count > 0) - { - _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: {info.FileName}"); + private static bool TryKill(this Process process, bool ignoreProcessTree) + { + // exceptions may be thrown for all kinds of reasons + // and the _same exception_ may be thrown for all kinds of reasons + // System.Diagnostics.Process is "fine" + try + { + process.Kill(!ignoreProcessTree); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception) +#pragma warning restore CA1031 // Do not catch general exception types + { + return false; + } - foreach (var arg in info.ArgumentList) - { - _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: {arg}"); - } - } - else - { - _ = builder.AppendLine(CultureInfo.InvariantCulture, $"{echoPrefix}: {info.FileName}{(string.IsNullOrEmpty(info.Arguments) ? "" : $" {info.Arguments}")}"); - } + return true; + } - return builder.ToString(); + private static void Run(this SemaphoreSlim sync, Func doubleCheckPredicate, Action action) + { + if (!doubleCheckPredicate()) + { + return; } - private static bool TryKill(this Process process, bool ignoreProcessTree) + try { - // exceptions may be thrown for all kinds of reasons - // and the _same exception_ may be thrown for all kinds of reasons - // System.Diagnostics.Process is "fine" - try - { - process.Kill(!ignoreProcessTree); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception) -#pragma warning restore CA1031 // Do not catch general exception types - { - return false; - } - - return true; + sync.Wait(); + } + catch (ObjectDisposedException) + { + return; } - private static void Run(this SemaphoreSlim sync, Func doubleCheckPredicate, Action action) + try { - if (!doubleCheckPredicate()) + if (doubleCheckPredicate()) { - return; + action(); } - + } + finally + { try { - sync.Wait(); + _ = sync.Release(); } catch (ObjectDisposedException) { - return; - } - - try - { - if (doubleCheckPredicate()) - { - action(); - } - } - finally - { - try - { - _ = sync.Release(); - } - catch (ObjectDisposedException) - { - } } } } diff --git a/SimpleExec/ProcessStartInfo.cs b/SimpleExec/ProcessStartInfo.cs index 4cab2463..a800beb2 100644 --- a/SimpleExec/ProcessStartInfo.cs +++ b/SimpleExec/ProcessStartInfo.cs @@ -2,42 +2,41 @@ using System.Collections.Generic; using System.Text; -namespace SimpleExec +namespace SimpleExec; + +internal static class ProcessStartInfo { - internal static class ProcessStartInfo + public static System.Diagnostics.ProcessStartInfo Create( + string name, + string args, + IEnumerable argList, + string workingDirectory, + bool redirectStandardStreams, + Action> configureEnvironment, + bool createNoWindow, + Encoding? encoding = null) { - public static System.Diagnostics.ProcessStartInfo Create( - string name, - string args, - IEnumerable argList, - string workingDirectory, - bool redirectStandardStreams, - Action> configureEnvironment, - bool createNoWindow, - Encoding? encoding = null) + var startInfo = new System.Diagnostics.ProcessStartInfo { - var startInfo = new System.Diagnostics.ProcessStartInfo - { - FileName = name, - Arguments = args, - WorkingDirectory = workingDirectory, - UseShellExecute = false, - RedirectStandardError = redirectStandardStreams, - RedirectStandardInput = redirectStandardStreams, - RedirectStandardOutput = redirectStandardStreams, - CreateNoWindow = createNoWindow, - StandardErrorEncoding = encoding, - StandardOutputEncoding = encoding, - }; + FileName = name, + Arguments = args, + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardError = redirectStandardStreams, + RedirectStandardInput = redirectStandardStreams, + RedirectStandardOutput = redirectStandardStreams, + CreateNoWindow = createNoWindow, + StandardErrorEncoding = encoding, + StandardOutputEncoding = encoding, + }; - foreach (var arg in argList) - { - startInfo.ArgumentList.Add(arg); - } + foreach (var arg in argList) + { + startInfo.ArgumentList.Add(arg); + } - configureEnvironment(startInfo.Environment); + configureEnvironment(startInfo.Environment); - return startInfo; - } + return startInfo; } } diff --git a/SimpleExecTester/Program.cs b/SimpleExecTester/Program.cs index 33f5e641..519c21a7 100644 --- a/SimpleExecTester/Program.cs +++ b/SimpleExecTester/Program.cs @@ -3,55 +3,54 @@ using System.Text; using System.Threading; -namespace SimpleExecTester +namespace SimpleExecTester; + +internal static class Program { - internal static class Program + public static int Main(string[] args) { - public static int Main(string[] args) + if (args.Contains("unicode")) { - if (args.Contains("unicode")) - { - Console.OutputEncoding = Encoding.Unicode; + Console.OutputEncoding = Encoding.Unicode; #if NET8_0_OR_GREATER - args = [.. args, "Pi (\u03a0)"]; + args = [.. args, "Pi (\u03a0)"]; #else - args = args.Append("Pi (\u03a0)").ToArray(); + args = args.Append("Pi (\u03a0)").ToArray(); #endif - } - - Console.Out.WriteLine($"Arg count: {args.Length}"); - - var input = args.Contains("in") - ? Console.In.ReadToEnd() - .Replace("\r", "\\r", StringComparison.Ordinal) - .Replace("\n", "\\n", StringComparison.Ordinal) - : null; + } - Console.Out.WriteLine($"SimpleExecTester (stdin): {input}"); - Console.Out.WriteLine($"SimpleExecTester (stdout): {string.Join(" ", args)}"); - Console.Error.WriteLine($"SimpleExecTester (stderr): {string.Join(" ", args)}"); + Console.Out.WriteLine($"Arg count: {args.Length}"); - if (args.Contains("large")) - { - Console.Out.WriteLine(new string('x', (int)Math.Pow(2, 12))); - Console.Error.WriteLine(new string('x', (int)Math.Pow(2, 12))); - } + var input = args.Contains("in") + ? Console.In.ReadToEnd() + .Replace("\r", "\\r", StringComparison.Ordinal) + .Replace("\n", "\\n", StringComparison.Ordinal) + : null; - var exitCode = 0; - if (args.FirstOrDefault(arg => int.TryParse(arg, out exitCode)) != null) - { - return exitCode; - } + Console.Out.WriteLine($"SimpleExecTester (stdin): {input}"); + Console.Out.WriteLine($"SimpleExecTester (stdout): {string.Join(" ", args)}"); + Console.Error.WriteLine($"SimpleExecTester (stderr): {string.Join(" ", args)}"); - if (args.Contains("sleep")) - { - Thread.Sleep(Timeout.Infinite); - return 0; - } + if (args.Contains("large")) + { + Console.Out.WriteLine(new string('x', (int)Math.Pow(2, 12))); + Console.Error.WriteLine(new string('x', (int)Math.Pow(2, 12))); + } - Console.WriteLine($"foo={Environment.GetEnvironmentVariable("foo")}"); + var exitCode = 0; + if (args.FirstOrDefault(arg => int.TryParse(arg, out exitCode)) != null) + { + return exitCode; + } + if (args.Contains("sleep")) + { + Thread.Sleep(Timeout.Infinite); return 0; } + + Console.WriteLine($"foo={Environment.GetEnvironmentVariable("foo")}"); + + return 0; } } diff --git a/SimpleExecTests/CancellingCommands.cs b/SimpleExecTests/CancellingCommands.cs index acc562fc..82d326f9 100644 --- a/SimpleExecTests/CancellingCommands.cs +++ b/SimpleExecTests/CancellingCommands.cs @@ -5,93 +5,92 @@ using SimpleExecTests.Infra; using Xunit; -namespace SimpleExecTests +namespace SimpleExecTests; + +public static class CancellingCommands { - public static class CancellingCommands + [Fact] + public static void RunningACommand() + { + // arrange + using var cancellationTokenSource = new CancellationTokenSource(); + + // use a cancellation token source to ensure value type equality comparison in assertion is meaningful + var cancellationToken = cancellationTokenSource.Token; + cancellationTokenSource.Cancel(); + + // act + var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} sleep", cancellationToken: cancellationToken)); + + // assert + Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); + } + + [Fact] + public static async Task RunningACommandAsync() { - [Fact] - public static void RunningACommand() - { - // arrange - using var cancellationTokenSource = new CancellationTokenSource(); - - // use a cancellation token source to ensure value type equality comparison in assertion is meaningful - var cancellationToken = cancellationTokenSource.Token; - cancellationTokenSource.Cancel(); - - // act - var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} sleep", cancellationToken: cancellationToken)); - - // assert - Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); - } - - [Fact] - public static async Task RunningACommandAsync() - { - // arrange - using var cancellationTokenSource = new CancellationTokenSource(); - - // use a cancellation token source to ensure value type equality comparison in assertion is meaningful - var cancellationToken = cancellationTokenSource.Token; + // arrange + using var cancellationTokenSource = new CancellationTokenSource(); + + // use a cancellation token source to ensure value type equality comparison in assertion is meaningful + var cancellationToken = cancellationTokenSource.Token; #if NET8_0_OR_GREATER - await cancellationTokenSource.CancelAsync(); + await cancellationTokenSource.CancelAsync(); #else - cancellationTokenSource.Cancel(); + cancellationTokenSource.Cancel(); #endif - // act - var exception = await Record.ExceptionAsync(() => Command.RunAsync("dotnet", $"exec {Tester.Path} sleep", cancellationToken: cancellationToken)); + // act + var exception = await Record.ExceptionAsync(() => Command.RunAsync("dotnet", $"exec {Tester.Path} sleep", cancellationToken: cancellationToken)); - // assert - Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); - } + // assert + Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); + } - [Fact] - public static async Task ReadingACommandAsync() - { - // arrange - using var cancellationTokenSource = new CancellationTokenSource(); + [Fact] + public static async Task ReadingACommandAsync() + { + // arrange + using var cancellationTokenSource = new CancellationTokenSource(); - // use a cancellation token source to ensure value type equality comparison in assertion is meaningful - var cancellationToken = cancellationTokenSource.Token; + // use a cancellation token source to ensure value type equality comparison in assertion is meaningful + var cancellationToken = cancellationTokenSource.Token; #if NET8_0_OR_GREATER - await cancellationTokenSource.CancelAsync(); + await cancellationTokenSource.CancelAsync(); #else - cancellationTokenSource.Cancel(); + cancellationTokenSource.Cancel(); #endif - // act - var exception = await Record.ExceptionAsync(() => Command.ReadAsync("dotnet", $"exec {Tester.Path} sleep", cancellationToken: cancellationToken)); + // act + var exception = await Record.ExceptionAsync(() => Command.ReadAsync("dotnet", $"exec {Tester.Path} sleep", cancellationToken: cancellationToken)); - // assert - Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); - } + // assert + Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public static async Task RunningACommandAsyncWithCreateNoWindow(bool createNoWindow) - { - // arrange - using var cancellationTokenSource = new CancellationTokenSource(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public static async Task RunningACommandAsyncWithCreateNoWindow(bool createNoWindow) + { + // arrange + using var cancellationTokenSource = new CancellationTokenSource(); - // use a cancellation token source to ensure value type equality comparison in assertion is meaningful - var cancellationToken = cancellationTokenSource.Token; + // use a cancellation token source to ensure value type equality comparison in assertion is meaningful + var cancellationToken = cancellationTokenSource.Token; - var command = Command.RunAsync( - "dotnet", $"exec {Tester.Path} sleep", createNoWindow: createNoWindow, cancellationToken: cancellationToken); + var command = Command.RunAsync( + "dotnet", $"exec {Tester.Path} sleep", createNoWindow: createNoWindow, cancellationToken: cancellationToken); - // act + // act #if NET8_0_OR_GREATER - await cancellationTokenSource.CancelAsync(); + await cancellationTokenSource.CancelAsync(); #else - cancellationTokenSource.Cancel(); + cancellationTokenSource.Cancel(); #endif - // assert - var exception = await Record.ExceptionAsync(async () => await command); - Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); - } + // assert + var exception = await Record.ExceptionAsync(() => command); + Assert.Equal(cancellationToken, Assert.IsType(exception).CancellationToken); } } diff --git a/SimpleExecTests/ConfiguringEnvironments.cs b/SimpleExecTests/ConfiguringEnvironments.cs index 649a05c0..acddaa6d 100644 --- a/SimpleExecTests/ConfiguringEnvironments.cs +++ b/SimpleExecTests/ConfiguringEnvironments.cs @@ -4,18 +4,17 @@ using SimpleExecTests.Infra; using Xunit; -namespace SimpleExecTests +namespace SimpleExecTests; + +public static class ConfiguringEnvironments { - public static class ConfiguringEnvironments + [Fact] + public static async Task ConfiguringEnvironment() { - [Fact] - public static async Task ConfiguringEnvironment() - { - // act - var (standardOutput, _) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} environment", configureEnvironment: env => env["foo"] = "bar"); + // act + var (standardOutput, _) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} environment", configureEnvironment: env => env["foo"] = "bar"); - // assert - Assert.Contains("foo=bar", standardOutput, StringComparison.Ordinal); - } + // assert + Assert.Contains("foo=bar", standardOutput, StringComparison.Ordinal); } } diff --git a/SimpleExecTests/EchoingCommands.cs b/SimpleExecTests/EchoingCommands.cs index d53bdedc..e96dae16 100644 --- a/SimpleExecTests/EchoingCommands.cs +++ b/SimpleExecTests/EchoingCommands.cs @@ -5,85 +5,84 @@ using SimpleExecTests.Infra; using Xunit; -namespace SimpleExecTests +namespace SimpleExecTests; + +public static class EchoingCommands { - public static class EchoingCommands + [Fact] + public static void EchoingACommand() + { + // arrange + Console.SetOut(Capture.Out); + + // act + Command.Run("dotnet", $"exec {Tester.Path} {TestName()}"); + + // assert + Assert.Contains(TestName(), Capture.Out.ToString()!, StringComparison.Ordinal); + } + + [Fact] + public static void EchoingACommandWithAnArgList() + { + // arrange + Console.SetOut(Capture.Out); + + // act + Command.Run("dotnet", new[] { "exec", Tester.Path, "he llo", "\"world \"today\"", }); + + // assert + var lines = Capture.Out.ToString()!.Split('\r', '\n').ToList(); + Assert.Contains(lines, line => line.EndsWith(": exec", StringComparison.Ordinal)); + Assert.Contains(lines, line => line.EndsWith($": {Tester.Path}", StringComparison.Ordinal)); + Assert.Contains(lines, line => line.EndsWith(": he llo", StringComparison.Ordinal)); + Assert.Contains(lines, line => line.EndsWith(": \"world \"today\"", StringComparison.Ordinal)); + } + + [Fact] + public static void SuppressingCommandEcho() + { + // arrange + Console.SetOut(Capture.Out); + + // act + Command.Run("dotnet", $"exec {Tester.Path} {TestName()}", noEcho: true); + + // assert + Assert.DoesNotContain(TestName(), Capture.Out.ToString()!, StringComparison.Ordinal); + } + + [Fact] + public static void EchoingACommandWithASpecificPrefix() + { + // arrange + Console.SetOut(Capture.Out); + + // act + Command.Run("dotnet", $"exec {Tester.Path} {TestName()}", noEcho: false, echoPrefix: $"{TestName()} prefix"); + + // assert + var error = Capture.Out.ToString()!; + + Assert.Contains(TestName(), error, StringComparison.Ordinal); + Assert.Contains($"{TestName()} prefix:", error, StringComparison.Ordinal); + } + + [Fact] + public static void SuppressingCommandEchoWithASpecificPrefix() { - [Fact] - public static void EchoingACommand() - { - // arrange - Console.SetOut(Capture.Out); - - // act - Command.Run("dotnet", $"exec {Tester.Path} {TestName()}"); - - // assert - Assert.Contains(TestName(), Capture.Out.ToString()!, StringComparison.Ordinal); - } - - [Fact] - public static void EchoingACommandWithAnArgList() - { - // arrange - Console.SetOut(Capture.Out); - - // act - Command.Run("dotnet", new[] { "exec", Tester.Path, "he llo", "\"world \"today\"", }); - - // assert - var lines = Capture.Out.ToString()!.Split('\r', '\n').ToList(); - Assert.Contains(lines, line => line.EndsWith(": exec", StringComparison.Ordinal)); - Assert.Contains(lines, line => line.EndsWith($": {Tester.Path}", StringComparison.Ordinal)); - Assert.Contains(lines, line => line.EndsWith(": he llo", StringComparison.Ordinal)); - Assert.Contains(lines, line => line.EndsWith(": \"world \"today\"", StringComparison.Ordinal)); - } - - [Fact] - public static void SuppressingCommandEcho() - { - // arrange - Console.SetOut(Capture.Out); - - // act - Command.Run("dotnet", $"exec {Tester.Path} {TestName()}", noEcho: true); - - // assert - Assert.DoesNotContain(TestName(), Capture.Out.ToString()!, StringComparison.Ordinal); - } - - [Fact] - public static void EchoingACommandWithASpecificPrefix() - { - // arrange - Console.SetOut(Capture.Out); - - // act - Command.Run("dotnet", $"exec {Tester.Path} {TestName()}", noEcho: false, echoPrefix: $"{TestName()} prefix"); - - // assert - var error = Capture.Out.ToString()!; - - Assert.Contains(TestName(), error, StringComparison.Ordinal); - Assert.Contains($"{TestName()} prefix:", error, StringComparison.Ordinal); - } - - [Fact] - public static void SuppressingCommandEchoWithASpecificPrefix() - { - // arrange - Console.SetOut(Capture.Out); - - // act - Command.Run("dotnet", $"exec {Tester.Path} {TestName()}", noEcho: true, echoPrefix: $"{TestName()} prefix"); - - // assert - var error = Capture.Out.ToString()!; - - Assert.DoesNotContain(TestName(), error, StringComparison.Ordinal); - Assert.DoesNotContain($"{TestName()} prefix:", error, StringComparison.Ordinal); - } - - private static string TestName([CallerMemberName] string _ = "") => _; + // arrange + Console.SetOut(Capture.Out); + + // act + Command.Run("dotnet", $"exec {Tester.Path} {TestName()}", noEcho: true, echoPrefix: $"{TestName()} prefix"); + + // assert + var error = Capture.Out.ToString()!; + + Assert.DoesNotContain(TestName(), error, StringComparison.Ordinal); + Assert.DoesNotContain($"{TestName()} prefix:", error, StringComparison.Ordinal); } + + private static string TestName([CallerMemberName] string _ = "") => _; } diff --git a/SimpleExecTests/ExitCodes.cs b/SimpleExecTests/ExitCodes.cs index dfb0c635..4829356d 100644 --- a/SimpleExecTests/ExitCodes.cs +++ b/SimpleExecTests/ExitCodes.cs @@ -3,68 +3,67 @@ using SimpleExecTests.Infra; using Xunit; -namespace SimpleExecTests +namespace SimpleExecTests; + +public static class ExitCodes { - public static class ExitCodes + [Theory] + [InlineData(0, false)] + [InlineData(1, false)] + [InlineData(2, true)] + public static void RunningACommand(int exitCode, bool shouldThrow) { - [Theory] - [InlineData(0, false)] - [InlineData(1, false)] - [InlineData(2, true)] - public static void RunningACommand(int exitCode, bool shouldThrow) - { - // act - var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} {exitCode}", handleExitCode: code => code == 1)); + // act + var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} {exitCode}", handleExitCode: code => code == 1)); - // assert - if (shouldThrow) - { - Assert.Equal(exitCode, Assert.IsType(exception).ExitCode); - } - else - { - Assert.Null(exception); - } + // assert + if (shouldThrow) + { + Assert.Equal(exitCode, Assert.IsType(exception).ExitCode); } - - [Theory] - [InlineData(0, false)] - [InlineData(1, false)] - [InlineData(2, true)] - public static async Task RunningACommandAsync(int exitCode, bool shouldThrow) + else { - // act - var exception = await Record.ExceptionAsync(async () => await Command.RunAsync("dotnet", $"exec {Tester.Path} {exitCode}", handleExitCode: code => code == 1)); - - // assert - if (shouldThrow) - { - Assert.Equal(exitCode, Assert.IsType(exception).ExitCode); - } - else - { - Assert.Null(exception); - } + Assert.Null(exception); } + } + + [Theory] + [InlineData(0, false)] + [InlineData(1, false)] + [InlineData(2, true)] + public static async Task RunningACommandAsync(int exitCode, bool shouldThrow) + { + // act + var exception = await Record.ExceptionAsync(() => Command.RunAsync("dotnet", $"exec {Tester.Path} {exitCode}", handleExitCode: code => code == 1)); - [Theory] - [InlineData(0, false)] - [InlineData(1, false)] - [InlineData(2, true)] - public static async Task ReadingACommandAsync(int exitCode, bool shouldThrow) + // assert + if (shouldThrow) { - // act - var exception = await Record.ExceptionAsync(async () => _ = await Command.ReadAsync("dotnet", $"exec {Tester.Path} {exitCode}", handleExitCode: code => code == 1)); + Assert.Equal(exitCode, Assert.IsType(exception).ExitCode); + } + else + { + Assert.Null(exception); + } + } + + [Theory] + [InlineData(0, false)] + [InlineData(1, false)] + [InlineData(2, true)] + public static async Task ReadingACommandAsync(int exitCode, bool shouldThrow) + { + // act + var exception = await Record.ExceptionAsync(async () => _ = await Command.ReadAsync("dotnet", $"exec {Tester.Path} {exitCode}", handleExitCode: code => code == 1)); - // assert - if (shouldThrow) - { - Assert.Equal(exitCode, Assert.IsType(exception).ExitCode); - } - else - { - Assert.Null(exception); - } + // assert + if (shouldThrow) + { + Assert.Equal(exitCode, Assert.IsType(exception).ExitCode); + } + else + { + Assert.Null(exception); } } } diff --git a/SimpleExecTests/Infra/Capture.cs b/SimpleExecTests/Infra/Capture.cs index 0183a785..7751562e 100644 --- a/SimpleExecTests/Infra/Capture.cs +++ b/SimpleExecTests/Infra/Capture.cs @@ -1,16 +1,11 @@ using System; using System.IO; -namespace SimpleExecTests.Infra +namespace SimpleExecTests.Infra; + +internal static class Capture { - internal static class Capture - { -#if NET6_0_OR_GREATER - private static readonly Lazy @out = new(() => new StringWriter()); -#else - private static readonly Lazy @out = new Lazy(() => new StringWriter()); -#endif + private static readonly Lazy @out = new(() => new StringWriter()); - public static TextWriter Out => @out.Value; - } + public static TextWriter Out => @out.Value; } diff --git a/SimpleExecTests/Infra/Tester.cs b/SimpleExecTests/Infra/Tester.cs index 81cd0163..1f209df9 100644 --- a/SimpleExecTests/Infra/Tester.cs +++ b/SimpleExecTests/Infra/Tester.cs @@ -1,8 +1,8 @@ -namespace SimpleExecTests.Infra +namespace SimpleExecTests.Infra; + +internal static class Tester { - internal static class Tester - { - public static string Path => + public static string Path => #if NET6_0 && DEBUG "../../../../SimpleExecTester/bin/Debug/net6.0/SimpleExecTester.dll"; #endif @@ -10,7 +10,7 @@ internal static class Tester "../../../../SimpleExecTester/bin/Release/net6.0/SimpleExecTester.dll"; #endif #if NET7_0 && DEBUG - $"../../../../SimpleExecTester/bin/Debug/net7.0/SimpleExecTester.dll"; + $"../../../../SimpleExecTester/bin/Debug/net7.0/SimpleExecTester.dll"; #endif #if NET7_0 && RELEASE $"../../../../SimpleExecTester/bin/Release/net7.0/SimpleExecTester.dll"; @@ -21,5 +21,4 @@ internal static class Tester #if NET8_0 && RELEASE $"../../../../SimpleExecTester/bin/Release/net8.0/SimpleExecTester.dll"; #endif - } } diff --git a/SimpleExecTests/Infra/WindowsFactAttribute.cs b/SimpleExecTests/Infra/WindowsFactAttribute.cs index 78de264c..000d8256 100644 --- a/SimpleExecTests/Infra/WindowsFactAttribute.cs +++ b/SimpleExecTests/Infra/WindowsFactAttribute.cs @@ -1,14 +1,13 @@ using System.Runtime.InteropServices; using Xunit; -namespace SimpleExecTests.Infra +namespace SimpleExecTests.Infra; + +internal sealed class WindowsFactAttribute : FactAttribute { - internal sealed class WindowsFactAttribute : FactAttribute + public override string Skip { - public override string Skip - { - get => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows only" : base.Skip; - set => base.Skip = value; - } + get => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows only" : base.Skip; + set => base.Skip = value; } } diff --git a/SimpleExecTests/Infra/WindowsTheoryAttribute.cs b/SimpleExecTests/Infra/WindowsTheoryAttribute.cs index c2e2efa3..fb41efa8 100644 --- a/SimpleExecTests/Infra/WindowsTheoryAttribute.cs +++ b/SimpleExecTests/Infra/WindowsTheoryAttribute.cs @@ -1,14 +1,13 @@ using System.Runtime.InteropServices; using Xunit; -namespace SimpleExecTests.Infra +namespace SimpleExecTests.Infra; + +internal sealed class WindowsTheoryAttribute : TheoryAttribute { - internal sealed class WindowsTheoryAttribute : TheoryAttribute + public override string Skip { - public override string Skip - { - get => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows only" : base.Skip; - set => base.Skip = value; - } + get => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows only" : base.Skip; + set => base.Skip = value; } } diff --git a/SimpleExecTests/ReadingCommands.cs b/SimpleExecTests/ReadingCommands.cs index 21a3f02c..b879ed58 100644 --- a/SimpleExecTests/ReadingCommands.cs +++ b/SimpleExecTests/ReadingCommands.cs @@ -7,114 +7,113 @@ using SimpleExecTests.Infra; using Xunit; -namespace SimpleExecTests +namespace SimpleExecTests; + +public static class ReadingCommands { - public static class ReadingCommands + [Theory] + [InlineData(false)] + [InlineData(true)] + public static async Task ReadingACommandAsync(bool largeOutput) { - [Theory] - [InlineData(false)] - [InlineData(true)] - public static async Task ReadingACommandAsync(bool largeOutput) - { - // act - var (standardOutput, standardError) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} hello world" + (largeOutput ? " large" : "")); + // act + var (standardOutput, standardError) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} hello world" + (largeOutput ? " large" : "")); - // assert - Assert.Contains("hello world", standardOutput, StringComparison.Ordinal); - Assert.Contains("hello world", standardError, StringComparison.Ordinal); - } + // assert + Assert.Contains("hello world", standardOutput, StringComparison.Ordinal); + Assert.Contains("hello world", standardError, StringComparison.Ordinal); + } - [Theory] - [InlineData(false)] - [InlineData(true)] - public static async Task ReadingACommandAsyncWithAnArgList(bool largeOutput) + [Theory] + [InlineData(false)] + [InlineData(true)] + public static async Task ReadingACommandAsyncWithAnArgList(bool largeOutput) + { + // arrange + var args = new List { "exec", Tester.Path, "he llo", "world", }; + if (largeOutput) { - // arrange - var args = new List { "exec", Tester.Path, "he llo", "world", }; - if (largeOutput) - { - args.Add("large"); - } - - // act - var (standardOutput, standardError) = await Command.ReadAsync("dotnet", args); - - // assert - Assert.Contains(largeOutput ? "Arg count: 3" : "Arg count: 2", standardOutput, StringComparison.Ordinal); - Assert.Contains("he llo world", standardOutput, StringComparison.Ordinal); - Assert.Contains("he llo world", standardError, StringComparison.Ordinal); + args.Add("large"); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public static async Task ReadingACommandWithInputAsync(bool largeOutput) - { - // act - var (standardOutput, standardError) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} hello world in" + (largeOutput ? " large" : ""), standardInput: "this is input"); + // act + var (standardOutput, standardError) = await Command.ReadAsync("dotnet", args); - // assert - Assert.Contains("hello world", standardOutput, StringComparison.Ordinal); - Assert.Contains("this is input", standardOutput, StringComparison.Ordinal); - Assert.Contains("hello world", standardError, StringComparison.Ordinal); - } + // assert + Assert.Contains(largeOutput ? "Arg count: 3" : "Arg count: 2", standardOutput, StringComparison.Ordinal); + Assert.Contains("he llo world", standardOutput, StringComparison.Ordinal); + Assert.Contains("he llo world", standardError, StringComparison.Ordinal); + } - [Theory] - [InlineData(false)] - [InlineData(true)] - public static async Task ReadingAUnicodeCommandAsync(bool largeOutput) - { - // act - var (standardOutput, standardError) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} hello world unicode" + (largeOutput ? " large" : ""), encoding: new UnicodeEncoding()); + [Theory] + [InlineData(false)] + [InlineData(true)] + public static async Task ReadingACommandWithInputAsync(bool largeOutput) + { + // act + var (standardOutput, standardError) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} hello world in" + (largeOutput ? " large" : ""), standardInput: "this is input"); - // assert - Assert.Contains("Pi (\u03a0)", standardOutput, StringComparison.Ordinal); - Assert.Contains("Pi (\u03a0)", standardError, StringComparison.Ordinal); - } + // assert + Assert.Contains("hello world", standardOutput, StringComparison.Ordinal); + Assert.Contains("this is input", standardOutput, StringComparison.Ordinal); + Assert.Contains("hello world", standardError, StringComparison.Ordinal); + } - [Fact] - public static async Task ReadingAFailingCommandAsync() - { - // act - var exception = await Record.ExceptionAsync(() => Command.ReadAsync("dotnet", $"exec {Tester.Path} 1 hello world")); - - // assert - var exitCodeReadException = Assert.IsType(exception); - Assert.Equal(1, exitCodeReadException.ExitCode); - Assert.Contains("hello world", exitCodeReadException.StandardOutput, StringComparison.Ordinal); - Assert.Contains("hello world", exitCodeReadException.StandardError, StringComparison.Ordinal); - } + [Theory] + [InlineData(false)] + [InlineData(true)] + public static async Task ReadingAUnicodeCommandAsync(bool largeOutput) + { + // act + var (standardOutput, standardError) = await Command.ReadAsync("dotnet", $"exec {Tester.Path} hello world unicode" + (largeOutput ? " large" : ""), encoding: new UnicodeEncoding()); - [Fact] - public static async Task ReadingACommandAsyncInANonExistentWorkDirectory() - { - // act - var exception = await Record.ExceptionAsync(() => Command.ReadAsync("dotnet", $"exec {Tester.Path}", "non-existent-working-directory")); + // assert + Assert.Contains("Pi (\u03a0)", standardOutput, StringComparison.Ordinal); + Assert.Contains("Pi (\u03a0)", standardError, StringComparison.Ordinal); + } - // assert - _ = Assert.IsType(exception); - } + [Fact] + public static async Task ReadingAFailingCommandAsync() + { + // act + var exception = await Record.ExceptionAsync(() => Command.ReadAsync("dotnet", $"exec {Tester.Path} 1 hello world")); + + // assert + var exitCodeReadException = Assert.IsType(exception); + Assert.Equal(1, exitCodeReadException.ExitCode); + Assert.Contains("hello world", exitCodeReadException.StandardOutput, StringComparison.Ordinal); + Assert.Contains("hello world", exitCodeReadException.StandardError, StringComparison.Ordinal); + } - [Fact] - public static async Task ReadingANonExistentCommandAsync() - { - // act - var exception = await Record.ExceptionAsync(() => Command.ReadAsync("simple-exec-tests-non-existent-command")); + [Fact] + public static async Task ReadingACommandAsyncInANonExistentWorkDirectory() + { + // act + var exception = await Record.ExceptionAsync(() => Command.ReadAsync("dotnet", $"exec {Tester.Path}", "non-existent-working-directory")); - // assert - _ = Assert.IsType(exception); - } + // assert + _ = Assert.IsType(exception); + } - [Theory] - [InlineData("")] - [InlineData(" ")] - public static async Task ReadingNoCommandAsync(string name) - { - // act - var exception = await Record.ExceptionAsync(() => Command.ReadAsync(name)); + [Fact] + public static async Task ReadingANonExistentCommandAsync() + { + // act + var exception = await Record.ExceptionAsync(() => Command.ReadAsync("simple-exec-tests-non-existent-command")); - // assert - Assert.Equal(nameof(name), Assert.IsType(exception).ParamName); - } + // assert + _ = Assert.IsType(exception); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public static async Task ReadingNoCommandAsync(string name) + { + // act + var exception = await Record.ExceptionAsync(() => Command.ReadAsync(name)); + + // assert + Assert.Equal(nameof(name), Assert.IsType(exception).ParamName); } } diff --git a/SimpleExecTests/RunningCommands.cs b/SimpleExecTests/RunningCommands.cs index 26609ffb..53a8667c 100644 --- a/SimpleExecTests/RunningCommands.cs +++ b/SimpleExecTests/RunningCommands.cs @@ -9,201 +9,200 @@ using SimpleExecTests.Infra; using Xunit; -namespace SimpleExecTests +namespace SimpleExecTests; + +public static class RunningCommands { - public static class RunningCommands + private static readonly string command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "hello-world.cmd" : "ls"; + + [Fact] + public static void RunningASucceedingCommand() { - private static readonly string command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "hello-world.cmd" : "ls"; + // act + var exception = Record.Exception(() => Command.Run(command)); - [Fact] - public static void RunningASucceedingCommand() - { - // act - var exception = Record.Exception(() => Command.Run(command)); + // assert + Assert.Null(exception); + } - // assert - Assert.Null(exception); - } + [Fact] + public static void RunningASucceedingCommandWithArgs() + { + // act + var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} hello world")); - [Fact] - public static void RunningASucceedingCommandWithArgs() - { - // act - var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} hello world")); + // assert + Assert.Null(exception); + } - // assert - Assert.Null(exception); - } + [Fact] + public static async Task RunningASucceedingCommandAsync() + { + // act + var exception = await Record.ExceptionAsync(() => Command.RunAsync(command)); - [Fact] - public static async Task RunningASucceedingCommandAsync() - { - // act - var exception = await Record.ExceptionAsync(() => Command.RunAsync(command)); + // assert + Assert.Null(exception); + } - // assert - Assert.Null(exception); - } + [Fact] + public static void RunningAFailingCommand() + { + // act + var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} 1 hello world")); - [Fact] - public static void RunningAFailingCommand() - { - // act - var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path} 1 hello world")); + // assert + Assert.Equal(1, Assert.IsType(exception).ExitCode); + } - // assert - Assert.Equal(1, Assert.IsType(exception).ExitCode); - } + [Fact] + public static async Task RunningAFailingCommandAsync() + { + // act + var exception = await Record.ExceptionAsync(() => Command.RunAsync("dotnet", $"exec {Tester.Path} 1 hello world")); - [Fact] - public static async Task RunningAFailingCommandAsync() - { - // act - var exception = await Record.ExceptionAsync(() => Command.RunAsync("dotnet", $"exec {Tester.Path} 1 hello world")); + // assert + Assert.Equal(1, Assert.IsType(exception).ExitCode); + } - // assert - Assert.Equal(1, Assert.IsType(exception).ExitCode); - } + [Fact] + public static void RunningACommandInANonExistentWorkDirectory() + { + // act + var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path}", "non-existent-working-directory")); - [Fact] - public static void RunningACommandInANonExistentWorkDirectory() - { - // act - var exception = Record.Exception(() => Command.Run("dotnet", $"exec {Tester.Path}", "non-existent-working-directory")); + // assert + _ = Assert.IsType(exception); + } - // assert - _ = Assert.IsType(exception); - } + [Fact] + public static async Task RunningACommandAsyncInANonExistentWorkDirectory() + { + // act + var exception = await Record.ExceptionAsync(() => Command.RunAsync("dotnet", $"exec {Tester.Path}", "non-existent-working-directory")); - [Fact] - public static async Task RunningACommandAsyncInANonExistentWorkDirectory() - { - // act - var exception = await Record.ExceptionAsync(() => Command.RunAsync("dotnet", $"exec {Tester.Path}", "non-existent-working-directory")); + // assert + _ = Assert.IsType(exception); + } - // assert - _ = Assert.IsType(exception); - } + [Fact] + public static void RunningANonExistentCommand() + { + // act + var exception = Record.Exception(() => Command.Run("simple-exec-tests-non-existent-command")); - [Fact] - public static void RunningANonExistentCommand() - { - // act - var exception = Record.Exception(() => Command.Run("simple-exec-tests-non-existent-command")); + // assert + _ = Assert.IsType(exception); + } - // assert - _ = Assert.IsType(exception); - } + [Fact] + public static async Task RunningANonExistentCommandAsync() + { + // act + var exception = await Record.ExceptionAsync(() => Command.RunAsync("simple-exec-tests-non-existent-command")); - [Fact] - public static async Task RunningANonExistentCommandAsync() - { - // act - var exception = await Record.ExceptionAsync(() => Command.RunAsync("simple-exec-tests-non-existent-command")); + // assert + _ = Assert.IsType(exception); + } - // assert - _ = Assert.IsType(exception); - } + [Theory] + [InlineData("")] + [InlineData(" ")] + public static void RunningNoCommand(string name) + { + // act + var exception = Record.Exception(() => Command.Run(name)); - [Theory] - [InlineData("")] - [InlineData(" ")] - public static void RunningNoCommand(string name) - { - // act - var exception = Record.Exception(() => Command.Run(name)); + // assert + Assert.Equal(nameof(name), Assert.IsType(exception).ParamName); + } - // assert - Assert.Equal(nameof(name), Assert.IsType(exception).ParamName); - } + [Theory] + [InlineData("")] + [InlineData(" ")] + public static async Task RunningNoCommandAsync(string name) + { + // act + var exception = await Record.ExceptionAsync(() => Command.RunAsync(name)); - [Theory] - [InlineData("")] - [InlineData(" ")] - public static async Task RunningNoCommandAsync(string name) - { - // act - var exception = await Record.ExceptionAsync(() => Command.RunAsync(name)); + // assert + Assert.Equal(nameof(name), Assert.IsType(exception).ParamName); + } - // assert - Assert.Equal(nameof(name), Assert.IsType(exception).ParamName); - } + [WindowsFact] + public static async Task RunningCommandsInPathOnWindows() + { + // arrange + var directory = Path.Combine( + Path.GetTempPath(), + "SimpleExecTests", + DateTimeOffset.UtcNow.UtcTicks.ToString(CultureInfo.InvariantCulture), + "RunningCommandsInPathOnWindows"); - [WindowsFact] - public static async Task RunningCommandsInPathOnWindows() + _ = Directory.CreateDirectory(directory); + + if (!SpinWait.SpinUntil(() => Directory.Exists(directory), 50)) { - // arrange - var directory = Path.Combine( - Path.GetTempPath(), - "SimpleExecTests", - DateTimeOffset.UtcNow.UtcTicks.ToString(CultureInfo.InvariantCulture), - "RunningCommandsInPathOnWindows"); - - _ = Directory.CreateDirectory(directory); - - if (!SpinWait.SpinUntil(() => Directory.Exists(directory), 50)) - { - throw new IOException($"Failed to create directory '{directory}'."); - } - - var name = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); - var fullName = Path.Combine(directory, Path.ChangeExtension(name, "cmd")); - await File.WriteAllTextAsync(fullName, "echo foo"); - - Environment.SetEnvironmentVariable( - "PATH", - $"{Environment.GetEnvironmentVariable("PATH")}{Path.PathSeparator}{directory}", - EnvironmentVariableTarget.Process); - - // act - var exception = Record.Exception(() => Command.Run(name)); - - // assert - Assert.Null(exception); + throw new IOException($"Failed to create directory '{directory}'."); } - [WindowsTheory] - [InlineData(".BAT;.CMD", "hello from bat")] - [InlineData(".CMD;.BAT", "hello from cmd")] - public static async Task RunningCommandsInPathOnWindowsWithSpecificPathExt( - string pathExt, string expected) + var name = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); + var fullName = Path.Combine(directory, Path.ChangeExtension(name, "cmd")); + await File.WriteAllTextAsync(fullName, "echo foo"); + + Environment.SetEnvironmentVariable( + "PATH", + $"{Environment.GetEnvironmentVariable("PATH")}{Path.PathSeparator}{directory}", + EnvironmentVariableTarget.Process); + + // act + var exception = Record.Exception(() => Command.Run(name)); + + // assert + Assert.Null(exception); + } + + [WindowsTheory] + [InlineData(".BAT;.CMD", "hello from bat")] + [InlineData(".CMD;.BAT", "hello from cmd")] + public static async Task RunningCommandsInPathOnWindowsWithSpecificPathExt( + string pathExt, string expected) + { + // arrange + var directory = Path.Combine( + Path.GetTempPath(), + "SimpleExecTests", + DateTimeOffset.UtcNow.UtcTicks.ToString(CultureInfo.InvariantCulture), + "RunningCommandsInPathOnWindows"); + + _ = Directory.CreateDirectory(directory); + + if (!SpinWait.SpinUntil(() => Directory.Exists(directory), 50)) { - // arrange - var directory = Path.Combine( - Path.GetTempPath(), - "SimpleExecTests", - DateTimeOffset.UtcNow.UtcTicks.ToString(CultureInfo.InvariantCulture), - "RunningCommandsInPathOnWindows"); - - _ = Directory.CreateDirectory(directory); - - if (!SpinWait.SpinUntil(() => Directory.Exists(directory), 50)) - { - throw new IOException($"Failed to create directory '{directory}'."); - } - - var name = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); - var batName = Path.Combine(directory, Path.ChangeExtension(name, "bat")); - await File.WriteAllTextAsync(batName, "@echo hello from bat"); - - var cmdName = Path.Combine(directory, Path.ChangeExtension(name, "cmd")); - await File.WriteAllTextAsync(cmdName, "@echo hello from cmd"); - - Environment.SetEnvironmentVariable( - "PATH", - $"{Environment.GetEnvironmentVariable("PATH")}{Path.PathSeparator}{directory}", - EnvironmentVariableTarget.Process); - - Environment.SetEnvironmentVariable( - "PATHEXT", - pathExt, - EnvironmentVariableTarget.Process); - - // act - var actual = (await Command.ReadAsync(name)).StandardOutput.Trim(); - - // assert - Assert.Equal(expected, actual); + throw new IOException($"Failed to create directory '{directory}'."); } + + var name = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); + var batName = Path.Combine(directory, Path.ChangeExtension(name, "bat")); + await File.WriteAllTextAsync(batName, "@echo hello from bat"); + + var cmdName = Path.Combine(directory, Path.ChangeExtension(name, "cmd")); + await File.WriteAllTextAsync(cmdName, "@echo hello from cmd"); + + Environment.SetEnvironmentVariable( + "PATH", + $"{Environment.GetEnvironmentVariable("PATH")}{Path.PathSeparator}{directory}", + EnvironmentVariableTarget.Process); + + Environment.SetEnvironmentVariable( + "PATHEXT", + pathExt, + EnvironmentVariableTarget.Process); + + // act + var actual = (await Command.ReadAsync(name)).StandardOutput.Trim(); + + // assert + Assert.Equal(expected, actual); } }