From fe3e63437d19f61d1f29ec794a5ed80859ccf41d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 06:50:49 +0000 Subject: [PATCH 1/2] Ensure build errors and failures are visible in stderr during redirection Modified ConsoleLogger and the CLI runner to ensure that build errors and tool failure notifications are written to the standard error stream when standard output is redirected (e.g., to $GITHUB_STEP_SUMMARY). Key changes: - Refactored ConsoleLogger to support flexible routing to any TextWriter. - LogFailed now mirrors output to Console.Error if output is redirected. - CLI runner now captures all stdout from dotnet commands. - Detected errors in stdout are mirrored to Console.Error immediately if redirection is active. - On command failure with suppressed stdout (like restore), the full captured log is dumped to stderr and the markdown summary. - Prevented ANSI escape codes from leaking into markdown summaries while preserving them for console stderr output. --- cli/Program.cs | 45 ++++++++++++++++---- src/_Hidden_FUnitImpl/ConsoleLogger.cs | 57 +++++++++++++++----------- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/cli/Program.cs b/cli/Program.cs index 8726a48..dea4dab 100644 --- a/cli/Program.cs +++ b/cli/Program.cs @@ -455,6 +455,7 @@ async ValueTask RunDotnetAsync( }; var callCounts = new ProcessCallbackCallCounts(); + var capturedStdout = new List(); proc.ErrorDataReceived += (sender, args) => { @@ -465,17 +466,27 @@ async ValueTask RunDotnetAsync( } }; - if (requireStdOutLogging) + proc.OutputDataReceived += (sender, args) => { - proc.OutputDataReceived += (sender, args) => + if (args.Data != null) { - if (args.Data != null) + Interlocked.Increment(ref callCounts.Stdout); + capturedStdout.Add(args.Data); + + var colorized = Colorize(args.Data, force: true); + bool hasErrorOrWarning = !string.Equals(args.Data, colorized, StringComparison.Ordinal); + + if (hasErrorOrWarning && (Console.IsOutputRedirected || !requireStdOutLogging)) { - Interlocked.Increment(ref callCounts.Stdout); - Console.WriteLine(Colorize(args.Data)); // DO NOT use ConsoleLogger here! + Console.Error.WriteLine(colorized); } - }; - } + + if (requireStdOutLogging) + { + Console.WriteLine(ConsoleLogger.EnableMarkdownOutput ? args.Data : colorized); // DO NOT use ConsoleLogger here! + } + } + }; if (!proc.Start()) { @@ -491,6 +502,22 @@ async ValueTask RunDotnetAsync( await proc.WaitForExitAsync(); + if (proc.ExitCode != 0 && !requireStdOutLogging) + { + foreach (var line in capturedStdout) + { + Console.Error.WriteLine(Colorize(line, force: true)); + } + + if (ConsoleLogger.EnableMarkdownOutput) + { + foreach (var line in capturedStdout) + { + Console.WriteLine(line); + } + } + } + if (ConsoleLogger.EnableMarkdownOutput) { if (requireDetailsTag) @@ -569,9 +596,9 @@ static string ColorizeInternal(string text) } -static string Colorize(string message) +static string Colorize(string message, bool force = false) { - if (ConsoleLogger.EnableMarkdownOutput || // TODO: use tag instead of ANSI escape + if ((!force && ConsoleLogger.EnableMarkdownOutput) || // TODO: use tag instead of ANSI escape string.IsNullOrWhiteSpace(message)) { return message; diff --git a/src/_Hidden_FUnitImpl/ConsoleLogger.cs b/src/_Hidden_FUnitImpl/ConsoleLogger.cs index 128fac0..9d340fb 100644 --- a/src/_Hidden_FUnitImpl/ConsoleLogger.cs +++ b/src/_Hidden_FUnitImpl/ConsoleLogger.cs @@ -27,7 +27,7 @@ internal static class ConsoleLogger RegexOptions.Compiled | RegexOptions.Multiline); #pragma warning restore SYSLIB1045 - private static void Write(object? obj) + private static void Write(System.IO.TextWriter writer, bool useMarkdown, object? obj) { var message = obj?.ToString(); if (message == null) @@ -35,7 +35,7 @@ private static void Write(object? obj) return; } - if (EnableMarkdownOutput) + if (useMarkdown) { // As GitHub doesn't allow changing text color, use more eye-catching emojis. message = re_markdownQuoteAwareTagCloser.Replace(message, ">") @@ -48,21 +48,21 @@ private static void Write(object? obj) // always!! message = message.Replace("\n", $"\n{new string(' ', SR.IndentationAdjustment)}", StringComparison.Ordinal); - Console.Write(message); + writer.Write(message); } - private static void WriteLine(object? obj = null) + private static void WriteLine(System.IO.TextWriter writer, bool useMarkdown, object? obj = null) { - Write(obj); - NewLine(); + Write(writer, useMarkdown, obj); + NewLine(writer); } - private static void NewLine() + private static void NewLine(System.IO.TextWriter writer) { - Console.WriteLine(); + writer.WriteLine(); } - private static void Color(string? ansiColor, object obj) + private static void Color(System.IO.TextWriter writer, bool useMarkdown, string? ansiColor, object obj) { var message = obj?.ToString(); @@ -70,7 +70,7 @@ private static void Color(string? ansiColor, object obj) // fix for markdown int numTrailingSpaces = 0; - if (message != null && EnableMarkdownOutput) + if (message != null && useMarkdown) { numTrailingSpaces = message.Length - message.TrimEnd(' ').Length; if (numTrailingSpaces > 0) @@ -81,7 +81,7 @@ private static void Color(string? ansiColor, object obj) var match = re_markdownUnorderedList.Match(message); if (match.Success) { - Write(match.Value); + Write(writer, useMarkdown, match.Value); message = message[match.Value.Length..]; } } @@ -91,13 +91,13 @@ private static void Color(string? ansiColor, object obj) // write color if (writeColor) { - if (!EnableMarkdownOutput) + if (!useMarkdown) { - Console.Write(ansiColor); + writer.Write(ansiColor); } else { - Console.Write(ansiColor switch + writer.Write(ansiColor switch { SR.AnsiColorFailed => SR.MarkdownColorFailed, SR.AnsiColorPassed => SR.MarkdownColorPassed, @@ -109,19 +109,19 @@ private static void Color(string? ansiColor, object obj) // write remaining message if (message != null) { - Write(message); + Write(writer, useMarkdown, message); } // reset color if (writeColor) { - if (!EnableMarkdownOutput) + if (!useMarkdown) { - Console.Write(SR.AnsiColorReset); + writer.Write(SR.AnsiColorReset); } else { - Console.Write(SR.MarkdownColorReset); + writer.Write(SR.MarkdownColorReset); } } @@ -129,7 +129,7 @@ private static void Color(string? ansiColor, object obj) // trailing spaces if (numTrailingSpaces > 0) { - Write(new string(' ', numTrailingSpaces)); + Write(writer, useMarkdown, new string(' ', numTrailingSpaces)); } } @@ -148,7 +148,7 @@ public static void LogInfoRaw(object? message = null) { lock (sync) { - Console.WriteLine(message); + Console.Out.WriteLine(message); } } @@ -160,7 +160,7 @@ public static void LogInfo(object? message = null) { lock (sync) { - WriteLine(message); + WriteLine(Console.Out, EnableMarkdownOutput, message); } } @@ -172,8 +172,8 @@ public static void LogPassed(object message) { lock (sync) { - Color(SR.AnsiColorPassed, message); - NewLine(); + Color(Console.Out, EnableMarkdownOutput, SR.AnsiColorPassed, message); + NewLine(Console.Out); } } @@ -185,8 +185,15 @@ public static void LogFailed(object message) { lock (sync) { - Color(SR.AnsiColorFailed, message); - NewLine(); + Color(Console.Out, EnableMarkdownOutput, SR.AnsiColorFailed, message); + NewLine(Console.Out); + + // failure should be also visible in standard error stream if stdout is redirected. + if (Console.IsOutputRedirected) + { + Color(Console.Error, false, SR.AnsiColorFailed, message); + NewLine(Console.Error); + } } } } From f84e8ad2714a9f8162efddebba5b1aa4b900210a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:25:30 +0000 Subject: [PATCH 2/2] Ensure build errors and failures are visible in stderr during redirection (CLI only) Modified the CLI runner to ensure that build errors and tool failure notifications are written to the standard error stream when standard output is redirected (e.g., to $GITHUB_STEP_SUMMARY). Key changes: - Introduced a local LogFailed helper in cli/Program.cs to mirror failure notifications to Console.Error when redirection is active. - Improved RunDotnetAsync to capture all stdout from dotnet commands. - Detected errors in stdout are mirrored to Console.Error immediately if redirection is active or stdout logging is suppressed. - On command failure with suppressed stdout (like restore), the full captured log is dumped to stderr and the markdown summary. - Fixed ANSI escape codes leaking into markdown summaries while preserving them for console stderr output. - All changes are contained within the CLI package. --- cli/Program.cs | 66 ++++++++++++++++---------- src/_Hidden_FUnitImpl/ConsoleLogger.cs | 57 ++++++++++------------ 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/cli/Program.cs b/cli/Program.cs index dea4dab..5c6aa84 100644 --- a/cli/Program.cs +++ b/cli/Program.cs @@ -170,7 +170,7 @@ FUnit Test Runner else { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); + LogFailed($"> [!CAUTION]"); if (failedTestCaseCount > 0) { @@ -178,12 +178,12 @@ FUnit Test Runner ? string.Empty : $" Rerun with '{SR.Flag_StackTrace}' option for more detailed log." ; - ConsoleLogger.LogFailed($"> {SR.MarkdownFailed} Total {failedTestCaseCount} test cases were failed.{guidance}"); + LogFailed($"> {SR.MarkdownFailed} Total {failedTestCaseCount} test cases were failed.{guidance}"); } if (failedTestFiles.Count > 0) { - ConsoleLogger.LogFailed($"> {SR.MarkdownFailed} {failedTestFiles.Count} of {validFUnitFiles.Count} test files were failed to build: {string.Join(", ", failedTestFiles.Select(Path.GetFileName))}"); + LogFailed($"> {SR.MarkdownFailed} {failedTestFiles.Count} of {validFUnitFiles.Count} test files were failed to build: {string.Join(", ", failedTestFiles.Select(Path.GetFileName))}"); } Environment.Exit(1); @@ -192,9 +192,9 @@ FUnit Test Runner else { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); + LogFailed($"> [!CAUTION]"); var patterns = fileGlobs.Length > 0 ? string.Join(", ", fileGlobs) : "**/*test*.cs"; - ConsoleLogger.LogFailed($"> No valid {FUnit} test files found matching the criteria: `{patterns}`"); + LogFailed($"> No valid {FUnit} test files found matching the criteria: `{patterns}`"); Environment.Exit(1); } @@ -229,8 +229,8 @@ int EnsureEnvironment() if (process == null) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed("> [!CAUTION]"); - ConsoleLogger.LogFailed("> Error: 'dotnet' command could not be started. Please ensure .NET SDK is installed and 'dotnet' command is accessible in your system's PATH."); + LogFailed("> [!CAUTION]"); + LogFailed("> Error: 'dotnet' command could not be started. Please ensure .NET SDK is installed and 'dotnet' command is accessible in your system's PATH."); return 1; } @@ -241,8 +241,8 @@ int EnsureEnvironment() if (string.IsNullOrEmpty(output)) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed("> [!CAUTION]"); - ConsoleLogger.LogFailed("> Error: 'dotnet --version' returned empty output."); + LogFailed("> [!CAUTION]"); + LogFailed("> Error: 'dotnet --version' returned empty output."); return 1; } @@ -250,8 +250,8 @@ int EnsureEnvironment() if (versionParts.Length == 0) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); - ConsoleLogger.LogFailed($"> Error: Could not parse .NET SDK version from output: '{output}' (no dot found)."); + LogFailed($"> [!CAUTION]"); + LogFailed($"> Error: Could not parse .NET SDK version from output: '{output}' (no dot found)."); return 1; } @@ -261,9 +261,9 @@ int EnsureEnvironment() if (majorVersion < MinimumRequiredDotnetVersion) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); - ConsoleLogger.LogFailed($"> Error: .NET SDK major version {output} is less than the required {MinimumRequiredDotnetVersion}."); - ConsoleLogger.LogFailed($"> Please update your .NET SDK to version {MinimumRequiredDotnetVersion} or higher."); + LogFailed($"> [!CAUTION]"); + LogFailed($"> Error: .NET SDK major version {output} is less than the required {MinimumRequiredDotnetVersion}."); + LogFailed($"> Please update your .NET SDK to version {MinimumRequiredDotnetVersion} or higher."); return 1; } @@ -275,8 +275,8 @@ int EnsureEnvironment() else { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); - ConsoleLogger.LogFailed($"> Error: Could not parse .NET SDK major version from output: '{output}'"); + LogFailed($"> [!CAUTION]"); + LogFailed($"> Error: Could not parse .NET SDK major version from output: '{output}'"); return 1; } @@ -329,8 +329,8 @@ static string BuildEscapedArguments(string[] args) if (exitCode != 0) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); - ConsoleLogger.LogFailed($"> Error: 'dotnet restore' command failed with exit code {exitCode}."); + LogFailed($"> [!CAUTION]"); + LogFailed($"> Error: 'dotnet restore' command failed with exit code {exitCode}."); return (exitCode, false); } @@ -349,8 +349,8 @@ static string BuildEscapedArguments(string[] args) if (exitCode != 0) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); - ConsoleLogger.LogFailed($"> Error: 'dotnet clean' command failed with exit code {exitCode}."); + LogFailed($"> [!CAUTION]"); + LogFailed($"> Error: 'dotnet clean' command failed with exit code {exitCode}."); return (exitCode, false); } @@ -378,8 +378,8 @@ static string BuildEscapedArguments(string[] args) if (exitCode != 0) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed($"> [!CAUTION]"); - ConsoleLogger.LogFailed($"> Error: 'dotnet build' command failed with exit code {exitCode}."); + LogFailed($"> [!CAUTION]"); + LogFailed($"> Error: 'dotnet build' command failed with exit code {exitCode}."); return (exitCode, false); } @@ -491,8 +491,8 @@ async ValueTask RunDotnetAsync( if (!proc.Start()) { ConsoleLogger.LogInfo(); - ConsoleLogger.LogFailed("> [!CAUTION]"); - ConsoleLogger.LogFailed("> Error: 'dotnet' command could not be started. Please ensure .NET SDK is installed and 'dotnet' command is accessible in your system's PATH."); + LogFailed("> [!CAUTION]"); + LogFailed("> Error: 'dotnet' command could not be started. Please ensure .NET SDK is installed and 'dotnet' command is accessible in your system's PATH."); return -1; } @@ -544,6 +544,24 @@ async ValueTask RunDotnetAsync( } +static void LogFailed(object message) +{ + ConsoleLogger.LogFailed(message); + if (Console.IsOutputRedirected) + { + var msg = message?.ToString(); + if (msg == null) return; + + // FUnit always!! + msg = msg.Replace("\n", $"\n{new string(' ', SR.IndentationAdjustment)}", StringComparison.Ordinal); + + Console.Error.Write(SR.AnsiColorFailed); + Console.Error.Write(msg); + Console.Error.WriteLine(SR.AnsiColorReset); + } +} + + #if DEBUG static void RunAllTests() { diff --git a/src/_Hidden_FUnitImpl/ConsoleLogger.cs b/src/_Hidden_FUnitImpl/ConsoleLogger.cs index 9d340fb..128fac0 100644 --- a/src/_Hidden_FUnitImpl/ConsoleLogger.cs +++ b/src/_Hidden_FUnitImpl/ConsoleLogger.cs @@ -27,7 +27,7 @@ internal static class ConsoleLogger RegexOptions.Compiled | RegexOptions.Multiline); #pragma warning restore SYSLIB1045 - private static void Write(System.IO.TextWriter writer, bool useMarkdown, object? obj) + private static void Write(object? obj) { var message = obj?.ToString(); if (message == null) @@ -35,7 +35,7 @@ private static void Write(System.IO.TextWriter writer, bool useMarkdown, object? return; } - if (useMarkdown) + if (EnableMarkdownOutput) { // As GitHub doesn't allow changing text color, use more eye-catching emojis. message = re_markdownQuoteAwareTagCloser.Replace(message, ">") @@ -48,21 +48,21 @@ private static void Write(System.IO.TextWriter writer, bool useMarkdown, object? // always!! message = message.Replace("\n", $"\n{new string(' ', SR.IndentationAdjustment)}", StringComparison.Ordinal); - writer.Write(message); + Console.Write(message); } - private static void WriteLine(System.IO.TextWriter writer, bool useMarkdown, object? obj = null) + private static void WriteLine(object? obj = null) { - Write(writer, useMarkdown, obj); - NewLine(writer); + Write(obj); + NewLine(); } - private static void NewLine(System.IO.TextWriter writer) + private static void NewLine() { - writer.WriteLine(); + Console.WriteLine(); } - private static void Color(System.IO.TextWriter writer, bool useMarkdown, string? ansiColor, object obj) + private static void Color(string? ansiColor, object obj) { var message = obj?.ToString(); @@ -70,7 +70,7 @@ private static void Color(System.IO.TextWriter writer, bool useMarkdown, string? // fix for markdown int numTrailingSpaces = 0; - if (message != null && useMarkdown) + if (message != null && EnableMarkdownOutput) { numTrailingSpaces = message.Length - message.TrimEnd(' ').Length; if (numTrailingSpaces > 0) @@ -81,7 +81,7 @@ private static void Color(System.IO.TextWriter writer, bool useMarkdown, string? var match = re_markdownUnorderedList.Match(message); if (match.Success) { - Write(writer, useMarkdown, match.Value); + Write(match.Value); message = message[match.Value.Length..]; } } @@ -91,13 +91,13 @@ private static void Color(System.IO.TextWriter writer, bool useMarkdown, string? // write color if (writeColor) { - if (!useMarkdown) + if (!EnableMarkdownOutput) { - writer.Write(ansiColor); + Console.Write(ansiColor); } else { - writer.Write(ansiColor switch + Console.Write(ansiColor switch { SR.AnsiColorFailed => SR.MarkdownColorFailed, SR.AnsiColorPassed => SR.MarkdownColorPassed, @@ -109,19 +109,19 @@ private static void Color(System.IO.TextWriter writer, bool useMarkdown, string? // write remaining message if (message != null) { - Write(writer, useMarkdown, message); + Write(message); } // reset color if (writeColor) { - if (!useMarkdown) + if (!EnableMarkdownOutput) { - writer.Write(SR.AnsiColorReset); + Console.Write(SR.AnsiColorReset); } else { - writer.Write(SR.MarkdownColorReset); + Console.Write(SR.MarkdownColorReset); } } @@ -129,7 +129,7 @@ private static void Color(System.IO.TextWriter writer, bool useMarkdown, string? // trailing spaces if (numTrailingSpaces > 0) { - Write(writer, useMarkdown, new string(' ', numTrailingSpaces)); + Write(new string(' ', numTrailingSpaces)); } } @@ -148,7 +148,7 @@ public static void LogInfoRaw(object? message = null) { lock (sync) { - Console.Out.WriteLine(message); + Console.WriteLine(message); } } @@ -160,7 +160,7 @@ public static void LogInfo(object? message = null) { lock (sync) { - WriteLine(Console.Out, EnableMarkdownOutput, message); + WriteLine(message); } } @@ -172,8 +172,8 @@ public static void LogPassed(object message) { lock (sync) { - Color(Console.Out, EnableMarkdownOutput, SR.AnsiColorPassed, message); - NewLine(Console.Out); + Color(SR.AnsiColorPassed, message); + NewLine(); } } @@ -185,15 +185,8 @@ public static void LogFailed(object message) { lock (sync) { - Color(Console.Out, EnableMarkdownOutput, SR.AnsiColorFailed, message); - NewLine(Console.Out); - - // failure should be also visible in standard error stream if stdout is redirected. - if (Console.IsOutputRedirected) - { - Color(Console.Error, false, SR.AnsiColorFailed, message); - NewLine(Console.Error); - } + Color(SR.AnsiColorFailed, message); + NewLine(); } } }