Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9bab5fa
Initial plan
Copilot Dec 11, 2025
7edc899
Add --ansi option with validation and localization
Copilot Dec 11, 2025
2698d68
Add unit and integration tests for --ansi option
Copilot Dec 11, 2025
9f195f8
Fix ANSI option handling to properly support force enable/disable modes
Copilot Dec 11, 2025
349dd3c
Address PR feedback: Add reusable validator, obsolescence support, an…
Copilot Dec 12, 2025
6b35380
Optimize CommandLineOptionArgumentValidator to avoid array allocations
Copilot Dec 12, 2025
c005a4f
Merge branch 'main' into copilot/add-ansi-option-to-force-output
Evangelink Dec 12, 2025
a469621
Apply suggestion from @Evangelink
Evangelink Dec 12, 2025
e9922d3
Add generic boolean validator and improve description with multiline …
Copilot Dec 12, 2025
f95c87a
Update xlf files with multiline description format
Copilot Dec 12, 2025
7772161
Remove duplicate properties, unused constants, and wrapper methods
Copilot Dec 12, 2025
67d07c6
Merge branch 'main' into copilot/add-ansi-option-to-force-output
Evangelink Dec 15, 2025
94213a7
Fix
Evangelink Dec 15, 2025
43f4b44
Fix acceptance tests
Evangelink Dec 15, 2025
a41fada
Fix MSTest acceptance test
Evangelink Dec 15, 2025
7be0121
Improve display or warnings + avoid obsolete in our test infra
Evangelink Dec 15, 2025
f805e56
Remove empty line
Evangelink Dec 15, 2025
d3008f7
Simplify code
Evangelink Dec 17, 2025
3a5bdef
Merge branch 'main' into copilot/add-ansi-option-to-force-output
Evangelink Dec 17, 2025
9769675
Fix test
Evangelink Dec 17, 2025
1bb0302
Merge branch 'main' into copilot/add-ansi-option-to-force-output
Evangelink Dec 17, 2025
fa1081c
Introduce AnsiMode enum for clearer ANSI control
Copilot Dec 18, 2025
fb51c86
Remove unnecessary customization parameters from CommandLineOptionArg…
Copilot Dec 18, 2025
317f322
Rename AnsiMode to OptionMode with On/Off values for reusability
Copilot Dec 18, 2025
937f73b
Rename OptionMode to TriStateMode for better clarity
Copilot Dec 18, 2025
9f462aa
Rename TriStateMode to ActivationMode
Copilot Dec 18, 2025
830bb20
Fix
Evangelink Dec 18, 2025
6a4bcd6
Final updates
Evangelink Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/Platform/Microsoft.Testing.Platform/CommandLine/AutoOnOff.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.CommandLine;

