From 282c88b9ee0b33ac542e53c5652aa89a196fc2da Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Wed, 24 Apr 2024 12:57:14 -0700 Subject: [PATCH] #783: Add -useAnsiColor flag to console runners (v3) --- .../Logger/ConsoleRunnerLoggerTests.cs | 29 +++++++++++++--- .../Frameworks/TestProjectConfiguration.cs | 12 +++++++ .../Loggers/ConsoleRunnerLogger.cs | 33 +++++++++++++++++-- .../Parsers/CommandLineParserBase.cs | 7 ++++ .../Utility/ConsoleHelper.cs | 16 +++++++-- src/xunit.v3.runner.console/ConsoleRunner.cs | 5 ++- .../ConsoleRunner.cs | 5 ++- 7 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/xunit.v3.runner.common.tests/Logger/ConsoleRunnerLoggerTests.cs b/src/xunit.v3.runner.common.tests/Logger/ConsoleRunnerLoggerTests.cs index 6477c8cf1..a9dcd410a 100644 --- a/src/xunit.v3.runner.common.tests/Logger/ConsoleRunnerLoggerTests.cs +++ b/src/xunit.v3.runner.common.tests/Logger/ConsoleRunnerLoggerTests.cs @@ -10,7 +10,7 @@ public void WriteLine_ColorsEnabled_PlainText() { var writer = new StringWriter(); var message = "foo bar"; - var sut = new ConsoleRunnerLogger(true); + var sut = new ConsoleRunnerLogger(true, false); sut.WriteLine(writer, message); @@ -22,7 +22,7 @@ public void WriteLine_ColorsEnabled_AnsiText() { var writer = new StringWriter(); var message = "\x1b[3m\x1b[36mhello world\u001b[0m || \x1b[94;103mbright blue on bright yellow\x1b[m"; - var sut = new ConsoleRunnerLogger(true); + var sut = new ConsoleRunnerLogger(true, false); sut.WriteLine(writer, message); @@ -34,7 +34,7 @@ public void WriteLine_ColorsDisabled_PlainText() { var writer = new StringWriter(); var message = "foo bar"; - var sut = new ConsoleRunnerLogger(false); + var sut = new ConsoleRunnerLogger(false, false); sut.WriteLine(writer, message); @@ -45,10 +45,31 @@ public void WriteLine_ColorsDisabled_PlainText() public void WriteLine_ColorsDisabled_AnsiText() { var writer = new StringWriter(); - var sut = new ConsoleRunnerLogger(false); + var sut = new ConsoleRunnerLogger(false, false); sut.WriteLine(writer, "\x1b[3m\x1b[36mhello world\u001b[0m || \x1b[94;103mbright blue on bright yellow\x1b[m"); Assert.Equal("hello world || bright blue on bright yellow" + Environment.NewLine, writer.ToString()); } + + [Fact] + public void CanForceAnsiColors() + { + var oldConsoleOut = Console.Out; + + try + { + var writer = new StringWriter(); + var sut = new ConsoleRunnerLogger(true, true); + Console.SetOut(writer); + + sut.LogError("This is an error message"); + + Assert.Equal("\u001b[91mThis is an error message\r\n\u001b[0m", writer.ToString()); + } + finally + { + Console.SetOut(oldConsoleOut); + } + } } diff --git a/src/xunit.v3.runner.common/Frameworks/TestProjectConfiguration.cs b/src/xunit.v3.runner.common/Frameworks/TestProjectConfiguration.cs index 2033814c2..86fda2497 100644 --- a/src/xunit.v3.runner.common/Frameworks/TestProjectConfiguration.cs +++ b/src/xunit.v3.runner.common/Frameworks/TestProjectConfiguration.cs @@ -103,6 +103,18 @@ public class TestProjectConfiguration /// public bool PauseOrDefault => Pause ?? false; + /// + /// Gets or sets a flag indicating that ANSI color usage should be forced on Windows. + /// ANSI color is always used for non-Windows. + /// + public bool? UseAnsiColor { get; set; } + + /// + /// Gets a flag indicating that ANSI color usage should be forced on Windows. ANSI color is + /// always used for non-Windows. If the flag is not set, returns the default value (false). + /// + public bool UseAnsiColorOrDefault => UseAnsiColor ?? false; + /// /// Gets or sets a flag indicating that the test runner should pause after all tests /// have run. diff --git a/src/xunit.v3.runner.common/Loggers/ConsoleRunnerLogger.cs b/src/xunit.v3.runner.common/Loggers/ConsoleRunnerLogger.cs index 83afe01da..ccb9266f8 100644 --- a/src/xunit.v3.runner.common/Loggers/ConsoleRunnerLogger.cs +++ b/src/xunit.v3.runner.common/Loggers/ConsoleRunnerLogger.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.IO; using System.Text.RegularExpressions; using Xunit.Internal; @@ -14,13 +15,33 @@ public class ConsoleRunnerLogger : IRunnerLogger readonly static Regex ansiSgrRegex = new Regex("\\e\\[\\d*(;\\d*)*m"); readonly bool useColors; + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use the new overload with the useAnsiColor flag")] + public ConsoleRunnerLogger(bool useColors) : + this(useColors, useAnsiColor: false, new object()) + { } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use the new overload with the useAnsiColor flag")] + public ConsoleRunnerLogger( + bool useColors, + object lockObject) : + this(useColors, useAnsiColor: false, lockObject) + { } + /// /// Initializes a new instance of the class. /// /// A flag to indicate whether colors should be used when /// logging messages. - public ConsoleRunnerLogger(bool useColors) - : this(useColors, new object()) + /// A flag to indicate whether ANSI colors should be + /// forced on Windows. + public ConsoleRunnerLogger( + bool useColors, + bool useAnsiColor) + : this(useColors, useAnsiColor, new object()) { } /// @@ -28,15 +49,21 @@ public ConsoleRunnerLogger(bool useColors) /// /// A flag to indicate whether colors should be used when /// logging messages. + /// A flag to indicate whether ANSI colors should be + /// forced on Windows. /// The lock object used to prevent console clashes. public ConsoleRunnerLogger( bool useColors, + bool useAnsiColor, object lockObject) { Guard.ArgumentNotNull(lockObject); this.useColors = useColors; LockObject = lockObject; + + if (useAnsiColor) + ConsoleHelper.UseAnsiColor(); } /// @@ -51,7 +78,7 @@ public ConsoleRunnerLogger(bool useColors) lock (LockObject) using (SetColor(ConsoleColor.Red)) - WriteLine(Console.Error, message); + WriteLine(Console.Out, message); } /// diff --git a/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs b/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs index 37eaa4ed2..bdab4b991 100644 --- a/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs +++ b/src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs @@ -84,6 +84,7 @@ public abstract class CommandLineParserBase AddParser("pause", OnPause, CommandLineGroup.General, null, "wait for input before running tests"); AddParser("preEnumerateTheories", OnPreEnumerateTheories, CommandLineGroup.General, null, "enable theory pre-enumeration (disabled by default)"); AddParser("stopOnFail", OnStopOnFail, CommandLineGroup.General, null, "stop on first test failure"); + AddParser("useAnsiColor", OnUseAnsiColor, CommandLineGroup.General, null, "force using ANSI color output on Windows (non-Windows always uses ANSI colors)"); AddParser("wait", OnWait, CommandLineGroup.General, null, "wait for input after completion"); // Filter options @@ -539,6 +540,12 @@ void OnTraitMinus(KeyValuePair option) projectAssembly.Configuration.Filters.ExcludedTraits.Add(name, value); } + void OnUseAnsiColor(KeyValuePair option) + { + GuardNoOptionValue(option); + Project.Configuration.UseAnsiColor = true; + } + void OnWait(KeyValuePair option) { GuardNoOptionValue(option); diff --git a/src/xunit.v3.runner.common/Utility/ConsoleHelper.cs b/src/xunit.v3.runner.common/Utility/ConsoleHelper.cs index 45ec5fb1e..b152d3a0f 100644 --- a/src/xunit.v3.runner.common/Utility/ConsoleHelper.cs +++ b/src/xunit.v3.runner.common/Utility/ConsoleHelper.cs @@ -15,17 +15,17 @@ public static class ConsoleHelper /// /// Equivalent to .. /// - public static Action ResetColor { get; } + public static Action ResetColor { get; private set; } /// /// Equivalent to .. /// - public static Action SetBackgroundColor { get; } + public static Action SetBackgroundColor { get; private set; } /// /// Equivalent to .. /// - public static Action SetForegroundColor { get; } + public static Action SetForegroundColor { get; private set; } static ConsoleHelper() { @@ -106,4 +106,14 @@ static void SetForegroundColorANSI(ConsoleColor c) static void ResetColorConsole() => Console.ResetColor(); + + /// + /// Force using ANSI color instead of deciding based on OS. + /// + public static void UseAnsiColor() + { + ResetColor = ResetColorANSI; + SetBackgroundColor = SetBackgroundColorANSI; + SetForegroundColor = SetForegroundColorANSI; + } } diff --git a/src/xunit.v3.runner.console/ConsoleRunner.cs b/src/xunit.v3.runner.console/ConsoleRunner.cs index 769d20d34..1ef15a3f7 100644 --- a/src/xunit.v3.runner.console/ConsoleRunner.cs +++ b/src/xunit.v3.runner.console/ConsoleRunner.cs @@ -66,6 +66,9 @@ public async ValueTask EntryPoint() } var project = commandLine.Parse(); + var useAnsiColor = project.Configuration.UseAnsiColorOrDefault; + if (useAnsiColor) + ConsoleHelper.UseAnsiColor(); if (project.Assemblies.Count == 0) throw new ArgumentException("must specify at least one assembly"); @@ -94,7 +97,7 @@ public async ValueTask EntryPoint() var globalDiagnosticMessages = project.Assemblies.Any(a => a.Configuration.DiagnosticMessagesOrDefault); globalInternalDiagnosticMessages = project.Assemblies.Any(a => a.Configuration.InternalDiagnosticMessagesOrDefault); noColor = project.Configuration.NoColorOrDefault; - logger = new ConsoleRunnerLogger(!noColor, consoleLock); + logger = new ConsoleRunnerLogger(!noColor, useAnsiColor, consoleLock); var globalDiagnosticMessageSink = ConsoleDiagnosticMessageSink.TryCreate(consoleLock, noColor, globalDiagnosticMessages, globalInternalDiagnosticMessages); var reporter = project.RunnerReporter; var reporterMessageHandler = await reporter.CreateMessageHandler(logger, globalDiagnosticMessageSink); diff --git a/src/xunit.v3.runner.inproc.console/ConsoleRunner.cs b/src/xunit.v3.runner.inproc.console/ConsoleRunner.cs index 2839cf6d9..9a85a3302 100644 --- a/src/xunit.v3.runner.inproc.console/ConsoleRunner.cs +++ b/src/xunit.v3.runner.inproc.console/ConsoleRunner.cs @@ -90,6 +90,9 @@ public async ValueTask EntryPoint() noColor = true; var project = commandLine.Parse(); + var useAnsiColor = project.Configuration.UseAnsiColorOrDefault; + if (useAnsiColor) + ConsoleHelper.UseAnsiColor(); AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; @@ -127,7 +130,7 @@ public async ValueTask EntryPoint() if (!automated) noColor = project.Configuration.NoColorOrDefault; - logger = new ConsoleRunnerLogger(!noColor, consoleLock); + logger = new ConsoleRunnerLogger(!noColor, useAnsiColor, consoleLock); _IMessageSink? globalDiagnosticMessageSink = automated