diff --git a/README.md b/README.md index 6388281..2bad560 100644 --- a/README.md +++ b/README.md @@ -394,7 +394,7 @@ Processes output in raw, undelimited chunks as they arrive from the process. ### `LineOutputHandler` -Inherits from `OutputHandler` and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives. +Inherits from `OutputHandler` and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives. Lines end at LF, CR or CRLF, the same as `Process.OutputDataReceived` and `StreamReader.ReadLine`; NEL (U+0085), LS (U+2028), PS (U+2029) and form feed stay inside the line, so line-delimited JSON that carries them raw arrives one record per call. #### Constructor diff --git a/RunCommand.Test/LineOutputHandlerTests.cs b/RunCommand.Test/LineOutputHandlerTests.cs index b21b819..3b84635 100644 --- a/RunCommand.Test/LineOutputHandlerTests.cs +++ b/RunCommand.Test/LineOutputHandlerTests.cs @@ -164,6 +164,47 @@ public void HandleStandardOutputDataShouldSplitOnEveryLineEndingKind() Assert.AreEqual("e", handler.outputBuffer.ToString()); } + [TestMethod] + [DataRow('\u2028', DisplayName = "LS")] + [DataRow('\u2029', DisplayName = "PS")] + [DataRow('\u0085', DisplayName = "NEL")] + [DataRow('\f', DisplayName = "FF")] + public void HandleStandardOutputDataShouldKeepAnUnusualSeparatorInsideTheLine(char separator) + { + // Arrange + string record = $"{{\"a\":\"x{separator}y\"}}"; + List lines = []; + LineOutputHandler handler = new(onStandardOutput: lines.Add); + + // Act + handler.HandleStandardOutputData($"{record}\n{record}\n"); + + // Assert + string[] expected = [record, record]; + Assert.AreSequenceEqual(expected, lines); + } + + [TestMethod] + [DataRow('\u2028', DisplayName = "LS")] + [DataRow('\u2029', DisplayName = "PS")] + [DataRow('\u0085', DisplayName = "NEL")] + [DataRow('\f', DisplayName = "FF")] + public void HandleStandardErrorDataShouldKeepAnUnusualSeparatorOnAChunkBoundaryInsideTheLine(char separator) + { + // Arrange + List lines = []; + LineOutputHandler handler = new(onStandardError: lines.Add); + + // Act + handler.HandleStandardErrorData($"x{separator}"); + handler.HandleStandardErrorData($"y\nz{separator}"); + handler.HandleStandardErrorData("\n"); + + // Assert + string[] expected = [$"x{separator}y", $"z{separator}"]; + Assert.AreSequenceEqual(expected, lines); + } + [TestMethod] public void CompleteShouldDeliverTheFinalUnterminatedLine() { diff --git a/RunCommand/LineOutputHandler.cs b/RunCommand/LineOutputHandler.cs index d04223a..fcf7212 100644 --- a/RunCommand/LineOutputHandler.cs +++ b/RunCommand/LineOutputHandler.cs @@ -200,9 +200,11 @@ private static void EmitLine(StringBuilder buffer, string data, int start, int e } /// - /// Determines whether a character other than CR ends a line, matching the set string.ReplaceLineEndings recognises. + /// Determines whether a character other than CR ends a line. Only LF does, matching + /// Process.OutputDataReceived and StreamReader.ReadLine: NEL, LS, PS and FF are ordinary characters, + /// since tools emit them raw inside JSON strings and a line-delimited record must arrive whole. /// /// The character to test. - /// if is LF, NEL, LS, PS or FF; otherwise . - private static bool IsLineBreak(char c) => c is '\n' or '\u0085' or '\u2028' or '\u2029' or '\f'; + /// if is LF; otherwise . + private static bool IsLineBreak(char c) => c == '\n'; }