// 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; namespace Microsoft.Testing.Platform.Services; internal static class ExitCodeIgnorePolicy { /// /// Applies the shared --ignore-exit-code / TESTINGPLATFORM_EXITCODE_IGNORE policy to the /// given exit code, returning when the code is in the ignore list. /// /// /// Kept as a single shared implementation so every exit-code verdict (test results, coverage threshold, /// ...) is filtered consistently. Verdicts that are computed after /// has already run (for example the /// coverage threshold verdict applied by the hosts) must be routed through this so they can be ignored /// the same way the built-in verdicts are. /// public static int Apply(int exitCode, ICommandLineOptions commandLineOptions, IEnvironment environment) { // If the user has specified the IgnoreExitCode, then we don't want to return a non-zero exit code if the exit code matches the one specified. string? exitCodeToIgnore = environment.GetEnvironmentVariable(EnvironmentVariableConstants.TESTINGPLATFORM_EXITCODE_IGNORE); if (RoslynString.IsNullOrEmpty(exitCodeToIgnore)) { if (commandLineOptions.TryGetOptionArgumentList(PlatformCommandLineProvider.IgnoreExitCodeOptionKey, out string[]? commandLineExitCodes) && commandLineExitCodes.Length > 0) { exitCodeToIgnore = commandLineExitCodes[0]; } } if (exitCodeToIgnore is not null) { if (ContainsExitCode(exitCodeToIgnore, exitCode)) { exitCode = (int)ExitCode.Success; } } return exitCode; } /// /// Returns when contains /// in its ';'-delimited list, without allocating a array, a substring, or a LINQ closure. /// /// /// Each segment is parsed using the invariant integer format (optional leading/trailing whitespace and an optional /// ASCII +/- sign). Exit codes are a machine-level contract supplied via the --ignore-exit-code /// option or the TESTINGPLATFORM_EXITCODE_IGNORE environment variable, so parsing is intentionally /// culture-invariant and identical on every target framework. /// private static bool ContainsExitCode(string exitCodeToIgnore, int exitCode) { int start = 0; while (start <= exitCodeToIgnore.Length) { int separatorIndex = exitCodeToIgnore.IndexOf(';', start); int end = separatorIndex < 0 ? exitCodeToIgnore.Length : separatorIndex; #if NETCOREAPP if (int.TryParse(exitCodeToIgnore.AsSpan(start, end - start), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedExitCode) && parsedExitCode == exitCode) #else if (TryParseExitCode(exitCodeToIgnore, start, end, out int parsedExitCode) && parsedExitCode == exitCode) #endif { return true; } if (separatorIndex < 0) { break; } start = separatorIndex + 1; } return false; } #if !NETCOREAPP /// /// Parses the [start, end) slice of as an without allocating a /// substring. Mirrors the invariant int.TryParse(ReadOnlySpan<char>, NumberStyles.Integer, CultureInfo.InvariantCulture, out int) /// used by the NETCOREAPP branch: optional leading/trailing whitespace and an optional leading ASCII /// +/- sign. /// private static bool TryParseExitCode(string value, int start, int end, out int result) { result = 0; while (start < end && char.IsWhiteSpace(value[start])) { start++; } while (end > start && char.IsWhiteSpace(value[end - 1])) { end--; } if (start >= end) { return false; } bool isNegative = value[start] == '-'; if (value[start] is '+' or '-') { start++; } if (start >= end) { return false; } // int.MinValue is -2147483648, so the negative branch must accept a magnitude one larger than int.MaxValue. long limit = isNegative ? -(long)int.MinValue : int.MaxValue; long accumulated = 0; for (int i = start; i < end; i++) { char c = value[i]; if (c is < '0' or > '9') { return false; } int digit = c - '0'; // Guard before multiplying so an arbitrarily long token cannot overflow and wrap into a valid value. if (accumulated > (limit - digit) / 10) { return false; } accumulated = (accumulated * 10) + digit; } result = (int)(isNegative ? -accumulated : accumulated); return true; } #endif }