Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ALC in ProjectReference scenarios #56662

Merged
merged 3 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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,6 +150,28 @@ public void AssemblyLoading_DependencyLocationNotAdded()
Assert.Equal(@"", actual);
}

[ConditionalFact(typeof(CoreClrOnly))]
public void AssemblyLoading_DependencyInDifferentDirectory()
{
StringBuilder sb = new StringBuilder();
var loader = new DefaultAnalyzerAssemblyLoader();

var tempDir = Temp.CreateDirectory();

var deltaFile = tempDir.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta1.Path);
loader.AddDependencyLocation(deltaFile.Path);
loader.AddDependencyLocation(_testFixture.Gamma.Path);
Assembly gamma = loader.LoadFromPath(_testFixture.Gamma.Path);

var b = gamma.CreateInstance("Gamma.G")!;
var writeMethod = b.GetType().GetMethod("Write")!;
writeMethod.Invoke(b, new object[] { sb, "Test G" });

var actual = sb.ToString();
Assert.Equal(@"Delta: Gamma: Test G
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta

How do we know which version of "Delta" was loaded? Was this the version from deltaFile.Path or from _textFixture.Delta1.Path (which seems like it might be the same path as _textFixture.Gamma.Path)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're asking which copy of Delta version 1, right? I think we can check what file paths the assemblies were loaded from using the TestAccessor, but I'd have to check.

", actual);
}

[Fact]
public void AssemblyLoading_MultipleVersions()
{
Expand Down Expand Up @@ -203,6 +225,166 @@ public void AssemblyLoading_MultipleVersions()
}
}

[Fact]
public void AssemblyLoading_MultipleVersions_NoExactMatch()
{
StringBuilder sb = new StringBuilder();

var loader = new DefaultAnalyzerAssemblyLoader();
loader.AddDependencyLocation(_testFixture.Delta1.Path);
loader.AddDependencyLocation(_testFixture.Epsilon.Path);
loader.AddDependencyLocation(_testFixture.Delta3.Path);

Assembly epsilon = loader.LoadFromPath(_testFixture.Epsilon.Path);
var e = epsilon.CreateInstance("Epsilon.E")!;
e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" });

#if NETCOREAPP
var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader);
Assert.Equal(1, alcs.Length);

Assert.Equal(new[] {
("Delta", "3.0.0.0", _testFixture.Delta3.Path),
("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)
}, alcs[0].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order());
#endif

var actual = sb.ToString();
if (ExecutionConditionUtil.IsCoreClr)
{
Assert.Equal(
@"Delta.3: Epsilon: Test E
",
actual);
}
else
{
Assert.Equal(
@"Delta: Epsilon: Test E
",
actual);
}
}

[Fact]
public void AssemblyLoading_MultipleVersions_MultipleEqualMatches()
{
StringBuilder sb = new StringBuilder();

// Delta2B and Delta2 have the same version, but we prefer Delta2 because it's in the same directory as Epsilon.
var loader = new DefaultAnalyzerAssemblyLoader();
loader.AddDependencyLocation(_testFixture.Delta2B.Path);
loader.AddDependencyLocation(_testFixture.Delta2.Path);
loader.AddDependencyLocation(_testFixture.Epsilon.Path);

Assembly epsilon = loader.LoadFromPath(_testFixture.Epsilon.Path);
var e = epsilon.CreateInstance("Epsilon.E")!;
e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" });

#if NETCOREAPP
var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader);
Assert.Equal(1, alcs.Length);

Assert.Equal(new[] {
("Delta", "2.0.0.0", _testFixture.Delta2.Path),
("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)
}, alcs[0].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order());
#endif

var actual = sb.ToString();
if (ExecutionConditionUtil.IsCoreClr)
{
Assert.Equal(
@"Delta.2: Epsilon: Test E
",
actual);
}
else
{
Assert.Equal(
@"Delta: Epsilon: Test E
",
actual);
}
}

