diff --git a/src/Microsoft.TestPlatform.Common/Utilities/AssemblyResolver.cs b/src/Microsoft.TestPlatform.Common/Utilities/AssemblyResolver.cs index 033c0c0327..6f8da1ad72 100644 --- a/src/Microsoft.TestPlatform.Common/Utilities/AssemblyResolver.cs +++ b/src/Microsoft.TestPlatform.Common/Utilities/AssemblyResolver.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; @@ -122,7 +123,71 @@ internal void AddSearchDirectories(IEnumerable directories) TPDebug.Assert(requestedName != null && !requestedName.Name.IsNullOrEmpty(), "AssemblyResolver.OnResolve: requested is null or name is empty!"); - foreach (var dir in _searchDirectories) + // Workaround: adding expected folder for the satellite assembly related to the current CurrentThread.CurrentUICulture relative to the current assembly location. + // After the move to the net461 the runtime doesn't resolve anymore the satellite assembly correctly. + // The expected workflow should be https://learn.microsoft.com/en-us/dotnet/core/extensions/package-and-deploy-resources#net-framework-resource-fallback-process + // But the resolution never fallback to the CultureInfo.Parent folder and fusion log return a failure like: + // ... + // LOG: The same bind was seen before, and was failed with hr = 0x80070002. + // ERR: Unrecoverable error occurred during pre - download check(hr = 0x80070002). + // ... + // The bizarre thing is that as a result we're failing caller task like discovery and when for reporting reason + // we're accessing again to the resource it works. + // Looks like a loading timing issue but we're not in control of the assembly loader order. + var isResource = requestedName.Name.EndsWith(".resources"); + string[]? satelliteLocation = null; + + // We help to resolve only test platform resources to be less invasive as possible with the default/expected behavior + if (isResource && requestedName.Name.StartsWith("Microsoft.VisualStudio.TestPlatform")) + { + try + { + string? currentAssemblyLocation = null; + try + { + currentAssemblyLocation = Assembly.GetExecutingAssembly().Location; + // In .NET 5 and later versions, for bundled assemblies, the value returned is an empty string. + currentAssemblyLocation = currentAssemblyLocation == string.Empty ? null : Path.GetDirectoryName(currentAssemblyLocation); + } + catch (NotSupportedException) + { + // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location + } + + if (currentAssemblyLocation is not null) + { + List satelliteLocations = new(); + + // We mimic the satellite workflow and we add CurrentUICulture and CurrentUICulture.Parent folder in order + string? currentUICulture = Thread.CurrentThread.CurrentUICulture?.Name; + if (currentUICulture is not null) + { + satelliteLocations.Add(Path.Combine(currentAssemblyLocation, currentUICulture)); + } + + // CurrentUICulture.Parent + string? parentCultureInfo = Thread.CurrentThread.CurrentUICulture?.Parent?.Name; + if (parentCultureInfo is not null) + { + satelliteLocations.Add(Path.Combine(currentAssemblyLocation, parentCultureInfo)); + } + + if (satelliteLocations.Count > 0) + { + satelliteLocation = satelliteLocations.ToArray(); + } + } + } + catch (Exception ex) + { + // We catch here because this is a workaround, we're trying to substitute the expected workflow of the runtime + // and this shouldn't be needed, but if we fail we want to log what's happened and give a chance to the in place + // resolution workflow + EqtTrace.Error($"AssemblyResolver.OnResolve: Exception during the custom satellite resolution\n{ex}"); + } + } + + foreach (var dir in (satelliteLocation is not null) ? _searchDirectories.Union(satelliteLocation) : _searchDirectories) { if (dir.IsNullOrEmpty()) { @@ -136,7 +201,6 @@ internal void AddSearchDirectories(IEnumerable directories) var assemblyPath = Path.Combine(dir, requestedName.Name + extension); try { - var isResource = requestedName.Name.EndsWith(".resources"); bool pushed = false; try {