From 5f440a09c6054747ab54b85408c9c1d5705fff12 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 9 Jul 2026 11:22:03 +0200 Subject: [PATCH 1/5] Fix #9762: share RunSettings/TestRunParameters provider logic via linked source Extract the resource-independent logic duplicated between the MSTest adapter's native Microsoft.Testing.Platform providers and the VSTestBridge providers into a new dependency-free linked shared file (src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs), linked into both projects. This removes the near-identical copies without reintroducing a dependency on Microsoft.Testing.Extensions.VSTestBridge. - CanReadFile, TryLoadRunSettingsAsync, HasEnvironmentVariables, ApplyEnvironmentVariables and FindInvalidTestParameter now live once. - Unifies the string.IsNullOrEmpty vs RoslynString.IsNullOrEmpty drift on string.IsNullOrEmpty. - Each package keeps its own resource strings, option registration and UWP guards; the shared type is fully guarded by #if !WINDOWS_UWP. - No public/behavioral change: option names, descriptions, arities and validation messages are unchanged. TestCaseFilter pair left untouched. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../MSTest.TestAdapter.csproj | 2 + ...stRunSettingsCommandLineOptionsProvider.cs | 16 +-- ...tRunSettingsEnvironmentVariableProvider.cs | 57 ++------- ...RunParametersCommandLineOptionsProvider.cs | 3 +- .../RunSettingsCommandLineOptionsProvider.cs | 15 +-- ...RunParametersCommandLineOptionsProvider.cs | 13 +- ...oft.Testing.Extensions.VSTestBridge.csproj | 2 + .../RunSettingsEnvironmentVariableProvider.cs | 63 ++-------- .../RunSettingsProviderHelper.cs | 117 ++++++++++++++++++ 9 files changed, 145 insertions(+), 143 deletions(-) create mode 100644 src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs diff --git a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj index 98523efea9..85460d81eb 100644 --- a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj +++ b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj @@ -107,6 +107,8 @@ + + diff --git a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.cs b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.cs index bccf259cf4..e304f86b0a 100644 --- a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.cs +++ b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsCommandLineOptionsProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if !WINDOWS_UWP +using Microsoft.Testing.Extensions; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.CommandLine; @@ -38,22 +39,9 @@ public override Task ValidateOptionArgumentsAsync(CommandLineO return !_fileSystem.ExistFile(filePath) ? ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, PlatformAdapterResources.RunsettingsFileDoesNotExist, filePath)) - : !CanReadFile(filePath) + : !RunSettingsProviderHelper.CanReadFile(_fileSystem, filePath) ? ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, PlatformAdapterResources.RunsettingsFileCannotBeRead, filePath)) : ValidationResult.ValidTask; } - - private bool CanReadFile(string filePath) - { - try - { - using IFileStream stream = _fileSystem.NewFileStream(filePath, FileMode.Open, FileAccess.Read); - return true; - } - catch (IOException) - { - return false; - } - } } #endif diff --git a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.cs b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.cs index c1ae378e91..75b7d41a8d 100644 --- a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.cs +++ b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestRunSettingsEnvironmentVariableProvider.cs @@ -2,12 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if !WINDOWS_UWP +using Microsoft.Testing.Extensions; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.TestHostControllers; using IEnvironment = Microsoft.Testing.Platform.Helpers.IEnvironment; -using IFileStream = Microsoft.Testing.Platform.Helpers.IFileStream; using IFileSystem = Microsoft.Testing.Platform.Helpers.IFileSystem; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestingPlatformAdapter; @@ -44,59 +44,18 @@ public MSTestRunSettingsEnvironmentVariableProvider(IExtension extension, IComma public async Task IsEnabledAsync() { - string? runSettingsFilePath = null; - string? runSettingsContent = null; + _runSettings = await RunSettingsProviderHelper.TryLoadRunSettingsAsync( + _commandLineOptions, + _fileSystem, + _environment, + MSTestRunSettingsCommandLineOptionsProvider.RunSettingsOptionName).ConfigureAwait(false); - if (_commandLineOptions.TryGetOptionArgumentList(MSTestRunSettingsCommandLineOptionsProvider.RunSettingsOptionName, out string[]? runsettings) - && runsettings.Length > 0 - && _fileSystem.ExistFile(runsettings[0])) - { - runSettingsFilePath = runsettings[0]; - } - - if (runSettingsFilePath is null) - { - runSettingsContent = _environment.GetEnvironmentVariable("TESTINGPLATFORM_EXPERIMENTAL_VSTEST_RUNSETTINGS"); - } - - if (runSettingsFilePath is null && string.IsNullOrEmpty(runSettingsContent)) - { - string? envVarFilePath = _environment.GetEnvironmentVariable("TESTINGPLATFORM_VSTESTBRIDGE_RUNSETTINGS_FILE"); - if (!string.IsNullOrEmpty(envVarFilePath) && _fileSystem.ExistFile(envVarFilePath!)) - { - runSettingsFilePath = envVarFilePath; - } - } - - if (runSettingsFilePath is not null) - { - using IFileStream fileStream = _fileSystem.NewFileStream(runSettingsFilePath, FileMode.Open, FileAccess.Read); -#if NETCOREAPP - _runSettings = await XDocument.LoadAsync(fileStream.Stream, LoadOptions.None, CancellationToken.None).ConfigureAwait(false); -#else - using StreamReader streamReader = new(fileStream.Stream); - _runSettings = XDocument.Parse(await streamReader.ReadToEndAsync().ConfigureAwait(false)); -#endif - } - else if (!string.IsNullOrEmpty(runSettingsContent)) - { - _runSettings = XDocument.Parse(runSettingsContent); - } - else - { - return false; - } - - return _runSettings.Element("RunSettings")?.Element("RunConfiguration")?.Element("EnvironmentVariables") is not null; + return _runSettings is not null && RunSettingsProviderHelper.HasEnvironmentVariables(_runSettings); } public Task UpdateAsync(IEnvironmentVariables environmentVariables) { - foreach (XElement element in _runSettings!.Element("RunSettings")!.Element("RunConfiguration")!.Element("EnvironmentVariables")!.Elements()) - { - environmentVariables.SetVariable(new(element.Name.ToString(), element.Value, true, true)); - } - + RunSettingsProviderHelper.ApplyEnvironmentVariables(_runSettings!, environmentVariables); return Task.CompletedTask; } diff --git a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.cs b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.cs index bbff73830f..261b2e236b 100644 --- a/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.cs +++ b/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestRunParametersCommandLineOptionsProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if !WINDOWS_UWP +using Microsoft.Testing.Extensions; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.CommandLine; @@ -26,7 +27,7 @@ public MSTestTestRunParametersCommandLineOptionsProvider(IExtension extension) public override Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) { - string? invalidArgument = arguments.FirstOrDefault(argument => !argument.Contains('=')); + string? invalidArgument = RunSettingsProviderHelper.FindInvalidTestParameter(arguments); return invalidArgument is not null ? ValidationResult.InvalidTask(string.Format(CultureInfo.CurrentCulture, PlatformAdapterResources.TestRunParameterOptionArgumentIsNotParameter, invalidArgument)) : ValidationResult.ValidTask; diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.cs index 00dbcfee1d..9dfdba30ff 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/RunSettingsCommandLineOptionsProvider.cs @@ -41,7 +41,7 @@ public override Task ValidateOptionArgumentsAsync(CommandLineO } // Even if the file exists, we want to validate we can open/read it. - if (!CanReadFile(filePath)) + if (!RunSettingsProviderHelper.CanReadFile(_fileSystem, filePath)) { return ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, ExtensionResources.RunsettingsFileCannotBeRead, filePath)); } @@ -49,17 +49,4 @@ public override Task ValidateOptionArgumentsAsync(CommandLineO // No problem found return ValidationResult.ValidTask; } - - private bool CanReadFile(string filePath) - { - try - { - using IFileStream stream = _fileSystem.NewFileStream(filePath, FileMode.Open, FileAccess.Read); - return true; - } - catch (IOException) - { - return false; - } - } } diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.cs index 034796db44..7aa2fdfb05 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/CommandLine/TestRunParametersCommandLineOptionsProvider.cs @@ -24,14 +24,9 @@ public TestRunParametersCommandLineOptionsProvider(IExtension extension) /// public override Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) { - foreach (string argument in arguments) - { - if (!argument.Contains('=')) - { - return ValidationResult.InvalidTask(string.Format(CultureInfo.CurrentCulture, ExtensionResources.TestRunParameterOptionArgumentIsNotParameter, argument)); - } - } - - return ValidationResult.ValidTask; + string? invalidArgument = RunSettingsProviderHelper.FindInvalidTestParameter(arguments); + return invalidArgument is not null + ? ValidationResult.InvalidTask(string.Format(CultureInfo.CurrentCulture, ExtensionResources.TestRunParameterOptionArgumentIsNotParameter, invalidArgument)) + : ValidationResult.ValidTask; } } diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csproj b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csproj index 603aaf6019..4affbf1263 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csproj +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/Microsoft.Testing.Extensions.VSTestBridge.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.cs index 31cacda715..79420a63fe 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/TestHostControllers/RunSettingsEnvironmentVariableProvider.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Extensions.VSTestBridge.CommandLine; -using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.TestHostControllers; @@ -36,66 +35,18 @@ public RunSettingsEnvironmentVariableProvider(IExtension extension, ICommandLine public async Task IsEnabledAsync() { - string? runSettingsFilePath = null; - string? runSettingsContent = null; + _runSettings = await RunSettingsProviderHelper.TryLoadRunSettingsAsync( + _commandLineOptions, + _fileSystem, + _environment, + RunSettingsCommandLineOptionsProvider.RunSettingsOptionName).ConfigureAwait(false); - // Try to get runsettings from command line - if (_commandLineOptions.TryGetOptionArgumentList(RunSettingsCommandLineOptionsProvider.RunSettingsOptionName, out string[]? runsettings) - && runsettings.Length > 0) - { - if (_fileSystem.ExistFile(runsettings[0])) - { - runSettingsFilePath = runsettings[0]; - } - } - - // If not from command line, try environment variable with content - if (runSettingsFilePath is null) - { - runSettingsContent = _environment.GetEnvironmentVariable("TESTINGPLATFORM_EXPERIMENTAL_VSTEST_RUNSETTINGS"); - } - - // If not from content env var, try environment variable with file path - if (runSettingsFilePath is null && RoslynString.IsNullOrEmpty(runSettingsContent)) - { - string? envVarFilePath = _environment.GetEnvironmentVariable("TESTINGPLATFORM_VSTESTBRIDGE_RUNSETTINGS_FILE"); - if (!RoslynString.IsNullOrEmpty(envVarFilePath) && _fileSystem.ExistFile(envVarFilePath)) - { - runSettingsFilePath = envVarFilePath; - } - } - - // If we have a file path, read from file - if (runSettingsFilePath is not null) - { - using IFileStream fileStream = _fileSystem.NewFileStream(runSettingsFilePath, FileMode.Open, FileAccess.Read); -#if NETCOREAPP - _runSettings = await XDocument.LoadAsync(fileStream.Stream, LoadOptions.None, CancellationToken.None).ConfigureAwait(false); -#else - using StreamReader streamReader = new(fileStream.Stream); - _runSettings = XDocument.Parse(await streamReader.ReadToEndAsync().ConfigureAwait(false)); -#endif - } - else if (!RoslynString.IsNullOrEmpty(runSettingsContent)) - { - // If we have content, parse it directly - _runSettings = XDocument.Parse(runSettingsContent); - } - else - { - return false; - } - - return _runSettings.Element("RunSettings")?.Element("RunConfiguration")?.Element("EnvironmentVariables") is not null; + return _runSettings is not null && RunSettingsProviderHelper.HasEnvironmentVariables(_runSettings); } public Task UpdateAsync(IEnvironmentVariables environmentVariables) { - foreach (XElement element in _runSettings!.Element("RunSettings")!.Element("RunConfiguration")!.Element("EnvironmentVariables")!.Elements()) - { - environmentVariables.SetVariable(new(element.Name.ToString(), element.Value, true, true)); - } - + RunSettingsProviderHelper.ApplyEnvironmentVariables(_runSettings!, environmentVariables); return Task.CompletedTask; } diff --git a/src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs b/src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs new file mode 100644 index 0000000000..c7b6253910 --- /dev/null +++ b/src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if !WINDOWS_UWP +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Extensions.TestHostControllers; +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Extensions; + +/// +/// Resource-independent logic shared by the VSTest bridge and the MSTest adapter's native Microsoft.Testing.Platform +/// integration for the --settings (.runsettings) and --test-parameter (TestRunParameters) providers. +/// The per-package concerns (resource strings, option registration, UWP guards) stay in the provider classes; only +/// the shared logic lives here. +/// +[SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "The shared helper is linked into projects that are allowed to use MTP APIs")] +internal static class RunSettingsProviderHelper +{ + /// + /// Attempts to open the file for reading to validate it can be read (independently of its existence check). + /// + internal static bool CanReadFile(IFileSystem fileSystem, string filePath) + { + try + { + using IFileStream stream = fileSystem.NewFileStream(filePath, FileMode.Open, FileAccess.Read); + return true; + } + catch (IOException) + { + return false; + } + } + + /// + /// Resolves and parses the .runsettings from (in order) the command-line option, the + /// TESTINGPLATFORM_EXPERIMENTAL_VSTEST_RUNSETTINGS content environment variable, or the + /// TESTINGPLATFORM_VSTESTBRIDGE_RUNSETTINGS_FILE file-path environment variable. Returns the parsed + /// , or when no runsettings could be resolved. + /// + internal static async Task TryLoadRunSettingsAsync( + ICommandLineOptions commandLineOptions, + IFileSystem fileSystem, + IEnvironment environment, + string runSettingsOptionName) + { + string? runSettingsFilePath = null; + string? runSettingsContent = null; + + // Try to get runsettings from command line. + if (commandLineOptions.TryGetOptionArgumentList(runSettingsOptionName, out string[]? runsettings) + && runsettings.Length > 0 + && fileSystem.ExistFile(runsettings[0])) + { + runSettingsFilePath = runsettings[0]; + } + + // If not from command line, try environment variable with content. + if (runSettingsFilePath is null) + { + runSettingsContent = environment.GetEnvironmentVariable("TESTINGPLATFORM_EXPERIMENTAL_VSTEST_RUNSETTINGS"); + } + + // If not from content env var, try environment variable with file path. + if (runSettingsFilePath is null && string.IsNullOrEmpty(runSettingsContent)) + { + string? envVarFilePath = environment.GetEnvironmentVariable("TESTINGPLATFORM_VSTESTBRIDGE_RUNSETTINGS_FILE"); + if (!string.IsNullOrEmpty(envVarFilePath) && fileSystem.ExistFile(envVarFilePath!)) + { + runSettingsFilePath = envVarFilePath; + } + } + + // If we have a file path, read from file. + if (runSettingsFilePath is not null) + { + using IFileStream fileStream = fileSystem.NewFileStream(runSettingsFilePath, FileMode.Open, FileAccess.Read); +#if NETCOREAPP + return await XDocument.LoadAsync(fileStream.Stream, LoadOptions.None, CancellationToken.None).ConfigureAwait(false); +#else + using StreamReader streamReader = new(fileStream.Stream); + return XDocument.Parse(await streamReader.ReadToEndAsync().ConfigureAwait(false)); +#endif + } + + // If we have content, parse it directly. + return !string.IsNullOrEmpty(runSettingsContent) + ? XDocument.Parse(runSettingsContent!) + : null; + } + + /// + /// Returns a value indicating whether the runsettings declares a <EnvironmentVariables> section. + /// + internal static bool HasEnvironmentVariables(XDocument runSettings) + => runSettings.Element("RunSettings")?.Element("RunConfiguration")?.Element("EnvironmentVariables") is not null; + + /// + /// Applies every <EnvironmentVariables> entry from the runsettings to the test host. + /// + internal static void ApplyEnvironmentVariables(XDocument runSettings, IEnvironmentVariables environmentVariables) + { + foreach (XElement element in runSettings.Element("RunSettings")!.Element("RunConfiguration")!.Element("EnvironmentVariables")!.Elements()) + { + environmentVariables.SetVariable(new(element.Name.ToString(), element.Value, true, true)); + } + } + + /// + /// Returns the first argument that is not a name=value pair (i.e. does not contain '='), or + /// when all arguments are valid. + /// + internal static string? FindInvalidTestParameter(string[] arguments) + => arguments.FirstOrDefault(argument => !argument.Contains('=')); +} +#endif From a229fe2fe32b3be6c690d5d3ef43b72b2afd3926 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 9 Jul 2026 13:27:48 +0200 Subject: [PATCH 2/5] Track internal APIs and broaden CanReadFile catch (#9762 CI fix) - Track RunSettingsProviderHelper in InternalAPI.Unshipped.txt for both the VSTestBridge and MSTest.TestAdapter projects (RS0051). Both projects use InternalsVisibleTo, so the internal-API analyzer tracks internal symbols; entries go in InternalAPI.Unshipped.txt, not PublicAPI.Unshipped.txt. - Also add the pre-existing untracked PlatformServicesConfigurationAdapter (non-UWP) to the adapter InternalAPI.Unshipped.txt to unblock RS0051. - Broaden CanReadFile catch to also handle UnauthorizedAccessException per review, so a permission error yields the friendly 'cannot be read' validation message instead of an unhandled exception. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InternalAPI/InternalAPI.Unshipped.txt | 9 +++++++++ .../InternalAPI.Unshipped.txt | 6 ++++++ .../SharedExtensionHelpers/RunSettingsProviderHelper.cs | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Adapter/MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt b/src/Adapter/MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt index 7dc5c58110..da16710179 100644 --- a/src/Adapter/MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/Adapter/MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt @@ -1 +1,10 @@ #nullable enable +Microsoft.Testing.Extensions.RunSettingsProviderHelper +Microsoft.VisualStudio.TestTools.UnitTesting.PlatformServicesConfigurationAdapter +Microsoft.VisualStudio.TestTools.UnitTesting.PlatformServicesConfigurationAdapter.PlatformServicesConfigurationAdapter(Microsoft.Testing.Platform.Configurations.IConfiguration! configuration) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.PlatformServicesConfigurationAdapter.this[string! key].get -> string? +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.ApplyEnvironmentVariables(System.Xml.Linq.XDocument! runSettings, Microsoft.Testing.Platform.Extensions.TestHostControllers.IEnvironmentVariables! environmentVariables) -> void +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.CanReadFile(Microsoft.Testing.Platform.Helpers.IFileSystem! fileSystem, string! filePath) -> bool +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.FindInvalidTestParameter(string![]! arguments) -> string? +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.HasEnvironmentVariables(System.Xml.Linq.XDocument! runSettings) -> bool +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.TryLoadRunSettingsAsync(Microsoft.Testing.Platform.CommandLine.ICommandLineOptions! commandLineOptions, Microsoft.Testing.Platform.Helpers.IFileSystem! fileSystem, Microsoft.Testing.Platform.Helpers.IEnvironment! environment, string! runSettingsOptionName) -> System.Threading.Tasks.Task! diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/InternalAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/InternalAPI.Unshipped.txt index 7dc5c58110..1981f188c1 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/InternalAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/InternalAPI.Unshipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Testing.Extensions.RunSettingsProviderHelper +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.ApplyEnvironmentVariables(System.Xml.Linq.XDocument! runSettings, Microsoft.Testing.Platform.Extensions.TestHostControllers.IEnvironmentVariables! environmentVariables) -> void +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.CanReadFile(Microsoft.Testing.Platform.Helpers.IFileSystem! fileSystem, string! filePath) -> bool +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.FindInvalidTestParameter(string![]! arguments) -> string? +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.HasEnvironmentVariables(System.Xml.Linq.XDocument! runSettings) -> bool +static Microsoft.Testing.Extensions.RunSettingsProviderHelper.TryLoadRunSettingsAsync(Microsoft.Testing.Platform.CommandLine.ICommandLineOptions! commandLineOptions, Microsoft.Testing.Platform.Helpers.IFileSystem! fileSystem, Microsoft.Testing.Platform.Helpers.IEnvironment! environment, string! runSettingsOptionName) -> System.Threading.Tasks.Task! diff --git a/src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs b/src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs index c7b6253910..ee67189ca8 100644 --- a/src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs +++ b/src/Platform/SharedExtensionHelpers/RunSettingsProviderHelper.cs @@ -27,7 +27,7 @@ internal static bool CanReadFile(IFileSystem fileSystem, string filePath) using IFileStream stream = fileSystem.NewFileStream(filePath, FileMode.Open, FileAccess.Read); return true; } - catch (IOException) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return false; } From fdaa831954820f8dbba221e1314ca23b410e98ab Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 9 Jul 2026 13:39:01 +0200 Subject: [PATCH 3/5] Track pre-existing untracked BaseSerializer internal APIs to unblock RS0051 Microsoft.Testing.Platform's BaseSerializer.ReadFields and WriteListPayload (added by #9774) were never added to InternalAPI.Unshipped.txt, so once #9752 enabled RS0051 internal-API enforcement both landed on main and left main red. This foundational project's failure cascades and blocks the whole build, so track the two methods in the base InternalAPI.Unshipped.txt (the diagnostic fires on netstandard2.0 too, so it belongs in the base file, not net/). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InternalAPI/InternalAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt index 7dc5c58110..c84445ece8 100644 --- a/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func! tryReadField) -> void +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action! writeItem) -> void From d661ee0506e5774959e859973d611f950f1dbc92 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 9 Jul 2026 14:11:08 +0200 Subject: [PATCH 4/5] Track BaseSerializer internal APIs in all projects that link it (RS0051) BaseSerializer.cs is compiled as linked shared source into HangDump, MSBuild, TrxReport and Retry (in addition to Microsoft.Testing.Platform), and each of those projects does its own RS0051 internal-API tracking. My previous commit only tracked ReadFields/WriteListPayload in Microsoft.Testing.Platform, so the four linking projects still failed RS0051 under CI's -warnaserror (locally these surface as warnings, which is why an earlier per-project build looked green). Add the same two entries to each project's InternalAPI.Unshipped.txt. Verified by reproduce-then-clear with CI-style flags (/p:ContinuousIntegrationBuild=true /p:RunAnalyzers=true --no-incremental): removing the lines re-emits RS0051; with the lines all five projects report zero RS0051/RS0016/RS0017. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InternalAPI/InternalAPI.Unshipped.txt | 2 ++ .../InternalAPI.Unshipped.txt | 2 ++ .../InternalAPI/InternalAPI.Unshipped.txt | 2 ++ .../InternalAPI/InternalAPI.Unshipped.txt | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt index 7dc5c58110..c84445ece8 100644 --- a/src/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Extensions.HangDump/InternalAPI/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func! tryReadField) -> void +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action! writeItem) -> void diff --git a/src/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt index 7dc5c58110..c84445ece8 100644 --- a/src/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Extensions.MSBuild/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func! tryReadField) -> void +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action! writeItem) -> void diff --git a/src/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt index 7dc5c58110..c84445ece8 100644 --- a/src/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Extensions.Retry/InternalAPI/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func! tryReadField) -> void +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action! writeItem) -> void diff --git a/src/Platform/Microsoft.Testing.Extensions.TrxReport/InternalAPI/InternalAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Extensions.TrxReport/InternalAPI/InternalAPI.Unshipped.txt index 7dc5c58110..c84445ece8 100644 --- a/src/Platform/Microsoft.Testing.Extensions.TrxReport/InternalAPI/InternalAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Extensions.TrxReport/InternalAPI/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func! tryReadField) -> void +static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action! writeItem) -> void From ba5b5e6013bf10a90bcc14ac213fbb2621d85d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 9 Jul 2026 19:14:43 +0200 Subject: [PATCH 5/5] Add test coverage for CanReadFile UnauthorizedAccessException branch The shared RunSettingsProviderHelper.CanReadFile intentionally broadens the caught exceptions to include UnauthorizedAccessException (a permission error on an existing .runsettings file now surfaces the friendly 'cannot be read' validation message). Add a unit test exercising that branch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...SettingsCommandLineOptionsProviderTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/CommandLine/RunSettingsCommandLineOptionsProviderTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/CommandLine/RunSettingsCommandLineOptionsProviderTests.cs index 8788c56af4..052ee79a02 100644 --- a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/CommandLine/RunSettingsCommandLineOptionsProviderTests.cs +++ b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/CommandLine/RunSettingsCommandLineOptionsProviderTests.cs @@ -53,6 +53,26 @@ public async Task RunSettingsOption_WhenFileCannotBeOpen_IsNotValid() Assert.AreEqual(string.Format(CultureInfo.CurrentCulture, ExtensionResources.RunsettingsFileCannotBeRead, filePath), result.ErrorMessage); } + [TestMethod] + public async Task RunSettingsOption_WhenFileCannotBeReadDueToPermissions_IsNotValid() + { + // Arrange + const string filePath = "file"; + var fileSystem = new Mock(MockBehavior.Strict); + fileSystem.Setup(fs => fs.ExistFile(filePath)).Returns(true); + fileSystem.Setup(fs => fs.NewFileStream(filePath, FileMode.Open, FileAccess.Read)).Throws(new UnauthorizedAccessException()); + + var provider = new RunSettingsCommandLineOptionsProvider(new TestExtension(), fileSystem.Object); + CommandLineOption option = provider.GetCommandLineOptions().Single(); + + // Act + ValidationResult result = await provider.ValidateOptionArgumentsAsync(option, [filePath]); + + // Assert + Assert.IsFalse(result.IsValid); + Assert.AreEqual(string.Format(CultureInfo.CurrentCulture, ExtensionResources.RunsettingsFileCannotBeRead, filePath), result.ErrorMessage); + } + [TestMethod] public async Task RunSettingsOption_WhenFileExistsAndCanBeOpen_IsValid() {