Skip to content

Show colors in AzDo and GH actions #5535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 2, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,13 @@ internal sealed class AnsiTerminal : ITerminal

private readonly IConsole _console;
private readonly string? _baseDirectory;
private readonly bool _useBusyIndicator;
private readonly StringBuilder _stringBuilder = new();
private bool _isBatching;
private AnsiTerminalTestProgressFrame _currentFrame = new(0, 0);

public AnsiTerminal(IConsole console, string? baseDirectory)
{
_console = console;
_baseDirectory = baseDirectory ?? Directory.GetCurrentDirectory();

// Output ansi code to get spinner on top of a terminal, to indicate in-progress task.
// https://github.com/dotnet/msbuild/issues/8958: iTerm2 treats ;9 code to post a notification instead, so disable progress reporting on Mac.
_useBusyIndicator = !RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
}

public int Width
Expand Down Expand Up @@ -243,72 +237,36 @@ public void AppendLink(string? path, int? lineNumber)
ResetColor();
}

public void MoveCursorUp(int lineCount)
public static void MoveCursorUp(int _)
{
string moveCursor = $"{AnsiCodes.CSI}{lineCount}{AnsiCodes.MoveUpToLineStart}";
if (_isBatching)
{
_stringBuilder.AppendLine(moveCursor);
}
else
{
_console.WriteLine(moveCursor);
}
return;
}

public void SetCursorHorizontal(int position)
public static void SetCursorHorizontal(int _)
{
string setCursor = AnsiCodes.SetCursorHorizontal(position);
if (_isBatching)
{
_stringBuilder.Append(setCursor);
}
else
{
_console.Write(setCursor);
}
return;
}

/// <summary>
/// Erases the previously printed live node output.
/// </summary>
public void EraseProgress()
{
if (_currentFrame.RenderedLines == null || _currentFrame.RenderedLines.Count == 0)
{
return;
}

AppendLine($"{AnsiCodes.CSI}{_currentFrame.RenderedLines.Count + 2}{AnsiCodes.MoveUpToLineStart}");
Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInDisplay}");
_currentFrame.Clear();
return;
}

public void RenderProgress(TestProgressState?[] progress)
{
AnsiTerminalTestProgressFrame newFrame = new(Width, Height);
newFrame.Render(_currentFrame, progress, terminal: this);

_currentFrame = newFrame;
return;
}

public void StartBusyIndicator()
{
if (_useBusyIndicator)
{
Append(AnsiCodes.SetBusySpinner);
}

HideCursor();
return;
}

public void StopBusyIndicator()
{
if (_useBusyIndicator)
{
Append(AnsiCodes.RemoveBusySpinner);
}

ShowCursor();
return;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public void AppendTestWorkerProgress(TestProgressState progress, RenderedProgres
}
}

terminal.SetCursorHorizontal(Width - durationString.Length);
AnsiTerminal.SetCursorHorizontal(Width - durationString.Length);
terminal.Append(durationString);
}

Expand All @@ -133,7 +133,7 @@ public void AppendTestWorkerDetail(TestDetailState detail, RenderedProgressItem

AppendToWidth(terminal, detail.Text, nonReservedWidth, ref charsTaken);

terminal.SetCursorHorizontal(Width - durationString.Length);
AnsiTerminal.SetCursorHorizontal(Width - durationString.Length);
terminal.Append(durationString);
}

Expand Down Expand Up @@ -182,7 +182,7 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat
{
// Move cursor back to 1st line of progress.
// + 2 because we output and empty line right below.
terminal.MoveCursorUp(previousFrame.RenderedLines.Count + 2);
AnsiTerminal.MoveCursorUp(previousFrame.RenderedLines.Count + 2);
}

