From d19c20b9201d6d33fc4d097a85315fff46efdbcf Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 2 Oct 2024 11:11:08 +0200 Subject: [PATCH 1/2] Attempt to reproduce #15, handling of quoted strings --- src/Proc/BufferedObservableProcess.cs | 3 ++ src/Proc/Extensions/ArgumentExtensions.cs | 2 - .../Extensions/ObserveOutputExtensions.cs | 21 ++++++++++ src/Proc/ObservableProcess.cs | 2 +- src/Proc/Proc.Exec.cs | 10 ++--- tests/Proc.Tests.Binary/Program.cs | 8 ++++ tests/Proc.Tests/PrintArgsTests.cs | 41 +++++++++++++++++++ tests/Proc.Tests/Proc.Tests.csproj | 3 +- tests/Proc.Tests/TestConsoleOutWriter.cs | 3 +- tests/Proc.Tests/TestsBase.cs | 9 +++- 10 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 tests/Proc.Tests/PrintArgsTests.cs diff --git a/src/Proc/BufferedObservableProcess.cs b/src/Proc/BufferedObservableProcess.cs index 0cb5345..7d839c8 100644 --- a/src/Proc/BufferedObservableProcess.cs +++ b/src/Proc/BufferedObservableProcess.cs @@ -74,11 +74,14 @@ private IDisposable KickOff(IObserver observer) if (Process.HasExited) { + Process.ReadStandardErrBlocking(_observer, BufferSize, () => ContinueReadingFromProcessReaders()); + Process.ReadStandardOutBlocking(_observer, BufferSize, () => ContinueReadingFromProcessReaders()); OnExit(observer); return Disposable.Empty; } _observer = observer; + StartAsyncReads(); Process.Exited += (o, s) => diff --git a/src/Proc/Extensions/ArgumentExtensions.cs b/src/Proc/Extensions/ArgumentExtensions.cs index 4a875e9..4cbda3d 100644 --- a/src/Proc/Extensions/ArgumentExtensions.cs +++ b/src/Proc/Extensions/ArgumentExtensions.cs @@ -1,7 +1,5 @@ -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; namespace ProcNet.Extensions; diff --git a/src/Proc/Extensions/ObserveOutputExtensions.cs b/src/Proc/Extensions/ObserveOutputExtensions.cs index cf8a70b..a5991ae 100644 --- a/src/Proc/Extensions/ObserveOutputExtensions.cs +++ b/src/Proc/Extensions/ObserveOutputExtensions.cs @@ -70,5 +70,26 @@ private static async Task BufferedRead(Process p, StreamReader r, IObserver observer, int bufferSize, Func keepBuffering) => + BufferedReadBlocking(process, process.StandardError, observer, bufferSize, ConsoleOut.ErrorOut, keepBuffering); + + public static void ReadStandardOutBlocking(this Process process, IObserver observer, int bufferSize, Func keepBuffering) => + BufferedReadBlocking(process, process.StandardOutput, observer, bufferSize, ConsoleOut.Out, keepBuffering); + + private static void BufferedReadBlocking(this Process p, StreamReader r, IObserver o, int b, Func m, Func keepBuffering) + { + using var sr = new StreamReader(r.BaseStream, Encoding.UTF8, true, b, true); + while (keepBuffering()) + { + var buffer = new char[b]; + var read = sr.Read(buffer, 0, buffer.Length); + + if (read > 0) + o.OnNext(m(buffer)); + else + if (sr.EndOfStream) break; + } + } + } } diff --git a/src/Proc/ObservableProcess.cs b/src/Proc/ObservableProcess.cs index 2b22943..33e8cc7 100644 --- a/src/Proc/ObservableProcess.cs +++ b/src/Proc/ObservableProcess.cs @@ -144,7 +144,7 @@ public virtual IDisposable SubscribeLinesAndCharacters( Action onNext, Action onError, Action onNextCharacters, Action onExceptionCharacters, - Action? onCompleted = null + Action onCompleted = null ) => Subscribe( Observer.Create(onNext, onError, onCompleted ?? delegate { }), diff --git a/src/Proc/Proc.Exec.cs b/src/Proc/Proc.Exec.cs index 3bef3ef..85a7d69 100644 --- a/src/Proc/Proc.Exec.cs +++ b/src/Proc/Proc.Exec.cs @@ -51,14 +51,13 @@ public static partial class Proc var info = new ProcessStartInfo(arguments.Binary) { UseShellExecute = false - #if !NETSTANDARD2_1 - , Arguments = args - #endif }; - #if NETSTANDARD2_1 +#if NETSTANDARD2_1 foreach (var arg in arguments.Args) info.ArgumentList.Add(arg); - #endif +#else + info.Arguments = args; +#endif var pwd = arguments.WorkingDirectory; if (!string.IsNullOrWhiteSpace(pwd)) info.WorkingDirectory = pwd; @@ -93,6 +92,7 @@ public static partial class Proc return exitCode; } + private static void HardWaitForExit(Process process, TimeSpan timeSpan) { using var task = Task.Run(() => process.WaitForExit()); diff --git a/tests/Proc.Tests.Binary/Program.cs b/tests/Proc.Tests.Binary/Program.cs index 32273ba..583d899 100644 --- a/tests/Proc.Tests.Binary/Program.cs +++ b/tests/Proc.Tests.Binary/Program.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -18,6 +19,7 @@ public static async Task Main(string[] args) var testCase = args[0].ToLowerInvariant(); + if (testCase == nameof(PrintArgs).ToLowerInvariant()) return PrintArgs(args.Skip(1).ToArray()); if (testCase == nameof(SingleLineNoEnter).ToLowerInvariant()) return SingleLineNoEnter(); if (testCase == nameof(TwoWrites).ToLowerInvariant()) return TwoWrites(); @@ -41,6 +43,12 @@ public static async Task Main(string[] args) return 1; } + private static int PrintArgs(string[] args) + { + foreach (var arg in args) + Console.WriteLine(arg); + return 0; + } private static int DelayedWriter() { Thread.Sleep(3000); diff --git a/tests/Proc.Tests/PrintArgsTests.cs b/tests/Proc.Tests/PrintArgsTests.cs new file mode 100644 index 0000000..c898d13 --- /dev/null +++ b/tests/Proc.Tests/PrintArgsTests.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace ProcNet.Tests; + +public class PrintArgsTests(ITestOutputHelper output) : TestsBase +{ + [Fact] + public void ProcSendsAllArguments() + { + string[] testArgs = ["hello", "world"]; + AssertOutput(testArgs); + } + + [Fact] + public void ArgumentsWithSpaceAreNotSplit() + { + string[] testArgs = ["hello", "world", "this argument has spaces"]; + AssertOutput(testArgs); + } + + [Fact] + public void ArgumentsSeesArgumentsAfterQuoted() + { + string[] testArgs = ["this argument has spaces", "hello", "world"]; + AssertOutput(testArgs); + } + + private void AssertOutput(string[] testArgs) + { + var args = TestCaseArguments("PrintArgs", testArgs); + var outputWriter = new TestConsoleOutWriter(output); + var result = Proc.Start(args, WaitTimeout, outputWriter); + result.ExitCode.Should().Be(0); + result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(testArgs.Length); + for (var i = 0; i < result.ConsoleOut.Count; i++) + result.ConsoleOut[i].Line.Should().Be(testArgs[i], i.ToString()); + } + +} diff --git a/tests/Proc.Tests/Proc.Tests.csproj b/tests/Proc.Tests/Proc.Tests.csproj index 1bd273d..e3eb6f2 100644 --- a/tests/Proc.Tests/Proc.Tests.csproj +++ b/tests/Proc.Tests/Proc.Tests.csproj @@ -1,9 +1,10 @@  - net8.0 + net462; net8.0 Proc.Tests ProcNet.Tests false + preview diff --git a/tests/Proc.Tests/TestConsoleOutWriter.cs b/tests/Proc.Tests/TestConsoleOutWriter.cs index 85579e9..7a12aa5 100644 --- a/tests/Proc.Tests/TestConsoleOutWriter.cs +++ b/tests/Proc.Tests/TestConsoleOutWriter.cs @@ -8,12 +8,13 @@ public class TestConsoleOutWriter(ITestOutputHelper output) : IConsoleOutWriter private readonly StringBuilder _sb = new(); public string[] Lines => _sb.ToString().Replace("\r\n", "\n").Split(new [] {"\n"}, StringSplitOptions.None); public string Text => _sb.ToString(); + private static char[] NewLineChars = Environment.NewLine.ToCharArray(); public void Write(Exception e) => throw e; public void Write(ConsoleOut consoleOut) { consoleOut.CharsOrString(c => _sb.Append(new string(c)), s => _sb.AppendLine(s)); - consoleOut.CharsOrString(c => output.WriteLine(new string(c)), output.WriteLine); + consoleOut.CharsOrString(c => output.WriteLine(new string(c).TrimEnd(NewLineChars)), s => output.WriteLine(s)); } } diff --git a/tests/Proc.Tests/TestsBase.cs b/tests/Proc.Tests/TestsBase.cs index 9a6b167..3de8117 100644 --- a/tests/Proc.Tests/TestsBase.cs +++ b/tests/Proc.Tests/TestsBase.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Reflection; namespace ProcNet.Tests @@ -25,11 +26,15 @@ private static string GetWorkingDir() return binaryFolder; } - protected static StartArguments TestCaseArguments(string testcase) => - new("dotnet", GetDll(), testcase) + protected static StartArguments TestCaseArguments(string testcase, params string[] args) + { + string[] arguments = [GetDll(), testcase]; + + return new StartArguments("dotnet", arguments.Concat(args)) { WorkingDirectory = GetWorkingDir(), }; + } protected static LongRunningArguments LongRunningTestCaseArguments(string testcase) => new("dotnet", GetDll(), testcase) From c2c4270aba5e4c17dc1cb7ca9789d572f3a95f82 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 2 Oct 2024 11:14:49 +0200 Subject: [PATCH 2/2] move back to just testing net8.0 --- tests/Proc.Tests/PrintArgsTests.cs | 6 ++++++ tests/Proc.Tests/Proc.Tests.csproj | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/Proc.Tests/PrintArgsTests.cs b/tests/Proc.Tests/PrintArgsTests.cs index c898d13..35892fc 100644 --- a/tests/Proc.Tests/PrintArgsTests.cs +++ b/tests/Proc.Tests/PrintArgsTests.cs @@ -26,6 +26,12 @@ public void ArgumentsSeesArgumentsAfterQuoted() string[] testArgs = ["this argument has spaces", "hello", "world"]; AssertOutput(testArgs); } + [Fact] + public void EscapedQuotes() + { + string[] testArgs = ["\"this argument has spaces\"", "hello", "world"]; + AssertOutput(testArgs); + } private void AssertOutput(string[] testArgs) { diff --git a/tests/Proc.Tests/Proc.Tests.csproj b/tests/Proc.Tests/Proc.Tests.csproj index e3eb6f2..913694a 100644 --- a/tests/Proc.Tests/Proc.Tests.csproj +++ b/tests/Proc.Tests/Proc.Tests.csproj @@ -1,6 +1,6 @@  - net462; net8.0 + net8.0 Proc.Tests ProcNet.Tests false