From 4f9f6f43845774093d0aaf711e3b9dc72519a709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 10 Jun 2026 15:51:53 +0200 Subject: [PATCH] Deduplicate CIEnvironmentDetector across TestFramework and Platform Fixes #8985. The CIEnvironmentDetector class was duplicated nearly verbatim in src/TestFramework/TestFramework/Internal/ and src/Platform/Microsoft.Testing.Platform/Helpers/, with the same arrays of CI environment variable names and the same IsCIEnvironment() logic in each copy. The TestFramework copy was added later than the Platform copy and the two had already begun to drift (XML doc shape, attribute usage). Make the Platform copy the single source of truth and link it into MSTest.TestFramework (the same pattern already used for RoslynHashCode and as Microsoft.Testing.Extensions.Telemetry already does for this file). The TestFramework project sets a TESTFRAMEWORK_CI_DETECTOR preprocessor symbol which toggles the small, real differences via #if guards: namespace, [Embedded]/[ExcludeFromCodeCoverage] attributes, public vs. internal ctor, the static Instance helper, and RoslynString.IsNullOrEmpty vs. string.IsNullOrEmpty. Also update eng/vendored-files.json to drop the now-removed testframework-ci-environment-detector entry and document the new linked-file relationship on the platform entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/vendored-files.json | 17 +-- .../Helpers/CIEnvironmentDetector.cs | 36 +++++- .../Internal/CIEnvironmentDetector.cs | 115 ------------------ .../TestFramework/TestFramework.csproj | 8 ++ 4 files changed, 43 insertions(+), 133 deletions(-) delete mode 100644 src/TestFramework/TestFramework/Internal/CIEnvironmentDetector.cs diff --git a/eng/vendored-files.json b/eng/vendored-files.json index afd5faebec..e2598745b7 100644 --- a/eng/vendored-files.json +++ b/eng/vendored-files.json @@ -192,7 +192,7 @@ { "id": "platform-ci-environment-detector", "local_path": "src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs", - "notes": "Adapted from dotnet CLI telemetry CI detection. Maintained per https://learn.microsoft.com/dotnet/core/tools/telemetry#continuous-integration-detection.", + "notes": "Adapted from dotnet CLI telemetry CI detection. Maintained per https://learn.microsoft.com/dotnet/core/tools/telemetry#continuous-integration-detection. This file is the single source of truth: it is linked into MSTest.TestFramework via src/TestFramework/TestFramework/TestFramework.csproj and toggled via the IS_CORE_MTP define.", "sources": [ { "repo": "dotnet/sdk", @@ -294,21 +294,6 @@ } ] }, - { - "id": "testframework-ci-environment-detector", - "local_path": "src/TestFramework/TestFramework/Internal/CIEnvironmentDetector.cs", - "notes": "Independent in-tree copy of the CI detector (test-framework variant exposing an IEnvironment-backed Instance) sharing the rules with the platform copy.", - "sources": [ - { - "repo": "dotnet/sdk", - "ref": "main", - "path": "src/Cli/Microsoft.DotNet.Cli.Definitions/Telemetry/CIEnvironmentDetectorForTelemetry.cs", - "baseline_ref_sha": "1ab3ae8e0338cdd06b3974959c5802ccc5a85978", - "baseline_blob_sha": "e10e1fc2cd973c6123fd983b16a8f0c1e9191c31", - "scope": "CI detection rules and environment variable lists" - } - ] - }, { "id": "source-gen-equatable-array", "local_path": "src/Analyzers/MSTest.SourceGeneration/Helpers/EquatableArray{T}.cs", diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs index f8f91094c9..4b62cf6c16 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/CIEnvironmentDetector.cs @@ -1,14 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +// NOTE: This file is the single source of truth for CI environment detection. It is the canonical +// copy for Microsoft.Testing.Platform and is also linked into Microsoft.Testing.Extensions.Telemetry +// (via Microsoft.Testing.Extensions.Telemetry.csproj) and MSTest.TestFramework (via +// src/TestFramework/TestFramework/TestFramework.csproj). The TESTFRAMEWORK_CI_DETECTOR define +// is set only in the MSTest.TestFramework project; the #if blocks below toggle namespace, +// attributes, constructor accessibility, the static Instance helper, and the IsNullOrEmpty +// implementation so the file fits both the Platform/Telemetry layer and the TestFramework layer. +#if TESTFRAMEWORK_CI_DETECTOR +namespace Microsoft.VisualStudio.TestTools.UnitTesting; +#else using Microsoft.CodeAnalysis; namespace Microsoft.Testing.Platform.Helpers; +#endif // 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 +#if !TESTFRAMEWORK_CI_DETECTOR [Embedded] [ExcludeFromCodeCoverage] +#endif internal sealed class CIEnvironmentDetector { // Systems that provide boolean values only, so we can simply parse and check for true @@ -58,12 +71,23 @@ internal sealed class CIEnvironmentDetector private readonly IEnvironment _environment; +#if TESTFRAMEWORK_CI_DETECTOR + /// + /// Gets the default instance that uses the real environment. + /// + public static CIEnvironmentDetector Instance { get; } = new(EnvironmentWrapper.Instance); +#endif + /// /// Initializes a new instance of the class. /// /// The environment abstraction to use for reading environment variables. +#if TESTFRAMEWORK_CI_DETECTOR + internal /* for testing purposes */ CIEnvironmentDetector(IEnvironment environment) => _environment = environment; +#else public CIEnvironmentDetector(IEnvironment environment) => _environment = environment; +#endif /// /// Detects if the current environment is a CI environment. @@ -84,7 +108,7 @@ public bool IsCIEnvironment() bool allVariablesPresent = true; foreach (string variable in variables) { - if (global::Microsoft.Testing.Platform.RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) + if (IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) { allVariablesPresent = false; break; @@ -99,7 +123,7 @@ public bool IsCIEnvironment() foreach (string variable in IfNonNullVariables) { - if (!global::Microsoft.Testing.Platform.RoslynString.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) + if (!IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) { return true; } @@ -107,4 +131,12 @@ public bool IsCIEnvironment() return false; } + +#if TESTFRAMEWORK_CI_DETECTOR + private static bool IsNullOrEmpty(string? value) + => string.IsNullOrEmpty(value); +#else + private static bool IsNullOrEmpty(string? value) + => global::Microsoft.Testing.Platform.RoslynString.IsNullOrEmpty(value); +#endif } diff --git a/src/TestFramework/TestFramework/Internal/CIEnvironmentDetector.cs b/src/TestFramework/TestFramework/Internal/CIEnvironmentDetector.cs deleted file mode 100644 index 2a59282f82..0000000000 --- a/src/TestFramework/TestFramework/Internal/CIEnvironmentDetector.cs +++ /dev/null @@ -1,115 +0,0 @@ -// 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.VisualStudio.TestTools.UnitTesting; - -/// -/// Provides CI environment detection capabilities. -/// -/// -/// Based on https://learn.microsoft.com/dotnet/core/tools/telemetry#continuous-integration-detection -/// and https://github.com/dotnet/sdk/blob/main/src/Cli/dotnet/Telemetry/CIEnvironmentDetectorForTelemetry.cs. -/// -internal sealed class CIEnvironmentDetector -{ - /// - /// Gets the default instance that uses the real environment. - /// - public static CIEnvironmentDetector Instance { get; } = new(EnvironmentWrapper.Instance); - - private readonly IEnvironment _environment; - - // Systems that provide boolean values only, so we can simply parse and check for true - private static readonly string[] BooleanVariables = - [ - // Azure Pipelines - https://docs.microsoft.com/azure/devops/pipelines/build/variables#system-variables-devops-services - "TF_BUILD", - - // GitHub Actions - https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables - "GITHUB_ACTIONS", - - // AppVeyor - https://www.appveyor.com/docs/environment-variables/ - "APPVEYOR", - - // A general-use flag - Many of the major players support this: AzDo, GitHub, GitLab, AppVeyor, Travis CI, CircleCI. - "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", - ]; - - // Systems where every variable must be present and not-null before returning true - private static readonly string[][] AllNotNullVariables = - [ - // AWS CodeBuild - https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html - ["CODEBUILD_BUILD_ID", "AWS_REGION"], - - // Jenkins - https://github.com/jenkinsci/jenkins/blob/master/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy - ["BUILD_ID", "BUILD_URL"], - - // Google Cloud Build - https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values#using_default_substitutions - ["BUILD_ID", "PROJECT_ID"], - ]; - - // Systems where the variable must be present and not-null - private static readonly string[] IfNonNullVariables = - [ - // TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters - "TEAMCITY_VERSION", - - // JetBrains Space - https://www.jetbrains.com/help/space/automation-environment-variables.html#general - "JB_SPACE_API_URL", - ]; - - /// - /// Initializes a new instance of the class. - /// - /// The environment abstraction to use for reading environment variables. - internal /* for testing purposes */ 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) - { - return true; - } - } - - foreach (string[] variables in AllNotNullVariables) - { - bool allVariablesPresent = true; - foreach (string variable in variables) - { - if (string.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) - { - allVariablesPresent = false; - break; - } - } - - if (allVariablesPresent) - { - return true; - } - } - - foreach (string variable in IfNonNullVariables) - { - if (!string.IsNullOrEmpty(_environment.GetEnvironmentVariable(variable))) - { - return true; - } - } - - return false; - } -} diff --git a/src/TestFramework/TestFramework/TestFramework.csproj b/src/TestFramework/TestFramework/TestFramework.csproj index 6afb0f4f90..a167fe2984 100644 --- a/src/TestFramework/TestFramework/TestFramework.csproj +++ b/src/TestFramework/TestFramework/TestFramework.csproj @@ -26,8 +26,16 @@ + + + + $(DefineConstants);TESTFRAMEWORK_CI_DETECTOR + +