From a831875329e7a02a39edf0fd6825c5b099a9745e Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Wed, 22 Apr 2026 10:09:22 +0200 Subject: [PATCH 1/2] Handle empty Assembly.Location on Android CoreCLR When Assembly.Location returns empty string (e.g. on Android CoreCLR where assemblies are memory-mapped), fall back to using the assembly name with a .dll extension as a synthetic path. This allows AreValidSources validation to pass and downstream Assembly.Load by name to find the already-loaded assembly. Fixes #7769 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ngleSessionVSTestAndTestAnywhereAdapter.cs | 27 ++++- ...eSessionVSTestBridgedTestFrameworkTests.cs | 105 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/SynchronizedSingleSessionVSTestBridgedTestFrameworkTests.cs diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs index 0789698909..c44dd19743 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs @@ -150,7 +150,7 @@ protected sealed override Task ExecuteRequestAsync(TestExecutionRequest request, => ExecuteRequestWithRequestCountGuardAsync(async () => { #pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - string[] testAssemblyPaths = [.. _getTestAssemblies().Select(x => x.Location)]; + string[] testAssemblyPaths = [.. _getTestAssemblies().Select(x => GetAssemblyPath(x))]; #pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file switch (request) { @@ -179,6 +179,31 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Gets the path of an assembly, falling back to the assembly name when + /// returns an empty string (e.g. on Android CoreCLR + /// where assemblies are memory-mapped). + /// +#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file + internal static string GetAssemblyPath(Assembly assembly) + { + string location = assembly.Location; + if (!string.IsNullOrEmpty(location)) + { + return location; + } + + // On platforms like Android CoreCLR, assemblies may be memory-mapped and + // Assembly.Location returns an empty string. Use the assembly name as a + // synthetic path since the downstream code (on .NET Core) loads assemblies + // by name via Assembly.Load, not by file path. + string name = assembly.GetName().Name + ?? throw new InvalidOperationException($"Cannot determine the name of assembly '{assembly}'."); + + return name + ".dll"; + } +#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file + private async Task ExecuteRequestWithRequestCountGuardAsync(Func asyncFunc) { _incomingRequestCounter.AddCount(); diff --git a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/SynchronizedSingleSessionVSTestBridgedTestFrameworkTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/SynchronizedSingleSessionVSTestBridgedTestFrameworkTests.cs new file mode 100644 index 0000000000..ec336f40d8 --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/SynchronizedSingleSessionVSTestBridgedTestFrameworkTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using System.Reflection.Emit; + +using Moq; + +namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests; + +[TestClass] +public sealed class SynchronizedSingleSessionVSTestBridgedTestFrameworkTests +{ +#if NETCOREAPP + [TestMethod] + public void GetAssemblyPath_WhenLocationIsNonEmpty_ReturnsLocation() + { + // Arrange - Assembly.Location is virtual on .NET Core, so Moq can mock it + var assembly = new Mock(); + assembly.Setup(a => a.Location).Returns(@"C:\path\to\MyTests.dll"); + + // Act + string result = SynchronizedSingleSessionVSTestBridgedTestFramework.GetAssemblyPath(assembly.Object); + + // Assert + Assert.AreEqual(@"C:\path\to\MyTests.dll", result); + } + + [TestMethod] + public void GetAssemblyPath_WhenLocationIsEmpty_ReturnsSyntheticPathFromAssemblyName() + { + // Arrange - simulate Android CoreCLR where Assembly.Location returns "" + var assembly = new Mock(); + assembly.Setup(a => a.Location).Returns(string.Empty); + assembly.Setup(a => a.GetName()).Returns(new AssemblyName("MyTests")); + + // Act + string result = SynchronizedSingleSessionVSTestBridgedTestFramework.GetAssemblyPath(assembly.Object); + + // Assert + Assert.AreEqual("MyTests.dll", result); + } + + [TestMethod] + public void GetAssemblyPath_WhenLocationIsNull_ReturnsSyntheticPathFromAssemblyName() + { + // Arrange + var assembly = new Mock(); + assembly.Setup(a => a.Location).Returns((string)null!); + assembly.Setup(a => a.GetName()).Returns(new AssemblyName("MyTests")); + + // Act + string result = SynchronizedSingleSessionVSTestBridgedTestFramework.GetAssemblyPath(assembly.Object); + + // Assert + Assert.AreEqual("MyTests.dll", result); + } + + [TestMethod] + public void GetAssemblyPath_WhenLocationIsEmpty_AndAssemblyNameIsNull_Throws() + { + // Arrange + var assembly = new Mock(); + assembly.Setup(a => a.Location).Returns(string.Empty); + assembly.Setup(a => a.GetName()).Returns(new AssemblyName()); + + // Act & Assert + Assert.ThrowsExactly( + () => SynchronizedSingleSessionVSTestBridgedTestFramework.GetAssemblyPath(assembly.Object)); + } + + [TestMethod] + public void GetAssemblyPath_WithDynamicInMemoryAssembly_ReturnsSyntheticPath() + { + // Arrange - create a real in-memory assembly that has empty Location + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName("InMemoryTestAssembly"), + AssemblyBuilderAccess.Run); + + // Verify our assumption: dynamic assemblies have empty Location + Assert.AreEqual(string.Empty, assemblyBuilder.Location); + + // Act + string result = SynchronizedSingleSessionVSTestBridgedTestFramework.GetAssemblyPath(assemblyBuilder); + + // Assert + Assert.AreEqual("InMemoryTestAssembly.dll", result); + } +#endif + + [TestMethod] + public void GetAssemblyPath_WithRealAssembly_ReturnsActualLocation() + { + // Arrange - use the currently executing assembly which has a real file-backed location + Assembly assembly = typeof(SynchronizedSingleSessionVSTestBridgedTestFrameworkTests).Assembly; + + // Act + string result = SynchronizedSingleSessionVSTestBridgedTestFramework.GetAssemblyPath(assembly); + + // Assert - should return the real path ending with .dll or .exe + Assert.AreEqual(assembly.Location, result); + Assert.IsTrue( + result.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || result.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)); + } +} From 57a7509b4c14f5e7619cbdd81a1cec9fe89098c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 23 Apr 2026 12:30:22 +0200 Subject: [PATCH 2/2] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amaury Levé --- .../SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs index c44dd19743..e2d903229e 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs @@ -150,7 +150,7 @@ protected sealed override Task ExecuteRequestAsync(TestExecutionRequest request, => ExecuteRequestWithRequestCountGuardAsync(async () => { #pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - string[] testAssemblyPaths = [.. _getTestAssemblies().Select(x => GetAssemblyPath(x))]; + string[] testAssemblyPaths = [.. _getTestAssemblies().Select(GetAssemblyPath)]; #pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file switch (request) { @@ -187,7 +187,9 @@ public void Dispose() #pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file internal static string GetAssemblyPath(Assembly assembly) { +#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file string location = assembly.Location; +#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file if (!string.IsNullOrEmpty(location)) { return location;