// When there is nothing to render, don't write empty lines, e.g. when we start the test run, and then we kick off build
Expand Down Expand Up @@ -215,7 +215,7 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat
if (previouslyRenderedLine.RenderedDurationLength == durationString.Length)
{
// Duration is the same length rewrite just it.
terminal.SetCursorHorizontal(MaxColumn);
AnsiTerminal.SetCursorHorizontal(MaxColumn);
terminal.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}");
currentLine.RenderedDurationLength = durationString.Length;
}
Expand Down Expand Up @@ -250,7 +250,7 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat
if (previouslyRenderedLine.RenderedDurationLength == durationString.Length)
{
// Duration is the same length rewrite just it.
terminal.SetCursorHorizontal(MaxColumn);
AnsiTerminal.SetCursorHorizontal(MaxColumn);
terminal.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}");
currentLine.RenderedDurationLength = durationString.Length;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ public TerminalTestReporter(IConsole console, TerminalTestReporterOptions option
{
// Autodetect.
(bool consoleAcceptsAnsiCodes, bool _, uint? originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes();
#pragma warning disable RS0030 // Do not use banned APIs
if (Environment.GetEnvironmentVariable("TF_BUILD") != null)
{
consoleAcceptsAnsiCodes = true;
}
#pragma warning restore RS0030 // Do not use banned APIs
_originalConsoleMode = originalConsoleMode;
terminalWithProgress = consoleAcceptsAnsiCodes || _options.ForceAnsi is true
? new TestProgressStateAwareTerminal(new AnsiTerminal(console, _options.BaseDirectory), showProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: ansiUpdateCadenceInMs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private async Task ConsoleTestsCoreAsync(string tfm, string? environmentVariable
};
}

TestHostResult testHostResult = await testHost.ExecuteAsync("--no-ansi --ignore-exit-code 8", environmentVariables);
TestHostResult testHostResult = await testHost.ExecuteAsync("--ignore-exit-code 8", environmentVariables);
testHostResult.AssertExitCodeIs(ExitCodes.Success);
testHostResult.AssertOutputContains("ABCDEF123");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void ExitOnProcessExit_Succeed(string tfm)
}
}

if (startTime.Elapsed.TotalSeconds > 55)
if (startTime.Elapsed.TotalSeconds > 60)
{
throw new Exception("Process PID not found in 60 seconds");
}
Expand All @@ -48,7 +48,8 @@ public void ExitOnProcessExit_Succeed(string tfm)
startTime = Stopwatch.StartNew();
while (!process.HasExited)
{
if (startTime.Elapsed.TotalSeconds > 55)
Thread.Sleep(1000);
if (startTime.Elapsed.TotalSeconds > 60)
{
throw new Exception("Process did not exit in 60 seconds");
}
Expand Down Expand Up @@ -83,7 +84,7 @@ public sealed class TestAssetFixture() : TestAssetFixtureBase(AcceptanceFixture.
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestFramework;

if (args.Length == 0)
if (!args.Contains("--exit-on-process-exit"))
{
int currentPid = Process.GetCurrentProcess().Id;
var currentEntryPoint = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!, Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()!.Location)
Expand All @@ -93,7 +94,7 @@ public sealed class TestAssetFixture() : TestAssetFixtureBase(AcceptanceFixture.
Environment.SetEnvironmentVariable("WaitTestHost", mutexName);
ProcessStartInfo processStartInfo = new();
processStartInfo.FileName = currentEntryPoint;
processStartInfo.Arguments = $"--exit-on-process-exit {currentPid}";
processStartInfo.Arguments = $"--exit-on-process-exit {currentPid} --no-progress --no-ansi";
processStartInfo.UseShellExecute = false;
var process = Process.Start(processStartInfo);
while (!Mutex.TryOpenExisting(mutexName, out Mutex? _))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ public async Task<TestHostResult> ExecuteAsync(
.ExecuteAsync(async () =>
{
CommandLine commandLine = new();
// Disable ANSI rendering so tests have easier time parsing the output.
// Disable progress so tests don't mix progress with overall progress, and with test process output.
int exitCode = await commandLine.RunAsyncAndReturnExitCodeAsync(
$"{FullName} {finalArguments}",
$"{FullName} --no-ansi --no-progress {finalArguments}",
environmentVariables: environmentVariables,
workingDirectory: null,
cleanDefaultEnvironmentVariableIfCustomAreProvided: true,
Expand Down
Loading