[Fact]
public void AssemblyLoading_MultipleVersions_ExactAndGreaterMatch()
{
StringBuilder sb = new StringBuilder();

var loader = new DefaultAnalyzerAssemblyLoader();
loader.AddDependencyLocation(_testFixture.Delta2B.Path);
loader.AddDependencyLocation(_testFixture.Delta3.Path);
loader.AddDependencyLocation(_testFixture.Epsilon.Path);

Assembly epsilon = loader.LoadFromPath(_testFixture.Epsilon.Path);
var e = epsilon.CreateInstance("Epsilon.E")!;
e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" });

#if NETCOREAPP
var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader);
Assert.Equal(1, alcs.Length);

Assert.Equal(new[] {
("Delta", "2.0.0.0", _testFixture.Delta2B.Path),
("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)
}, alcs[0].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order());
#endif

var actual = sb.ToString();
if (ExecutionConditionUtil.IsCoreClr)
{
Assert.Equal(
@"Delta.2B: Epsilon: Test E
",
actual);
}
else
{
Assert.Equal(
@"Delta: Epsilon: Test E
",
actual);
}
}

[Fact]
public void AssemblyLoading_MultipleVersions_WorseMatchInSameDirectory()
{
StringBuilder sb = new StringBuilder();

var tempDir = Temp.CreateDirectory();
var epsilonFile = tempDir.CreateFile("Epsilon.dll").CopyContentFrom(_testFixture.Epsilon.Path);
var delta1File = tempDir.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta1.Path);

// Epsilon wants Delta2, but since Delta1 is in the same directory, we prefer Delta1 over Delta2.
var loader = new DefaultAnalyzerAssemblyLoader();
loader.AddDependencyLocation(delta1File.Path);
loader.AddDependencyLocation(_testFixture.Delta2.Path);
loader.AddDependencyLocation(epsilonFile.Path);

Assembly epsilon = loader.LoadFromPath(epsilonFile.Path);
var e = epsilon.CreateInstance("Epsilon.E")!;
e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" });

#if NETCOREAPP
var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader);
Assert.Equal(1, alcs.Length);

Assert.Equal(new[] {
("Delta", "1.0.0.0", delta1File.Path),
("Epsilon", "0.0.0.0", epsilonFile.Path)
}, alcs[0].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order());
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting a helper method: VerifyLoadContexts(new[] { ("Delta", ..., ...), ("Epsilon", ..., ...) });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


var actual = sb.ToString();
Assert.Equal(
@"Delta: Epsilon: Test E
",
actual);
}

[Fact]
public void AssemblyLoading_MultipleVersions_MultipleLoaders()
{
Expand Down Expand Up @@ -280,19 +462,11 @@ public void AssemblyLoading_MultipleVersions_MissingVersion()
var eWrite = e.GetType().GetMethod("Write")!;

var actual = sb.ToString();
if (ExecutionConditionUtil.IsCoreClr)
{
var exception = Assert.Throws<TargetInvocationException>(() => eWrite.Invoke(e, new object[] { sb, "Test E" }));
Assert.IsAssignableFrom<FileNotFoundException>(exception.InnerException);
}
else
{
eWrite.Invoke(e, new object[] { sb, "Test E" });
Assert.Equal(
eWrite.Invoke(e, new object[] { sb, "Test E" });
Assert.Equal(
@"Delta: Gamma: Test G
",
actual);
}
actual);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,37 @@ public void AssemblyLoading_Delete()
",
actual);
}

[ConditionalFact(typeof(CoreClrOnly))]
public void AssemblyLoading_DependencyInDifferentDirectory_Delete()
{
StringBuilder sb = new StringBuilder();
var loader = new ShadowCopyAnalyzerAssemblyLoader();

var tempDir1 = Temp.CreateDirectory();
var tempDir2 = Temp.CreateDirectory();
var tempDir3 = Temp.CreateDirectory();

var delta1File = tempDir1.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta1.Path);
var delta2File = tempDir2.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta2.Path);
var gammaFile = tempDir3.CreateFile("Gamma.dll").CopyContentFrom(_testFixture.Gamma.Path);

