Skip to content

Commit

Permalink
#783: Add -useAnsiColor flag to console runners (v3)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradwilson committed Apr 24, 2024
1 parent d1c4f65 commit 282c88b
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 12 deletions.
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);
}
}
}
12 changes: 12 additions & 0 deletions src/xunit.v3.runner.common/Frameworks/TestProjectConfiguration.cs
Expand Up @@ -103,6 +103,18 @@ public class TestProjectConfiguration
/// </summary>
public bool PauseOrDefault => Pause ?? false;

/// <summary>
/// Gets or sets a flag indicating that ANSI color usage should be forced on Windows.
/// ANSI color is always used for non-Windows.
/// </summary>
public bool? UseAnsiColor { get; set; }

/// <summary>
/// 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 (<c>false</c>).
/// </summary>
public bool UseAnsiColorOrDefault => UseAnsiColor ?? false;

/// <summary>
/// Gets or sets a flag indicating that the test runner should pause after all tests
/// have run.
Expand Down
33 changes: 30 additions & 3 deletions 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;
Expand All @@ -14,29 +15,55 @@ public class ConsoleRunnerLogger : IRunnerLogger
readonly static Regex ansiSgrRegex = new Regex("\\e\\[\\d*(;\\d*)*m");
readonly bool useColors;

/// <summary/>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Please use the new overload with the useAnsiColor flag")]
public ConsoleRunnerLogger(bool useColors) :
this(useColors, useAnsiColor: false, new object())
{ }

/// <summary/>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Please use the new overload with the useAnsiColor flag")]
public ConsoleRunnerLogger(
bool useColors,
object lockObject) :
this(useColors, useAnsiColor: false, lockObject)
{ }

/// <summary>
/// Initializes a new instance of the <see cref="ConsoleRunnerLogger"/> class.
/// </summary>
/// <param name="useColors">A flag to indicate whether colors should be used when
/// logging messages.</param>
public ConsoleRunnerLogger(bool useColors)
: this(useColors, new object())
/// <param name="useAnsiColor">A flag to indicate whether ANSI colors should be
/// forced on Windows.</param>
public ConsoleRunnerLogger(
bool useColors,
bool useAnsiColor)
: this(useColors, useAnsiColor, new object())
{ }

/// <summary>
/// Initializes a new instance of the <see cref="ConsoleRunnerLogger"/> class.
/// </summary>
/// <param name="useColors">A flag to indicate whether colors should be used when
/// logging messages.</param>
/// <param name="useAnsiColor">A flag to indicate whether ANSI colors should be
/// forced on Windows.</param>
/// <param name="lockObject">The lock object used to prevent console clashes.</param>
public ConsoleRunnerLogger(
bool useColors,
bool useAnsiColor,
object lockObject)
{
Guard.ArgumentNotNull(lockObject);

this.useColors = useColors;
LockObject = lockObject;

if (useAnsiColor)
ConsoleHelper.UseAnsiColor();
}

/// <inheritdoc/>
Expand All @@ -51,7 +78,7 @@ public ConsoleRunnerLogger(bool useColors)

lock (LockObject)
using (SetColor(ConsoleColor.Red))
WriteLine(Console.Error, message);
WriteLine(Console.Out, message);
}

/// <inheritdoc/>
Expand Down
7 changes: 7 additions & 0 deletions src/xunit.v3.runner.common/Parsers/CommandLineParserBase.cs
Expand Up @@ -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
Expand Down Expand Up @@ -539,6 +540,12 @@ void OnTraitMinus(KeyValuePair<string, string?> option)
projectAssembly.Configuration.Filters.ExcludedTraits.Add(name, value);
}

void OnUseAnsiColor(KeyValuePair<string, string?> option)
{
GuardNoOptionValue(option);
Project.Configuration.UseAnsiColor = true;
}

void OnWait(KeyValuePair<string, string?> option)
{
GuardNoOptionValue(option);
Expand Down
16 changes: 13 additions & 3 deletions src/xunit.v3.runner.common/Utility/ConsoleHelper.cs
Expand Up @@ -15,17 +15,17 @@ public static class ConsoleHelper
/// <summary>
/// Equivalent to <see cref="Console"/>.<see cref="Console.ResetColor"/>.
/// </summary>
public static Action ResetColor { get; }
public static Action ResetColor { get; private set; }

/// <summary>
/// Equivalent to <see cref="Console"/>.<see cref="Console.BackgroundColor"/>.
/// </summary>
public static Action<ConsoleColor> SetBackgroundColor { get; }
public static Action<ConsoleColor> SetBackgroundColor { get; private set; }

/// <summary>
/// Equivalent to <see cref="Console"/>.<see cref="Console.ForegroundColor"/>.
/// </summary>
public static Action<ConsoleColor> SetForegroundColor { get; }
public static Action<ConsoleColor> SetForegroundColor { get; private set; }

static ConsoleHelper()
{
Expand Down Expand Up @@ -106,4 +106,14 @@ static void SetForegroundColorANSI(ConsoleColor c)

static void ResetColorConsole() =>
Console.ResetColor();

/// <summary>
/// Force using ANSI color instead of deciding based on OS.
/// </summary>
public static void UseAnsiColor()
{
ResetColor = ResetColorANSI;
SetBackgroundColor = SetBackgroundColorANSI;
SetForegroundColor = SetForegroundColorANSI;
}
}
5 changes: 4 additions & 1 deletion src/xunit.v3.runner.console/ConsoleRunner.cs
Expand Up @@ -66,6 +66,9 @@ public async ValueTask<int> 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");

Expand Down Expand Up @@ -94,7 +97,7 @@ public async ValueTask<int> 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);
Expand Down
5 changes: 4 additions & 1 deletion src/xunit.v3.runner.inproc.console/ConsoleRunner.cs
Expand Up @@ -90,6 +90,9 @@ public async ValueTask<int> EntryPoint()
noColor = true;

var project = commandLine.Parse();
var useAnsiColor = project.Configuration.UseAnsiColorOrDefault;
if (useAnsiColor)
ConsoleHelper.UseAnsiColor();

AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;

Expand Down Expand Up @@ -127,7 +130,7 @@ public async ValueTask<int> EntryPoint()
if (!automated)
noColor = project.Configuration.NoColorOrDefault;

logger = new ConsoleRunnerLogger(!noColor, consoleLock);
logger = new ConsoleRunnerLogger(!noColor, useAnsiColor, consoleLock);

_IMessageSink? globalDiagnosticMessageSink =
automated
Expand Down

0 comments on commit 282c88b

Please sign in to comment.