Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,18 @@ await _policiesService.RegisterOnAbortCallbackAsync(
// When --ansi auto is explicitly specified, it overrides --no-ansi too.
bool effectiveNoAnsi = noAnsi && ansiOverride == AnsiOverride.None;

// Honor the de-facto cross-tool NO_COLOR convention (https://no-color.org): when present with any
// non-empty value, suppress ANSI color/cursor codes unless the user explicitly opted in via
// `--ansi on` (which still wins, matching the precedence documented in --help).
bool noColorEnv = !RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable("NO_COLOR"));

bool inCI = new CIEnvironmentDetector(_environment).IsCIEnvironment();
bool isLLMEnvironment = new LLMEnvironmentDetector(_environment).IsLLMEnvironment();

AnsiMode ansiMode = ansiOverride switch
{
// User explicitly forced ANSI on (e.g. `--ansi on`). Bypass CI / LLM / redirection detection
// so colors and cursor movement are emitted even when stdout is redirected.
// User explicitly forced ANSI on (e.g. `--ansi on`). Bypass CI / LLM / NO_COLOR / redirection
// detection so colors and cursor movement are emitted even when stdout is redirected.
AnsiOverride.ForceOn => AnsiMode.ForceAnsi,

// User explicitly disabled ANSI (`--ansi off`).
Expand All @@ -181,7 +186,8 @@ await _policiesService.RegisterOnAbortCallbackAsync(
// No --ansi argument was provided, or `--ansi auto` was provided.
// Fall back to environment-based detection.
// In LLM environments, prefer simple text output so that the LLM can parse it easily.
_ when effectiveNoAnsi || isLLMEnvironment => AnsiMode.NoAnsi,
// NO_COLOR (https://no-color.org) is honored as well: any non-empty value suppresses ANSI.
_ when effectiveNoAnsi || noColorEnv || isLLMEnvironment => AnsiMode.NoAnsi,
_ when inCI => AnsiMode.SimpleAnsi,
_ => AnsiMode.AnsiIfPossible,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,77 @@ public async Task AnsiOption_MissingArgument_FailsCommandLineValidation()
result.AssertOutputContains("Option '--ansi' from provider 'Terminal test reporter' (UID: TerminalTestReporterCommandLineOptionsProvider) expects at least 1 arguments");
}

[TestMethod]
public async Task NoColorEnvVar_NonEmpty_DisablesAnsiOutput()
{
// The de-facto NO_COLOR convention (https://no-color.org): when present with any non-empty
// value, color (and in our implementation, all ANSI escape codes) must be suppressed.
// TestHost.ExecuteAsync auto-injects `--no-ansi --no-progress` which would already suppress ESC,
// so we use `--ansi auto` together with the CI env (which would otherwise produce SimpleAnsi
// ESC) to deterministically prove NO_COLOR alone forces NoAnsi. With NO_COLOR set the output
// must contain no ESC even though `--ansi auto` is opted in.
Dictionary<string, string?> environmentVariables = new(InCIEnvironmentVariablesWithLLMCleared)
{
["NO_COLOR"] = "1",
};

var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent);
TestHostResult result = await testHost.ExecuteAsync(
"--ansi auto",
environmentVariables: environmentVariables,
cancellationToken: TestContext.CancellationToken);

result.AssertExitCodeIs(ExitCode.Success);
Assert.IsFalse(
result.StandardOutput.Contains(EscapeCharacter, StringComparison.Ordinal),
$"Expected output to NOT contain ANSI escape characters when NO_COLOR is set, but got:\n{result.StandardOutput}");
}

[TestMethod]
public async Task NoColorEnvVar_Empty_IsIgnored()
{
// Per the NO_COLOR spec, ONLY a non-empty value disables color. An empty NO_COLOR must be a no-op.
// With `--ansi auto` in a CI environment (and LLM detection cleared), MTP picks SimpleAnsi which
// does emit color ESC sequences. Asserting ESC presence proves empty NO_COLOR did not force NoAnsi.
Dictionary<string, string?> environmentVariables = new(InCIEnvironmentVariablesWithLLMCleared)
{
["NO_COLOR"] = string.Empty,
};

var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent);
TestHostResult result = await testHost.ExecuteAsync(
"--ansi auto",
environmentVariables: environmentVariables,
cancellationToken: TestContext.CancellationToken);

result.AssertExitCodeIs(ExitCode.Success);
Assert.IsTrue(
result.StandardOutput.Contains(EscapeCharacter, StringComparison.Ordinal),
$"Expected output to contain ANSI escape characters when NO_COLOR is empty (spec: only non-empty disables color), but got:\n{result.StandardOutput}");
}

[TestMethod]
public async Task NoColorEnvVar_OverriddenByExplicitAnsiOn()
{
// Precedence rule documented in TerminalOutputDevice: `--ansi on` short-circuits CI / LLM /
// NO_COLOR detection. NO_COLOR is honored "unless the user explicitly opted in via --ansi on".
Dictionary<string, string?> environmentVariables = new(InCIEnvironmentVariablesWithLLMCleared)
{
["NO_COLOR"] = "1",
};

var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent);
TestHostResult result = await testHost.ExecuteAsync(
"--ansi on",
environmentVariables: environmentVariables,
cancellationToken: TestContext.CancellationToken);

result.AssertExitCodeIs(ExitCode.Success);
Assert.IsTrue(
result.StandardOutput.Contains(EscapeCharacter, StringComparison.Ordinal),
$"Expected output to contain ANSI escape characters because `--ansi on` overrides NO_COLOR, but got:\n{result.StandardOutput}");
}

public TestContext TestContext { get; set; } = null!;

// Env vars that force the platform to consider it to be in CI while clearing the known LLM env
Expand All @@ -119,6 +190,7 @@ public async Task AnsiOption_MissingArgument_FailsCommandLineValidation()
private static Dictionary<string, string?> InCIEnvironmentVariablesWithLLMCleared => new()
{
["GITHUB_ACTIONS"] = "true",
["NO_COLOR"] = string.Empty,
["CLAUDECODE"] = string.Empty,
["CLAUDE_CODE_ENTRYPOINT"] = string.Empty,
["CURSOR_EDITOR"] = string.Empty,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ public static class WellKnownEnvironmentVariables
// Isolate from the skip banner in case of parent, children tests
"TESTINGPLATFORM_CONSOLEOUTPUTDEVICE_SKIP_BANNER",

// The de-facto cross-tool NO_COLOR convention (https://no-color.org). MTP honors any non-empty
// value as "disable ANSI". We filter it out so acceptance tests that rely on ANSI output are not
// affected when contributors / CI happen to have NO_COLOR set in their shell. Tests that need
// to exercise NO_COLOR behavior must set the variable explicitly.
"NO_COLOR",

// LLM / AI agent CLI environment variables - keep in sync with
// src/Platform/Microsoft.Testing.Platform/Helpers/LLMEnvironmentDetector.cs.
// We filter these out so acceptance tests are not affected by the ambient
Expand Down