loader.AddDependencyLocation(delta1File.Path);
loader.AddDependencyLocation(delta2File.Path);
loader.AddDependencyLocation(gammaFile.Path);
Assembly gamma = loader.LoadFromPath(gammaFile.Path);

var b = gamma.CreateInstance("Gamma.G")!;
var writeMethod = b.GetType().GetMethod("Write")!;
writeMethod.Invoke(b, new object[] { sb, "Test G" });

File.Delete(delta1File.Path);
File.Delete(delta2File.Path);
File.Delete(gammaFile.Path);

var actual = sb.ToString();
Assert.Equal(@"Delta: Gamma: Test G
", actual);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,21 +154,10 @@ private AssemblyIdentity AddToCache(string fullPath, AssemblyIdentity identity)
}

#nullable enable
protected bool IsKnownDependencyLocation(string fullPath)
protected HashSet<string>? GetPaths(string simpleName)
{
CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath));
var simpleName = PathUtilities.GetFileName(fullPath, includeExtension: false);
if (!_knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths))
{
return false;
}

if (!paths.Contains(fullPath))
{
return false;
}

return true;
_knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths);
return paths;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -126,7 +127,8 @@ public DirectoryLoadContext(string directory, DefaultAnalyzerAssemblyLoader load
}

var assemblyPath = Path.Combine(Directory, simpleName + ".dll");
if (!_loader.IsKnownDependencyLocation(assemblyPath))
var paths = _loader.GetPaths(simpleName);
if (paths is null)
{
// The analyzer didn't explicitly register this dependency. Most likely the
// assembly we're trying to load here is netstandard or a similar framework
Expand All @@ -136,14 +138,46 @@ public DirectoryLoadContext(string directory, DefaultAnalyzerAssemblyLoader load
return _compilerLoadContext.LoadFromAssemblyName(assemblyName);
}

var pathToLoad = _loader.GetPathToLoad(assemblyPath);
return LoadFromAssemblyPath(pathToLoad);
Debug.Assert(paths.Any());
// A matching assembly in this directory was specified via /analyzer.
if (paths.Contains(assemblyPath))
{
return LoadFromAssemblyPath(_loader.GetPathToLoad(assemblyPath));
}

AssemblyName? bestCandidateName = null;
string? bestCandidatePath = null;
// The assembly isn't expected to be found at 'assemblyPath',
// but some assembly with the same simple name is known to the loader.
foreach (var candidatePath in paths)
{
// Note: we assume that the assembly really can be found at 'candidatePath'
// (without 'GetPathToLoad'), and that calling GetAssemblyName doesn't cause us
// to hold a lock on the file. This prevents unnecessary shadow copies.
var candidateName = AssemblyName.GetAssemblyName(candidatePath);
// Checking FullName ensures that version and PublicKeyToken match exactly.
if (candidateName.FullName.Equals(assemblyName.FullName, StringComparison.OrdinalIgnoreCase))
RikkiGibson marked this conversation as resolved.
Show resolved Hide resolved
{
return LoadFromAssemblyPath(_loader.GetPathToLoad(candidatePath));
}
else if (bestCandidateName is null || bestCandidateName.Version < candidateName.Version)
{
bestCandidateName = candidateName;
bestCandidatePath = candidatePath;
}
}

Debug.Assert(bestCandidateName != null);
Debug.Assert(bestCandidatePath != null);

return LoadFromAssemblyPath(_loader.GetPathToLoad(bestCandidatePath));
}

protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var assemblyPath = Path.Combine(Directory, unmanagedDllName + ".dll");
if (!_loader.IsKnownDependencyLocation(assemblyPath))
var paths = _loader.GetPaths(unmanagedDllName);
if (paths is null || !paths.Contains(assemblyPath))
{
return IntPtr.Zero;
}
Expand Down
Loading