Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 41 additions & 0 deletions RunCommand.Test/LineOutputHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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<string> 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()
{
Expand Down
8 changes: 5 additions & 3 deletions RunCommand/LineOutputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,11 @@ private static void EmitLine(StringBuilder buffer, string data, int start, int e
}

/// <summary>
/// Determines whether a character other than CR ends a line, matching the set <c>string.ReplaceLineEndings</c> recognises.
/// Determines whether a character other than CR ends a line. Only LF does, matching
/// <c>Process.OutputDataReceived</c> and <c>StreamReader.ReadLine</c>: NEL, LS, PS and FF are ordinary characters,
/// since tools emit them raw inside JSON strings and a line-delimited record must arrive whole.
/// </summary>
/// <param name="c">The character to test.</param>
/// <returns><see langword="true"/> if <paramref name="c"/> is LF, NEL, LS, PS or FF; otherwise <see langword="false"/>.</returns>
private static bool IsLineBreak(char c) => c is '\n' or '\u0085' or '\u2028' or '\u2029' or '\f';
/// <returns><see langword="true"/> if <paramref name="c"/> is LF; otherwise <see langword="false"/>.</returns>
private static bool IsLineBreak(char c) => c == '\n';
}
Loading