Skip to content

Commit efc614f

Browse files
authored
Fix assembly resolution error (#2948)
1 parent b4ab8ee commit efc614f

File tree

3 files changed

+88
-63
lines changed

3 files changed

+88
-63
lines changed

src/Adapter/MSTestAdapter.PlatformServices/AssemblyResolver.cs

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class AssemblyResolver :
8686
/// </summary>
8787
private readonly object _syncLock = new();
8888

89+
private static List<string>? s_currentlyLoading;
8990
private bool _disposed;
9091

9192
/// <summary>
@@ -335,7 +336,7 @@ protected virtual
335336
if (EqtTrace.IsInfoEnabled)
336337
{
337338
EqtTrace.Info(
338-
"AssemblyResolver: {0}: Failed to create assemblyName. Reason:{1} ",
339+
"MSTest.AssemblyResolver.OnResolve: Failed to create assemblyName '{0}'. Reason: {1} ",
339340
name,
340341
ex);
341342
}
@@ -344,7 +345,7 @@ protected virtual
344345
return null;
345346
}
346347

347-
DebugEx.Assert(requestedName != null && !StringEx.IsNullOrEmpty(requestedName.Name), "AssemblyResolver.OnResolve: requested is null or name is empty!");
348+
DebugEx.Assert(requestedName != null && !StringEx.IsNullOrEmpty(requestedName.Name), "MSTest.AssemblyResolver.OnResolve: requested is null or name is empty!");
348349

349350
foreach (string dir in searchDirectorypaths)
350351
{
@@ -359,15 +360,39 @@ protected virtual
359360
{
360361
if (EqtTrace.IsVerboseEnabled)
361362
{
362-
EqtTrace.Verbose("AssemblyResolver: Searching assembly: {0} in the directory: {1}", requestedName.Name, dir);
363+
EqtTrace.Verbose("MSTest.AssemblyResolver.OnResolve: Searching assembly '{0}' in the directory '{1}'", requestedName.Name, dir);
363364
}
364365
});
365366

366367
foreach (string extension in new string[] { ".dll", ".exe" })
367368
{
368369
string assemblyPath = Path.Combine(dir, requestedName.Name + extension);
369370

371+
bool isPushed = false;
372+
bool isResource = requestedName.Name.EndsWith(".resources", StringComparison.InvariantCulture);
373+
if (isResource)
374+
{
375+
// Are we recursively looking up the same resource? Note - our backout code will set
376+
// the ResourceHelper's currentlyLoading stack to null if an exception occurs.
377+
if (s_currentlyLoading != null && s_currentlyLoading.Count > 0 && s_currentlyLoading.LastIndexOf(assemblyPath) != -1)
378+
{
379+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Assembly '{0}' is searching for itself recursively '{1}', returning as not found.", name, assemblyPath);
380+
_resolvedAssemblies[name] = null;
381+
return null;
382+
}
383+
384+
s_currentlyLoading ??= new List<string>();
385+
s_currentlyLoading.Add(assemblyPath); // Push
386+
isPushed = true;
387+
}
388+
370389
Assembly? assembly = SearchAndLoadAssembly(assemblyPath, name, requestedName, isReflectionOnly);
390+
if (isResource && isPushed)
391+
{
392+
DebugEx.Assert(s_currentlyLoading is not null, "_currentlyLoading should not be null");
393+
s_currentlyLoading.RemoveAt(s_currentlyLoading.Count - 1); // Pop
394+
}
395+
371396
if (assembly != null)
372397
{
373398
return assembly;
@@ -447,7 +472,7 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
447472
{
448473
if (StringEx.IsNullOrEmpty(args.Name))
449474
{
450-
Debug.Fail("AssemblyResolver.OnResolve: args.Name is null or empty.");
475+
Debug.Fail("MSTest.AssemblyResolver.OnResolve: args.Name is null or empty.");
451476
return null;
452477
}
453478

@@ -457,7 +482,7 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
457482
{
458483
if (EqtTrace.IsInfoEnabled)
459484
{
460-
EqtTrace.Info("AssemblyResolver: Resolving assembly: {0}.", args.Name);
485+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolving assembly '{0}'", args.Name);
461486
}
462487
});
463488

@@ -469,7 +494,7 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
469494
{
470495
if (EqtTrace.IsInfoEnabled)
471496
{
472-
EqtTrace.Info("AssemblyResolver: Resolving assembly after applying policy: {0}.", assemblyNameToLoad);
497+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolving assembly after applying policy '{0}'", assemblyNameToLoad);
473498
}
474499
});
475500

@@ -482,62 +507,58 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
482507
}
483508

