diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs index 298987ef008..2449288f3e1 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -396,6 +396,106 @@ public void SdkResolverLoaderHonorsAdditionalResolversFolder() } } + /// + /// Test that LoadResolverAssembly handles fallback behavior correctly based on isRunningInVS. + /// Uses MockSdkResolverLoader to simulate both VS and non-VS behaviors . + /// + [Theory] + [InlineData(true, false)] // isRunningInVS = true, no fallback, should fail when Assembly.Load fails + [InlineData(false, true)] // isRunningInVS = false, has fallback, should succeed with LoadFrom + public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool isRunningInVS, bool shouldSucceed) + { + using (var env = TestEnvironment.Create(_output)) + { + // Create resolver folder structure with the specific name that triggers special logic + var testRoot = env.CreateFolder().Path; + var resolverFolder = Path.Combine(testRoot, "Microsoft.DotNet.MSBuildSdkResolver"); + Directory.CreateDirectory(resolverFolder); + + var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); + + // Create file based on test scenario + // For shouldSucceed=false: create invalid file to test Assembly.Load failure + // For shouldSucceed=true: we don't create a file - we'll simulate success in the mock + // to avoid side effects from loading Microsoft.Build.dll copy + if (!shouldSucceed) + { + // For no-fallback test: create invalid assembly content to force Assembly.Load to fail + File.WriteAllText(assemblyFile, "invalid assembly content"); + } + + // Use MockSdkResolverLoader to simulate behavior without modifying global state + // We avoid actually calling Assembly.LoadFrom with Microsoft.Build.dll copy to prevent side effects + var loader = new MockSdkResolverLoader + { + FindPotentialSdkResolversFunc = (_, __) => new List { assemblyFile }, + GetResolverTypesFunc = assembly => new[] { typeof(MockSdkResolverWithAssemblyPath) }, + LoadResolverAssemblyFunc = (resolverPath) => + { + string resolverFileName = Path.GetFileNameWithoutExtension(resolverPath); + if (resolverFileName.Equals("Microsoft.DotNet.MSBuildSdkResolver", StringComparison.OrdinalIgnoreCase)) + { + // Capture test parameters via closure + bool simulatedIsRunningInVS = isRunningInVS; + bool simulatedShouldSucceed = shouldSucceed; + + if (simulatedIsRunningInVS) + { + // VS behavior: try Assembly.Load directly, no fallback + // We only call Assembly.Load for invalid file case (shouldSucceed=false) + // to test that exception propagates correctly + AssemblyName assemblyName = new AssemblyName(resolverFileName) + { + CodeBase = resolverPath, + }; + // This will throw if file is invalid (no fallback) + return Assembly.Load(assemblyName); + } + else + { + // Non-VS behavior: try Assembly.Load first, fallback to LoadFrom if it fails + // We simulate this without actually calling Assembly.Load or Assembly.LoadFrom + // to avoid side effects from loading Microsoft.Build.dll copy + if (simulatedShouldSucceed) + { + // Simulate successful fallback: return an existing assembly + // We use an assembly that's already loaded and contains a valid resolver type + // to avoid side effects from loading Microsoft.Build.dll copy + return typeof(MockSdkResolverWithAssemblyPath).Assembly; + } + else + { + // This branch shouldn't be reached in non-VS + shouldSucceed=false case + // but if it is, simulate Assembly.Load failure + throw new BadImageFormatException("Assembly could not be loaded"); + } + } + } + return Assembly.LoadFrom(resolverPath); + } + }; + + if (shouldSucceed) + { + // Test that loading succeeds with fallback logic + var resolvers = loader.LoadAllResolvers(new MockElementLocation("file")); + resolvers.ShouldNotBeNull(); + resolvers.Count.ShouldBeGreaterThan(0); + } + else + { + // Should throw InvalidProjectFileException because: + // 1. Simulated isRunningInVS = true → no fallback + // 2. Assembly.Load fails on invalid assembly + // 3. No fallback → exception propagates + var exception = Should.Throw(() => + loader.LoadAllResolvers(new MockElementLocation("file"))); + + exception.Message.ShouldContain("could not be loaded"); + } + } + } + private sealed class MockSdkResolverThatDoesNotLoad : SdkResolverBase { public const string ExpectedMessage = "A8BB8B3131D3475D881ACD3AF8D75BD6"; @@ -435,7 +535,13 @@ private sealed class MockSdkResolverWithAssemblyPath : SdkResolverBase { public string AssemblyPath; - public MockSdkResolverWithAssemblyPath(string assemblyPath = "") + // Parameterless constructor for reflection-based instantiation + public MockSdkResolverWithAssemblyPath() + : this("") + { + } + + public MockSdkResolverWithAssemblyPath(string assemblyPath) { AssemblyPath = assemblyPath; }