| File: Commands\Test\TestCommandDefinition.cs | Web Access |
| Project: src\sdk\src\Cli\Microsoft.DotNet.Cli.Definitions\Microsoft.DotNet.Cli.Definitions.csproj (Microsoft.DotNet.Cli.Definitions) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.DotNet.Cli.CommandLine; using Command = System.CommandLine.Command; namespace Microsoft.DotNet.Cli.Commands.Test; internal abstract partial class TestCommandDefinition : Command { private const string Link = "https://aka.ms/dotnet-test"; private const string VSTestRunnerName = "VSTest"; private const string MicrosoftTestingPlatformRunnerName = "Microsoft.Testing.Platform"; private const string TestRunnerEnvironmentVariableName = "DOTNET_TEST_RUNNER"; public readonly TargetPlatformOptions TargetPlatformOptions = new(CommandDefinitionStrings.TestRuntimeOptionDescription); public readonly Option<string> FrameworkOption = CommonOptions.CreateFrameworkOption(CommandDefinitionStrings.TestFrameworkOptionDescription); public readonly Option<string?> ConfigurationOption = CommonOptions.CreateConfigurationOption(CommandDefinitionStrings.TestConfigurationOptionDescription); public readonly Option<Utils.VerbosityOptions?> VerbosityOption = CommonOptions.CreateVerbosityOption(); public TestCommandDefinition(string description) : base("test", description) { this.DocsLink = Link; TreatUnmatchedTokensAsErrors = false; } public static TestCommandDefinition Create() => Create(Environment.CurrentDirectory, Environment.GetEnvironmentVariable(TestRunnerEnvironmentVariableName)); internal static TestCommandDefinition Create(string startDirectory) => Create(startDirectory, Environment.GetEnvironmentVariable(TestRunnerEnvironmentVariableName)); internal static TestCommandDefinition Create(string startDirectory, string? testRunnerEnvironmentValue) { // The DOTNET_TEST_RUNNER environment variable lets users pick a runner without editing // (or carrying multiple) global.json files. When set to a recognized value it takes // precedence over the global.json `test.runner` setting. // // The runner name is resolved during CLI parser construction, which runs for *every* // dotnet invocation (e.g. `dotnet --version`, `dotnet build`), so an unrecognized env // var value must not crash unrelated commands. Unknown/whitespace values silently fall // back to the global.json lookup; an unknown value in global.json itself remains a // hard error because that file is explicit, in-repo configuration the user can fix. string? trimmedEnvValue = testRunnerEnvironmentValue?.Trim(); if (!string.IsNullOrEmpty(trimmedEnvValue) && TryResolveRunner(trimmedEnvValue) is { } runnerFromEnv) { return runnerFromEnv; } string? globalJsonRunnerName = TryGetRunnerNameFromGlobalJson(startDirectory); if (globalJsonRunnerName is null) { return new VSTest(); } return TryResolveRunner(globalJsonRunnerName) ?? throw new InvalidOperationException(string.Format(CommandDefinitionStrings.CmdUnsupportedTestRunnerDescription, globalJsonRunnerName)); } private static TestCommandDefinition? TryResolveRunner(string name) { if (name.Equals(VSTestRunnerName, StringComparison.OrdinalIgnoreCase)) { return new VSTest(); } if (name.Equals(MicrosoftTestingPlatformRunnerName, StringComparison.OrdinalIgnoreCase)) { return new MicrosoftTestingPlatform(); } return null; } private static string? TryGetRunnerNameFromGlobalJson(string startDirectory) { string? globalJsonPath = GetGlobalJsonPath(startDirectory); if (globalJsonPath is null) { return null; } try { string jsonText = File.ReadAllText(globalJsonPath); GlobalJsonModel? globalJson = JsonSerializer.Deserialize(jsonText, GlobalJsonSerializerContext.Default.GlobalJsonModel); return globalJson?.Test?.RunnerName; } catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException) { // The global.json file is unreadable or malformed (for example, empty or mid-edit). This // method is invoked very early during CLI parser construction, so throwing here would // bring down ALL commands (including `dotnet --version`). Fall back to the default // runner; if the user actually runs `dotnet test`, the test command itself will surface // a clearer error when it tries to load the configuration. return null; } } private static string? GetGlobalJsonPath(string? startDir) { string? directory = startDir; while (directory != null) { string globalJsonPath = Path.Combine(directory, "global.json"); if (File.Exists(globalJsonPath)) { return globalJsonPath; } directory = Path.GetDirectoryName(directory); } return null; } private sealed class GlobalJsonModel { [JsonPropertyName("test")] public GlobalJsonTestNode Test { get; set; } = null!; } private sealed class GlobalJsonTestNode { [JsonPropertyName("runner")] public string RunnerName { get; set; } = null!; } [JsonSourceGenerationOptions(ReadCommentHandling = JsonCommentHandling.Skip)] [JsonSerializable(typeof(GlobalJsonModel))] private partial class GlobalJsonSerializerContext : JsonSerializerContext; }