484509
assembly = SearchAssembly(_searchDirectories, assemblyNameToLoad, isReflectionOnly);
485-
486510
if (assembly != null)
487511
{
488512
return assembly;
489513
}
490514

491-
if (_directoryList != null && _directoryList.Count != 0)
515+
// required assembly is not present in searchDirectories??
516+
// see, if we can find it in user specified search directories.
517+
while (assembly == null && _directoryList?.Count > 0)
492518
{
493-
// required assembly is not present in searchDirectories??
494-
// see, if we can find it in user specified search directories.
495-
while (assembly == null && _directoryList.Count > 0)
496-
{
497-
// instead of loading whole search directory in one time, we are adding directory on the basis of need
498-
RecursiveDirectoryPath currentNode = _directoryList.Dequeue();
499-
500-
List<string> incrementalSearchDirectory = [];
519+
// instead of loading whole search directory in one time, we are adding directory on the basis of need
520+
RecursiveDirectoryPath currentNode = _directoryList.Dequeue();
501521

502-
if (DoesDirectoryExist(currentNode.DirectoryPath))
503-
{
504-
incrementalSearchDirectory.Add(currentNode.DirectoryPath);
505-
506-
if (currentNode.IncludeSubDirectories)
507-
{
508-
// Add all its sub-directory in depth first search order.
509-
AddSubdirectories(currentNode.DirectoryPath, incrementalSearchDirectory);
510-
}
522+
List<string> incrementalSearchDirectory = [];
511523

512-
// Add this directory list in this.searchDirectories so that when we will try to resolve some other
513-
// assembly, then it will look in this whole directory first.
514-
_searchDirectories.AddRange(incrementalSearchDirectory);
524+
if (DoesDirectoryExist(currentNode.DirectoryPath))
525+
{
526+
incrementalSearchDirectory.Add(currentNode.DirectoryPath);
515527

516-
assembly = SearchAssembly(incrementalSearchDirectory, assemblyNameToLoad, isReflectionOnly);
517-
}
518-
else
528+
if (currentNode.IncludeSubDirectories)
519529
{
520-
// generate warning that path does not exist.
521-
SafeLog(
522-
assemblyNameToLoad,
523-
() =>
524-
{
525-
if (EqtTrace.IsWarningEnabled)
526-
{
527-
EqtTrace.Warning(
528-
"The Directory: {0}, does not exist",
529-
currentNode.DirectoryPath);
530-
}
531-
});
530+
// Add all its sub-directory in depth first search order.
531+
AddSubdirectories(currentNode.DirectoryPath, incrementalSearchDirectory);
532532
}
533-
}
534533

535-
if (assembly != null)
534+
// Add this directory list in this.searchDirectories so that when we will try to resolve some other
535+
// assembly, then it will look in this whole directory first.
536+
_searchDirectories.AddRange(incrementalSearchDirectory);
537+
538+
assembly = SearchAssembly(incrementalSearchDirectory, assemblyNameToLoad, isReflectionOnly);
539+
}
540+
else
536541
{
537-
return assembly;
542+
// generate warning that path does not exist.
543+
SafeLog(
544+
assemblyNameToLoad,
545+
() =>
546+
{
547+
if (EqtTrace.IsWarningEnabled)
548+
{
549+
EqtTrace.Warning(
550+
"MSTest.AssemblyResolver.OnResolve: the directory '{0}', does not exist",
551+
currentNode.DirectoryPath);
552+
}
553+
});
538554
}
539555
}
540556

557+
if (assembly != null)
558+
{
559+
return assembly;
560+
}
561+
541562
// Try for default load for System dlls that can't be found in search paths. Needs to loaded just by name.
542563
try
543564
{
@@ -580,7 +601,7 @@ private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender,
580601
{
581602
if (EqtTrace.IsInfoEnabled)
582603
{
583-
EqtTrace.Info("AssemblyResolver: {0}: Failed to load assembly. Reason: {1}", assemblyNameToLoad, ex);
604+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason: {1}", assemblyNameToLoad, ex);
584605
}
585606
});
586607
}
@@ -609,7 +630,7 @@ private bool TryLoadFromCache(string assemblyName, bool isReflectionOnly, out As
609630
{
610631
if (EqtTrace.IsInfoEnabled)
611632
{
612-
EqtTrace.Info("AssemblyResolver: Resolved: {0}.", assemblyName);
633+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolved '{0}'", assemblyName);
613634
}
614635
});
615636
return true;
@@ -685,7 +706,7 @@ private static void SafeLog(string? assemblyName, Action loggerAction)
685706
{
686707
if (EqtTrace.IsInfoEnabled)
687708
{
688-
EqtTrace.Info("AssemblyResolver: Resolved assembly: {0}. ", assemblyName);
709+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Resolved assembly '{0}'", assemblyName);
689710
}
690711
});
691712

