Skip to content

Commit

Permalink
✨ Support custom LoadContext for assembly scanning
Browse files Browse the repository at this point in the history
  • Loading branch information
pleonex committed Nov 30, 2023
1 parent ee53d7f commit d2f49c5
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 63 deletions.
30 changes: 15 additions & 15 deletions src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,29 @@ public void TestPreconditionYarhlMediaIsInPluginsFolder()
public void LoadingYarhlMediaFromPath()
{
string assemblyPath = Path.Combine(GetPluginsDirectory(), "Yarhl.Media.Text.dll");
Assembly? loaded = TypeLocator.Instance.LoadContext.TryLoadFromAssemblyPath(assemblyPath);
Assembly? loaded = TypeLocator.Default.LoadContext.TryLoadFromAssemblyPath(assemblyPath);

Assert.That(loaded, Is.Not.Null);
Assert.That(loaded!.GetName().Name, Is.EqualTo("Yarhl.Media.Text"));

Assert.That(
TypeLocator.Instance.LoadContext.Assemblies.Select(a => a.GetName().Name),
TypeLocator.Default.LoadContext.Assemblies.Select(a => a.GetName().Name),
Does.Contain("Yarhl.Media.Text"));
}

[Test]
public void LoadingInvalidAssemblyReturnsNull()
{
string assemblyPath = Path.Combine(GetPluginsDirectory(), "MyBadPlugin.dll");
Assembly? loaded = TypeLocator.Instance.LoadContext.TryLoadFromAssemblyPath(assemblyPath);
Assembly? loaded = TypeLocator.Default.LoadContext.TryLoadFromAssemblyPath(assemblyPath);

Assert.That(loaded, Is.Null);
}

[Test]
public void LoadingIgnoreSystemLibraries()
{
IEnumerable<Assembly> loaded = TypeLocator.Instance.LoadContext.TryLoadFromBaseLoadDirectory();
IEnumerable<Assembly> loaded = TypeLocator.Default.LoadContext.TryLoadFromBaseLoadDirectory();

Assert.That(loaded.Select(a => a.GetName().Name), Does.Not.Contain("testhost"));
}
Expand All @@ -80,50 +80,50 @@ public void LoadingIgnoreSystemLibraries()
public void LoadingExecutingDirGetsYarhl()
{
// We cannot use ConverterLocator as it will load Yarhl as it uses some of its types.
IEnumerable<Assembly> loaded = TypeLocator.Instance.LoadContext.TryLoadFromBaseLoadDirectory();
IEnumerable<Assembly> loaded = TypeLocator.Default.LoadContext.TryLoadFromBaseLoadDirectory();

Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl"));

Assert.That(
TypeLocator.Instance.LoadContext.Assemblies.Select(a => a.GetName().Name),
TypeLocator.Default.LoadContext.Assemblies.Select(a => a.GetName().Name),
Does.Contain("Yarhl"));
}

[Test]
public void LoadingPluginsDirGetsYarhlMedia()
{
string pluginDir = GetPluginsDirectory();
IEnumerable<Assembly> loaded = TypeLocator.Instance.LoadContext
IEnumerable<Assembly> loaded = TypeLocator.Default.LoadContext
.TryLoadFromDirectory(pluginDir, false);

Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl.Media.Text"));

Assert.That(
TypeLocator.Instance.LoadContext.Assemblies.Select(a => a.GetName().Name),
TypeLocator.Default.LoadContext.Assemblies.Select(a => a.GetName().Name),
Does.Contain("Yarhl.Media.Text"));
}

[Test]
public void LoadingPluginsDirRecursiveGetsYarhlMedia()
{
string programDir = GetProgramDirectory();
IEnumerable<Assembly> loaded = TypeLocator.Instance.LoadContext
IEnumerable<Assembly> loaded = TypeLocator.Default.LoadContext
.TryLoadFromDirectory(programDir, true);

Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl.Media.Text"));

Assert.That(
TypeLocator.Instance.LoadContext.Assemblies.Select(a => a.GetName().Name),
TypeLocator.Default.LoadContext.Assemblies.Select(a => a.GetName().Name),
Does.Contain("Yarhl.Media.Text"));
}