/// <summary>
/// Specifies the activation mode for command line options that support auto-detection or explicit on/off states.
/// </summary>
internal enum AutoOnOff
{
/// <summary>
/// Auto-detect the appropriate setting.
/// </summary>
Auto,

/// <summary>
/// Force enable the option.
/// </summary>
On,

/// <summary>
/// Force disable the option.
/// </summary>
Off,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.CommandLine;

/// <summary>
/// Provides validation helpers for command line option arguments.
/// </summary>
internal static class CommandLineOptionArgumentValidator
{
private static readonly string[] DefaultOnValues = ["on", "true", "enable", "1"];
private static readonly string[] DefaultOffValues = ["off", "false", "disable", "0"];

/// <summary>
/// Validates that an argument is one of the accepted on/off boolean values.
/// </summary>
/// <param name="argument">The argument to validate.</param>
/// <returns>True if the argument is valid; otherwise, false.</returns>
public static bool IsValidBooleanArgument(string argument)
{
foreach (string onValue in DefaultOnValues)
{
if (onValue.Equals(argument, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

foreach (string offValue in DefaultOffValues)
{
if (offValue.Equals(argument, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

/// <summary>
/// Validates that an argument is one of the accepted on/off/auto values.
/// </summary>
/// <param name="argument">The argument to validate.</param>
/// <returns>True if the argument is valid; otherwise, false.</returns>
public static bool IsValidBooleanAutoArgument(string argument)
=> "auto".Equals(argument, StringComparison.OrdinalIgnoreCase)
|| IsValidBooleanArgument(argument);

/// <summary>
/// Determines if an argument represents an "on/enabled" state.
/// </summary>
/// <param name="argument">The argument to check.</param>
/// <returns>True if the argument represents an enabled state; otherwise, false.</returns>
public static bool IsOnValue(string argument)
{
foreach (string onValue in DefaultOnValues)
{
if (onValue.Equals(argument, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

/// <summary>
/// Determines if an argument represents an "off/disabled" state.
/// </summary>
/// <param name="argument">The argument to check.</param>
/// <returns>True if the argument represents a disabled state; otherwise, false.</returns>
public static bool IsOffValue(string argument)
{
foreach (string offValue in DefaultOffValues)
{
if (offValue.Equals(argument, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Resources;
using Microsoft.Testing.Platform.Services;
Expand Down Expand Up @@ -86,36 +87,30 @@ public TerminalTestReporter(
_testApplicationCancellationTokenSource = testApplicationCancellationTokenSource;
_options = options;

Func<bool?> showProgress = _options.ShowProgress;
TestProgressStateAwareTerminal terminalWithProgress;

// When not writing to ANSI we write the progress to screen and leave it there so we don't want to write it more often than every few seconds.
int nonAnsiUpdateCadenceInMs = 3_000;
// When writing to ANSI we update the progress in place and it should look responsive so we update every half second, because we only show seconds on the screen, so it is good enough.
int ansiUpdateCadenceInMs = 500;
if (!_options.UseAnsi || _options.ForceAnsi is false)
{
terminalWithProgress = new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs);
}
else

(_terminalWithProgress, _originalConsoleMode) = _options.AnsiMode switch
{
if (_options.UseCIAnsi)
{
// We are told externally that we are in CI, use simplified ANSI mode.
terminalWithProgress = new TestProgressStateAwareTerminal(new SimpleAnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: nonAnsiUpdateCadenceInMs);
}
else
{
// We are not in CI, or in CI non-compatible with simple ANSI, autodetect terminal capabilities
(bool consoleAcceptsAnsiCodes, bool _, uint? originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes();
_originalConsoleMode = originalConsoleMode;
terminalWithProgress = consoleAcceptsAnsiCodes || _options.ForceAnsi is true
? new TestProgressStateAwareTerminal(new AnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: ansiUpdateCadenceInMs)
: new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs);
}
}
AutoOnOff.Off => (new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), _options.ShowProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs), null),
AutoOnOff.On => _options.UseCIAnsi
? (new TestProgressStateAwareTerminal(new SimpleAnsiTerminal(console), _options.ShowProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: nonAnsiUpdateCadenceInMs), null)
: (new TestProgressStateAwareTerminal(new AnsiTerminal(console), _options.ShowProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: ansiUpdateCadenceInMs), null),
AutoOnOff.Auto => AutoDetectTerminal(console, _options.ShowProgress, nonAnsiUpdateCadenceInMs, ansiUpdateCadenceInMs),
_ => throw new NotSupportedException("Unsupported ANSI mode: " + _options.AnsiMode),
};
}

_terminalWithProgress = terminalWithProgress;
private static (TestProgressStateAwareTerminal Terminal, uint? OriginalConsoleMode) AutoDetectTerminal(IConsole console, Func<bool?> showProgress, int nonAnsiUpdateCadenceInMs, int ansiUpdateCadenceInMs)
{
// We are not in CI, or in CI non-compatible with simple ANSI, autodetect terminal capabilities
(bool consoleAcceptsAnsiCodes, bool _, uint? originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes();
TestProgressStateAwareTerminal terminalWithProgress = consoleAcceptsAnsiCodes
? new TestProgressStateAwareTerminal(new AnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: ansiUpdateCadenceInMs)
: new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs);
return (terminalWithProgress, originalConsoleMode);
}

public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal sealed class TerminalTestReporterCommandLineOptionsProvider : ICommandL
{
public const string NoProgressOption = "no-progress";
public const string NoAnsiOption = "no-ansi";
public const string AnsiOption = "ansi";
public const string OutputOption = "output";
public const string OutputOptionNormalArgument = "normal";
public const string OutputOptionDetailedArgument = "detailed";
Expand All @@ -36,7 +37,8 @@ public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions()
=>
[
new(NoProgressOption, PlatformResources.TerminalNoProgressOptionDescription, ArgumentArity.Zero, isHidden: false),
new(NoAnsiOption, PlatformResources.TerminalNoAnsiOptionDescription, ArgumentArity.Zero, isHidden: false),
new(NoAnsiOption, PlatformResources.TerminalNoAnsiOptionDescription, ArgumentArity.Zero, isHidden: false, PlatformResources.TerminalNoAnsiOptionObsoleteMessage),
new(AnsiOption, PlatformResources.TerminalAnsiOptionDescription, ArgumentArity.ExactlyOne, isHidden: false),
new(OutputOption, PlatformResources.TerminalOutputOptionDescription, ArgumentArity.ExactlyOne, isHidden: false),
];

Expand All @@ -45,6 +47,9 @@ public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption com
{
NoProgressOption => ValidationResult.ValidTask,
NoAnsiOption => ValidationResult.ValidTask,
AnsiOption => CommandLineOptionArgumentValidator.IsValidBooleanAutoArgument(arguments[0])
? ValidationResult.ValidTask
: ValidationResult.InvalidTask(PlatformResources.TerminalAnsiOptionInvalidArgument),
OutputOption => OutputOptionNormalArgument.Equals(arguments[0], StringComparison.OrdinalIgnoreCase) || OutputOptionDetailedArgument.Equals(arguments[0], StringComparison.OrdinalIgnoreCase)
? ValidationResult.ValidTask
: ValidationResult.InvalidTask(PlatformResources.TerminalOutputOptionInvalidArgument),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Platform.CommandLine;

namespace Microsoft.Testing.Platform.OutputDevice.Terminal;

internal sealed class TerminalTestReporterOptions
Expand All @@ -27,19 +29,14 @@ internal sealed class TerminalTestReporterOptions
/// </summary>
public bool ShowActiveTests { get; init; }

/// <summary>
/// Gets a value indicating whether we should use ANSI escape codes or disable them. When true the capabilities of the console are autodetected.
/// </summary>
public bool UseAnsi { get; init; }

/// <summary>
/// Gets a value indicating whether we are running in compatible CI, and should use simplified ANSI renderer, which colors output, but does not move cursor.
/// Setting <see cref="UseAnsi"/> to false will disable this option.
/// Setting <see cref="AnsiMode"/> to <see cref="AutoOnOff.Off"/> will disable this option.
/// </summary>
public bool UseCIAnsi { get; init; }

/// <summary>
/// Gets a value indicating whether we should force ANSI escape codes. When true the ANSI is used without auto-detecting capabilities of the console. This is needed only for testing.
/// Gets the ANSI mode for the terminal output.
/// </summary>
internal /* for testing */ bool? ForceAnsi { get; init; }
public AutoOnOff AnsiMode { get; init; } = AutoOnOff.Auto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,39 @@ await _policiesService.RegisterOnAbortCallbackAsync(

_isListTests = _commandLineOptions.IsOptionSet(PlatformCommandLineProvider.DiscoverTestsOptionKey);
_isServerMode = _commandLineOptions.IsOptionSet(PlatformCommandLineProvider.ServerOptionKey);
bool noAnsi = _commandLineOptions.IsOptionSet(TerminalTestReporterCommandLineOptionsProvider.NoAnsiOption);

// Determine ANSI output setting
AutoOnOff ansiMode;
if (_commandLineOptions.TryGetOptionArgumentList(TerminalTestReporterCommandLineOptionsProvider.AnsiOption, out string[]? ansiArguments) && ansiArguments?.Length > 0)
{
// New --ansi option takes precedence
string ansiValue = ansiArguments[0];
if (CommandLineOptionArgumentValidator.IsOnValue(ansiValue))
{
// Force enable ANSI
ansiMode = AutoOnOff.On;
}
else if (CommandLineOptionArgumentValidator.IsOffValue(ansiValue))
{
// Force disable ANSI
ansiMode = AutoOnOff.Off;
}
else
{
// Auto mode - detect capabilities
ansiMode = AutoOnOff.Auto;
}
}
else if (_commandLineOptions.IsOptionSet(TerminalTestReporterCommandLineOptionsProvider.NoAnsiOption))
{
// Backward compatibility with --no-ansi
ansiMode = AutoOnOff.Off;
}
else
{
// Default is auto mode - detect capabilities
ansiMode = AutoOnOff.Auto;
}

// TODO: Replace this with proper CI detection that we already have in telemetry. https://github.com/microsoft/testfx/issues/5533#issuecomment-2838893327
bool inCI = string.Equals(_environment.GetEnvironmentVariable("TF_BUILD"), "true", StringComparison.OrdinalIgnoreCase) || string.Equals(_environment.GetEnvironmentVariable("GITHUB_ACTIONS"), "true", StringComparison.OrdinalIgnoreCase);
Expand Down Expand Up @@ -158,7 +190,7 @@ await _policiesService.RegisterOnAbortCallbackAsync(
{
ShowPassedTests = showPassed,
MinimumExpectedTests = PlatformCommandLineProvider.GetMinimumExpectedTests(_commandLineOptions),
UseAnsi = !noAnsi,
AnsiMode = ansiMode,
UseCIAnsi = inCI,
ShowActiveTests = true,
ShowProgress = shouldShowProgress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,18 @@ Read more about Microsoft Testing Platform telemetry: https://aka.ms/testingplat
<data name="TerminalNoAnsiOptionDescription" xml:space="preserve">
<value>Disable outputting ANSI escape characters to screen.</value>
</data>
<data name="TerminalNoAnsiOptionObsoleteMessage" xml:space="preserve">
<value>Use '--ansi off' instead of '--no-ansi'.</value>
</data>
<data name="TerminalAnsiOptionDescription" xml:space="preserve">
<value>Control ANSI escape characters output.
--ansi auto - Auto-detect terminal capabilities (default)
--ansi on|true|enable|1 - Force enable ANSI escape sequences
--ansi off|false|disable|0 - Force disable ANSI escape sequences</value>
</data>
<data name="TerminalAnsiOptionInvalidArgument" xml:space="preserve">
<value>--ansi expects a single parameter with value 'auto', 'on', or 'off' (also accepts 'true', 'enable', '1', 'false', 'disable', '0').</value>
</data>
<data name="TerminalNoProgressOptionDescription" xml:space="preserve">
<value>Disable reporting progress to screen.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -808,11 +808,32 @@ Přečtěte si další informace o telemetrii Microsoft Testing Platform: https:
<target state="translated">Zprostředkovatel telemetrie je už nastavený.</target>
<note />
</trans-unit>
<trans-unit id="TerminalAnsiOptionDescription">
<source>Control ANSI escape characters output.
--ansi auto - Auto-detect terminal capabilities (default)
--ansi on|true|enable|1 - Force enable ANSI escape sequences
--ansi off|false|disable|0 - Force disable ANSI escape sequences</source>
<target state="new">Control ANSI escape characters output.
--ansi auto - Auto-detect terminal capabilities (default)
--ansi on|true|enable|1 - Force enable ANSI escape sequences
--ansi off|false|disable|0 - Force disable ANSI escape sequences</target>
<note />
</trans-unit>
<trans-unit id="TerminalAnsiOptionInvalidArgument">
<source>--ansi expects a single parameter with value 'auto', 'on', or 'off' (also accepts 'true', 'enable', '1', 'false', 'disable', '0').</source>
<target state="new">--ansi expects a single parameter with value 'auto', 'on', or 'off' (also accepts 'true', 'enable', '1', 'false', 'disable', '0').</target>
<note />
</trans-unit>
<trans-unit id="TerminalNoAnsiOptionDescription">
<source>Disable outputting ANSI escape characters to screen.</source>
<target state="translated">Zakažte výstup řídicích znaků ANSI na obrazovku.</target>
<note />
</trans-unit>
<trans-unit id="TerminalNoAnsiOptionObsoleteMessage">
<source>Use '--ansi off' instead of '--no-ansi'.</source>
<target state="new">Use '--ansi off' instead of '--no-ansi'.</target>
<note />
</trans-unit>
<trans-unit id="TerminalNoProgressOptionDescription">
<source>Disable reporting progress to screen.</source>
<target state="translated">Zakažte zobrazování průběhu vytváření sestav na obrazovce.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -808,11 +808,32 @@ Weitere Informationen zu Microsoft Testing Platform-Telemetriedaten: https://aka
<target state="translated">Der Telemetrieanbieter ist bereits festgelegt</target>
<note />
</trans-unit>
<trans-unit id="TerminalAnsiOptionDescription">
<source>Control ANSI escape characters output.
--ansi auto - Auto-detect terminal capabilities (default)
--ansi on|true|enable|1 - Force enable ANSI escape sequences
--ansi off|false|disable|0 - Force disable ANSI escape sequences</source>
<target state="new">Control ANSI escape characters output.
--ansi auto - Auto-detect terminal capabilities (default)
--ansi on|true|enable|1 - Force enable ANSI escape sequences
--ansi off|false|disable|0 - Force disable ANSI escape sequences</target>
<note />
</trans-unit>
<trans-unit id="TerminalAnsiOptionInvalidArgument">
<source>--ansi expects a single parameter with value 'auto', 'on', or 'off' (also accepts 'true', 'enable', '1', 'false', 'disable', '0').</source>
<target state="new">--ansi expects a single parameter with value 'auto', 'on', or 'off' (also accepts 'true', 'enable', '1', 'false', 'disable', '0').</target>
<note />
</trans-unit>
<trans-unit id="TerminalNoAnsiOptionDescription">
<source>Disable outputting ANSI escape characters to screen.</source>
<target state="translated">Deaktivieren Sie die Ausgabe von ANSI-Escape-Zeichen auf dem Bildschirm.</target>
<note />
</trans-unit>
<trans-unit id="TerminalNoAnsiOptionObsoleteMessage">
<source>Use '--ansi off' instead of '--no-ansi'.</source>
<target state="new">Use '--ansi off' instead of '--no-ansi'.</target>
<note />
</trans-unit>
<trans-unit id="TerminalNoProgressOptionDescription">
<source>Disable reporting progress to screen.</source>
<target state="translated">Deaktivieren Sie die Berichterstellung für den Status des Bildschirms.</target>
Expand Down
Loading
Loading