@@ -699,7 +720,7 @@ private static void SafeLog(string? assemblyName, Action loggerAction)
699720
{
700721
if (EqtTrace.IsInfoEnabled)
701722
{
702-
EqtTrace.Info("AssemblyResolver: Failed to load assembly: {0}. Reason:{1} ", assemblyName, ex);
723+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason:{1} ", assemblyName, ex);
703724
}
704725
});
705726

@@ -717,7 +738,7 @@ private static void SafeLog(string? assemblyName, Action loggerAction)
717738
{
718739
if (EqtTrace.IsInfoEnabled)
719740
{
720-
EqtTrace.Info("AssemblyResolver: Failed to load assembly: {0}. Reason:{1} ", assemblyName, ex);
741+
EqtTrace.Info("MSTest.AssemblyResolver.OnResolve: Failed to load assembly '{0}'. Reason:{1} ", assemblyName, ex);
721742
}
722743
});
723744
}

src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,8 @@ internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IFrame
102102
/// </summary>
103103
public void SetupHost()
104104
{
105-
#if NETFRAMEWORK || NET
106-
List<string> resolutionPaths = GetResolutionPaths(
107-
_sourceFileName,
108-
#if NETFRAMEWORK
109-
VSInstallationUtilities.IsCurrentProcessRunningInPortableMode());
110-
#else
111-
false);
112-
#endif
105+
#if NET
106+
List<string> resolutionPaths = GetResolutionPaths(_sourceFileName, false);
113107

114108
if (EqtTrace.IsInfoEnabled)
115109
{
@@ -125,10 +119,20 @@ public void SetupHost()
125119
{
126120
assemblyResolver.Dispose();
127121
}
122+
#elif NETFRAMEWORK
123+
List<string> resolutionPaths = GetResolutionPaths(_sourceFileName, VSInstallationUtilities.IsCurrentProcessRunningInPortableMode());
128124

129-
#endif
125+
if (EqtTrace.IsInfoEnabled)
126+
{
127+
EqtTrace.Info("DesktopTestSourceHost.SetupHost(): Creating assembly resolver with resolution paths {0}.", string.Join(",", resolutionPaths));
128+
}
129+
130+
// NOTE: These 2 lines are super important, see https://github.com/microsoft/testfx/issues/2922
131+
// It's not entirely clear why but not assigning directly the resolver to the field (or/and) disposing the resolver in
132+
// case of an error in TryAddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver causes the issue.
133+
_parentDomainAssemblyResolver = new AssemblyResolver(resolutionPaths);
134+
_ = TryAddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver(_parentDomainAssemblyResolver, Path.GetDirectoryName(_sourceFileName)!);
130135

131-
#if NETFRAMEWORK
132136
// Case when DisableAppDomain setting is present in runsettings and no child-appdomain needs to be created
133137
if (!_isAppDomainCreationDisabled)
134138
{

test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AssemblyResolverTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ namespace MSTest.Acceptance.IntegrationTests;
1010
[TestGroup]
1111
public class AssemblyResolverTests : AcceptanceTestBase
1212
{
13-
private readonly TestAssetFixture _testAssetFixture;
1413
private const string AssetName = "AssemblyResolverCrash";
14+
private readonly TestAssetFixture _testAssetFixture;
1515

1616
// There's a bug in TAFX where we need to use it at least one time somewhere to use it inside the fixture self (AcceptanceFixture).
1717
public AssemblyResolverTests(ITestExecutionContext testExecutionContext, TestAssetFixture testAssetFixture,
@@ -31,7 +31,7 @@ public async Task RunningTests_DoesNotHitResourceRecursionIssueAndDoesNotCrashTh
3131

3232
var testHost = TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetFramework[0].Arguments);
3333

34-
var testHostResult = await testHost.ExecuteAsync();
34+
TestHostResult testHostResult = await testHost.ExecuteAsync();
3535

3636
testHostResult.AssertExitCodeIs(ExitCodes.Success);
3737
}

0 commit comments

Comments
 (0)