diff --git a/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs b/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs index 630149957a..7b21892200 100644 --- a/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs +++ b/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs @@ -955,34 +955,43 @@ private string GetTestHostPath(string runtimeConfigDevPath, string depsFilePath, { #if NETCOREAPP using var doc = JsonDocument.Parse(stream); - var runtimeOptions = doc.RootElement.GetProperty("runtimeOptions"); - var additionalProbingPaths = runtimeOptions.GetProperty("additionalProbingPaths"); - foreach (var x in additionalProbingPaths.EnumerateArray()) + + if (doc.RootElement.TryGetProperty("runtimeOptions", out var runtimeOptions) && + runtimeOptions.TryGetProperty("additionalProbingPaths", out var additionalProbingPaths)) { - EqtTrace.Verbose("DotnetTestHostmanager: Looking for path {0} in folder {1}", testHostPath, x.GetString()); - string testHostFullPath; - try - { - testHostFullPath = Path.Combine(x.GetString()!, testHostPath); - } - catch (ArgumentException) + foreach (var x in additionalProbingPaths.EnumerateArray()) { - // https://github.com/Microsoft/vstest/issues/847 - // skip any invalid paths and continue checking the others - continue; - } + EqtTrace.Verbose("DotnetTestHostmanager: Looking for path {0} in folder {1}", testHostPath, x.GetString()); + string testHostFullPath; + try + { + testHostFullPath = Path.Combine(x.GetString()!, testHostPath); + } + catch (ArgumentException) + { + // https://github.com/Microsoft/vstest/issues/847 + // skip any invalid paths and continue checking the others + continue; + } - if (_fileHelper.Exists(testHostFullPath)) - { - EqtTrace.Verbose("DotnetTestHostmanager: Found testhost.dll in {0}", testHostFullPath); - return testHostFullPath; + if (_fileHelper.Exists(testHostFullPath)) + { + EqtTrace.Verbose("DotnetTestHostmanager: Found testhost.dll in {0}", testHostFullPath); + return testHostFullPath; + } } } #else using var reader = new StreamReader(stream); var parsed = Json.Deserialize(reader) as IDictionary; - var runtimeOpts = parsed?["runtimeOptions"] as IDictionary; - var probingPaths = runtimeOpts?["additionalProbingPaths"] as IList; + var runtimeOpts = parsed is not null && parsed.TryGetValue("runtimeOptions", out var runtimeOptsObj) + ? runtimeOptsObj as IDictionary + : null; + + var probingPaths = runtimeOpts is not null && runtimeOpts.TryGetValue("additionalProbingPaths", out var probingPathsObj) + ? probingPathsObj as IList + : null; + if (probingPaths is not null) { foreach (var x in probingPaths) diff --git a/test/Microsoft.TestPlatform.TestHostProvider.UnitTests/Hosting/DotnetTestHostManagerFilesystemIntegrationTests.cs b/test/Microsoft.TestPlatform.TestHostProvider.UnitTests/Hosting/DotnetTestHostManagerFilesystemIntegrationTests.cs index 7226c3d4f7..27bb05c09a 100644 --- a/test/Microsoft.TestPlatform.TestHostProvider.UnitTests/Hosting/DotnetTestHostManagerFilesystemIntegrationTests.cs +++ b/test/Microsoft.TestPlatform.TestHostProvider.UnitTests/Hosting/DotnetTestHostManagerFilesystemIntegrationTests.cs @@ -153,6 +153,56 @@ public void GetTestHostProcessStartInfo_FindsTestHostDllViaRealDepsJsonAndRuntim startInfo.Arguments.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)); } + [TestMethod] + public void GetTestHostProcessStartInfo_DoesNotThrowWhenRuntimeConfigDevJsonHasNoAdditionalProbingPaths() + { + // Arrange — a runtimeconfig.dev.json that exists but only carries runtime options other than + // additionalProbingPaths (for example Hot Reload switches generated by the SDK for net6.0+ Debug builds). + // The manager must not fail reading it; it should fall back to finding testhost.dll next to the source. + string sourceDll = Path.Combine(_tempDir, "TestProject.dll"); + string depsJsonPath = Path.Combine(_tempDir, "TestProject.deps.json"); + + File.WriteAllText(sourceDll, "fake test dll"); + + // Minimal deps.json — testhost not in it, so resolution falls through to the source directory. + string depsJson = """ + { + "runtimeTarget": { "name": ".NETCoreApp,Version=v8.0" }, + "compilationOptions": {}, + "targets": { ".NETCoreApp,Version=v8.0": {} }, + "libraries": {} + } + """; + File.WriteAllText(depsJsonPath, depsJson); + + // runtimeconfig.dev.json without an "additionalProbingPaths" property. + string runtimeConfigDevJson = """ + { + "runtimeOptions": { + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": true, + "System.StartupHookProvider.IsSupported": true + } + } + } + """; + File.WriteAllText(Path.Combine(_tempDir, "TestProject.runtimeconfig.dev.json"), runtimeConfigDevJson); + + // Place testhost.dll next to source so the manager can resolve it without probing paths. + string testhostNextToSource = Path.Combine(_tempDir, "testhost.dll"); + File.WriteAllText(testhostNextToSource, "fake testhost"); + + var manager = CreateManager(new FileHelper()); + manager.Initialize(_mockMessageLogger.Object, ".NETCoreApp,Version=v8.0"); + + // Act — this previously threw KeyNotFoundException when additionalProbingPaths was missing. + var startInfo = manager.GetTestHostProcessStartInfo(new[] { sourceDll }, null, _connectionInfo); + + // Assert — resolution succeeds and falls back to testhost.dll next to the source. + Assert.IsNotNull(startInfo.Arguments); + Assert.Contains("testhost.dll", startInfo.Arguments); + } + [TestMethod] public void GetTestHostProcessStartInfo_PassesDepsFileArgWhenDepsJsonExists() {