Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(GetAssemblyPath)];
#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
Comment thread
Evangelink marked this conversation as resolved.
switch (request)
{
Expand Down Expand Up @@ -179,6 +179,33 @@ public void Dispose()
GC.SuppressFinalize(this);
}

/// <summary>
/// Gets the path of an assembly, falling back to the assembly name when
/// <see cref="Assembly.Location"/> returns an empty string (e.g. on Android CoreCLR
/// where assemblies are memory-mapped).
/// </summary>
#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;
Comment thread
Evangelink marked this conversation as resolved.
#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
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<Task> asyncFunc)
{
_incomingRequestCounter.AddCount();
Expand Down
Original file line number Diff line number Diff line change
@@ -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>();
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>();
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>();
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>();
assembly.Setup(a => a.Location).Returns(string.Empty);
assembly.Setup(a => a.GetName()).Returns(new AssemblyName());

Comment thread
Evangelink marked this conversation as resolved.
// Act & Assert
Assert.ThrowsExactly<InvalidOperationException>(
() => 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));
}
}