[Test]
public void FindFormatFromPluginsDir()
{
string pluginDir = GetPluginsDirectory();
TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false);
TypeLocator.Default.LoadContext.TryLoadFromDirectory(pluginDir, false);

var formats = ConverterLocator.Instance.Formats;
var formats = ConverterLocator.Default.Formats;
Assert.That(formats, Is.Not.Empty);
Assert.That(
formats.Select(t => t.Name),
Expand All @@ -134,13 +134,13 @@ public void FindFormatFromPluginsDir()
public void FindConverterFromPluginsDir()
{
string pluginDir = GetPluginsDirectory();
TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false);
TypeLocator.Default.LoadContext.TryLoadFromDirectory(pluginDir, false);

Type poType = ConverterLocator.Instance.Formats
Type poType = ConverterLocator.Default.Formats
.Single(f => f.Name == "Yarhl.Media.Text.Po")
.Type;

var converters = ConverterLocator.Instance.Converters
var converters = ConverterLocator.Default.Converters
.Where(f => f.CanConvert(poType));
Assert.That(converters, Is.Not.Empty);
Assert.That(
Expand Down
26 changes: 19 additions & 7 deletions src/Yarhl.Plugins/FileFormat/ConverterLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ public sealed class ConverterLocator
private static readonly object LockObj = new();
private static ConverterLocator? singleInstance;

private readonly TypeLocator locator;
private readonly List<InterfaceImplementationInfo> formatsMetadata;
private readonly List<ConverterTypeInfo> convertersMetadata;

/// <summary>
/// Initializes a new instance of the <see cref="ConverterLocator"/> class.
/// </summary>
private ConverterLocator()
/// <param name="locator">The type locator to use internally.</param>
public ConverterLocator(TypeLocator locator)
{
this.locator = locator;

formatsMetadata = new List<InterfaceImplementationInfo>();
Formats = formatsMetadata;

Expand All @@ -48,10 +52,18 @@ private ConverterLocator()
}

/// <summary>
/// Gets the plugin manager instance.
/// Initializes a new instance of the <see cref="ConverterLocator"/> class.
/// </summary>
private ConverterLocator()
: this(TypeLocator.Default)
{
}

/// <summary>
/// Gets the singleton instance using the default TypeLocator.
/// </summary>
/// <remarks><para>It initializes the manager if needed.</para></remarks>
public static ConverterLocator Instance {
public static ConverterLocator Default {
get {
if (singleInstance == null) {
lock (LockObj) {
Expand Down Expand Up @@ -83,13 +95,13 @@ public static ConverterLocator Instance {
public void ScanAssemblies()
{
formatsMetadata.Clear();
convertersMetadata.Clear();

formatsMetadata.AddRange(
TypeLocator.Instance.FindImplementationsOf(typeof(IFormat)));
locator.FindImplementationsOf(typeof(IFormat)));

convertersMetadata.Clear();
convertersMetadata.AddRange(
TypeLocator.Instance
.FindImplementationsOfGeneric(typeof(IConverter<,>))
locator.FindImplementationsOfGeneric(typeof(IConverter<,>))
.Select(x => new ConverterTypeInfo(x)));
}
}
13 changes: 11 additions & 2 deletions src/Yarhl.Plugins/TypeLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ public sealed class TypeLocator
private static readonly object LockObj = new();
private static TypeLocator? singleInstance;

/// <summary>
/// Initializes a new instance of the <see cref="TypeLocator"/> class.
/// </summary>
/// <param name="loadContext">The load context to search assemblies.</param>
public TypeLocator(AssemblyLoadContext loadContext)
{
LoadContext = loadContext;
}

/// <summary>
/// Initializes a new instance of the <see cref="TypeLocator"/> class.
/// </summary>
Expand All @@ -42,12 +51,12 @@ private TypeLocator()
}

/// <summary>
/// Gets the singleton instance.
/// Gets a singleton instance that use the default AssemblyLoadContext.
/// </summary>
/// <remarks>
/// <para>It initializes the type if needed on the first call.</para>
/// </remarks>
public static TypeLocator Instance {
public static TypeLocator Default {
get {
if (singleInstance == null) {
lock (LockObj) {
Expand Down
4 changes: 2 additions & 2 deletions src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public abstract class BaseGeneralTests<T>
[Test]
public void FormatIsFoundAndIsUnique()
{
var formats = ConverterLocator.Instance.Formats
var formats = ConverterLocator.Default.Formats
.Select(f => f.Type);
Assert.That(formats, Does.Contain(typeof(T)));
Assert.That(formats, Is.Unique);
Expand All @@ -41,7 +41,7 @@ public void FormatIsFoundAndIsUnique()
[Test]
public void FormatNameMatchAndIsUnique()
{
var names = ConverterLocator.Instance.Formats
var names = ConverterLocator.Default.Formats
.Select(f => f.Name);
Assert.That(names, Does.Contain(Name));
Assert.That(names, Is.Unique);
Expand Down
53 changes: 37 additions & 16 deletions src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace Yarhl.UnitTests.Plugins.FileFormat;

using System;
using System.Linq;
using System.Runtime.Loader;
using NUnit.Framework;
using Yarhl.FileFormat;
using Yarhl.FileSystem;
Expand All @@ -34,25 +35,45 @@ public class ConvertersLocatorTests
[Test]
public void InstanceIsSingleton()
{
ConverterLocator instance1 = ConverterLocator.Instance;
ConverterLocator instance2 = ConverterLocator.Instance;
ConverterLocator instance1 = ConverterLocator.Default;
ConverterLocator instance2 = ConverterLocator.Default;

Assert.That(instance1, Is.SameAs(instance2));
}

[Test]
public void InstanceIsInitialized()
{
ConverterLocator instance = ConverterLocator.Instance;
ConverterLocator instance = ConverterLocator.Default;

Assert.That(instance.Formats, Is.Not.Null);
Assert.That(instance.Converters, Is.Not.Null);
}

[Test]
public void InstancePerformsAssemblyScanningOnInitialization()
{
// At least the formats defined in this assembly for testing should be there.
Assert.That(ConverterLocator.Default.Formats, Is.Not.Empty);

Assert.That(new ConverterLocator(TypeLocator.Default).Formats, Is.Not.Null);
}

[Test]
public void InitializeWithCustomLoadContextProvidesIsolation()
{
var loadContext = new AssemblyLoadContext(nameof(InitializeWithCustomLoadContextProvidesIsolation));
TypeLocator isolatedLocator = new TypeLocator(loadContext);
ConverterLocator converterLocator = new ConverterLocator(isolatedLocator);

Assert.That(converterLocator.Formats, Is.Empty);
Assert.That(converterLocator.Converters, Is.Empty);
}

[Test]
public void LocateFormatsWithTypeInfo()
{
InterfaceImplementationInfo myFormat = ConverterLocator.Instance.Formats
InterfaceImplementationInfo myFormat = ConverterLocator.Default.Formats
.FirstOrDefault(i => i.Type == typeof(DerivedSourceFormat));

Assert.That(myFormat, Is.Not.Null);
Expand All @@ -63,7 +84,7 @@ public void LocateFormatsWithTypeInfo()
[Test]
public void FormatsAreNotDuplicated()
{
InterfaceImplementationInfo[] formats = ConverterLocator.Instance.Formats
InterfaceImplementationInfo[] formats = ConverterLocator.Default.Formats
.Where(f => f.Type == typeof(MySourceFormat))
.ToArray();

Expand All @@ -74,18 +95,18 @@ public void FormatsAreNotDuplicated()
public void LocateFormatsFindYarhlBaseFormats()
{
Assert.That(
ConverterLocator.Instance.Formats.Select(f => f.Type),
ConverterLocator.Default.Formats.Select(f => f.Type),
Does.Contain(typeof(BinaryFormat)));

Assert.That(
ConverterLocator.Instance.Formats.Select(f => f.Type),
ConverterLocator.Default.Formats.Select(f => f.Type),
Does.Contain(typeof(NodeContainerFormat)));
}

[Test]
public void LocateConvertersWithTypeInfo()
{
ConverterTypeInfo result = ConverterLocator.Instance.Converters
ConverterTypeInfo result = ConverterLocator.Default.Converters
.FirstOrDefault(i => i.Type == typeof(MyConverter));

Assert.That(result, Is.Not.Null);
Expand All @@ -98,7 +119,7 @@ public void LocateConvertersWithTypeInfo()
[Test]
public void ConvertersAreNotDuplicated()
{
ConverterTypeInfo[] results = ConverterLocator.Instance.Converters
ConverterTypeInfo[] results = ConverterLocator.Default.Converters
.Where(f => f.Type == typeof(MyConverter))
.ToArray();

Expand All @@ -108,7 +129,7 @@ public void ConvertersAreNotDuplicated()
[Test]
public void ScanAssembliesDoesNotDuplicateFindings()
{
ConverterLocator.Instance.ScanAssemblies();
ConverterLocator.Default.ScanAssemblies();

FormatsAreNotDuplicated();
ConvertersAreNotDuplicated();
Expand All @@ -117,7 +138,7 @@ public void ScanAssembliesDoesNotDuplicateFindings()
[Test]
public void LocateConverterWithParameters()
{
ConverterTypeInfo[] results = ConverterLocator.Instance.Converters
ConverterTypeInfo[] results = ConverterLocator.Default.Converters
.Where(f => f.Type == typeof(MyConverterParametrized))
.ToArray();

Expand All @@ -127,7 +148,7 @@ public void LocateConverterWithParameters()
[Test]
public void LocateSingleInnerConverter()
{
ConverterTypeInfo converter = ConverterLocator.Instance.Converters
ConverterTypeInfo converter = ConverterLocator.Default.Converters
.FirstOrDefault(c => c.Type == typeof(SingleOuterConverter.SingleInnerConverter));

Assert.That(converter, Is.Not.Null);
Expand All @@ -136,7 +157,7 @@ public void LocateSingleInnerConverter()
[Test]
public void LocateSingleOuterConverter()
{
ConverterTypeInfo converter = ConverterLocator.Instance.Converters
ConverterTypeInfo converter = ConverterLocator.Default.Converters
.FirstOrDefault(c => c.Type == typeof(SingleOuterConverter));

Assert.That(converter, Is.Not.Null);
Expand All @@ -145,7 +166,7 @@ public void LocateSingleOuterConverter()
[Test]
public void LocateTwoConvertersInSameClass()
{
ConverterTypeInfo[] converters = ConverterLocator.Instance.Converters
ConverterTypeInfo[] converters = ConverterLocator.Default.Converters
.Where(c => c.Type == typeof(TwoConverters))
.ToArray();

Expand Down Expand Up @@ -176,7 +197,7 @@ public void LocateTwoConvertersInSameClass()
[Test]
public void LocateDerivedConverter()
{
ConverterTypeInfo[] converters = ConverterLocator.Instance.Converters
ConverterTypeInfo[] converters = ConverterLocator.Default.Converters
.Where(c => c.Type == typeof(DerivedConverter))
.ToArray();

Expand All @@ -186,7 +207,7 @@ public void LocateDerivedConverter()
[Test]
public void LocateConvertsWithOtherInterfaces()
{
ConverterTypeInfo[] converters = ConverterLocator.Instance.Converters
ConverterTypeInfo[] converters = ConverterLocator.Default.Converters
.Where(c => c.Type == typeof(ConverterAndOtherInterface))
.ToArray();

Expand Down
Loading

0 comments on commit d2f49c5

Please sign in to comment.