From 77cecf4e6d9ec3c9403dae8b208f997d6fa245d3 Mon Sep 17 00:00:00 2001 From: Jacques Eloff Date: Thu, 23 Jul 2026 14:06:10 -0700 Subject: [PATCH] Fix flaky output truncation in CLI integration tests StreamForwarder read the child process's redirected output on a thread-pool thread (Task.Run) one character at a time. Under CI load the thread pool can be starved, delaying the reader so the OS pipe fills and the child blocks on Console.Write. The child's asynchronous console logger (AddSimpleConsole) then drops queued messages when its bounded flush-on-dispose (~1.5s) times out, truncating captured output and producing cut-off Verify received.txt files. Read on a dedicated long-running thread and in 4096-char chunks so the pipe is drained promptly and the child is never blocked long enough to lose output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a88c2700-ee2f-4a55-9272-f19d9b8537d7 --- .../StreamForwarder.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/tools/Shared/Microsoft.TemplateEngine.CommandUtils/StreamForwarder.cs b/tools/Shared/Microsoft.TemplateEngine.CommandUtils/StreamForwarder.cs index dfe1ff6567f..b0825ec3d3c 100644 --- a/tools/Shared/Microsoft.TemplateEngine.CommandUtils/StreamForwarder.cs +++ b/tools/Shared/Microsoft.TemplateEngine.CommandUtils/StreamForwarder.cs @@ -49,29 +49,44 @@ public StreamForwarder ForwardTo(Action writeLine) return this; } - public Task BeginRead(TextReader reader) => Task.Run(() => Read(reader)); + // Use a dedicated long-running thread rather than a thread-pool thread (Task.Run). + // Under load (e.g. CI running many tests in parallel) the thread pool can be + // starved, delaying the reader from draining the child's redirected stream. When + // that happens the OS pipe buffer fills, the child blocks on Console.Write, and an + // asynchronous console logger (AddSimpleConsole) can drop queued messages when its + // bounded flush-on-dispose (~1.5s) times out - producing truncated captured output. + public Task BeginRead(TextReader reader) => + Task.Factory.StartNew( + () => Read(reader), + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); public void Read(TextReader reader) { - int bufferSize = 1; + // Read in reasonably sized chunks so the pipe is drained quickly and the child + // process is never blocked writing to a full pipe for long. + const int bufferSize = 4096; char currentCharacter; char[] buffer = new char[bufferSize]; _builder = new StringBuilder(); - // Using Read with buffer size 1 to prevent looping endlessly - // like we would when using Read() with no buffer - while ((_ = reader.Read(buffer, 0, bufferSize)) > 0) + int read; + while ((read = reader.Read(buffer, 0, bufferSize)) > 0) { - currentCharacter = buffer[0]; - - if (currentCharacter == FlushBuilderCharacter) - { - WriteBuilder(); - } - else if (currentCharacter != CarriageReturn) + for (int i = 0; i < read; i++) { - _ = _builder.Append(currentCharacter); + currentCharacter = buffer[i]; + + if (currentCharacter == FlushBuilderCharacter) + { + WriteBuilder(); + } + else if (currentCharacter != CarriageReturn) + { + _ = _builder.Append(currentCharacter); + } } }