From 1130e7fc28cf9c0bb8a00a99e1e3f3b453017462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 20 May 2026 17:05:01 +0200 Subject: [PATCH 1/3] Consolidate CI detection into core platform helper Move CIEnvironmentDetectorForTelemetry from the Telemetry extension into Microsoft.Testing.Platform as CIEnvironmentDetector (instance-based with IEnvironment for testability, marked [Embedded] so the Telemetry extension source-links its own copy instead of relying on InternalsVisibleTo). Use the consolidated detector in TerminalOutputDevice instead of the inline TF_BUILD || GITHUB_ACTIONS check, closing the long-standing TODO that asked to reuse the telemetry CI detection (microsoft/testfx#5533). The terminal now switches to its CI-friendly SimpleAnsi mode for the broader set of CI providers detected by the standard .NET CI-detection list. Use --no-ansi if you prefer plain output. Also reword SimpleAnsiTerminal documentation to be CI-generic rather than AzDO-specific. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AppInsightsProvider.cs | 2 +- ...rosoft.Testing.Extensions.Telemetry.csproj | 1 + .../Helpers/CIEnvironmentDetector.cs} | 50 +++++++++++++++---- .../Terminal/SimpleAnsiTerminal.cs | 4 +- .../OutputDevice/TerminalOutputDevice.cs | 3 +- 5 files changed, 44 insertions(+), 16 deletions(-) rename src/Platform/{Microsoft.Testing.Extensions.Telemetry/CIEnvironmentDetectorForTelemetry.cs => Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs} (61%) diff --git a/src/Platform/Microsoft.Testing.Extensions.Telemetry/AppInsightsProvider.cs b/src/Platform/Microsoft.Testing.Extensions.Telemetry/AppInsightsProvider.cs index 647c516d13..edd545de3c 100644 --- a/src/Platform/Microsoft.Testing.Extensions.Telemetry/AppInsightsProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.Telemetry/AppInsightsProvider.cs @@ -93,7 +93,7 @@ public AppInsightsProvider( string sessionId) { _ = bool.TryParse(configuration[PlatformConfigurationConstants.PlatformTelemetryIsDevelopmentRepository], out _isDevelopmentRepository); - _isCi = CIEnvironmentDetectorForTelemetry.IsCIEnvironment(); + _isCi = new CIEnvironmentDetector(environment).IsCIEnvironment(); _environment = environment; _currentSessionId = sessionId; _testApplicationCancellationTokenSource = testApplicationCancellationTokenSource; diff --git a/src/Platform/Microsoft.Testing.Extensions.Telemetry/Microsoft.Testing.Extensions.Telemetry.csproj b/src/Platform/Microsoft.Testing.Extensions.Telemetry/Microsoft.Testing.Extensions.Telemetry.csproj index 9817dd8471..1c9ee840cf 100644 --- a/src/Platform/Microsoft.Testing.Extensions.Telemetry/Microsoft.Testing.Extensions.Telemetry.csproj +++ b/src/Platform/Microsoft.Testing.Extensions.Telemetry/Microsoft.Testing.Extensions.Telemetry.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Platform/Microsoft.Testing.Extensions.Telemetry/CIEnvironmentDetectorForTelemetry.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs similarity index 61% rename from src/Platform/Microsoft.Testing.Extensions.Telemetry/CIEnvironmentDetectorForTelemetry.cs rename to src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs index 0ac25bde4a..75ff8c8caf 100644 --- a/src/Platform/Microsoft.Testing.Extensions.Telemetry/CIEnvironmentDetectorForTelemetry.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs @@ -1,13 +1,14 @@ // 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; +using Microsoft.CodeAnalysis; -namespace Microsoft.Testing.Extensions.Telemetry; +namespace Microsoft.Testing.Platform.Helpers; // Detection of CI: https://learn.microsoft.com/dotnet/core/tools/telemetry#continuous-integration-detection -// From: https://github.com/dotnet/sdk/blob/main/src/Cli/dotnet/Telemetry/CIEnvironmentDetectorForTelemetry.cs -internal sealed class CIEnvironmentDetectorForTelemetry +// Based on: https://github.com/dotnet/sdk/blob/main/src/Cli/Microsoft.DotNet.Cli.Definitions/Telemetry/CIEnvironmentDetectorForTelemetry.cs +[Embedded] +internal sealed class CIEnvironmentDetector { // Systems that provide boolean values only, so we can simply parse and check for true private static readonly string[] BooleanVariables = @@ -22,14 +23,13 @@ internal sealed class CIEnvironmentDetectorForTelemetry "APPVEYOR", // A general-use flag - Many of the major players support this: AzDo, GitHub, GitLab, AppVeyor, Travis CI, CircleCI. - // Given this, we could potentially remove all of these other options? "CI", // Travis CI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables "TRAVIS", // CircleCI - https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables - "CIRCLECI" + "CIRCLECI", ]; // Systems where every variable must be present and not-null before returning true @@ -52,14 +52,32 @@ internal sealed class CIEnvironmentDetectorForTelemetry "TEAMCITY_VERSION", // JetBrains Space - https://www.jetbrains.com/help/space/automation-environment-variables.html#general - "JB_SPACE_API_URL" + "JB_SPACE_API_URL", ]; - public static bool IsCIEnvironment() + private readonly IEnvironment _environment; + + /// + /// Gets the default instance that uses the real environment. + /// + public static CIEnvironmentDetector Instance { get; } = new(new SystemEnvironment()); + + /// + /// Initializes a new instance of the class. + /// + /// The environment abstraction to use for reading environment variables. + public CIEnvironmentDetector(IEnvironment environment) + => _environment = environment; + + /// + /// Detects if the current environment is a CI environment. + /// + /// true if running in a CI environment; otherwise, false. + public bool IsCIEnvironment() { foreach (string booleanVariable in BooleanVariables) { - if (bool.TryParse(Environment.GetEnvironmentVariable(booleanVariable), out bool envVar) && envVar) + if (bool.TryParse(_environment.GetEnvironmentVariable(booleanVariable), out bool envVar) && envVar) { return true; } @@ -67,7 +85,17 @@ public static bool IsCIEnvironment() foreach (string[] variables in AllNotNullVariables) { - if (variables.All(variable => !RoslynString.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable)))) + bool allVariablesPresent = true; + foreach (string variable in variables) + { + if (RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) + { + allVariablesPresent = false; + break; + } + } + + if (allVariablesPresent) { return true; } @@ -75,7 +103,7 @@ public static bool IsCIEnvironment() foreach (string variable in IfNonNullVariables) { - if (!RoslynString.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) + if (!RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) { return true; } diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleAnsiTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleAnsiTerminal.cs index 12b4f4eee0..84656db087 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleAnsiTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleAnsiTerminal.cs @@ -6,8 +6,8 @@ namespace Microsoft.Testing.Platform.OutputDevice.Terminal; /// -/// Simple terminal that uses 4-bit ANSI for colors but does not move cursor and does not do other fancy stuff to stay compatible with CI systems like AzDO. -/// The colors are set on start of every line to properly color multiline strings in AzDO output. +/// Simple terminal that uses 4-bit ANSI for colors but does not move cursor and does not do other fancy stuff to stay compatible with CI systems. +/// The colors are set on start of every line to properly color multiline strings in CI log viewers that reset color state at each newline (e.g. Azure DevOps). /// internal sealed class SimpleAnsiTerminal : SimpleTerminal { diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs index a3563ec9a2..623540dc95 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.cs @@ -142,8 +142,7 @@ await _policiesService.RegisterOnAbortCallbackAsync( _isServerMode = _commandLineOptions.IsOptionSet(PlatformCommandLineProvider.ServerOptionKey); bool noAnsi = _commandLineOptions.IsOptionSet(TerminalTestReporterCommandLineOptionsProvider.NoAnsiOption); - // 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); + bool inCI = new CIEnvironmentDetector(_environment).IsCIEnvironment(); AnsiMode ansiMode = AnsiMode.AnsiIfPossible; // In LLM environments, prefer simple text output so that LLM can parse it easily. From d105efd2d624d4f22bd244775f4db1b645071b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 20 May 2026 19:15:35 +0200 Subject: [PATCH 2/3] Address review: add [ExcludeFromCodeCoverage], drop unused Instance - Add [ExcludeFromCodeCoverage] to CIEnvironmentDetector to match the established pattern for embedded helpers (e.g., RoslynString). Without it, the source-linked copy in Microsoft.Testing.Extensions.Telemetry would be included in coverage measurements for that project. - Remove the unused static Instance property. Both current callers (TerminalOutputDevice and AppInsightsProvider) construct the detector directly with their injected IEnvironment, so the static field was dead code that also allocated a SystemEnvironment unconditionally at class initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/CIEnvironmentDetector.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs index 75ff8c8caf..05beef4d7d 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs @@ -8,6 +8,7 @@ namespace Microsoft.Testing.Platform.Helpers; // Detection of CI: https://learn.microsoft.com/dotnet/core/tools/telemetry#continuous-integration-detection // Based on: https://github.com/dotnet/sdk/blob/main/src/Cli/Microsoft.DotNet.Cli.Definitions/Telemetry/CIEnvironmentDetectorForTelemetry.cs [Embedded] +[ExcludeFromCodeCoverage] internal sealed class CIEnvironmentDetector { // Systems that provide boolean values only, so we can simply parse and check for true @@ -57,11 +58,6 @@ internal sealed class CIEnvironmentDetector private readonly IEnvironment _environment; - /// - /// Gets the default instance that uses the real environment. - /// - public static CIEnvironmentDetector Instance { get; } = new(new SystemEnvironment()); - /// /// Initializes a new instance of the class. /// From 9a689923133ffd9267c220a209e2038a9b39fa21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 21 May 2026 10:22:19 +0200 Subject: [PATCH 3/3] Qualify RoslynString usage in CI detector Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/CIEnvironmentDetector.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs index 05beef4d7d..f8f91094c9 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs @@ -84,7 +84,7 @@ public bool IsCIEnvironment() bool allVariablesPresent = true; foreach (string variable in variables) { - if (RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) + if (global::Microsoft.Testing.Platform.RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) { allVariablesPresent = false; break; @@ -99,7 +99,7 @@ public bool IsCIEnvironment() foreach (string variable in IfNonNullVariables) { - if (!RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) + if (!global::Microsoft.Testing.Platform.RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) { return true; }