From 894d051bad0d510f5d2b661c8a87ae9edd298f62 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Tue, 28 Nov 2023 18:12:08 +0100 Subject: [PATCH 01/12] :sparkles: Re-implement Yarhl.Plugins without MEF --- src/Directory.Packages.props | 8 +- src/Yarhl.IntegrationTests/PluginDiscovery.cs | 29 +- .../AssemblyLoadContextExtensions.cs | 120 ++++++++ .../FileFormat/ConverterMetadata.cs | 145 ---------- .../FileFormat/ConverterTypeInfo.cs | 86 ++++++ .../FileFormat/ConvertersLocator.cs | 91 +++++++ ... => GenericInterfaceImplementationInfo.cs} | 48 +--- ...data.cs => InterfaceImplementationInfo.cs} | 32 +-- src/Yarhl.Plugins/PluginManager.cs | 254 ----------------- src/Yarhl.Plugins/TypeLocator.cs | 165 +++++++++++ src/Yarhl.Plugins/Yarhl.Plugins.csproj | 4 - .../FileFormat/BaseGeneralTests.cs | 10 +- .../FileFormat/TestConvertersDefinition.cs | 3 - .../Plugins/ConverterFindableTests.cs | 117 -------- .../Plugins/ConverterMetadataTests.cs | 232 ---------------- .../FileFormat/ConverterTypeInfoTests.cs | 233 ++++++++++++++++ .../FileFormat/ConvertersLocatorTests.cs | 144 ++++++++++ .../TestConvertersDefinition.cs | 27 +- .../Plugins/PluginManagerTests.cs | 256 ------------------ .../Plugins/TestTypesDefinition.cs} | 77 ++++-- .../Plugins/TypeLocatorTests.cs | 114 ++++++++ src/Yarhl.UnitTests/Yarhl.UnitTests.csproj | 2 +- 22 files changed, 1080 insertions(+), 1117 deletions(-) create mode 100644 src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs delete mode 100644 src/Yarhl.Plugins/FileFormat/ConverterMetadata.cs create mode 100644 src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs create mode 100644 src/Yarhl.Plugins/FileFormat/ConvertersLocator.cs rename src/Yarhl.Plugins/{FileFormat/FormatMetadata.cs => GenericInterfaceImplementationInfo.cs} (50%) rename src/Yarhl.Plugins/{FileFormat/IExportMetadata.cs => InterfaceImplementationInfo.cs} (62%) delete mode 100644 src/Yarhl.Plugins/PluginManager.cs create mode 100644 src/Yarhl.Plugins/TypeLocator.cs delete mode 100644 src/Yarhl.UnitTests/Plugins/ConverterFindableTests.cs delete mode 100644 src/Yarhl.UnitTests/Plugins/ConverterMetadataTests.cs create mode 100644 src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs create mode 100644 src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs rename src/Yarhl.UnitTests/Plugins/{ => FileFormat}/TestConvertersDefinition.cs (75%) delete mode 100644 src/Yarhl.UnitTests/Plugins/PluginManagerTests.cs rename src/{Yarhl.Plugins/AssemblyUtils.cs => Yarhl.UnitTests/Plugins/TestTypesDefinition.cs} (51%) create mode 100644 src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 517499f8..1d55a5b5 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,19 +1,19 @@ - - - - + + + + diff --git a/src/Yarhl.IntegrationTests/PluginDiscovery.cs b/src/Yarhl.IntegrationTests/PluginDiscovery.cs index ab5ebfa2..6ec9e902 100644 --- a/src/Yarhl.IntegrationTests/PluginDiscovery.cs +++ b/src/Yarhl.IntegrationTests/PluginDiscovery.cs @@ -24,6 +24,7 @@ namespace Yarhl.IntegrationTests using System.Linq; using NUnit.Framework; using Yarhl.Plugins; + using Yarhl.Plugins.FileFormat; [TestFixture] public class PluginDiscovery @@ -31,8 +32,8 @@ public class PluginDiscovery [Test] public void YarhlMediaIsInPluginsFolder() { - string programDir = AppDomain.CurrentDomain.BaseDirectory; - string pluginDir = Path.Combine(programDir, PluginManager.PluginDirectory); + string programDir = Path.GetDirectoryName(Environment.ProcessPath); + string pluginDir = Path.Combine(programDir, "Plugins"); Assert.IsTrue(Directory.Exists(pluginDir)); Assert.IsTrue(File.Exists(Path.Combine(pluginDir, "Yarhl.Media.Text.dll"))); @@ -43,25 +44,33 @@ public void YarhlMediaIsInPluginsFolder() [Test] public void CanFoundPoByFormat() { - var formats = PluginManager.Instance.GetFormats(); + string programDir = Path.GetDirectoryName(Environment.ProcessPath); + string pluginDir = Path.Combine(programDir, "Plugins"); + TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); + + var formats = ConvertersLocator.Instance.Formats; Assert.That(formats, Is.Not.Empty); Assert.That( - formats.Select(t => t.Metadata.Name), + formats.Select(t => t.Name), Does.Contain("Yarhl.Media.Text.Po")); } [Test] public void CanFoundPoConverterFromTypes() { - Type poType = PluginManager.Instance.GetFormats() - .Single(f => f.Metadata.Name == "Yarhl.Media.Text.Po") - .Metadata.Type; + string programDir = Path.GetDirectoryName(Environment.ProcessPath); + string pluginDir = Path.Combine(programDir, "Plugins"); + TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); + + Type poType = ConvertersLocator.Instance.Formats + .Single(f => f.Name == "Yarhl.Media.Text.Po") + .Type; - var converters = PluginManager.Instance.GetConverters() - .Where(f => f.Metadata.CanConvert(poType)); + var converters = ConvertersLocator.Instance.Converters + .Where(f => f.CanConvert(poType)); Assert.That(converters, Is.Not.Empty); Assert.That( - converters.Select(t => t.Metadata.Name), + converters.Select(t => t.Name), Does.Contain("Yarhl.Media.Text.Po2Binary")); } } diff --git a/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs b/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs new file mode 100644 index 00000000..fe1c9063 --- /dev/null +++ b/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs @@ -0,0 +1,120 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Plugins; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +/// +/// Extension methods to load assemblies from disk. +/// +public static class AssemblyLoadContextExtensions +{ + private static readonly string[] IgnoredLibraries = { + "System.", + "Microsoft.", + "netstandard", + "nuget", + "nunit", + "testhost", + }; + + /// + /// Try to load the assemblies from the given file paths. + /// + /// The load context to use to load. + /// The list of assembly paths to load. + /// A collection of assemblies that could be loaded. + /// + /// If an assembly fails to load it will be silently skipped. + /// + public static IEnumerable TryLoadFromAssembliesPath(this AssemblyLoadContext loader, IEnumerable paths) + { + // Skip libraries that match the ignored libraries to prevent loading dependencies. + return paths + .Select(p => new { Name = Path.GetFileName(p), Path = p }) + .Where(p => !Array.Exists( + IgnoredLibraries, + ign => p.Name.StartsWith(ign, StringComparison.OrdinalIgnoreCase))) + .Select(p => p.Path) + .Select(loader.TryLoadFromAssemblyPath) + .Where(a => a is not null) + .ToList()!; // force to run + } + + /// + /// Try to load every .NET assembly from the given directory. + /// + /// The load context to use to load. + /// The directory to find assemblies. + /// + /// Value indicating whether it should search all directories or only the top directory. + /// + /// A collection of assemblies that could be loaded. + /// + /// If an assembly fails to load it will be silently skipped. + /// + public static IEnumerable TryLoadFromDirectory(this AssemblyLoadContext loader, string directory, bool recursive) + { + var options = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + string[] libraryAssemblies = Directory.GetFiles(directory, "*.dll", options); + string[] programAssembly = Directory.GetFiles(directory, "*.exe"); + + return TryLoadFromAssembliesPath(loader, programAssembly.Concat(libraryAssemblies)); + } + + /// + /// Try to load every .NET assembly in the directory of the current process. + /// + /// The load context to use to load. + /// A collection of assemblies that could be loaded. + /// + /// If an assembly fails to load it will be silently skipped. + /// + public static IEnumerable TryLoadFromExecutingDirectory(this AssemblyLoadContext loader) + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath) ?? + throw new ArgumentException("Cannot determine process directory"); + + string[] libraryAssemblies = Directory.GetFiles(programDir, "*.dll"); + string[] programAssembly = Directory.GetFiles(programDir, "*.exe"); + + return TryLoadFromAssembliesPath(loader, programAssembly.Concat(libraryAssemblies)); + } + + /// + /// Try to load the assembly from the given path. + /// + /// The assembly load context. + /// Assembly to load. + /// The load assembly or null on error. + public static Assembly? TryLoadFromAssemblyPath(this AssemblyLoadContext loader, string path) + { + try { + return loader.LoadFromAssemblyPath(path); + } catch (BadImageFormatException) { + // Probably not a .NET assembly. + return null; + } + } +} diff --git a/src/Yarhl.Plugins/FileFormat/ConverterMetadata.cs b/src/Yarhl.Plugins/FileFormat/ConverterMetadata.cs deleted file mode 100644 index 100a6451..00000000 --- a/src/Yarhl.Plugins/FileFormat/ConverterMetadata.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2019 SceneGate - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -namespace Yarhl.Plugins.FileFormat -{ - using System; - - /// - /// Metadata associated to a IConverter interface. - /// - public class ConverterMetadata : IExportMetadata - { - /// - /// Initializes a new instance of the class. - /// - public ConverterMetadata() - { - // MEF should always set these properties, so they won't be null. - // We set some initial values to ensure later they are not set to null. - Name = ""; - Type = typeof(ConverterMetadata); - InternalSources = Type.EmptyTypes; - InternalDestinations = Type.EmptyTypes; - } - - /// - /// Gets or sets the full name of the type. Shortcut of Type.FullName. - /// - /// The full name of the type. - public string Name { get; set; } - - /// - /// Gets or sets the type of class implementing the converter. - /// - /// Type of the converter. - public Type Type { get; set; } - - /// - /// Gets or sets a single type or list of types that the converter - /// can convert from. - /// - /// Single or list of types for conversion. - public object InternalSources { get; set; } - - /// - /// Gets or sets a single type or list of types the converter can - /// convert to. - /// - /// Single or list of types the converter can convert to. - public object InternalDestinations { get; set; } - - /// - /// Gets a list of source types that can convert from. - /// - /// List of source types that can convert from. - public Type[] GetSources() - { - if (InternalSources is not Type[] sourceList) { - sourceList = new[] { (Type)InternalSources }; - } - - return sourceList; - } - - /// - /// Gets a list of destination types it can convert to. - /// - /// Destination types it can convert to. - public Type[] GetDestinations() - { - if (InternalDestinations is not Type[] destList) { - destList = new[] { (Type)InternalDestinations }; - } - - return destList; - } - - /// - /// Check if the associated converter can convert from a given type. - /// It checks applying covariance rules. - /// - /// Source type for conversion. - /// If this converter can realize the operation. - public bool CanConvert(Type source) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - Type[] sources = GetSources(); - for (int i = 0; i < sources.Length; i++) { - if (sources[i].IsAssignableFrom(source)) { - return true; - } - } - - return false; - } - - /// - /// Check if the associated converter can convert from a given type - /// into another. It checks applying covariance and contravariance - /// rules. - /// - /// Source type for conversion. - /// Destination type for conversion. - /// If this converter can realize the operation. - public bool CanConvert(Type source, Type dest) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - - if (dest == null) - throw new ArgumentNullException(nameof(dest)); - - Type[] sources = GetSources(); - Type[] destinations = GetDestinations(); - - for (int i = 0; i < sources.Length; i++) { - bool matchSource = sources[i].IsAssignableFrom(source); - bool matchDest = dest.IsAssignableFrom(destinations[i]); - if (matchSource && matchDest) { - return true; - } - } - - return false; - } - } -} diff --git a/src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs b/src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs new file mode 100644 index 00000000..3a983eab --- /dev/null +++ b/src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs @@ -0,0 +1,86 @@ +// Copyright (c) 2019 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Plugins.FileFormat; + +using System; + +/// +/// Provides information from a type that implements a converter. +/// +public record ConverterTypeInfo( + string Name, + Type Type, + Type InterfaceImplemented, + IReadOnlyList GenericTypes) + : GenericInterfaceImplementationInfo(Name, Type, InterfaceImplemented, GenericTypes) +{ + /// + /// Initializes a new instance of the class. + /// + /// The generic implementor information. + public ConverterTypeInfo(GenericInterfaceImplementationInfo info) + : this(info.Name, info.Type, info.InterfaceImplemented, info.GenericTypes) + { + if (info.GenericTypes.Count != 2) { + throw new ArgumentException("Invalid number of generics. Expected 2."); + } + } + + /// + /// Gets the source type the converter can convert from. + /// + public Type SourceType => GenericTypes[0]; + + /// + /// Gets the destination type the converter can convert to. + /// + public Type DestinationType => GenericTypes[1]; + + /// + /// Check if this converter type can convert from the given source type. + /// It checks applying covariance rules. + /// + /// Source type for conversion. + /// If this converter can realize the operation. + public bool CanConvert(Type source) + { + ArgumentNullException.ThrowIfNull(source); + + return SourceType.IsAssignableFrom(source); + } + + /// + /// Check if this converter type can convert from the given source type + /// into the given desination type. It checks applying covariance and + /// contravariance rules. + /// + /// Source type for conversion. + /// Destination type for conversion. + /// If this converter can realize the operation. + public bool CanConvert(Type source, Type dest) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(dest); + + bool matchSource = SourceType.IsAssignableFrom(source); + bool matchDest = dest.IsAssignableFrom(DestinationType); + return matchSource && matchDest; + } +} diff --git a/src/Yarhl.Plugins/FileFormat/ConvertersLocator.cs b/src/Yarhl.Plugins/FileFormat/ConvertersLocator.cs new file mode 100644 index 00000000..0de2d048 --- /dev/null +++ b/src/Yarhl.Plugins/FileFormat/ConvertersLocator.cs @@ -0,0 +1,91 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Plugins.FileFormat; + +using System.Collections.Generic; +using Yarhl.FileFormat; + +/// +/// Locates converter types across assemblies and provide their information. +/// +public sealed class ConvertersLocator +{ + private static readonly object LockObj = new(); + private static ConvertersLocator? singleInstance; + + private readonly List formatsMetadata; + private readonly List convertersMetadata; + + /// + /// Initializes a new instance of the class. + /// + private ConvertersLocator() + { + formatsMetadata = new List(); + Formats = formatsMetadata; + + convertersMetadata = new List(); + Converters = convertersMetadata; + + ScanAssemblies(); + } + + /// + /// Gets the plugin manager instance. + /// + /// It initializes the manager if needed. + public static ConvertersLocator Instance { + get { + if (singleInstance == null) { + lock (LockObj) { + singleInstance ??= new ConvertersLocator(); + } + } + + return singleInstance; + } + } + + /// + /// Gets the list of Yarhl formats information from loaded assemblies. + /// + public IReadOnlyList Formats { get; } + + /// + /// Gets the list of Yarhl converters information from loaded assemblies. + /// + public IReadOnlyList Converters { get; } + + /// + /// Scan the assemblies from the load context to look for formats and converters. + /// + public void ScanAssemblies() + { + formatsMetadata.Clear(); + formatsMetadata.AddRange( + TypeLocator.Instance.FindImplementationsOf(typeof(IFormat))); + + convertersMetadata.Clear(); + convertersMetadata.AddRange( + TypeLocator.Instance + .FindImplementationsOfGeneric(typeof(IConverter<,>)) + .Select(x => new ConverterTypeInfo(x))); + } +} diff --git a/src/Yarhl.Plugins/FileFormat/FormatMetadata.cs b/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs similarity index 50% rename from src/Yarhl.Plugins/FileFormat/FormatMetadata.cs rename to src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs index 6ba8f748..b77f3335 100644 --- a/src/Yarhl.Plugins/FileFormat/FormatMetadata.cs +++ b/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019 SceneGate +// Copyright (c) 2023 SceneGate // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -17,36 +17,18 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace Yarhl.Plugins.FileFormat -{ - using System; +namespace Yarhl.Plugins; - /// - /// Metadata associated to a Format class. - /// - public class FormatMetadata : IExportMetadata - { - /// - /// Initializes a new instance of the class. - /// - public FormatMetadata() - { - // MEF should always set these properties, so they won't be null. - // We set some initial values to ensure later they are not set to null. - Name = ""; - Type = typeof(FormatMetadata); - } - - /// - /// Gets or sets the type full name. Shortcut of Type.FullName. - /// - /// The full name of the type. - public string Name { get; set; } - - /// - /// Gets or sets the type of the format. - /// - /// The type of the format. - public Type Type { get; set; } - } -} +/// +/// Provides information about a type that implements a generic interface. +/// +/// The name of the implementation type. Shortcut for Type.Name. +/// The type that implements the interface. +/// Interface implemented. +/// The list of types specified in the generic. +public record GenericInterfaceImplementationInfo( + string Name, + Type Type, + Type InterfaceImplemented, + IReadOnlyList GenericTypes) + : InterfaceImplementationInfo(Name, Type, InterfaceImplemented); diff --git a/src/Yarhl.Plugins/FileFormat/IExportMetadata.cs b/src/Yarhl.Plugins/InterfaceImplementationInfo.cs similarity index 62% rename from src/Yarhl.Plugins/FileFormat/IExportMetadata.cs rename to src/Yarhl.Plugins/InterfaceImplementationInfo.cs index 6727d1a0..2c8116b4 100644 --- a/src/Yarhl.Plugins/FileFormat/IExportMetadata.cs +++ b/src/Yarhl.Plugins/InterfaceImplementationInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019 SceneGate +// Copyright (c) 2023 SceneGate // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -17,26 +17,12 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace Yarhl.Plugins.FileFormat -{ - using System; +namespace Yarhl.Plugins; - /// - /// Base metadata associated to a exported type. - /// - public interface IExportMetadata - { - /// - /// Gets or sets the name of the extension. - /// Usually it's the FullName property of Type. - /// - /// Name of the extension. - string Name { get; set; } - - /// - /// Gets or sets the type of the extension. - /// - /// The type of the extension. - Type Type { get; set; } - } -} +/// +/// Provides information about a type that implements an interface. +/// +/// The name of the implementation type. Shortcut for Type.Name. +/// The type that implements the interface. +/// Interface implemented. +public record InterfaceImplementationInfo(string Name, Type Type, Type InterfaceImplemented); diff --git a/src/Yarhl.Plugins/PluginManager.cs b/src/Yarhl.Plugins/PluginManager.cs deleted file mode 100644 index 52dec354..00000000 --- a/src/Yarhl.Plugins/PluginManager.cs +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) 2019 SceneGate - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -namespace Yarhl.Plugins -{ - using System; - using System.Collections.Generic; - using System.Composition; - using System.Composition.Convention; - using System.Composition.Hosting; - using System.IO; - using System.Linq; - using System.Reflection; - using Yarhl.Plugins.FileFormat; - - /// - /// Plugin manager. - /// - /// - /// Plugin assemblies are loaded from the directory with the Yarhl - /// assembly and the 'Plugins' subfolder with its children. - /// - public sealed class PluginManager - { - static readonly string[] IgnoredLibraries = { - "System.", - "Microsoft.", - "netstandard", - "nuget", - "nunit", - "testhost", - }; - - static readonly object LockObj = new object(); - static PluginManager? singleInstance; - - readonly CompositionHost container; - - /// - /// Initializes a new instance of the class. - /// - PluginManager() - { - container = InitializeContainer(); - } - - /// - /// Gets the name of the plugins directory. - /// - public static string PluginDirectory => "Plugins"; - - /// - /// Gets the plugin manager instance. - /// - /// It initializes the manager if needed. - public static PluginManager Instance { - get { - if (singleInstance == null) { - lock (LockObj) { - if (singleInstance == null) - singleInstance = new PluginManager(); - } - } - - return singleInstance; - } - } - - /// - /// Finds all the extensions from the given base type. - /// - /// The extensions. - /// Type of the extension point. - public IEnumerable FindExtensions() - { - return container.GetExports(); - } - - /// - /// Finds all the extensions from the given base type. - /// - /// The extensions. - /// Type of the extension point. - public IEnumerable FindExtensions(Type extension) - { - if (extension == null) - throw new ArgumentNullException(nameof(extension)); - - return container.GetExports(extension); - } - - /// - /// Finds all the extensions from the given base type and return their - /// lazy type for initialization. - /// - /// Type of the extension point. - /// The lazy extensions. - public IEnumerable> FindLazyExtensions() - { - return container.GetExports>(); - } - - /// - /// Finds all the extensions from the given base type and returns - /// a factory to initialize the type. - /// - /// Type of the extension point. - /// The extension factory. - public IEnumerable FindLazyExtensions(Type extension) - { - if (extension == null) { - throw new ArgumentNullException(nameof(extension)); - } - - Type lazyType = typeof(ExportFactory<>).MakeGenericType(extension); - return container.GetExports(lazyType); - } - - /// - /// Finds all the extensions from the given base type and returns - /// a factory to initialize the type and its associated metadata. - /// - /// Type of the extension point. - /// Type of the metadata. - /// The extension factory. - public IEnumerable> FindLazyExtensions() - where TMetadata : IExportMetadata - { - // Because of technical limitations / bugs there can be upto - // 3 copies of the same extension. We filter by type. - return container.GetExports>() - .GroupBy(f => f.Metadata.Type) - .Select(f => f.First()); - } - - /// - /// Get a list of format extensions. - /// - /// Enumerable of lazy formats with metadata. - public IEnumerable> GetFormats() - { - return FindLazyExtensions(); - } - - /// - /// Get a list of converter extensions. - /// - /// Enumerable of lazy converters with metadata. - public IEnumerable> GetConverters() - { - return FindLazyExtensions(); - } - - static void DefineFormatConventions(ConventionBuilder conventions) - { - _ = conventions - .ForTypesDerivedFrom() - .Export( - export => export - .AddMetadata("Name", t => t.FullName) - .AddMetadata("Type", t => t)) - .SelectConstructor(ctors => - ctors.OrderBy(ctor => ctor.GetParameters().Length) - .First()); - } - - static void DefineConverterConventions(ConventionBuilder conventions) - { - static bool ConverterInterfaceFilter(Type i) => - i.IsGenericType && - i.GetGenericTypeDefinition().IsEquivalentTo(typeof(Yarhl.FileFormat.IConverter<,>)); - - // We export three types each converter: - // 1.- Export the specific generic converter types - // 2.- Export the IConverter interfaces with the interfaces metadata - // 3.- Export again the IConverter interface to fill common metadata - _ = conventions - .ForTypesDerivedFrom(typeof(Yarhl.FileFormat.IConverter<,>)) - .ExportInterfaces(ConverterInterfaceFilter) - .ExportInterfaces( - ConverterInterfaceFilter, - (inter, export) => export - .AddMetadata("InternalSources", inter.GenericTypeArguments[0]) - .AddMetadata("InternalDestinations", inter.GenericTypeArguments[1]) - .AsContractType()) - .Export( - export => export - .AddMetadata("Name", t => t.FullName) - .AddMetadata("Type", t => t)) - .SelectConstructor(ctors => - ctors.OrderBy(ctor => ctor.GetParameters().Length) - .First()); - } - - static IEnumerable LoadAssemblies(IEnumerable paths) - { - // Skip libraries that match the ignored libraries because - // MEF would try to load its dependencies. - return paths - .Select(p => new { Name = Path.GetFileName(p), Path = p }) - .Where(p => !Array.Exists( - IgnoredLibraries, - ign => p.Name.StartsWith(ign, StringComparison.OrdinalIgnoreCase))) - .Select(p => p.Path) - .LoadAssemblies(); - } - - static CompositionHost InitializeContainer() - { - var conventions = new ConventionBuilder(); - DefineFormatConventions(conventions); - DefineConverterConventions(conventions); - - var containerConfig = new ContainerConfiguration() - .WithDefaultConventions(conventions); - - // Assemblies from the program directory (including this one). - var programDir = AppDomain.CurrentDomain.BaseDirectory; - var libraryAssemblies = Directory.GetFiles(programDir, "*.dll"); - var programAssembly = Directory.GetFiles(programDir, "*.exe"); - _ = containerConfig - .WithAssemblies(LoadAssemblies(libraryAssemblies)) - .WithAssemblies(LoadAssemblies(programAssembly)); - - // Assemblies from the Plugin directory and subfolders - string pluginDir = Path.Combine(programDir, PluginDirectory); - if (Directory.Exists(pluginDir)) { - var pluginFiles = Directory.GetFiles( - pluginDir, - "*.dll", - SearchOption.AllDirectories); - _ = containerConfig.WithAssemblies(LoadAssemblies(pluginFiles)); - } - - return containerConfig.CreateContainer(); - } - } -} diff --git a/src/Yarhl.Plugins/TypeLocator.cs b/src/Yarhl.Plugins/TypeLocator.cs new file mode 100644 index 00000000..d873d7db --- /dev/null +++ b/src/Yarhl.Plugins/TypeLocator.cs @@ -0,0 +1,165 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Plugins; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +/// +/// Type locator. Find implementation of a given interface across loaded assemblies. +/// +public sealed class TypeLocator +{ + private static readonly object LockObj = new(); + private static TypeLocator? singleInstance; + + /// + /// Initializes a new instance of the class. + /// + private TypeLocator() + { + LoadContext = AssemblyLoadContext.Default; + } + + /// + /// Gets the singleton instance. + /// + /// + /// It initializes the type if needed on the first call. + /// + public static TypeLocator Instance { + get { + if (singleInstance == null) { + lock (LockObj) { + singleInstance ??= new TypeLocator(); + } + } + + return singleInstance; + } + } + + /// + /// Gets the assembly load context containing the assemblies to scan. + /// + /// + /// Use the returned instance to load new assemblies. + /// + public AssemblyLoadContext LoadContext { get; } + + /// + /// Finds and returns a collection of types that implements the given + /// base type across all the loaded assemblies. + /// + /// The base type to find implementors. + /// A collection of types implementing the base type. + public IEnumerable FindImplementationsOf(Type baseType) + { + ArgumentNullException.ThrowIfNull(baseType); + + return LoadContext.Assemblies + .Where(a => !a.IsDynamic) // don't support iterating through types in .NET 6 + .SelectMany(assembly => FindImplementationsOf(baseType, assembly)); + } + + /// + /// Finds and returns a collection of types that implements the given + /// base type in the assembly. + /// + /// The base type to find implementors. + /// The assembly to scan. + /// A collection of types implementing the base type. + public IEnumerable FindImplementationsOf(Type baseType, Assembly assembly) + { + ArgumentNullException.ThrowIfNull(baseType); + ArgumentNullException.ThrowIfNull(assembly); + + return assembly.ExportedTypes + .Where(baseType.IsAssignableFrom) + .Where(t => t.IsClass && !t.IsAbstract) + .Select(type => new InterfaceImplementationInfo(type.FullName!, type, baseType)); + } + + /// + /// Finds and returns a collection of types that implements the given + /// generic interface across all the loaded assemblies. + /// + /// The generic interface type to find implementors. + /// A collection of types implementing the interface. + /// + /// The list may contain several times the same if it implements the same interface + /// multiple types with different generic types. + /// + public IEnumerable FindImplementationsOfGeneric(Type baseType) + { + ArgumentNullException.ThrowIfNull(baseType); + + return LoadContext.Assemblies + .Where(a => !a.IsDynamic) // don't support iterating through types in .NET 6 + .SelectMany(assembly => FindImplementationsOfGeneric(baseType, assembly)); + } + + /// + /// Finds and returns a collection of types that implements the given + /// generic type in the assembly. + /// + /// The generic type to find implementors. + /// The assembly to scan. + /// A collection of types implementing the base type. + /// + /// The list may contain several entries for the same implementation type + /// if it implements several type the generic with different parameters. + /// + public IEnumerable FindImplementationsOfGeneric( + Type baseType, + Assembly assembly) + { + ArgumentNullException.ThrowIfNull(baseType); + ArgumentNullException.ThrowIfNull(assembly); + + bool ValidImplementationInterface(Type type) => + type.IsGenericType + && type.GetGenericTypeDefinition().IsEquivalentTo(baseType); + + IEnumerable converterTypes = assembly.ExportedTypes + .Where(t => Array.Exists(t.GetInterfaces(), ValidImplementationInterface)) + .Where(t => t.IsClass && !t.IsAbstract); + + foreach (Type type in converterTypes) { + // A class may implement the interface several + // times with different generic types + IEnumerable interfaceImplementations = type.GetInterfaces() + .Where(ValidImplementationInterface) + .Select(i => i.GenericTypeArguments); + + foreach (Type[] genericTypes in interfaceImplementations) { + var metadata = new GenericInterfaceImplementationInfo( + type.FullName!, + type, + baseType, + genericTypes); + yield return metadata; + } + } + } +} diff --git a/src/Yarhl.Plugins/Yarhl.Plugins.csproj b/src/Yarhl.Plugins/Yarhl.Plugins.csproj index c8d8727d..8c458c51 100644 --- a/src/Yarhl.Plugins/Yarhl.Plugins.csproj +++ b/src/Yarhl.Plugins/Yarhl.Plugins.csproj @@ -15,10 +15,6 @@ - - - - diff --git a/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs b/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs index 6b12ad54..b9acb8b8 100644 --- a/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs +++ b/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs @@ -22,7 +22,7 @@ namespace Yarhl.UnitTests.FileFormat using System.Linq; using NUnit.Framework; using Yarhl.FileFormat; - using Yarhl.Plugins; + using Yarhl.Plugins.FileFormat; public abstract class BaseGeneralTests where T : IFormat @@ -32,8 +32,8 @@ public abstract class BaseGeneralTests [Test] public void FormatIsFoundAndIsUnique() { - var formats = PluginManager.Instance.GetFormats() - .Select(f => f.Metadata.Type); + var formats = ConvertersLocator.Instance.Formats + .Select(f => f.Type); Assert.That(formats, Does.Contain(typeof(T))); Assert.That(formats, Is.Unique); } @@ -41,8 +41,8 @@ public void FormatIsFoundAndIsUnique() [Test] public void FormatNameMatchAndIsUnique() { - var names = PluginManager.Instance.GetFormats() - .Select(f => f.Metadata.Name); + var names = ConvertersLocator.Instance.Formats + .Select(f => f.Name); Assert.That(names, Does.Contain(Name)); Assert.That(names, Is.Unique); } diff --git a/src/Yarhl.UnitTests/FileFormat/TestConvertersDefinition.cs b/src/Yarhl.UnitTests/FileFormat/TestConvertersDefinition.cs index 3dbc01bc..28cf623f 100644 --- a/src/Yarhl.UnitTests/FileFormat/TestConvertersDefinition.cs +++ b/src/Yarhl.UnitTests/FileFormat/TestConvertersDefinition.cs @@ -1,7 +1,6 @@ namespace Yarhl.UnitTests.FileFormat; using System; -using System.Composition; using System.Globalization; using Yarhl.FileFormat; @@ -93,7 +92,6 @@ public IntFormat Convert(StringFormat source) } } -[PartNotDiscoverable] public class StringFormatConverterWithConstructor : IConverter { private readonly NumberStyles style; @@ -111,7 +109,6 @@ public IntFormat Convert(StringFormat source) } } -[PartNotDiscoverable] public class StringFormatConverterWithSeveralConstructors : IConverter { diff --git a/src/Yarhl.UnitTests/Plugins/ConverterFindableTests.cs b/src/Yarhl.UnitTests/Plugins/ConverterFindableTests.cs deleted file mode 100644 index b3f52b72..00000000 --- a/src/Yarhl.UnitTests/Plugins/ConverterFindableTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2019 SceneGate - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -namespace Yarhl.UnitTests.Plugins -{ - using System; - using System.Linq; - using NUnit.Framework; - using Yarhl.FileFormat; - using Yarhl.Plugins; - - [TestFixture] - public class ConverterFindableTests - { - [Test] - public void FindSingleInnerConverter() - { - IConverter converter = null; - Assert.That( - () => converter = PluginManager.Instance - .FindExtensions>() - .Single(), - Throws.Nothing); - Assert.That( - converter, - Is.InstanceOf()); - Assert.That(converter.Convert("4"), Is.EqualTo(4)); - } - - [Test] - public void FindSingleOuterConverter() - { - IConverter converter = null; - Assert.That( - () => converter = PluginManager.Instance - .FindExtensions>() - .Single(), - Throws.Nothing); - Assert.That(converter, Is.InstanceOf()); - Assert.That(converter.Convert("5"), Is.EqualTo(5)); - } - - [Test] - public void FindTwoConvertersInSameClass() - { - var converter1 = PluginManager.Instance - .FindExtensions>(); - Assert.IsInstanceOf(converter1.Single()); - - Assert.DoesNotThrow(() => - converter1.Single(t => - Array.Exists(t.GetType().GetInterfaces(), i => - i.IsGenericType && - i.GenericTypeArguments.Length == 2 && - i.GenericTypeArguments[0] == typeof(string) && - i.GenericTypeArguments[1] == typeof(int)))); - - var converter2 = PluginManager.Instance - .FindExtensions>(); - Assert.IsInstanceOf(converter2.Single()); - - Assert.DoesNotThrow(() => - converter2.Single(t => - Array.Exists(t.GetType().GetInterfaces(), i => - i.IsGenericType && - i.GenericTypeArguments.Length == 2 && - i.GenericTypeArguments[0] == typeof(int) && - i.GenericTypeArguments[1] == typeof(string)))); - } - - [Test] - public void FindDerivedConverter() - { - var converters = PluginManager.Instance - .FindExtensions>(); - IConverter converter = null; - Assert.That( - () => converter = converters.Single(), - Throws.Nothing); - Assert.IsInstanceOf(converter); - Assert.IsInstanceOf(converter); - Assert.That(converter.Convert("3"), Is.EqualTo(3)); - } - - [Test] - public void FindConvertsWithOtherInterfaces() - { - IConverter converter = null; - Assert.That( - () => converter = PluginManager.Instance - .FindExtensions>() - .Single(), - Throws.Nothing); - Assert.That(converter, Is.InstanceOf()); - Assert.That(converter.Convert("3"), Is.EqualTo(3)); - Assert.That( - ((ConverterAndOtherInterface)converter).Dispose, - Throws.Nothing); - } - } -} diff --git a/src/Yarhl.UnitTests/Plugins/ConverterMetadataTests.cs b/src/Yarhl.UnitTests/Plugins/ConverterMetadataTests.cs deleted file mode 100644 index 6bab4457..00000000 --- a/src/Yarhl.UnitTests/Plugins/ConverterMetadataTests.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) 2019 SceneGate - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -namespace Yarhl.UnitTests.Plugins -{ - using System; - using System.Diagnostics.CodeAnalysis; - using NUnit.Framework; - using Yarhl.Plugins.FileFormat; - - [TestFixture] - public class ConverterMetadataTests - { - [Test] - public void GetAndSetProperties() - { - var metadata = new ConverterMetadata { - Name = "test", - Type = typeof(int), - InternalSources = typeof(string), - InternalDestinations = typeof(DateTime), - }; - Assert.That(metadata.Name, Is.EqualTo("test")); - Assert.That(metadata.Type, Is.EqualTo(typeof(int))); - Assert.That(metadata.InternalSources, Is.EqualTo(typeof(string))); - Assert.That(metadata.InternalDestinations, Is.EqualTo(typeof(DateTime))); - } - - [Test] - public void GetSourcesReturnOneElementArrayForSingleSource() - { - var metadata = new ConverterMetadata { - InternalSources = typeof(int), - }; - Type[] sources = metadata.GetSources(); - Assert.That(sources.Length, Is.EqualTo(1)); - Assert.That(sources[0], Is.EqualTo(typeof(int))); - } - - [Test] - public void GetSourcesReturnTwoElementsArrayForListOfSources() - { - var metadata = new ConverterMetadata { - InternalSources = new Type[] { typeof(int), typeof(string) }, - }; - Type[] sources = metadata.GetSources(); - Assert.That(sources.Length, Is.EqualTo(2)); - Assert.That(sources[0], Is.EqualTo(typeof(int))); - Assert.That(sources[1], Is.EqualTo(typeof(string))); - } - - [Test] - public void GetSourcesReturnEmptyArrayForDefaultValue() - { - var metadata = new ConverterMetadata(); - Assert.That(metadata.GetSources(), Is.Empty); - } - - [Test] - public void GetDestinationsReturnOneElementArrayForSingleSource() - { - var metadata = new ConverterMetadata { - InternalDestinations = typeof(int), - }; - Type[] dests = metadata.GetDestinations(); - Assert.That(dests.Length, Is.EqualTo(1)); - Assert.That(dests[0], Is.EqualTo(typeof(int))); - } - - [Test] - public void GetDestinationsReturnTwoElementsArrayForListOfSources() - { - var metadata = new ConverterMetadata { - InternalDestinations = new Type[] { typeof(int), typeof(string) }, - }; - Type[] dests = metadata.GetDestinations(); - Assert.That(dests.Length, Is.EqualTo(2)); - Assert.That(dests[0], Is.EqualTo(typeof(int))); - Assert.That(dests[1], Is.EqualTo(typeof(string))); - } - - [Test] - public void GetDestinationsReturnEmptyArrayForDefaultValue() - { - var metadata = new ConverterMetadata(); - Assert.That(metadata.GetDestinations(), Is.Empty); - } - - [Test] - public void CanConvertSourceThrowsExceptionIfNullArgument() - { - var metadata = new ConverterMetadata { - InternalSources = new Type[] { typeof(int), typeof(string) }, - }; - Assert.That( - () => metadata.CanConvert(null), - Throws.ArgumentNullException); - } - - [Test] - public void CanConvertReturnsTrueForExactType() - { - var metadata = new ConverterMetadata { - InternalSources = typeof(int), - }; - Assert.That(metadata.CanConvert(typeof(int)), Is.True); - } - - [Test] - public void CanConvertReturnsTrueForTypeInList() - { - var metadata = new ConverterMetadata { - InternalSources = new[] { typeof(string), typeof(int) }, - }; - Assert.That(metadata.CanConvert(typeof(int)), Is.True); - } - - [Test] - public void CanConvertReturnsTrueForDerivedTypes() - { - var metadata = new ConverterMetadata { - InternalSources = typeof(Base), - }; - Assert.That(metadata.CanConvert(typeof(Derived)), Is.True); - } - - [Test] - public void CanConvertReturnsFalseForDifferentTypes() - { - var metadata = new ConverterMetadata { - InternalSources = new[] { typeof(string), typeof(int) }, - }; - Assert.That(metadata.CanConvert(typeof(DateTime)), Is.False); - } - - [Test] - public void CanConvertReturnsForExactSourceAndDest() - { - var metadata = new ConverterMetadata { - InternalSources = typeof(int), - InternalDestinations = typeof(string), - }; - Assert.That( - metadata.CanConvert(typeof(int), typeof(string)), - Is.True); - Assert.That( - metadata.CanConvert(typeof(string), typeof(string)), - Is.False); - Assert.That( - metadata.CanConvert(typeof(int), typeof(int)), - Is.False); - } - - [Test] - public void CanConvertReturnsForSourceAndDestInSameOrderList() - { - var metadata = new ConverterMetadata { - InternalSources = new[] { typeof(int), typeof(DateTime) }, - InternalDestinations = new[] { typeof(string), typeof(sbyte) }, - }; - Assert.That( - metadata.CanConvert(typeof(DateTime), typeof(sbyte)), - Is.True); - Assert.That( - metadata.CanConvert(typeof(DateTime), typeof(string)), - Is.False); - } - - [Test] - public void CanConvertReturnsForSourceAndDestDerived() - { - var metadata = new ConverterMetadata { - InternalSources = new[] { typeof(Base) }, - InternalDestinations = new[] { typeof(Derived) }, - }; - Assert.That( - metadata.CanConvert(typeof(Derived), typeof(Base)), - Is.True); - - metadata = new ConverterMetadata { - InternalSources = new[] { typeof(Derived) }, - InternalDestinations = new[] { typeof(Base) }, - }; - Assert.That( - metadata.CanConvert(typeof(Base), typeof(Base)), - Is.False); - Assert.That( - metadata.CanConvert(typeof(Derived), typeof(Derived)), - Is.False); - } - - [Test] - public void CanConvertSourceDestThrowsExceptionIfNullArgument() - { - var metadata = new ConverterMetadata { - InternalSources = new Type[] { typeof(int), typeof(string) }, - InternalDestinations = new Type[] { typeof(int), typeof(string) }, - }; - Assert.That( - () => metadata.CanConvert(null, typeof(int)), - Throws.ArgumentNullException); - Assert.That( - () => metadata.CanConvert(typeof(int), null), - Throws.ArgumentNullException); - } - - class Base - { - } - - [SuppressMessage("Build", "CA1812", Justification = "Indirect instances")] - class Derived : Base - { - } - } -} diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs new file mode 100644 index 00000000..4497367e --- /dev/null +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs @@ -0,0 +1,233 @@ +// Copyright (c) 2019 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.UnitTests.Plugins.FileFormat; + +using System; +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Yarhl.Plugins.FileFormat; + +[TestFixture] +public class ConverterTypeInfoTests +{ + /* + [Test] + public void GetAndSetProperties() + { + var metadata = new ConverterTypeInfo { + Name = "test", + Type = typeof(int), + InternalSources = typeof(string), + InternalDestinations = typeof(DateTime), + }; + Assert.That(metadata.Name, Is.EqualTo("test")); + Assert.That(metadata.Type, Is.EqualTo(typeof(int))); + Assert.That(metadata.InternalSources, Is.EqualTo(typeof(string))); + Assert.That(metadata.InternalDestinations, Is.EqualTo(typeof(DateTime))); + } + + [Test] + public void GetSourcesReturnOneElementArrayForSingleSource() + { + var metadata = new ConverterTypeInfo { + InternalSources = typeof(int), + }; + Type[] sources = metadata.GetSources(); + Assert.That(sources.Length, Is.EqualTo(1)); + Assert.That(sources[0], Is.EqualTo(typeof(int))); + } + + [Test] + public void GetSourcesReturnTwoElementsArrayForListOfSources() + { + var metadata = new ConverterTypeInfo { + InternalSources = new Type[] { typeof(int), typeof(string) }, + }; + Type[] sources = metadata.GetSources(); + Assert.That(sources.Length, Is.EqualTo(2)); + Assert.That(sources[0], Is.EqualTo(typeof(int))); + Assert.That(sources[1], Is.EqualTo(typeof(string))); + } + + [Test] + public void GetSourcesReturnEmptyArrayForDefaultValue() + { + var metadata = new ConverterTypeInfo(); + Assert.That(metadata.GetSources(), Is.Empty); + } + + [Test] + public void GetDestinationsReturnOneElementArrayForSingleSource() + { + var metadata = new ConverterTypeInfo { + InternalDestinations = typeof(int), + }; + Type[] dests = metadata.GetDestinations(); + Assert.That(dests.Length, Is.EqualTo(1)); + Assert.That(dests[0], Is.EqualTo(typeof(int))); + } + + [Test] + public void GetDestinationsReturnTwoElementsArrayForListOfSources() + { + var metadata = new ConverterTypeInfo { + InternalDestinations = new Type[] { typeof(int), typeof(string) }, + }; + Type[] dests = metadata.GetDestinations(); + Assert.That(dests.Length, Is.EqualTo(2)); + Assert.That(dests[0], Is.EqualTo(typeof(int))); + Assert.That(dests[1], Is.EqualTo(typeof(string))); + } + + [Test] + public void GetDestinationsReturnEmptyArrayForDefaultValue() + { + var metadata = new ConverterTypeInfo(); + Assert.That(metadata.GetDestinations(), Is.Empty); + } + + [Test] + public void CanConvertSourceThrowsExceptionIfNullArgument() + { + var metadata = new ConverterTypeInfo { + InternalSources = new Type[] { typeof(int), typeof(string) }, + }; + Assert.That( + () => metadata.CanConvert(null), + Throws.ArgumentNullException); + } + + [Test] + public void CanConvertReturnsTrueForExactType() + { + var metadata = new ConverterTypeInfo { + InternalSources = typeof(int), + }; + Assert.That(metadata.CanConvert(typeof(int)), Is.True); + } + + [Test] + public void CanConvertReturnsTrueForTypeInList() + { + var metadata = new ConverterTypeInfo { + InternalSources = new[] { typeof(string), typeof(int) }, + }; + Assert.That(metadata.CanConvert(typeof(int)), Is.True); + } + + [Test] + public void CanConvertReturnsTrueForDerivedTypes() + { + var metadata = new ConverterTypeInfo { + InternalSources = typeof(Base), + }; + Assert.That(metadata.CanConvert(typeof(Derived)), Is.True); + } + + [Test] + public void CanConvertReturnsFalseForDifferentTypes() + { + var metadata = new ConverterTypeInfo { + InternalSources = new[] { typeof(string), typeof(int) }, + }; + Assert.That(metadata.CanConvert(typeof(DateTime)), Is.False); + } + + [Test] + public void CanConvertReturnsForExactSourceAndDest() + { + var metadata = new ConverterTypeInfo { + InternalSources = typeof(int), + InternalDestinations = typeof(string), + }; + Assert.That( + metadata.CanConvert(typeof(int), typeof(string)), + Is.True); + Assert.That( + metadata.CanConvert(typeof(string), typeof(string)), + Is.False); + Assert.That( + metadata.CanConvert(typeof(int), typeof(int)), + Is.False); + } + + [Test] + public void CanConvertReturnsForSourceAndDestInSameOrderList() + { + var metadata = new ConverterTypeInfo { + InternalSources = new[] { typeof(int), typeof(DateTime) }, + InternalDestinations = new[] { typeof(string), typeof(sbyte) }, + }; + Assert.That( + metadata.CanConvert(typeof(DateTime), typeof(sbyte)), + Is.True); + Assert.That( + metadata.CanConvert(typeof(DateTime), typeof(string)), + Is.False); + } + + [Test] + public void CanConvertReturnsForSourceAndDestDerived() + { + var metadata = new ConverterTypeInfo { + InternalSources = new[] { typeof(Base) }, + InternalDestinations = new[] { typeof(Derived) }, + }; + Assert.That( + metadata.CanConvert(typeof(Derived), typeof(Base)), + Is.True); + + metadata = new ConverterTypeInfo { + InternalSources = new[] { typeof(Derived) }, + InternalDestinations = new[] { typeof(Base) }, + }; + Assert.That( + metadata.CanConvert(typeof(Base), typeof(Base)), + Is.False); + Assert.That( + metadata.CanConvert(typeof(Derived), typeof(Derived)), + Is.False); + } + + [Test] + public void CanConvertSourceDestThrowsExceptionIfNullArgument() + { + var metadata = new ConverterTypeInfo { + InternalSources = new Type[] { typeof(int), typeof(string) }, + InternalDestinations = new Type[] { typeof(int), typeof(string) }, + }; + Assert.That( + () => metadata.CanConvert(null, typeof(int)), + Throws.ArgumentNullException); + Assert.That( + () => metadata.CanConvert(typeof(int), null), + Throws.ArgumentNullException); + } + + class Base + { + } + + [SuppressMessage("Build", "CA1812", Justification = "Indirect instances")] + class Derived : Base + { + } + */ +} diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs new file mode 100644 index 00000000..1ff08d0d --- /dev/null +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) 2019 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.UnitTests.Plugins.FileFormat; + +using System; +using System.Linq; +using NUnit.Framework; +using Yarhl.FileFormat; +using Yarhl.Plugins; + +[TestFixture] +public class ConvertersLocatorTests +{ + /* + [Test] + public void FormatMetadataContainsNameAndType() + { + var format = TypeLocator.Instance.GetFormats() + .Single(p => p.Metadata.Type == typeof(StringFormat)); + Assert.That( + format.Metadata.Name, + Is.EqualTo(typeof(StringFormat).FullName)); + } + + [Test] + public void FormatsAreNotDuplicated() + { + Assert.That( + TypeLocator.Instance.GetFormats().Select(f => f.Metadata.Type), + Is.Unique); + } + + [Test] + public void GetFormatsReturnsKnownFormats() + { + Assert.That( + TypeLocator.Instance.GetFormats().Select(f => f.Metadata.Name), + Does.Contain(typeof(BinaryFormat).FullName)); + } + + [Test] + public void FindSingleInnerConverter() + { + IConverter converter = null; + Assert.That( + () => converter = TypeLocator.Instance + .FindExtensions>() + .Single(), + Throws.Nothing); + Assert.That( + converter, + Is.InstanceOf()); + Assert.That(converter.Convert("4"), Is.EqualTo(4)); + } + + [Test] + public void FindSingleOuterConverter() + { + IConverter converter = null; + Assert.That( + () => converter = TypeLocator.Instance + .FindExtensions>() + .Single(), + Throws.Nothing); + Assert.That(converter, Is.InstanceOf()); + Assert.That(converter.Convert("5"), Is.EqualTo(5)); + } + + [Test] + public void FindTwoConvertersInSameClass() + { + var converter1 = TypeLocator.Instance + .FindExtensions>(); + Assert.IsInstanceOf(converter1.Single()); + + Assert.DoesNotThrow(() => + converter1.Single(t => + Array.Exists(t.GetType().GetInterfaces(), i => + i.IsGenericType && + i.GenericTypeArguments.Length == 2 && + i.GenericTypeArguments[0] == typeof(string) && + i.GenericTypeArguments[1] == typeof(int)))); + + var converter2 = TypeLocator.Instance + .FindExtensions>(); + Assert.IsInstanceOf(converter2.Single()); + + Assert.DoesNotThrow(() => + converter2.Single(t => + Array.Exists(t.GetType().GetInterfaces(), i => + i.IsGenericType && + i.GenericTypeArguments.Length == 2 && + i.GenericTypeArguments[0] == typeof(int) && + i.GenericTypeArguments[1] == typeof(string)))); + } + + [Test] + public void FindDerivedConverter() + { + var converters = TypeLocator.Instance + .FindExtensions>(); + IConverter converter = null; + Assert.That( + () => converter = converters.Single(), + Throws.Nothing); + Assert.IsInstanceOf(converter); + Assert.IsInstanceOf(converter); + Assert.That(converter.Convert("3"), Is.EqualTo(3)); + } + + [Test] + public void FindConvertsWithOtherInterfaces() + { + IConverter converter = null; + Assert.That( + () => converter = TypeLocator.Instance + .FindExtensions>() + .Single(), + Throws.Nothing); + Assert.That(converter, Is.InstanceOf()); + Assert.That(converter.Convert("3"), Is.EqualTo(3)); + Assert.That( + ((ConverterAndOtherInterface)converter).Dispose, + Throws.Nothing); + } + */ +} diff --git a/src/Yarhl.UnitTests/Plugins/TestConvertersDefinition.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs similarity index 75% rename from src/Yarhl.UnitTests/Plugins/TestConvertersDefinition.cs rename to src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs index ebf52355..54fe7997 100644 --- a/src/Yarhl.UnitTests/Plugins/TestConvertersDefinition.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs @@ -1,10 +1,35 @@ -namespace Yarhl.UnitTests.Plugins; +namespace Yarhl.UnitTests.Plugins.FileFormat; using System; using Yarhl.FileFormat; #pragma warning disable SA1649 // File name match type name +public class PluginFormat : Yarhl.FileFormat.IFormat +{ + public static int Value => 0; +} + +public class PluginConverter : Yarhl.FileFormat.IConverter +{ + public int Convert(PluginFormat source) + { + return PluginFormat.Value; + } +} + +public class PluginConverterParametrized : Yarhl.FileFormat.IConverter +{ + public PluginConverterParametrized(bool ignoreMe) + { + } + + public int Convert(PluginFormat source) + { + return PluginFormat.Value; + } +} + public class BasicConverter : IConverter { public byte Convert(string source) diff --git a/src/Yarhl.UnitTests/Plugins/PluginManagerTests.cs b/src/Yarhl.UnitTests/Plugins/PluginManagerTests.cs deleted file mode 100644 index b1b5fb5c..00000000 --- a/src/Yarhl.UnitTests/Plugins/PluginManagerTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2019 SceneGate - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -namespace Yarhl.UnitTests.Plugins -{ - using System; - using System.Composition; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using NUnit.Framework; - using Yarhl.IO; - using Yarhl.Plugins; - using Yarhl.Plugins.FileFormat; - using Yarhl.UnitTests.FileFormat; - - [TestFixture] - public class PluginManagerTests - { - public interface IExistsInterface - { - } - - [SuppressMessage("", "S2326", Justification = "Test class")] - public interface IGenericExport - { - } - - [Test] - public void InstanceInitializePluginManager() - { - Assert.IsNotNull(PluginManager.Instance); - } - - [Test] - public void FormatMetadataContainsNameAndType() - { - var format = PluginManager.Instance.GetFormats() - .Single(p => p.Metadata.Type == typeof(StringFormat)); - Assert.That( - format.Metadata.Name, - Is.EqualTo(typeof(StringFormat).FullName)); - } - - [Test] - public void FormatsAreNotDuplicated() - { - Assert.That( - PluginManager.Instance.GetFormats().Select(f => f.Metadata.Type), - Is.Unique); - } - - [Test] - public void GetFormatsReturnsKnownFormats() - { - Assert.That( - PluginManager.Instance.GetFormats().Select(f => f.Metadata.Name), - Does.Contain(typeof(BinaryFormat).FullName)); - } - - [Test] - public void FindExtensionByGenericType() - { - var extensions = PluginManager.Instance - .FindExtensions() - .ToList(); - Assert.IsInstanceOf(typeof(IExistsInterface), extensions.Single()); - } - - [Test] - public void FindSpecificExtensionByGenericTypeReturnsEmpty() - { - var extensions = PluginManager.Instance - .FindExtensions(); - Assert.IsEmpty(extensions); - } - - [Test] - public void FindExtensionByType() - { - var extensions = PluginManager.Instance - .FindExtensions(typeof(IExistsInterface)); - Assert.IsInstanceOf(typeof(IExistsInterface), extensions.Single()); - } - - [Test] - public void FindExtensionByTypeNotRegisteredReturnsEmpty() - { - var extensions = PluginManager.Instance - .FindExtensions(typeof(ExistsClass)); - Assert.IsEmpty(extensions); - } - - [Test] - public void FindExtensionWithNullTypeThrowsException() - { - Assert.Throws(() => - PluginManager.Instance.FindExtensions(null)); - } - - [Test] - public void FindGenericExtensions() - { - var extensions = PluginManager.Instance - .FindExtensions(typeof(IGenericExport)); - Assert.IsInstanceOf(typeof(IGenericExport), extensions.Single()); - } - - [Test] - public void FindGenericExtensionsNotRegisteredReturnsEmpty() - { - var extensions = PluginManager.Instance - .FindExtensions(typeof(IGenericExport)); - Assert.IsEmpty(extensions); - } - - [Test] - public void FindLazyExtensionByGeneric() - { - var extensions = PluginManager.Instance - .FindLazyExtensions(); - Assert.That(extensions.Count(), Is.EqualTo(1)); - Assert.That(() => extensions.Single().CreateExport(), Throws.Exception); - } - - [Test] - public void FindLazyExtensionByType() - { - var extensions = PluginManager.Instance - .FindLazyExtensions(typeof(ConstructorWithException)); - Assert.That(extensions.Count(), Is.EqualTo(1)); - Assert.That( - extensions.Single().GetType(), - Is.EqualTo(typeof(ExportFactory))); - Assert.That( - () => ((ExportFactory)extensions.Single()).CreateExport().Value, - Throws.Exception); - } - - [Test] - public void FindLazyExtensionByTypeWithNullThrowsException() - { - Assert.That( - () => PluginManager.Instance.FindLazyExtensions(null), - Throws.ArgumentNullException); - } - - [Test] - public void FindLazyExtensionWithMetadata() - { - var formats = PluginManager.Instance - .FindLazyExtensions() - .Select(f => f.Metadata.Type); - Assert.That(formats, Does.Contain(typeof(PluginFormat))); - } - - [Test] - public void FindLazyExtesionWithMetadataIsUnique() - { - var formats = PluginManager.Instance - .FindLazyExtensions() - .Select(f => f.Metadata.Type); - Assert.That(formats, Is.Unique); - } - - [Test] - public void GetFormatsReturnsListWithMetadata() - { - var formats = PluginManager.Instance.GetFormats() - .Select(f => f.Metadata.Type); - Assert.That(formats, Does.Contain(typeof(PluginFormat))); - } - - [Test] - public void GetConvertersWithMetadataReturnsListWithMetadata() - { - var formats = PluginManager.Instance.GetConverters() - .Select(f => f.Metadata.Type); - Assert.That(formats, Does.Contain(typeof(PluginConverter))); - - var conv = (PluginConverter)PluginManager.Instance.GetConverters() - .Single(f => f.Metadata.Type == typeof(PluginConverter)) - .CreateExport().Value; - Assert.That(conv.Convert(new PluginFormat()), Is.EqualTo(0)); - } - - [Test] - [Ignore("To be re-implemented without MEF")] - public void GetConvertersWithParametersReturnsMetadata() - { - var formats = PluginManager.Instance.GetConverters() - .Select(f => f.Metadata.Type); - Assert.That(formats, Does.Contain(typeof(PluginConverterParametrized))); - } - - [Export(typeof(IExistsInterface))] - public class ExistsClass : IExistsInterface - { - } - - [Export(typeof(IGenericExport))] - public class GenericExport : IGenericExport - { - } - - [Export] - public class ConstructorWithException - { - public ConstructorWithException() - { - throw new Exception(); - } - } - - public class PluginFormat : Yarhl.FileFormat.IFormat - { - public static int Value => 0; - } - - public class PluginConverter : Yarhl.FileFormat.IConverter - { - public int Convert(PluginFormat source) - { - return PluginFormat.Value; - } - } - - [PartNotDiscoverable] // TODO: After re-implement without MEF - public class PluginConverterParametrized : Yarhl.FileFormat.IConverter - { - public PluginConverterParametrized(bool ignoreMe) - { - } - - public int Convert(PluginFormat source) - { - return PluginFormat.Value; - } - } - } -} diff --git a/src/Yarhl.Plugins/AssemblyUtils.cs b/src/Yarhl.UnitTests/Plugins/TestTypesDefinition.cs similarity index 51% rename from src/Yarhl.Plugins/AssemblyUtils.cs rename to src/Yarhl.UnitTests/Plugins/TestTypesDefinition.cs index 1610d70a..3522b3ca 100644 --- a/src/Yarhl.Plugins/AssemblyUtils.cs +++ b/src/Yarhl.UnitTests/Plugins/TestTypesDefinition.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019 SceneGate +// Copyright (c) 2023 SceneGate // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -17,36 +17,55 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace Yarhl.Plugins +namespace Yarhl.UnitTests.Plugins; + +using System; + +#pragma warning disable SA1649 // File name should match first type name +#pragma warning disable S2326 // Unused type parameters should be removed + +public interface IExistsInterface +{ +} + +public interface IGenericInterface { - using System; - using System.Collections.Generic; - using System.Reflection; - using System.Runtime.Loader; +} - /// - /// Utilities to work with Assemblies. - /// - internal static class AssemblyUtils +public interface IGenericInterface +{ +} + +public class ExistsClass : IExistsInterface +{ +} + +public class Generic1Class : IGenericInterface +{ +} + +public class Generic2Class : IGenericInterface +{ +} + +public class GenericMultipleClass : + IGenericInterface, + IGenericInterface +{ +} + +public abstract class AbstractClass : IExistsInterface +{ +} + +public abstract class AbstractGenericClass : IGenericInterface +{ +} + +public class ConstructorWithException +{ + public ConstructorWithException() { - /// - /// Load assemblies. - /// - /// List of assemblies to load. - /// The assemblies. - public static IEnumerable LoadAssemblies(this IEnumerable paths) - { - List assemblies = new List(); - foreach (string path in paths) { - try { - Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path); - assemblies.Add(assembly); - } catch (BadImageFormatException) { - // Bad IL. Skip. - } - } - - return assemblies; - } + throw new Exception(); } } diff --git a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs new file mode 100644 index 00000000..af19343f --- /dev/null +++ b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) 2019 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.UnitTests.Plugins; + +using System.Linq; +using NUnit.Framework; +using Yarhl.IO; +using Yarhl.Plugins; + +[TestFixture] +public class TypeLocatorTests +{ + [Test] + public void InstanceInitializePluginManager() + { + var instance = TypeLocator.Instance; + Assert.That(instance, Is.Not.Null); + Assert.That(instance.LoadContext, Is.Not.Null); + } + + [Test] + public void InstanceIsCreatedOnce() + { + var instance1 = TypeLocator.Instance; + var instance2 = TypeLocator.Instance; + Assert.That(instance1, Is.SameAs(instance2)); + } + + [Test] + public void FindImplementationOfInterface() + { + var extensions = TypeLocator.Instance + .FindImplementationsOf(typeof(IExistsInterface)) + .ToList(); + + Assert.That(extensions, Has.Count.EqualTo(1)); + Assert.Multiple(() => { + Assert.That(extensions[0].Name, Is.EqualTo(typeof(ExistsClass).FullName)); + Assert.That(extensions[0].Type, Is.EqualTo(typeof(ExistsClass))); + Assert.That(extensions[0].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); + }); + } + + [Test] + public void FindImplementationOfInterfaceWithAssembly() + { + var extensions = TypeLocator.Instance + .FindImplementationsOf(typeof(IExistsInterface), typeof(IExistsInterface).Assembly) + .ToList(); + + Assert.That(extensions, Has.Count.EqualTo(1)); + Assert.Multiple(() => { + Assert.That(extensions[0].Name, Is.EqualTo(typeof(ExistsClass).FullName)); + Assert.That(extensions[0].Type, Is.EqualTo(typeof(ExistsClass))); + Assert.That(extensions[0].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); + }); + } + + [Test] + public void FindImplementationOfClass() + { + var extensions = TypeLocator.Instance + .FindImplementationsOf(typeof(ExistsClass)) + .ToList(); + + Assert.That(extensions, Has.Count.EqualTo(1)); + Assert.Multiple(() => { + Assert.That(extensions[0].Name, Is.EqualTo(typeof(ExistsClass).FullName)); + Assert.That(extensions[0].Type, Is.EqualTo(typeof(ExistsClass))); + Assert.That(extensions[0].InterfaceImplemented, Is.EqualTo(typeof(ExistsClass))); + }); + } + + [Test] + public void FindImplementationOfInterfaceDifferentAssemblyReturnsEmpty() + { + var extensions = TypeLocator.Instance + .FindImplementationsOf(typeof(IExistsInterface), typeof(IBinary).Assembly) + .ToList(); + + Assert.That(extensions, Has.Count.EqualTo(0)); + } + + [Test] + public void FindImplementationWithNullTypeThrowsException() + { + Assert.That( + () => TypeLocator.Instance.FindImplementationsOf(null), + Throws.ArgumentNullException); + Assert.That( + () => TypeLocator.Instance.FindImplementationsOf(null, typeof(IExistsInterface).Assembly), + Throws.ArgumentNullException); + Assert.That( + () => TypeLocator.Instance.FindImplementationsOf(typeof(IExistsInterface), null), + Throws.ArgumentNullException); + } +} diff --git a/src/Yarhl.UnitTests/Yarhl.UnitTests.csproj b/src/Yarhl.UnitTests/Yarhl.UnitTests.csproj index acb1a915..f5e771a3 100644 --- a/src/Yarhl.UnitTests/Yarhl.UnitTests.csproj +++ b/src/Yarhl.UnitTests/Yarhl.UnitTests.csproj @@ -10,8 +10,8 @@ - + From 0d7547b7037479a041c581e4c1533d3fb655e7af Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Tue, 28 Nov 2023 18:39:44 +0100 Subject: [PATCH 02/12] :umbrella: Integration tests for assembly loading from dirs --- .../AssemblyLoadContextExtensionsTests.cs | 156 ++++++++++++++++++ src/Yarhl.IntegrationTests/PluginDiscovery.cs | 77 --------- .../Yarhl.IntegrationTests.csproj | 3 + .../AssemblyLoadContextExtensions.cs | 6 + 4 files changed, 165 insertions(+), 77 deletions(-) create mode 100644 src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs delete mode 100644 src/Yarhl.IntegrationTests/PluginDiscovery.cs diff --git a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs new file mode 100644 index 00000000..51b825b3 --- /dev/null +++ b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) 2019 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.IntegrationTests; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using Yarhl.Plugins; +using Yarhl.Plugins.FileFormat; + +// By forcing to run in parallel each test needs to re-load the assemblies. +[TestFixture] +[Parallelizable(ParallelScope.Children)] +public class AssemblyLoadContextExtensionsTests +{ + [Test] + public void TestPreconditionYarhlMediaIsInPluginsFolder() + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + string pluginDir = Path.Combine(programDir, "Plugins"); + Assert.IsTrue(Directory.Exists(pluginDir)); + + Assert.IsTrue(File.Exists(Path.Combine(pluginDir, "Yarhl.Media.Text.dll"))); + Assert.IsTrue(File.Exists(Path.Combine(pluginDir, "MyBadPlugin.dll"))); + + Assert.IsFalse(File.Exists(Path.Combine(programDir, "Yarhl.Media.Text.dll"))); + } + + [Test] + public void LoadingYarhlMediaFromPath() + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + string assemblyPath = Path.Combine(programDir, "Plugins", "Yarhl.Media.Text.dll"); + Assembly? loaded = TypeLocator.Instance.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), + Does.Contain("Yarhl.Media.Text")); + } + + [Test] + public void LoadingInvalidAssemblyReturnsNull() + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + string assemblyPath = Path.Combine(programDir, "Plugins", "MyBadPlugin.dll"); + Assembly? loaded = TypeLocator.Instance.LoadContext.TryLoadFromAssemblyPath(assemblyPath); + + Assert.That(loaded, Is.Null); + } + + [Test] + public void LoadingIgnoreSystemLibraries() + { + IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromExecutingDirectory(); + + Assert.That(loaded.Select(a => a.GetName().Name), Does.Not.Contain("testhost")); + } + + [Test] + public void LoadingExecutingDirGetsYarhl() + { + // We cannot use ConverterLocator as it will load Yarhl as it uses some of its types. + IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromExecutingDirectory(); + + Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl")); + + Assert.That( + TypeLocator.Instance.LoadContext.Assemblies.Select(a => a.GetName().Name), + Does.Contain("Yarhl")); + } + + [Test] + public void LoadingPluginsDirGetsYarhlMedia() + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + string pluginDir = Path.Combine(programDir, "Plugins"); + IEnumerable loaded = TypeLocator.Instance.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), + Does.Contain("Yarhl.Media.Text")); + } + + [Test] + public void LoadingPluginsDirRecursiveGetsYarhlMedia() + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + IEnumerable loaded = TypeLocator.Instance.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), + Does.Contain("Yarhl.Media.Text")); + } + + [Test] + public void FindFormatFromPluginsDir() + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + string pluginDir = Path.Combine(programDir, "Plugins"); + TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); + + var formats = ConvertersLocator.Instance.Formats; + Assert.That(formats, Is.Not.Empty); + Assert.That( + formats.Select(t => t.Name), + Does.Contain("Yarhl.Media.Text.Po")); + } + + [Test] + public void FindConverterFromPluginsDir() + { + string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + string pluginDir = Path.Combine(programDir, "Plugins"); + TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); + + Type poType = ConvertersLocator.Instance.Formats + .Single(f => f.Name == "Yarhl.Media.Text.Po") + .Type; + + var converters = ConvertersLocator.Instance.Converters + .Where(f => f.CanConvert(poType)); + Assert.That(converters, Is.Not.Empty); + Assert.That( + converters.Select(t => t.Name), + Does.Contain("Yarhl.Media.Text.Po2Binary")); + } +} diff --git a/src/Yarhl.IntegrationTests/PluginDiscovery.cs b/src/Yarhl.IntegrationTests/PluginDiscovery.cs deleted file mode 100644 index 6ec9e902..00000000 --- a/src/Yarhl.IntegrationTests/PluginDiscovery.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2019 SceneGate - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -namespace Yarhl.IntegrationTests -{ - using System; - using System.IO; - using System.Linq; - using NUnit.Framework; - using Yarhl.Plugins; - using Yarhl.Plugins.FileFormat; - - [TestFixture] - public class PluginDiscovery - { - [Test] - public void YarhlMediaIsInPluginsFolder() - { - string programDir = Path.GetDirectoryName(Environment.ProcessPath); - string pluginDir = Path.Combine(programDir, "Plugins"); - Assert.IsTrue(Directory.Exists(pluginDir)); - - Assert.IsTrue(File.Exists(Path.Combine(pluginDir, "Yarhl.Media.Text.dll"))); - - Assert.IsFalse(File.Exists(Path.Combine(programDir, "Yarhl.Media.Text.dll"))); - } - - [Test] - public void CanFoundPoByFormat() - { - string programDir = Path.GetDirectoryName(Environment.ProcessPath); - string pluginDir = Path.Combine(programDir, "Plugins"); - TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); - - var formats = ConvertersLocator.Instance.Formats; - Assert.That(formats, Is.Not.Empty); - Assert.That( - formats.Select(t => t.Name), - Does.Contain("Yarhl.Media.Text.Po")); - } - - [Test] - public void CanFoundPoConverterFromTypes() - { - string programDir = Path.GetDirectoryName(Environment.ProcessPath); - string pluginDir = Path.Combine(programDir, "Plugins"); - TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); - - Type poType = ConvertersLocator.Instance.Formats - .Single(f => f.Name == "Yarhl.Media.Text.Po") - .Type; - - var converters = ConvertersLocator.Instance.Converters - .Where(f => f.CanConvert(poType)); - Assert.That(converters, Is.Not.Empty); - Assert.That( - converters.Select(t => t.Name), - Does.Contain("Yarhl.Media.Text.Po2Binary")); - } - } -} diff --git a/src/Yarhl.IntegrationTests/Yarhl.IntegrationTests.csproj b/src/Yarhl.IntegrationTests/Yarhl.IntegrationTests.csproj index e00ea5e8..91fdfefb 100644 --- a/src/Yarhl.IntegrationTests/Yarhl.IntegrationTests.csproj +++ b/src/Yarhl.IntegrationTests/Yarhl.IntegrationTests.csproj @@ -4,6 +4,9 @@ Plugin integration tests for Yarhl. net6.0;net8.0 + + enable + enable diff --git a/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs b/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs index fe1c9063..70ee9b71 100644 --- a/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs +++ b/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs @@ -46,6 +46,8 @@ public static class AssemblyLoadContextExtensions /// The list of assembly paths to load. /// A collection of assemblies that could be loaded. /// + /// SECURITY NOTE: Ensure that you trust those assemblies. You may introduce + /// a security risk by running arbitrary code. /// If an assembly fails to load it will be silently skipped. /// public static IEnumerable TryLoadFromAssembliesPath(this AssemblyLoadContext loader, IEnumerable paths) @@ -72,6 +74,8 @@ public static IEnumerable TryLoadFromAssembliesPath(this AssemblyLoadC /// /// A collection of assemblies that could be loaded. /// + /// SECURITY NOTE: Ensure that you trust those assemblies. You may introduce + /// a security risk by running arbitrary code. /// If an assembly fails to load it will be silently skipped. /// public static IEnumerable TryLoadFromDirectory(this AssemblyLoadContext loader, string directory, bool recursive) @@ -89,6 +93,8 @@ public static IEnumerable TryLoadFromDirectory(this AssemblyLoadContex /// The load context to use to load. /// A collection of assemblies that could be loaded. /// + /// SECURITY NOTE: Ensure that you trust those assemblies. You may introduce + /// a security risk by running arbitrary code. /// If an assembly fails to load it will be silently skipped. /// public static IEnumerable TryLoadFromExecutingDirectory(this AssemblyLoadContext loader) From be1ec13d0d0b8415149ab42ebccde9d498593fcd Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Wed, 29 Nov 2023 10:42:27 +0100 Subject: [PATCH 03/12] :umbrella: Implement tests for TypeLocator generic --- .../GenericInterfaceImplementationInfo.cs | 4 +- .../InterfaceImplementationInfo.cs | 2 +- src/Yarhl.Plugins/TypeLocator.cs | 27 +-- .../Plugins/TestTypesDefinition.cs | 8 + .../Plugins/TypeLocatorTests.cs | 172 ++++++++++++++++-- 5 files changed, 180 insertions(+), 33 deletions(-) diff --git a/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs b/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs index b77f3335..90d3fef5 100644 --- a/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs +++ b/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs @@ -22,9 +22,9 @@ namespace Yarhl.Plugins; /// /// Provides information about a type that implements a generic interface. /// -/// The name of the implementation type. Shortcut for Type.Name. +/// The name of the implementation type. Shortcut for Type.FullName. /// The type that implements the interface. -/// Interface implemented. +/// The actual generic interface with type arguments implemented. /// The list of types specified in the generic. public record GenericInterfaceImplementationInfo( string Name, diff --git a/src/Yarhl.Plugins/InterfaceImplementationInfo.cs b/src/Yarhl.Plugins/InterfaceImplementationInfo.cs index 2c8116b4..5284bd7e 100644 --- a/src/Yarhl.Plugins/InterfaceImplementationInfo.cs +++ b/src/Yarhl.Plugins/InterfaceImplementationInfo.cs @@ -22,7 +22,7 @@ namespace Yarhl.Plugins; /// /// Provides information about a type that implements an interface. /// -/// The name of the implementation type. Shortcut for Type.Name. +/// The name of the implementation type. Shortcut for Type.FullName. /// The type that implements the interface. /// Interface implemented. public record InterfaceImplementationInfo(string Name, Type Type, Type InterfaceImplemented); diff --git a/src/Yarhl.Plugins/TypeLocator.cs b/src/Yarhl.Plugins/TypeLocator.cs index d873d7db..7cc1519b 100644 --- a/src/Yarhl.Plugins/TypeLocator.cs +++ b/src/Yarhl.Plugins/TypeLocator.cs @@ -141,25 +141,16 @@ public IEnumerable FindImplementationsOfGene type.IsGenericType && type.GetGenericTypeDefinition().IsEquivalentTo(baseType); - IEnumerable converterTypes = assembly.ExportedTypes + return assembly.ExportedTypes + .Where(t => t.IsClass && !t.IsAbstract) .Where(t => Array.Exists(t.GetInterfaces(), ValidImplementationInterface)) - .Where(t => t.IsClass && !t.IsAbstract); - - foreach (Type type in converterTypes) { - // A class may implement the interface several - // times with different generic types - IEnumerable interfaceImplementations = type.GetInterfaces() + .SelectMany(type => type.GetInterfaces() // A class may implement a generic interface multiple times .Where(ValidImplementationInterface) - .Select(i => i.GenericTypeArguments); - - foreach (Type[] genericTypes in interfaceImplementations) { - var metadata = new GenericInterfaceImplementationInfo( - type.FullName!, - type, - baseType, - genericTypes); - yield return metadata; - } - } + .Select(implementedInterface => + new GenericInterfaceImplementationInfo( + type.FullName!, + type, + implementedInterface, + implementedInterface.GenericTypeArguments))); } } diff --git a/src/Yarhl.UnitTests/Plugins/TestTypesDefinition.cs b/src/Yarhl.UnitTests/Plugins/TestTypesDefinition.cs index 3522b3ca..d287eafd 100644 --- a/src/Yarhl.UnitTests/Plugins/TestTypesDefinition.cs +++ b/src/Yarhl.UnitTests/Plugins/TestTypesDefinition.cs @@ -54,10 +54,18 @@ public class GenericMultipleClass : { } +public interface ISecondInterface : IExistsInterface +{ +} + public abstract class AbstractClass : IExistsInterface { } +public interface ISecondGenericInterface : IGenericInterface +{ +} + public abstract class AbstractGenericClass : IGenericInterface { } diff --git a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs index af19343f..91a49738 100644 --- a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs +++ b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2019 SceneGate +// Copyright (c) 2023 SceneGate // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -30,7 +30,7 @@ public class TypeLocatorTests [Test] public void InstanceInitializePluginManager() { - var instance = TypeLocator.Instance; + TypeLocator instance = TypeLocator.Instance; Assert.That(instance, Is.Not.Null); Assert.That(instance.LoadContext, Is.Not.Null); } @@ -38,8 +38,8 @@ public void InstanceInitializePluginManager() [Test] public void InstanceIsCreatedOnce() { - var instance1 = TypeLocator.Instance; - var instance2 = TypeLocator.Instance; + TypeLocator instance1 = TypeLocator.Instance; + TypeLocator instance2 = TypeLocator.Instance; Assert.That(instance1, Is.SameAs(instance2)); } @@ -50,11 +50,12 @@ public void FindImplementationOfInterface() .FindImplementationsOf(typeof(IExistsInterface)) .ToList(); - Assert.That(extensions, Has.Count.EqualTo(1)); + int index = extensions.FindIndex(i => i.Type == typeof(ExistsClass)); + Assert.That(index, Is.Not.EqualTo(-1)); + Assert.Multiple(() => { - Assert.That(extensions[0].Name, Is.EqualTo(typeof(ExistsClass).FullName)); - Assert.That(extensions[0].Type, Is.EqualTo(typeof(ExistsClass))); - Assert.That(extensions[0].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); + Assert.That(extensions[index].Name, Is.EqualTo(typeof(ExistsClass).FullName)); + Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); }); } @@ -65,11 +66,12 @@ public void FindImplementationOfInterfaceWithAssembly() .FindImplementationsOf(typeof(IExistsInterface), typeof(IExistsInterface).Assembly) .ToList(); - Assert.That(extensions, Has.Count.EqualTo(1)); + int index = extensions.FindIndex(i => i.Type == typeof(ExistsClass)); + Assert.That(index, Is.Not.EqualTo(-1)); + Assert.Multiple(() => { - Assert.That(extensions[0].Name, Is.EqualTo(typeof(ExistsClass).FullName)); - Assert.That(extensions[0].Type, Is.EqualTo(typeof(ExistsClass))); - Assert.That(extensions[0].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); + Assert.That(extensions[index].Name, Is.EqualTo(typeof(ExistsClass).FullName)); + Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); }); } @@ -111,4 +113,150 @@ public void FindImplementationWithNullTypeThrowsException() () => TypeLocator.Instance.FindImplementationsOf(typeof(IExistsInterface), null), Throws.ArgumentNullException); } + + [Test] + public void FindImplementationIgnoresAbstractClasses() + { + var results = TypeLocator.Instance + .FindImplementationsOf(typeof(IExistsInterface)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(AbstractClass)), Is.Null); + } + + [Test] + public void FindImplementationIgnoresInterfaces() + { + var results = TypeLocator.Instance + .FindImplementationsOf(typeof(IExistsInterface)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(ISecondInterface)), Is.Null); + } + + [Test] + public void FindImplementationCanFindConstructorWithException() + { + var results = TypeLocator.Instance + .FindImplementationsOf(typeof(ConstructorWithException)) + .ToList(); + + Assert.That(results, Has.Count.EqualTo(1)); + Assert.Multiple(() => { + Assert.That(results[0].Name, Is.EqualTo(typeof(ConstructorWithException).FullName)); + Assert.That(results[0].Type, Is.EqualTo(typeof(ConstructorWithException))); + Assert.That(results[0].InterfaceImplemented, Is.EqualTo(typeof(ConstructorWithException))); + }); + } + + [Test] + public void FindImplementationOfGenericInterface1() + { + var extensions = TypeLocator.Instance + .FindImplementationsOfGeneric(typeof(IGenericInterface<>)) + .ToList(); + + int index = extensions.FindIndex(i => i.Type == typeof(Generic1Class)); + Assert.That(index, Is.Not.EqualTo(-1)); + + Assert.Multiple(() => { + Assert.That(extensions[index].Name, Is.EqualTo(typeof(Generic1Class).FullName)); + Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IGenericInterface))); + Assert.That(extensions[index].GenericTypes, Has.Count.EqualTo(1)); + Assert.That(extensions[index].GenericTypes[0], Is.EqualTo(typeof(int))); + }); + + // For code coverage -.- + GenericInterfaceImplementationInfo copyInfo = extensions[index] with { }; + Assert.That(copyInfo, Is.EqualTo(extensions[index])); + } + + [Test] + public void FindImplementationOfGenericInterface2() + { + var extensions = TypeLocator.Instance + .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) + .ToList(); + + int index = extensions.FindIndex(i => i.Type == typeof(Generic2Class)); + Assert.That(index, Is.Not.EqualTo(-1)); + + Assert.Multiple(() => { + Assert.That(extensions[index].Name, Is.EqualTo(typeof(Generic2Class).FullName)); + Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IGenericInterface))); + Assert.That(extensions[index].GenericTypes, Has.Count.EqualTo(2)); + Assert.That(extensions[index].GenericTypes[0], Is.EqualTo(typeof(string))); + Assert.That(extensions[index].GenericTypes[1], Is.EqualTo(typeof(int))); + }); + } + + [Test] + public void FindImplementationOfGenericClass() + { + var extensions = TypeLocator.Instance + .FindImplementationsOf(typeof(Generic2Class)) + .ToList(); + + int index = extensions.FindIndex(i => i.Type == typeof(Generic2Class)); + Assert.That(index, Is.Not.EqualTo(-1)); + + Assert.Multiple(() => { + Assert.That(extensions[index].Name, Is.EqualTo(typeof(Generic2Class).FullName)); + Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(Generic2Class))); + }); + } + + [Test] + public void FindImplementationOfMultipleGenericInterfaces() + { + var extensions = TypeLocator.Instance + .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) + .Where(i => i.Type == typeof(GenericMultipleClass)) + .ToList(); + + Assert.That(extensions, Has.Count.EqualTo(2)); + + Assert.Multiple(() => { + Assert.That(extensions[0].Name, Is.EqualTo(typeof(GenericMultipleClass).FullName)); + Assert.That(extensions[1].Name, Is.EqualTo(typeof(GenericMultipleClass).FullName)); + + Assert.That(extensions[0].GenericTypes, Has.Count.EqualTo(2)); + Assert.That(extensions[1].GenericTypes, Has.Count.EqualTo(2)); + + int indexString2Int = extensions.FindIndex( + i => i.InterfaceImplemented == typeof(IGenericInterface)); + Assert.That(extensions[indexString2Int].GenericTypes[0], Is.EqualTo(typeof(string))); + Assert.That(extensions[indexString2Int].GenericTypes[1], Is.EqualTo(typeof(int))); + Assert.That( + extensions[indexString2Int].InterfaceImplemented, + Is.EqualTo(typeof(IGenericInterface))); + + int indexInt2String = indexString2Int == 0 ? 1 : 0; + Assert.That(extensions[indexInt2String].GenericTypes[0], Is.EqualTo(typeof(int))); + Assert.That(extensions[indexInt2String].GenericTypes[1], Is.EqualTo(typeof(string))); + Assert.That( + extensions[indexInt2String].InterfaceImplemented, + Is.EqualTo(typeof(IGenericInterface))); + }); + } + + [Test] + public void FindImplementationOfGenericIgnoresInterfaces() + { + var results = TypeLocator.Instance + .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(ISecondGenericInterface)), Is.Null); + } + + [Test] + public void FindImplementationOfGenericIgnoresAbstractClasses() + { + var results = TypeLocator.Instance + .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(AbstractGenericClass)), Is.Null); + } } From 9ea472a428b851268bda0c6fc557eb8a743bc68a Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Wed, 29 Nov 2023 11:19:07 +0100 Subject: [PATCH 04/12] :umbrella: Add tests for ConverterTypeInfo --- .../FileFormat/ConverterTypeInfoTests.cs | 196 ++++++------------ .../FileFormat/TestConvertersDefinition.cs | 35 ++-- 2 files changed, 82 insertions(+), 149 deletions(-) diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs index 4497367e..450b80f5 100644 --- a/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs @@ -19,215 +19,145 @@ // SOFTWARE. namespace Yarhl.UnitTests.Plugins.FileFormat; -using System; -using System.Diagnostics.CodeAnalysis; using NUnit.Framework; +using Yarhl.FileFormat; +using Yarhl.Plugins; using Yarhl.Plugins.FileFormat; [TestFixture] public class ConverterTypeInfoTests { - /* - [Test] - public void GetAndSetProperties() - { - var metadata = new ConverterTypeInfo { - Name = "test", - Type = typeof(int), - InternalSources = typeof(string), - InternalDestinations = typeof(DateTime), - }; - Assert.That(metadata.Name, Is.EqualTo("test")); - Assert.That(metadata.Type, Is.EqualTo(typeof(int))); - Assert.That(metadata.InternalSources, Is.EqualTo(typeof(string))); - Assert.That(metadata.InternalDestinations, Is.EqualTo(typeof(DateTime))); - } + private readonly ConverterTypeInfo converterInfo = new( + typeof(MyConverter).FullName!, + typeof(MyConverter), + typeof(IConverter), + new[] { typeof(MySourceFormat), typeof(MyDestFormat) }); [Test] - public void GetSourcesReturnOneElementArrayForSingleSource() + public void SourceAndDestinationInfoFromGenericTypes() { - var metadata = new ConverterTypeInfo { - InternalSources = typeof(int), - }; - Type[] sources = metadata.GetSources(); - Assert.That(sources.Length, Is.EqualTo(1)); - Assert.That(sources[0], Is.EqualTo(typeof(int))); + Assert.That(converterInfo.SourceType, Is.EqualTo(typeof(MySourceFormat))); + Assert.That(converterInfo.DestinationType, Is.EqualTo(typeof(MyDestFormat))); } [Test] - public void GetSourcesReturnTwoElementsArrayForListOfSources() + public void CreateFromGenericInfo() { - var metadata = new ConverterTypeInfo { - InternalSources = new Type[] { typeof(int), typeof(string) }, - }; - Type[] sources = metadata.GetSources(); - Assert.That(sources.Length, Is.EqualTo(2)); - Assert.That(sources[0], Is.EqualTo(typeof(int))); - Assert.That(sources[1], Is.EqualTo(typeof(string))); - } + var genericInfo = new GenericInterfaceImplementationInfo( + typeof(MyConverter).FullName!, + typeof(MyConverter), + typeof(IConverter), + new[] { typeof(MySourceFormat), typeof(MyDestFormat) }); - [Test] - public void GetSourcesReturnEmptyArrayForDefaultValue() - { - var metadata = new ConverterTypeInfo(); - Assert.That(metadata.GetSources(), Is.Empty); - } + var info = new ConverterTypeInfo(genericInfo); + Assert.Multiple(() => { + Assert.That(info.Name, Is.EqualTo(genericInfo.Name)); + Assert.That(info.Type, Is.EqualTo(genericInfo.Type)); + Assert.That(info.InterfaceImplemented, Is.EqualTo(genericInfo.InterfaceImplemented)); + Assert.That(info.GenericTypes, Is.EquivalentTo(genericInfo.GenericTypes)); - [Test] - public void GetDestinationsReturnOneElementArrayForSingleSource() - { - var metadata = new ConverterTypeInfo { - InternalDestinations = typeof(int), - }; - Type[] dests = metadata.GetDestinations(); - Assert.That(dests.Length, Is.EqualTo(1)); - Assert.That(dests[0], Is.EqualTo(typeof(int))); - } + Assert.That(info.SourceType, Is.EqualTo(typeof(MySourceFormat))); + Assert.That(info.DestinationType, Is.EqualTo(typeof(MyDestFormat))); + }); - [Test] - public void GetDestinationsReturnTwoElementsArrayForListOfSources() - { - var metadata = new ConverterTypeInfo { - InternalDestinations = new Type[] { typeof(int), typeof(string) }, - }; - Type[] dests = metadata.GetDestinations(); - Assert.That(dests.Length, Is.EqualTo(2)); - Assert.That(dests[0], Is.EqualTo(typeof(int))); - Assert.That(dests[1], Is.EqualTo(typeof(string))); + var copy = info with { }; + Assert.That(copy, Is.EqualTo(info)); } [Test] - public void GetDestinationsReturnEmptyArrayForDefaultValue() + public void CreateFromGenericInfoThrowsIfMoreThanTwoGenericTypes() { - var metadata = new ConverterTypeInfo(); - Assert.That(metadata.GetDestinations(), Is.Empty); + var generic3Info = new GenericInterfaceImplementationInfo( + typeof(MyConverter).FullName!, + typeof(MyConverter), + typeof(IConverter), + new[] { typeof(MySourceFormat), typeof(MyDestFormat), typeof(string) }); + + var generic1Info = new GenericInterfaceImplementationInfo( + typeof(MyConverter).FullName!, + typeof(MyConverter), + typeof(IConverter), + new[] { typeof(MySourceFormat) }); + + Assert.That(() => new ConverterTypeInfo(generic3Info), Throws.ArgumentException); + Assert.That(() => new ConverterTypeInfo(generic1Info), Throws.ArgumentException); } [Test] public void CanConvertSourceThrowsExceptionIfNullArgument() { - var metadata = new ConverterTypeInfo { - InternalSources = new Type[] { typeof(int), typeof(string) }, - }; Assert.That( - () => metadata.CanConvert(null), + () => converterInfo.CanConvert(null), Throws.ArgumentNullException); } [Test] public void CanConvertReturnsTrueForExactType() { - var metadata = new ConverterTypeInfo { - InternalSources = typeof(int), - }; - Assert.That(metadata.CanConvert(typeof(int)), Is.True); + Assert.That(converterInfo.CanConvert(typeof(MySourceFormat)), Is.True); } [Test] - public void CanConvertReturnsTrueForTypeInList() + public void CanConvertReturnsTrueForDerivedTypes() { - var metadata = new ConverterTypeInfo { - InternalSources = new[] { typeof(string), typeof(int) }, - }; - Assert.That(metadata.CanConvert(typeof(int)), Is.True); + Assert.That(converterInfo.CanConvert(typeof(DerivedSourceFormat)), Is.True); } [Test] - public void CanConvertReturnsTrueForDerivedTypes() + public void CanConvertReturnsFalseForBaseTypes() { - var metadata = new ConverterTypeInfo { - InternalSources = typeof(Base), - }; - Assert.That(metadata.CanConvert(typeof(Derived)), Is.True); + Assert.That(converterInfo.CanConvert(typeof(IFormat)), Is.False); } [Test] public void CanConvertReturnsFalseForDifferentTypes() { - var metadata = new ConverterTypeInfo { - InternalSources = new[] { typeof(string), typeof(int) }, - }; - Assert.That(metadata.CanConvert(typeof(DateTime)), Is.False); + Assert.That(converterInfo.CanConvert(typeof(MyDestFormat)), Is.False); } [Test] public void CanConvertReturnsForExactSourceAndDest() { - var metadata = new ConverterTypeInfo { - InternalSources = typeof(int), - InternalDestinations = typeof(string), - }; Assert.That( - metadata.CanConvert(typeof(int), typeof(string)), + converterInfo.CanConvert(typeof(MySourceFormat), typeof(MyDestFormat)), Is.True); Assert.That( - metadata.CanConvert(typeof(string), typeof(string)), - Is.False); - Assert.That( - metadata.CanConvert(typeof(int), typeof(int)), + converterInfo.CanConvert(typeof(MyDestFormat), typeof(MySourceFormat)), Is.False); } [Test] - public void CanConvertReturnsForSourceAndDestInSameOrderList() + public void CanConvertReturnsForSourceAndDestDerived() { - var metadata = new ConverterTypeInfo { - InternalSources = new[] { typeof(int), typeof(DateTime) }, - InternalDestinations = new[] { typeof(string), typeof(sbyte) }, - }; + // Source is a derived format Assert.That( - metadata.CanConvert(typeof(DateTime), typeof(sbyte)), + converterInfo.CanConvert(typeof(DerivedSourceFormat), typeof(MyDestFormat)), Is.True); - Assert.That( - metadata.CanConvert(typeof(DateTime), typeof(string)), - Is.False); - } - [Test] - public void CanConvertReturnsForSourceAndDestDerived() - { - var metadata = new ConverterTypeInfo { - InternalSources = new[] { typeof(Base) }, - InternalDestinations = new[] { typeof(Derived) }, - }; + // Destination is a base type Assert.That( - metadata.CanConvert(typeof(Derived), typeof(Base)), + converterInfo.CanConvert(typeof(MySourceFormat), typeof(IFormat)), Is.True); - metadata = new ConverterTypeInfo { - InternalSources = new[] { typeof(Derived) }, - InternalDestinations = new[] { typeof(Base) }, - }; + // Cannot convert base type Assert.That( - metadata.CanConvert(typeof(Base), typeof(Base)), + converterInfo.CanConvert(typeof(IFormat), typeof(MyDestFormat)), Is.False); + + // Cannot convert to derived type Assert.That( - metadata.CanConvert(typeof(Derived), typeof(Derived)), + converterInfo.CanConvert(typeof(MySourceFormat), typeof(DerivedDestFormat)), Is.False); } [Test] public void CanConvertSourceDestThrowsExceptionIfNullArgument() { - var metadata = new ConverterTypeInfo { - InternalSources = new Type[] { typeof(int), typeof(string) }, - InternalDestinations = new Type[] { typeof(int), typeof(string) }, - }; Assert.That( - () => metadata.CanConvert(null, typeof(int)), + () => converterInfo.CanConvert(null, typeof(int)), Throws.ArgumentNullException); Assert.That( - () => metadata.CanConvert(typeof(int), null), + () => converterInfo.CanConvert(typeof(int), null), Throws.ArgumentNullException); } - - class Base - { - } - - [SuppressMessage("Build", "CA1812", Justification = "Indirect instances")] - class Derived : Base - { - } - */ } diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs index 54fe7997..c37af95d 100644 --- a/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs @@ -5,36 +5,39 @@ #pragma warning disable SA1649 // File name match type name -public class PluginFormat : Yarhl.FileFormat.IFormat +public class MySourceFormat : IFormat { - public static int Value => 0; } -public class PluginConverter : Yarhl.FileFormat.IConverter +public class MyDestFormat : IFormat { - public int Convert(PluginFormat source) - { - return PluginFormat.Value; - } } -public class PluginConverterParametrized : Yarhl.FileFormat.IConverter +public class DerivedSourceFormat : MySourceFormat { - public PluginConverterParametrized(bool ignoreMe) - { - } +} + +public class DerivedDestFormat : IFormat +{ +} - public int Convert(PluginFormat source) +public class MyConverter : IConverter +{ + public MyDestFormat Convert(MySourceFormat source) { - return PluginFormat.Value; + return new MyDestFormat(); } } -public class BasicConverter : IConverter +public class MyConverterParametrized : IConverter { - public byte Convert(string source) + public MyConverterParametrized(bool ignoreMe) + { + } + + public MyDestFormat Convert(MySourceFormat source) { - return System.Convert.ToByte(source); + return new MyDestFormat(); } } From 1716416fc1fa1cbbf6cc8c5dace14ab2af129c97 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Wed, 29 Nov 2023 11:59:07 +0100 Subject: [PATCH 05/12] :umbrella: Implement tests for ConvertsLocator --- .../AssemblyLoadContextExtensionsTests.cs | 6 +- ...nvertersLocator.cs => ConverterLocator.cs} | 16 +- .../FileFormat/BaseGeneralTests.cs | 4 +- .../FileFormat/ConvertersLocatorTests.cs | 207 +++++++++++------- .../FileFormat/TestConvertersDefinition.cs | 57 ++--- 5 files changed, 164 insertions(+), 126 deletions(-) rename src/Yarhl.Plugins/FileFormat/{ConvertersLocator.cs => ConverterLocator.cs} (86%) diff --git a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs index 51b825b3..c78851ac 100644 --- a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs +++ b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs @@ -128,7 +128,7 @@ public void FindFormatFromPluginsDir() string pluginDir = Path.Combine(programDir, "Plugins"); TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); - var formats = ConvertersLocator.Instance.Formats; + var formats = ConverterLocator.Instance.Formats; Assert.That(formats, Is.Not.Empty); Assert.That( formats.Select(t => t.Name), @@ -142,11 +142,11 @@ public void FindConverterFromPluginsDir() string pluginDir = Path.Combine(programDir, "Plugins"); TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); - Type poType = ConvertersLocator.Instance.Formats + Type poType = ConverterLocator.Instance.Formats .Single(f => f.Name == "Yarhl.Media.Text.Po") .Type; - var converters = ConvertersLocator.Instance.Converters + var converters = ConverterLocator.Instance.Converters .Where(f => f.CanConvert(poType)); Assert.That(converters, Is.Not.Empty); Assert.That( diff --git a/src/Yarhl.Plugins/FileFormat/ConvertersLocator.cs b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs similarity index 86% rename from src/Yarhl.Plugins/FileFormat/ConvertersLocator.cs rename to src/Yarhl.Plugins/FileFormat/ConverterLocator.cs index 0de2d048..8cc4b421 100644 --- a/src/Yarhl.Plugins/FileFormat/ConvertersLocator.cs +++ b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs @@ -25,18 +25,18 @@ namespace Yarhl.Plugins.FileFormat; /// /// Locates converter types across assemblies and provide their information. /// -public sealed class ConvertersLocator +public sealed class ConverterLocator { private static readonly object LockObj = new(); - private static ConvertersLocator? singleInstance; + private static ConverterLocator? singleInstance; private readonly List formatsMetadata; private readonly List convertersMetadata; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - private ConvertersLocator() + private ConverterLocator() { formatsMetadata = new List(); Formats = formatsMetadata; @@ -51,11 +51,11 @@ private ConvertersLocator() /// Gets the plugin manager instance. /// /// It initializes the manager if needed. - public static ConvertersLocator Instance { + public static ConverterLocator Instance { get { if (singleInstance == null) { lock (LockObj) { - singleInstance ??= new ConvertersLocator(); + singleInstance ??= new ConverterLocator(); } } @@ -76,6 +76,10 @@ private ConvertersLocator() /// /// Scan the assemblies from the load context to look for formats and converters. /// + /// + /// This method is already called when the instance is created. Only needed + /// after loading additional assemblies. + /// public void ScanAssemblies() { formatsMetadata.Clear(); diff --git a/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs b/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs index b9acb8b8..2cc40a69 100644 --- a/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs +++ b/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs @@ -32,7 +32,7 @@ public abstract class BaseGeneralTests [Test] public void FormatIsFoundAndIsUnique() { - var formats = ConvertersLocator.Instance.Formats + var formats = ConverterLocator.Instance.Formats .Select(f => f.Type); Assert.That(formats, Does.Contain(typeof(T))); Assert.That(formats, Is.Unique); @@ -41,7 +41,7 @@ public void FormatIsFoundAndIsUnique() [Test] public void FormatNameMatchAndIsUnique() { - var names = ConvertersLocator.Instance.Formats + var names = ConverterLocator.Instance.Formats .Select(f => f.Name); Assert.That(names, Does.Contain(Name)); Assert.That(names, Is.Unique); diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs index 1ff08d0d..ce2fff0e 100644 --- a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs @@ -23,122 +23,173 @@ namespace Yarhl.UnitTests.Plugins.FileFormat; using System.Linq; using NUnit.Framework; using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; using Yarhl.Plugins; +using Yarhl.Plugins.FileFormat; [TestFixture] public class ConvertersLocatorTests { - /* [Test] - public void FormatMetadataContainsNameAndType() + public void InstanceIsSingleton() { - var format = TypeLocator.Instance.GetFormats() - .Single(p => p.Metadata.Type == typeof(StringFormat)); - Assert.That( - format.Metadata.Name, - Is.EqualTo(typeof(StringFormat).FullName)); + ConverterLocator instance1 = ConverterLocator.Instance; + ConverterLocator instance2 = ConverterLocator.Instance; + + Assert.That(instance1, Is.SameAs(instance2)); } [Test] - public void FormatsAreNotDuplicated() + public void InstanceIsInitialized() { - Assert.That( - TypeLocator.Instance.GetFormats().Select(f => f.Metadata.Type), - Is.Unique); + ConverterLocator instance = ConverterLocator.Instance; + + Assert.That(instance.Formats, Is.Not.Null); + Assert.That(instance.Converters, Is.Not.Null); } [Test] - public void GetFormatsReturnsKnownFormats() + public void LocateFormatsWithTypeInfo() { - Assert.That( - TypeLocator.Instance.GetFormats().Select(f => f.Metadata.Name), - Does.Contain(typeof(BinaryFormat).FullName)); + InterfaceImplementationInfo myFormat = ConverterLocator.Instance.Formats + .FirstOrDefault(i => i.Type == typeof(DerivedSourceFormat)); + + Assert.That(myFormat, Is.Not.Null); + Assert.That(myFormat.InterfaceImplemented, Is.EqualTo(typeof(IFormat))); + Assert.That(myFormat.Name, Is.EqualTo(typeof(DerivedSourceFormat).FullName)); + } + + [Test] + public void FormatsAreNotDuplicated() + { + InterfaceImplementationInfo[] formats = ConverterLocator.Instance.Formats + .Where(f => f.Type == typeof(MySourceFormat)) + .ToArray(); + + Assert.That(formats, Has.Length.EqualTo(1)); } [Test] - public void FindSingleInnerConverter() + public void LocateFormatsFindYarhlBaseFormats() { - IConverter converter = null; Assert.That( - () => converter = TypeLocator.Instance - .FindExtensions>() - .Single(), - Throws.Nothing); + ConverterLocator.Instance.Formats.Select(f => f.Type), + Does.Contain(typeof(BinaryFormat))); + Assert.That( - converter, - Is.InstanceOf()); - Assert.That(converter.Convert("4"), Is.EqualTo(4)); + ConverterLocator.Instance.Formats.Select(f => f.Type), + Does.Contain(typeof(NodeContainerFormat))); } [Test] - public void FindSingleOuterConverter() + public void LocateConvertersWithTypeInfo() { - IConverter converter = null; - Assert.That( - () => converter = TypeLocator.Instance - .FindExtensions>() - .Single(), - Throws.Nothing); - Assert.That(converter, Is.InstanceOf()); - Assert.That(converter.Convert("5"), Is.EqualTo(5)); + ConverterTypeInfo result = ConverterLocator.Instance.Converters + .FirstOrDefault(i => i.Type == typeof(MyConverter)); + + Assert.That(result, Is.Not.Null); + Assert.That(result.InterfaceImplemented, Is.EqualTo(typeof(IConverter))); + Assert.That(result.Name, Is.EqualTo(typeof(MyConverter).FullName)); + Assert.That(result.SourceType, Is.EqualTo(typeof(MySourceFormat))); + Assert.That(result.DestinationType, Is.EqualTo(typeof(MyDestFormat))); } [Test] - public void FindTwoConvertersInSameClass() + public void ConvertersAreNotDuplicated() { - var converter1 = TypeLocator.Instance - .FindExtensions>(); - Assert.IsInstanceOf(converter1.Single()); - - Assert.DoesNotThrow(() => - converter1.Single(t => - Array.Exists(t.GetType().GetInterfaces(), i => - i.IsGenericType && - i.GenericTypeArguments.Length == 2 && - i.GenericTypeArguments[0] == typeof(string) && - i.GenericTypeArguments[1] == typeof(int)))); - - var converter2 = TypeLocator.Instance - .FindExtensions>(); - Assert.IsInstanceOf(converter2.Single()); - - Assert.DoesNotThrow(() => - converter2.Single(t => - Array.Exists(t.GetType().GetInterfaces(), i => - i.IsGenericType && - i.GenericTypeArguments.Length == 2 && - i.GenericTypeArguments[0] == typeof(int) && - i.GenericTypeArguments[1] == typeof(string)))); + ConverterTypeInfo[] results = ConverterLocator.Instance.Converters + .Where(f => f.Type == typeof(MyConverter)) + .ToArray(); + + Assert.That(results, Has.Length.EqualTo(1)); } [Test] - public void FindDerivedConverter() + public void ScanAssembliesDoesNotDuplicateFindings() { - var converters = TypeLocator.Instance - .FindExtensions>(); - IConverter converter = null; - Assert.That( - () => converter = converters.Single(), - Throws.Nothing); - Assert.IsInstanceOf(converter); - Assert.IsInstanceOf(converter); - Assert.That(converter.Convert("3"), Is.EqualTo(3)); + ConverterLocator.Instance.ScanAssemblies(); + + FormatsAreNotDuplicated(); + ConvertersAreNotDuplicated(); + } + + [Test] + public void LocateConverterWithParameters() + { + ConverterTypeInfo[] results = ConverterLocator.Instance.Converters + .Where(f => f.Type == typeof(MyConverterParametrized)) + .ToArray(); + + Assert.That(results.Length, Is.EqualTo(1)); } [Test] - public void FindConvertsWithOtherInterfaces() + public void LocateSingleInnerConverter() { - IConverter converter = null; + ConverterTypeInfo converter = ConverterLocator.Instance.Converters + .FirstOrDefault(c => c.Type == typeof(SingleOuterConverter.SingleInnerConverter)); + + Assert.That(converter, Is.Not.Null); + } + + [Test] + public void LocateSingleOuterConverter() + { + ConverterTypeInfo converter = ConverterLocator.Instance.Converters + .FirstOrDefault(c => c.Type == typeof(SingleOuterConverter)); + + Assert.That(converter, Is.Not.Null); + } + + [Test] + public void LocateTwoConvertersInSameClass() + { + ConverterTypeInfo[] converters = ConverterLocator.Instance.Converters + .Where(c => c.Type == typeof(TwoConverters)) + .ToArray(); + + Assert.That(converters.Length, Is.EqualTo(2)); + Assert.That( + Array.Exists( + converters, + c => c.InterfaceImplemented == typeof(IConverter)), + Is.True); Assert.That( - () => converter = TypeLocator.Instance - .FindExtensions>() - .Single(), - Throws.Nothing); - Assert.That(converter, Is.InstanceOf()); - Assert.That(converter.Convert("3"), Is.EqualTo(3)); + Array.Exists( + converters, + c => c.SourceType == typeof(MySourceFormat) && c.DestinationType == typeof(MyDestFormat)), + Is.True); + + Assert.That( + Array.Exists( + converters, + c => c.InterfaceImplemented == typeof(IConverter)), + Is.True); Assert.That( - ((ConverterAndOtherInterface)converter).Dispose, - Throws.Nothing); + Array.Exists( + converters, + c => c.SourceType == typeof(MyDestFormat) && c.DestinationType == typeof(MySourceFormat)), + Is.True); + } + + [Test] + public void LocateDerivedConverter() + { + ConverterTypeInfo[] converters = ConverterLocator.Instance.Converters + .Where(c => c.Type == typeof(DerivedConverter)) + .ToArray(); + + Assert.That(converters.Length, Is.EqualTo(1)); + } + + [Test] + public void LocateConvertsWithOtherInterfaces() + { + ConverterTypeInfo[] converters = ConverterLocator.Instance.Converters + .Where(c => c.Type == typeof(ConverterAndOtherInterface)) + .ToArray(); + + Assert.That(converters.Length, Is.EqualTo(1)); } - */ } diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs index c37af95d..1effa185 100644 --- a/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs @@ -41,74 +41,57 @@ public MyDestFormat Convert(MySourceFormat source) } } -public class SingleOuterConverterExample : IConverter +public class SingleOuterConverter : IConverter { - public uint Convert(string source) + public MyDestFormat Convert(MySourceFormat source) { - return System.Convert.ToUInt32(source); + return new MyDestFormat(); } - public class SingleInnerConverterExample : IConverter + public class SingleInnerConverter : IConverter { - public ulong Convert(string source) + public MyDestFormat Convert(MySourceFormat source) { - return System.Convert.ToUInt64(source); + return new MyDestFormat(); } } } public sealed class ConverterAndOtherInterface : - IConverter, + IConverter, IDisposable { - public short Convert(string source) + public MyDestFormat Convert(MySourceFormat source) { - return System.Convert.ToInt16(source); + return new MyDestFormat(); } public void Dispose() { - // Test dispose - } -} - -public class TwoConvertersExample : - IConverter, IConverter -{ - public int Convert(string source) - { - return System.Convert.ToInt32(source); - } - - public string Convert(int source) - { - return source.ToString(); + GC.SuppressFinalize(this); } } -public class DuplicatedConverter1 : - IConverter +public class TwoConverters : + IConverter, + IConverter { - public sbyte Convert(string source) + public MyDestFormat Convert(MySourceFormat source) { - return System.Convert.ToSByte(source); + return new MyDestFormat(); } -} -public class DuplicatedConverter2 : - IConverter -{ - public sbyte Convert(string source) + public MySourceFormat Convert(MyDestFormat source) { - return System.Convert.ToSByte(source); + return new MySourceFormat(); } } -public abstract class BaseAbstractConverter : IConverter +public abstract class BaseAbstractConverter : IConverter { - public long Convert(string source) + public MyDestFormat Convert(MySourceFormat source) { - return System.Convert.ToInt64(source); + return new MyDestFormat(); } } From 97d6c77f37ecf20b5781213acd45a04d8b17fa04 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Wed, 29 Nov 2023 12:16:04 +0100 Subject: [PATCH 06/12] :umbrella: Fix tests running on linux --- .../AssemblyLoadContextExtensionsTests.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs index c78851ac..76703016 100644 --- a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs +++ b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs @@ -36,21 +36,19 @@ public class AssemblyLoadContextExtensionsTests [Test] public void TestPreconditionYarhlMediaIsInPluginsFolder() { - string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; - string pluginDir = Path.Combine(programDir, "Plugins"); + string pluginDir = GetPluginsDirectory(); Assert.IsTrue(Directory.Exists(pluginDir)); Assert.IsTrue(File.Exists(Path.Combine(pluginDir, "Yarhl.Media.Text.dll"))); Assert.IsTrue(File.Exists(Path.Combine(pluginDir, "MyBadPlugin.dll"))); - Assert.IsFalse(File.Exists(Path.Combine(programDir, "Yarhl.Media.Text.dll"))); + Assert.IsFalse(File.Exists(Path.Combine(GetProgramDirectory(), "Yarhl.Media.Text.dll"))); } [Test] public void LoadingYarhlMediaFromPath() { - string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; - string assemblyPath = Path.Combine(programDir, "Plugins", "Yarhl.Media.Text.dll"); + string assemblyPath = Path.Combine(GetPluginsDirectory(), "Yarhl.Media.Text.dll"); Assembly? loaded = TypeLocator.Instance.LoadContext.TryLoadFromAssemblyPath(assemblyPath); Assert.That(loaded, Is.Not.Null); @@ -64,8 +62,7 @@ public void LoadingYarhlMediaFromPath() [Test] public void LoadingInvalidAssemblyReturnsNull() { - string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; - string assemblyPath = Path.Combine(programDir, "Plugins", "MyBadPlugin.dll"); + string assemblyPath = Path.Combine(GetPluginsDirectory(), "MyBadPlugin.dll"); Assembly? loaded = TypeLocator.Instance.LoadContext.TryLoadFromAssemblyPath(assemblyPath); Assert.That(loaded, Is.Null); @@ -95,8 +92,7 @@ public void LoadingExecutingDirGetsYarhl() [Test] public void LoadingPluginsDirGetsYarhlMedia() { - string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; - string pluginDir = Path.Combine(programDir, "Plugins"); + string pluginDir = GetPluginsDirectory(); IEnumerable loaded = TypeLocator.Instance.LoadContext .TryLoadFromDirectory(pluginDir, false); @@ -110,7 +106,7 @@ public void LoadingPluginsDirGetsYarhlMedia() [Test] public void LoadingPluginsDirRecursiveGetsYarhlMedia() { - string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; + string programDir = GetProgramDirectory(); IEnumerable loaded = TypeLocator.Instance.LoadContext .TryLoadFromDirectory(programDir, true); @@ -124,8 +120,7 @@ public void LoadingPluginsDirRecursiveGetsYarhlMedia() [Test] public void FindFormatFromPluginsDir() { - string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; - string pluginDir = Path.Combine(programDir, "Plugins"); + string pluginDir = GetPluginsDirectory(); TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); var formats = ConverterLocator.Instance.Formats; @@ -138,8 +133,7 @@ public void FindFormatFromPluginsDir() [Test] public void FindConverterFromPluginsDir() { - string programDir = Path.GetDirectoryName(Environment.ProcessPath)!; - string pluginDir = Path.Combine(programDir, "Plugins"); + string pluginDir = GetPluginsDirectory(); TypeLocator.Instance.LoadContext.TryLoadFromDirectory(pluginDir, false); Type poType = ConverterLocator.Instance.Formats @@ -153,4 +147,10 @@ public void FindConverterFromPluginsDir() converters.Select(t => t.Name), Does.Contain("Yarhl.Media.Text.Po2Binary")); } + + private static string GetProgramDirectory() => + AppDomain.CurrentDomain.BaseDirectory; + + private static string GetPluginsDirectory() => + Path.Combine(GetProgramDirectory(), "Plugins"); } From 1880ec9559b04076d15d79086549bb4a1b7cc357 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Wed, 29 Nov 2023 13:58:45 +0100 Subject: [PATCH 07/12] =?UTF-8?q?=F0=9F=90=9B=20Load=20from=20domain=20Bas?= =?UTF-8?q?eDirectory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cover cases where the executable is run with dotnet host. --- .../AssemblyLoadContextExtensionsTests.cs | 4 ++-- src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs index 76703016..fcbcb1c4 100644 --- a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs +++ b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs @@ -71,7 +71,7 @@ public void LoadingInvalidAssemblyReturnsNull() [Test] public void LoadingIgnoreSystemLibraries() { - IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromExecutingDirectory(); + IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromBaseLoadDirectory(); Assert.That(loaded.Select(a => a.GetName().Name), Does.Not.Contain("testhost")); } @@ -80,7 +80,7 @@ public void LoadingIgnoreSystemLibraries() public void LoadingExecutingDirGetsYarhl() { // We cannot use ConverterLocator as it will load Yarhl as it uses some of its types. - IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromExecutingDirectory(); + IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromBaseLoadDirectory(); Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl")); diff --git a/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs b/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs index 70ee9b71..642cfaa7 100644 --- a/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs +++ b/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs @@ -88,7 +88,8 @@ public static IEnumerable TryLoadFromDirectory(this AssemblyLoadContex } /// - /// Try to load every .NET assembly in the directory of the current process. + /// Try to load every .NET assembly in the current domain base directory. + /// This is usually the process path or the entry assembly path. /// /// The load context to use to load. /// A collection of assemblies that could be loaded. @@ -97,9 +98,9 @@ public static IEnumerable TryLoadFromDirectory(this AssemblyLoadContex /// a security risk by running arbitrary code. /// If an assembly fails to load it will be silently skipped. /// - public static IEnumerable TryLoadFromExecutingDirectory(this AssemblyLoadContext loader) + public static IEnumerable TryLoadFromBaseLoadDirectory(this AssemblyLoadContext loader) { - string programDir = Path.GetDirectoryName(Environment.ProcessPath) ?? + string programDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) ?? throw new ArgumentException("Cannot determine process directory"); string[] libraryAssemblies = Directory.GetFiles(programDir, "*.dll"); From ee53d7f028465688586d67fd729e433db19c9419 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Wed, 29 Nov 2023 18:46:40 +0100 Subject: [PATCH 08/12] =?UTF-8?q?=F0=9F=93=9AUpdate=20index=20and=20plugin?= =?UTF-8?q?s=20overview=20Also=20new=20precondition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/articles/{Changelog.md => changelog.md} | 0 docs/articles/core/formats/converters.md | 6 ++++ docs/articles/core/toc.yml | 6 +++- docs/articles/plugins/load-assembly.md | 4 +++ docs/articles/plugins/locate-types.md | 3 ++ docs/articles/plugins/overview.md | 35 ++++++++++++++++++- docs/index.md | 5 ++- src/Yarhl.Plugins/TypeLocator.cs | 7 ++++ .../Plugins/TypeLocatorTests.cs | 15 ++++++++ 10 files changed, 79 insertions(+), 3 deletions(-) rename docs/articles/{Changelog.md => changelog.md} (100%) create mode 100644 docs/articles/plugins/load-assembly.md create mode 100644 docs/articles/plugins/locate-types.md diff --git a/README.md b/README.md index 310e7372..936e0bb2 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ featured binary IO and plugin support to support common formats. It's built in - Table text replacements - **Common encodings**: euc-jp, token-escaped encoding - **API for simple encoding implementations** +- 🔌**Plugin** API to load and find types in .NET assemblies. ## Get started diff --git a/docs/articles/Changelog.md b/docs/articles/changelog.md similarity index 100% rename from docs/articles/Changelog.md rename to docs/articles/changelog.md diff --git a/docs/articles/core/formats/converters.md b/docs/articles/core/formats/converters.md index 15269146..d4b32b66 100644 --- a/docs/articles/core/formats/converters.md +++ b/docs/articles/core/formats/converters.md @@ -7,6 +7,12 @@ method [`TDst Convert(TSrc)`](). This method creates a new object in the target type _converting_ the data from the input. +```mermaid +flowchart LR + po(Po) --> converter["Binary2Po.Convert()\nIConverter#60;Po, BinaryFormat#62;"] + converter --> binary(Binary) +``` + For instance the converter [`Po2Binary`](xref:Yarhl.Media.Text.Po2Binary) implements `IConverter`. It allows to convert a [`Po`](xref:Yarhl.Media.Text.Po) model format into a diff --git a/docs/articles/core/toc.yml b/docs/articles/core/toc.yml index d9910d28..ad8932d2 100644 --- a/docs/articles/core/toc.yml +++ b/docs/articles/core/toc.yml @@ -45,5 +45,9 @@ href: ./binary/custom-streams.md - name: 🔌 Plugins -- name: 🚧 Overview +- name: Overview href: ../plugins/overview.md +- name: Loading assemblies + href: ../plugins/load-assembly.md +- name: Locating converter types + href: ../plugins/locate-types.md diff --git a/docs/articles/plugins/load-assembly.md b/docs/articles/plugins/load-assembly.md new file mode 100644 index 00000000..49a22c49 --- /dev/null +++ b/docs/articles/plugins/load-assembly.md @@ -0,0 +1,4 @@ +# Loading .NET assemblies + +The .NET libraries for _reflection_ provide already APIs to load additional .NET +assemblies. diff --git a/docs/articles/plugins/locate-types.md b/docs/articles/plugins/locate-types.md new file mode 100644 index 00000000..313d3724 --- /dev/null +++ b/docs/articles/plugins/locate-types.md @@ -0,0 +1,3 @@ +# Locate types + +TODO diff --git a/docs/articles/plugins/overview.md b/docs/articles/plugins/overview.md index 6a1cfb1c..52239fb9 100644 --- a/docs/articles/plugins/overview.md +++ b/docs/articles/plugins/overview.md @@ -1,3 +1,36 @@ # Plugins overview -TODO +`Yarhl.Plugins` provides a set of APIs that helps to load .NET assemblies and +find types. + +Its main goal is to find [converter](../core/formats/converters.md) and +[format](../core/formats/formats.md) types in external .NET assemblies. Generic +applications, like [SceneGate](https://github.com/SceneGate/SceneGate), that +have no knowledge in the converters to use, could use the APIs to find and +propose them to the user. + +The _plugins_ are regular .NET libraries or executable that contains +implementations of _converters_ and _formats_. They don't need to implement any +additional interface or fullfil other requirements. + +> [!WARNING] +> Loading a .NET assembly will load also its dependencies. You may run into +> dependency issues if they use different versions of a base library such as +> Yarhl or Newtonsoft.Json. + +The main APIs are: + +- `AssemblyLoadContextExtensions`: extension methods for `AssemblyLoadContext` + to load .NET assemblies from disk. +- `TypeLocator`: find types that implement a specific interface. +- `ConverterLocator`: find _converter_ and _format_ types. + +```mermaid +flowchart TB + Application ---> |Load external .NET assemblies| AssemblyLoadContext + Application --> |Find converters| ConverterLocator + ConverterLocator --> |Find implementations\nof IConverter<,>| TypeLocator + TypeLocator --> |Iterate through types in\nloaded assemblies| AssemblyLoadContext +``` + +You can get more information in their subpage. diff --git a/docs/index.md b/docs/index.md index 8d57aadb..3775a682 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,7 @@ formats** It empowers you with... serialization. - 📃 ... **standard formats** implementation like **PO** for translations. - 📂 ... virtual **file system** to unpack and pack containers efficiently. +- 🔌... **plugin** API to find formats and converters in .NET assemblies. ## Usage @@ -25,7 +26,9 @@ libraries only support .NET LTS versions: **.NET 6.0** and **.NET 8.0**. - `Yarhl.Media.Text`: translation formats and converters (Po), table replacer. - `Yarhl.Media.Text.Encoding`: _euc-jp_ and token-escaped encodings. - [![Yarhl.Plugins](https://img.shields.io/nuget/v/Yarhl.Plugins?label=Yarhl.Plugins&logo=nuget)](https://www.nuget.org/packages/Yarhl.Plugins) - - `Yarhl.Plugins`: discover formats and converters from .NET assemblies. + - `Yarhl.Plugins`: load nearby .NET assemblies and find type implementations. + - `Yarhl.Plugins.FileFormat`: find formats and converters from loaded + assemblies. > [!NOTE] > _Are you planning to try a preview version?_ diff --git a/src/Yarhl.Plugins/TypeLocator.cs b/src/Yarhl.Plugins/TypeLocator.cs index 7cc1519b..154add02 100644 --- a/src/Yarhl.Plugins/TypeLocator.cs +++ b/src/Yarhl.Plugins/TypeLocator.cs @@ -94,6 +94,13 @@ public IEnumerable FindImplementationsOf(Type baseT ArgumentNullException.ThrowIfNull(baseType); ArgumentNullException.ThrowIfNull(assembly); + if (baseType.IsGenericTypeDefinition) { + throw new ArgumentException( + "Generic type definition doesn't work on this method. " + + $"Use {nameof(FindImplementationsOfGeneric)} instead.", + nameof(baseType)); + } + return assembly.ExportedTypes .Where(baseType.IsAssignableFrom) .Where(t => t.IsClass && !t.IsAbstract) diff --git a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs index 91a49738..80db0363 100644 --- a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs +++ b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs @@ -114,6 +114,21 @@ public void FindImplementationWithNullTypeThrowsException() Throws.ArgumentNullException); } + [Test] + public void FindImplementationThrowsWithGenericTypeDefinitions() + { + Assert.That( + () => TypeLocator.Instance + .FindImplementationsOf(typeof(IGenericInterface<,>)) + .ToArray(), + Throws.ArgumentException); + Assert.That( + () => TypeLocator.Instance + .FindImplementationsOf(typeof(IGenericInterface<,>), typeof(IGenericInterface<,>).Assembly) + .ToArray(), + Throws.ArgumentException); + } + [Test] public void FindImplementationIgnoresAbstractClasses() { From d2f49c579613cb729b910f70ab0f4fcada3a8d05 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Thu, 30 Nov 2023 12:11:01 +0100 Subject: [PATCH 09/12] =?UTF-8?q?=E2=9C=A8=20Support=20custom=20LoadContex?= =?UTF-8?q?t=20for=20assembly=20scanning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AssemblyLoadContextExtensionsTests.cs | 30 +++++----- .../FileFormat/ConverterLocator.cs | 26 ++++++--- src/Yarhl.Plugins/TypeLocator.cs | 13 ++++- .../FileFormat/BaseGeneralTests.cs | 4 +- .../FileFormat/ConvertersLocatorTests.cs | 53 ++++++++++++------ .../Plugins/TypeLocatorTests.cs | 55 ++++++++++++------- 6 files changed, 118 insertions(+), 63 deletions(-) diff --git a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs index fcbcb1c4..fd1541e8 100644 --- a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs +++ b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs @@ -49,13 +49,13 @@ 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")); } @@ -63,7 +63,7 @@ public void LoadingYarhlMediaFromPath() 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); } @@ -71,7 +71,7 @@ public void LoadingInvalidAssemblyReturnsNull() [Test] public void LoadingIgnoreSystemLibraries() { - IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromBaseLoadDirectory(); + IEnumerable loaded = TypeLocator.Default.LoadContext.TryLoadFromBaseLoadDirectory(); Assert.That(loaded.Select(a => a.GetName().Name), Does.Not.Contain("testhost")); } @@ -80,12 +80,12 @@ public void LoadingIgnoreSystemLibraries() public void LoadingExecutingDirGetsYarhl() { // We cannot use ConverterLocator as it will load Yarhl as it uses some of its types. - IEnumerable loaded = TypeLocator.Instance.LoadContext.TryLoadFromBaseLoadDirectory(); + IEnumerable 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")); } @@ -93,13 +93,13 @@ public void LoadingExecutingDirGetsYarhl() public void LoadingPluginsDirGetsYarhlMedia() { string pluginDir = GetPluginsDirectory(); - IEnumerable loaded = TypeLocator.Instance.LoadContext + IEnumerable 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")); } @@ -107,13 +107,13 @@ public void LoadingPluginsDirGetsYarhlMedia() public void LoadingPluginsDirRecursiveGetsYarhlMedia() { string programDir = GetProgramDirectory(); - IEnumerable loaded = TypeLocator.Instance.LoadContext + IEnumerable 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")); } @@ -121,9 +121,9 @@ public void LoadingPluginsDirRecursiveGetsYarhlMedia() 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), @@ -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( diff --git a/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs index 8cc4b421..410d1656 100644 --- a/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs +++ b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs @@ -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 formatsMetadata; private readonly List convertersMetadata; /// /// Initializes a new instance of the class. /// - private ConverterLocator() + /// The type locator to use internally. + public ConverterLocator(TypeLocator locator) { + this.locator = locator; + formatsMetadata = new List(); Formats = formatsMetadata; @@ -48,10 +52,18 @@ private ConverterLocator() } /// - /// Gets the plugin manager instance. + /// Initializes a new instance of the class. + /// + private ConverterLocator() + : this(TypeLocator.Default) + { + } + + /// + /// Gets the singleton instance using the default TypeLocator. /// /// It initializes the manager if needed. - public static ConverterLocator Instance { + public static ConverterLocator Default { get { if (singleInstance == null) { lock (LockObj) { @@ -83,13 +95,13 @@ private ConverterLocator() 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))); } } diff --git a/src/Yarhl.Plugins/TypeLocator.cs b/src/Yarhl.Plugins/TypeLocator.cs index 154add02..1891663c 100644 --- a/src/Yarhl.Plugins/TypeLocator.cs +++ b/src/Yarhl.Plugins/TypeLocator.cs @@ -33,6 +33,15 @@ public sealed class TypeLocator private static readonly object LockObj = new(); private static TypeLocator? singleInstance; + /// + /// Initializes a new instance of the class. + /// + /// The load context to search assemblies. + public TypeLocator(AssemblyLoadContext loadContext) + { + LoadContext = loadContext; + } + /// /// Initializes a new instance of the class. /// @@ -42,12 +51,12 @@ private TypeLocator() } /// - /// Gets the singleton instance. + /// Gets a singleton instance that use the default AssemblyLoadContext. /// /// /// It initializes the type if needed on the first call. /// - public static TypeLocator Instance { + public static TypeLocator Default { get { if (singleInstance == null) { lock (LockObj) { diff --git a/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs b/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs index 2cc40a69..4b3b3940 100644 --- a/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs +++ b/src/Yarhl.UnitTests/FileFormat/BaseGeneralTests.cs @@ -32,7 +32,7 @@ public abstract class BaseGeneralTests [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); @@ -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); diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs index ce2fff0e..bbf23cf5 100644 --- a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs @@ -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; @@ -34,8 +35,8 @@ 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)); } @@ -43,16 +44,36 @@ public void InstanceIsSingleton() [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); @@ -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(); @@ -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); @@ -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(); @@ -108,7 +129,7 @@ public void ConvertersAreNotDuplicated() [Test] public void ScanAssembliesDoesNotDuplicateFindings() { - ConverterLocator.Instance.ScanAssemblies(); + ConverterLocator.Default.ScanAssemblies(); FormatsAreNotDuplicated(); ConvertersAreNotDuplicated(); @@ -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(); @@ -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); @@ -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); @@ -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(); @@ -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(); @@ -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(); diff --git a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs index 80db0363..2cf3b86e 100644 --- a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs +++ b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs @@ -20,7 +20,9 @@ namespace Yarhl.UnitTests.Plugins; using System.Linq; +using System.Runtime.Loader; using NUnit.Framework; +using Yarhl.FileFormat; using Yarhl.IO; using Yarhl.Plugins; @@ -30,7 +32,7 @@ public class TypeLocatorTests [Test] public void InstanceInitializePluginManager() { - TypeLocator instance = TypeLocator.Instance; + TypeLocator instance = TypeLocator.Default; Assert.That(instance, Is.Not.Null); Assert.That(instance.LoadContext, Is.Not.Null); } @@ -38,15 +40,26 @@ public void InstanceInitializePluginManager() [Test] public void InstanceIsCreatedOnce() { - TypeLocator instance1 = TypeLocator.Instance; - TypeLocator instance2 = TypeLocator.Instance; + TypeLocator instance1 = TypeLocator.Default; + TypeLocator instance2 = TypeLocator.Default; Assert.That(instance1, Is.SameAs(instance2)); } + [Test] + public void InitializeWithCustomLoadContextProvidesIsolation() + { + var loadContext = new AssemblyLoadContext(nameof(InitializeWithCustomLoadContextProvidesIsolation)); + TypeLocator isolatedLocator = new TypeLocator(loadContext); + + Assert.That( + isolatedLocator.FindImplementationsOf(typeof(IFormat)).ToArray(), + Is.Empty); + } + [Test] public void FindImplementationOfInterface() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOf(typeof(IExistsInterface)) .ToList(); @@ -62,7 +75,7 @@ public void FindImplementationOfInterface() [Test] public void FindImplementationOfInterfaceWithAssembly() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOf(typeof(IExistsInterface), typeof(IExistsInterface).Assembly) .ToList(); @@ -78,7 +91,7 @@ public void FindImplementationOfInterfaceWithAssembly() [Test] public void FindImplementationOfClass() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOf(typeof(ExistsClass)) .ToList(); @@ -93,7 +106,7 @@ public void FindImplementationOfClass() [Test] public void FindImplementationOfInterfaceDifferentAssemblyReturnsEmpty() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOf(typeof(IExistsInterface), typeof(IBinary).Assembly) .ToList(); @@ -104,13 +117,13 @@ public void FindImplementationOfInterfaceDifferentAssemblyReturnsEmpty() public void FindImplementationWithNullTypeThrowsException() { Assert.That( - () => TypeLocator.Instance.FindImplementationsOf(null), + () => TypeLocator.Default.FindImplementationsOf(null), Throws.ArgumentNullException); Assert.That( - () => TypeLocator.Instance.FindImplementationsOf(null, typeof(IExistsInterface).Assembly), + () => TypeLocator.Default.FindImplementationsOf(null, typeof(IExistsInterface).Assembly), Throws.ArgumentNullException); Assert.That( - () => TypeLocator.Instance.FindImplementationsOf(typeof(IExistsInterface), null), + () => TypeLocator.Default.FindImplementationsOf(typeof(IExistsInterface), null), Throws.ArgumentNullException); } @@ -118,12 +131,12 @@ public void FindImplementationWithNullTypeThrowsException() public void FindImplementationThrowsWithGenericTypeDefinitions() { Assert.That( - () => TypeLocator.Instance + () => TypeLocator.Default .FindImplementationsOf(typeof(IGenericInterface<,>)) .ToArray(), Throws.ArgumentException); Assert.That( - () => TypeLocator.Instance + () => TypeLocator.Default .FindImplementationsOf(typeof(IGenericInterface<,>), typeof(IGenericInterface<,>).Assembly) .ToArray(), Throws.ArgumentException); @@ -132,7 +145,7 @@ public void FindImplementationThrowsWithGenericTypeDefinitions() [Test] public void FindImplementationIgnoresAbstractClasses() { - var results = TypeLocator.Instance + var results = TypeLocator.Default .FindImplementationsOf(typeof(IExistsInterface)) .ToList(); @@ -142,7 +155,7 @@ public void FindImplementationIgnoresAbstractClasses() [Test] public void FindImplementationIgnoresInterfaces() { - var results = TypeLocator.Instance + var results = TypeLocator.Default .FindImplementationsOf(typeof(IExistsInterface)) .ToList(); @@ -152,7 +165,7 @@ public void FindImplementationIgnoresInterfaces() [Test] public void FindImplementationCanFindConstructorWithException() { - var results = TypeLocator.Instance + var results = TypeLocator.Default .FindImplementationsOf(typeof(ConstructorWithException)) .ToList(); @@ -167,7 +180,7 @@ public void FindImplementationCanFindConstructorWithException() [Test] public void FindImplementationOfGenericInterface1() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOfGeneric(typeof(IGenericInterface<>)) .ToList(); @@ -189,7 +202,7 @@ public void FindImplementationOfGenericInterface1() [Test] public void FindImplementationOfGenericInterface2() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) .ToList(); @@ -208,7 +221,7 @@ public void FindImplementationOfGenericInterface2() [Test] public void FindImplementationOfGenericClass() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOf(typeof(Generic2Class)) .ToList(); @@ -224,7 +237,7 @@ public void FindImplementationOfGenericClass() [Test] public void FindImplementationOfMultipleGenericInterfaces() { - var extensions = TypeLocator.Instance + var extensions = TypeLocator.Default .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) .Where(i => i.Type == typeof(GenericMultipleClass)) .ToList(); @@ -258,7 +271,7 @@ public void FindImplementationOfMultipleGenericInterfaces() [Test] public void FindImplementationOfGenericIgnoresInterfaces() { - var results = TypeLocator.Instance + var results = TypeLocator.Default .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) .ToList(); @@ -268,7 +281,7 @@ public void FindImplementationOfGenericIgnoresInterfaces() [Test] public void FindImplementationOfGenericIgnoresAbstractClasses() { - var results = TypeLocator.Instance + var results = TypeLocator.Default .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) .ToList(); From 205839759c847dab1e36b21dd1f062b1472b474d Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Thu, 30 Nov 2023 12:40:33 +0100 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=93=96=20Document=20loading=20.NET?= =?UTF-8?q?=20assemblies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/articles/core/toc.yml | 2 +- docs/articles/plugins/load-assembly.md | 89 +++++++++++++++++++++++++- docs/articles/plugins/locate-types.md | 6 ++ docs/articles/plugins/overview.md | 15 ++--- docs/docfx.json | 3 +- 5 files changed, 102 insertions(+), 13 deletions(-) diff --git a/docs/articles/core/toc.yml b/docs/articles/core/toc.yml index ad8932d2..7eedc259 100644 --- a/docs/articles/core/toc.yml +++ b/docs/articles/core/toc.yml @@ -49,5 +49,5 @@ href: ../plugins/overview.md - name: Loading assemblies href: ../plugins/load-assembly.md -- name: Locating converter types +- name: Find converters href: ../plugins/locate-types.md diff --git a/docs/articles/plugins/load-assembly.md b/docs/articles/plugins/load-assembly.md index 49a22c49..637b84b6 100644 --- a/docs/articles/plugins/load-assembly.md +++ b/docs/articles/plugins/load-assembly.md @@ -1,4 +1,89 @@ # Loading .NET assemblies -The .NET libraries for _reflection_ provide already APIs to load additional .NET -assemblies. +.NET provide already APIs to load additional assemblies via +[`AssemblyLoadContext`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext). +Yarhl provides extensions methods for `AssemblyLoadContext` to facilitate +loading files from disk. + +You can use the main `AssemblyLoadContext` from `AssemblyLoadContext.Default` to +load them. For advanced use cases, it's possible to create a new +`AssemblyLoadContext` that would provide isolation. + +> [!TIP] +> If you plan to use [`ConverterLocator`](./locate-types.md#converterlocator), +> remember to call `ScanAssemblies` after loading new assemblies. + + + +> [!WARNING] +> Loading a .NET assembly may load also its required dependencies. You may run +> into dependency issues if they use different versions of a base library such +> as Yarhl or Newtonsoft.Json. + + + +> [!IMPORTANT] +> There may a security risk by loading **untrusted** assemblies from a file or a +> directory. .NET does provide any security feature to validate it's not +> malicious code. + +## Load from file paths + +The method +[`TryLoadFromAssemblyPath`]() +will try to load the .NET assembly in the given path. If this assembly fails to +load (e.g. it's not a .NET binary) it will return `null`. + +Similar, the method +[`TryLoadFromAssembliesPath`]() +will try to load every assembly in the list of paths given. If any of them fails +to load, no exception will be raised and it would be skipped. + +Additionally, this API will skip any file where its name starts with any of the +following prefixes. The goal is to prevent loading unwanted dependencies. If you +want to force loading them, use `TryLoadFromAssemblyPath`. + +- `System.` +- `Microsoft.` +- `netstandard` +- `nuget` +- `nunit` +- `testhost` + +## Load from a directory + +The method +[`TryLoadFromDirectory`]() +will try to load every file in the given directory with an extension `.dll` or +`.exe`. If any of them fails, no error will be reported and it would be skipped. + +Via an argument it's possible to configure if it should load files from the +given directory or from its subdirectories recursively as well. + +## Load from executing directory + +A common use case it's to load every assembly from the executable directory. +Because .NET will load an assembly lazily, only when type actually need it, upon +startup not every assembly from the executable directory could be loaded. + +The method +[`TryLoadFromBaseLoadDirectory`]() +addresses this use case by loading every `.dll` and `.exe` from the current +`AppDomain.CurrentDomain.BaseDirectory`. + +> [!TIP] +> To use _plugins_ in a _controlled way_, the application may add a set of +> `PackageReference`s. After running `dotnet publish` these dependencies will be +> copied to the output directory. At startup call +> `AssemblyLoadContext.Default.TryLoadFromBaseLoadDirectory` to load all of +> them. Otherwise, unless the application also references their types, the +> assemblies will not be loaded. + + + +> [!NOTE] +> It does not use `Environment.ProcessPath` because sometimes the application +> (or tests) may run by passing the main library file to the `dotnet` host +> application (e.g. `dotnet MyApp.dll`). In that case it would scan the +> installation path of the .NET SDK instead of the application installation +> directory. diff --git a/docs/articles/plugins/locate-types.md b/docs/articles/plugins/locate-types.md index 313d3724..99fd78a6 100644 --- a/docs/articles/plugins/locate-types.md +++ b/docs/articles/plugins/locate-types.md @@ -1,3 +1,9 @@ # Locate types +## TypeLocator + +The `TypeLocator` is a _singleton_ class that p + +## ConverterLocator + TODO diff --git a/docs/articles/plugins/overview.md b/docs/articles/plugins/overview.md index 52239fb9..63893c04 100644 --- a/docs/articles/plugins/overview.md +++ b/docs/articles/plugins/overview.md @@ -13,17 +13,14 @@ The _plugins_ are regular .NET libraries or executable that contains implementations of _converters_ and _formats_. They don't need to implement any additional interface or fullfil other requirements. -> [!WARNING] -> Loading a .NET assembly will load also its dependencies. You may run into -> dependency issues if they use different versions of a base library such as -> Yarhl or Newtonsoft.Json. - The main APIs are: -- `AssemblyLoadContextExtensions`: extension methods for `AssemblyLoadContext` - to load .NET assemblies from disk. -- `TypeLocator`: find types that implement a specific interface. -- `ConverterLocator`: find _converter_ and _format_ types. +- [`AssemblyLoadContextExtensions`](./load-assembly.md): extension methods for + `AssemblyLoadContext` to load .NET assemblies from disk. +- [`TypeLocator`](./locate-types.md#typelocator): find types that implement a + specific interface. +- [`ConverterLocator`](./locate-types.md#converterlocator): find _converter_ and + _format_ types. ```mermaid flowchart TB diff --git a/docs/docfx.json b/docs/docfx.json index 2776af6d..68fee65c 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -5,7 +5,8 @@ { "files": [ "Yarhl/*.csproj", - "Yarhl.Media.Text/*.csproj" + "Yarhl.Media.Text/*.csproj", + "Yarhl.Plugins/*.csproj", ], "src": "../src" } From e7edbcc147528f44ba777b241e4e6310a29e0f2a Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Thu, 30 Nov 2023 13:30:44 +0100 Subject: [PATCH 11/12] :shirt: Improve naming of types --- .../FileFormat/ConverterLocator.cs | 6 +-- .../FileFormat/ConverterTypeInfo.cs | 8 ++-- ...fo.cs => GenericTypeImplementationInfo.cs} | 18 +++++---- ...ationInfo.cs => TypeImplementationInfo.cs} | 7 ++-- src/Yarhl.Plugins/TypeLocator.cs | 12 +++--- .../FileFormat/ConverterTypeInfoTests.cs | 10 ++--- .../FileFormat/ConvertersLocatorTests.cs | 7 ++-- .../Plugins/TypeLocatorTests.cs | 39 ++++++++----------- 8 files changed, 51 insertions(+), 56 deletions(-) rename src/Yarhl.Plugins/{GenericInterfaceImplementationInfo.cs => GenericTypeImplementationInfo.cs} (74%) rename src/Yarhl.Plugins/{InterfaceImplementationInfo.cs => TypeImplementationInfo.cs} (80%) diff --git a/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs index 410d1656..1225f67b 100644 --- a/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs +++ b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs @@ -31,7 +31,7 @@ public sealed class ConverterLocator private static ConverterLocator? singleInstance; private readonly TypeLocator locator; - private readonly List formatsMetadata; + private readonly List formatsMetadata; private readonly List convertersMetadata; /// @@ -42,7 +42,7 @@ public ConverterLocator(TypeLocator locator) { this.locator = locator; - formatsMetadata = new List(); + formatsMetadata = new List(); Formats = formatsMetadata; convertersMetadata = new List(); @@ -78,7 +78,7 @@ private ConverterLocator() /// /// Gets the list of Yarhl formats information from loaded assemblies. /// - public IReadOnlyList Formats { get; } + public IReadOnlyList Formats { get; } /// /// Gets the list of Yarhl converters information from loaded assemblies. diff --git a/src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs b/src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs index 3a983eab..f5deb96f 100644 --- a/src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs +++ b/src/Yarhl.Plugins/FileFormat/ConverterTypeInfo.cs @@ -29,16 +29,16 @@ public record ConverterTypeInfo( Type Type, Type InterfaceImplemented, IReadOnlyList GenericTypes) - : GenericInterfaceImplementationInfo(Name, Type, InterfaceImplemented, GenericTypes) + : GenericTypeImplementationInfo(Name, Type, InterfaceImplemented, GenericTypes) { /// /// Initializes a new instance of the class. /// /// The generic implementor information. - public ConverterTypeInfo(GenericInterfaceImplementationInfo info) - : this(info.Name, info.Type, info.InterfaceImplemented, info.GenericTypes) + public ConverterTypeInfo(GenericTypeImplementationInfo info) + : this(info.Name, info.Type, info.GenericBaseType, info.GenericTypeParameters) { - if (info.GenericTypes.Count != 2) { + if (info.GenericTypeParameters.Count != 2) { throw new ArgumentException("Invalid number of generics. Expected 2."); } } diff --git a/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs b/src/Yarhl.Plugins/GenericTypeImplementationInfo.cs similarity index 74% rename from src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs rename to src/Yarhl.Plugins/GenericTypeImplementationInfo.cs index 90d3fef5..ef286b77 100644 --- a/src/Yarhl.Plugins/GenericInterfaceImplementationInfo.cs +++ b/src/Yarhl.Plugins/GenericTypeImplementationInfo.cs @@ -20,15 +20,17 @@ namespace Yarhl.Plugins; /// -/// Provides information about a type that implements a generic interface. +/// Provides information about a type that implements a generic base type. /// /// The name of the implementation type. Shortcut for Type.FullName. -/// The type that implements the interface. -/// The actual generic interface with type arguments implemented. -/// The list of types specified in the generic. -public record GenericInterfaceImplementationInfo( +/// The implementation type. +/// The actual generic base type with type parameters implemented. +/// +/// The collection of the type parameters in the generic base type implemented. +/// +public record GenericTypeImplementationInfo( string Name, Type Type, - Type InterfaceImplemented, - IReadOnlyList GenericTypes) - : InterfaceImplementationInfo(Name, Type, InterfaceImplemented); + Type GenericBaseType, + IReadOnlyList GenericTypeParameters) + : TypeImplementationInfo(Name, Type); diff --git a/src/Yarhl.Plugins/InterfaceImplementationInfo.cs b/src/Yarhl.Plugins/TypeImplementationInfo.cs similarity index 80% rename from src/Yarhl.Plugins/InterfaceImplementationInfo.cs rename to src/Yarhl.Plugins/TypeImplementationInfo.cs index 5284bd7e..164960b1 100644 --- a/src/Yarhl.Plugins/InterfaceImplementationInfo.cs +++ b/src/Yarhl.Plugins/TypeImplementationInfo.cs @@ -20,9 +20,8 @@ namespace Yarhl.Plugins; /// -/// Provides information about a type that implements an interface. +/// Provides information about a type that implements a base type. /// /// The name of the implementation type. Shortcut for Type.FullName. -/// The type that implements the interface. -/// Interface implemented. -public record InterfaceImplementationInfo(string Name, Type Type, Type InterfaceImplemented); +/// The implementation type. +public record TypeImplementationInfo(string Name, Type Type); diff --git a/src/Yarhl.Plugins/TypeLocator.cs b/src/Yarhl.Plugins/TypeLocator.cs index 1891663c..51b10973 100644 --- a/src/Yarhl.Plugins/TypeLocator.cs +++ b/src/Yarhl.Plugins/TypeLocator.cs @@ -82,7 +82,7 @@ private TypeLocator() /// /// The base type to find implementors. /// A collection of types implementing the base type. - public IEnumerable FindImplementationsOf(Type baseType) + public IEnumerable FindImplementationsOf(Type baseType) { ArgumentNullException.ThrowIfNull(baseType); @@ -98,7 +98,7 @@ public IEnumerable FindImplementationsOf(Type baseT /// The base type to find implementors. /// The assembly to scan. /// A collection of types implementing the base type. - public IEnumerable FindImplementationsOf(Type baseType, Assembly assembly) + public IEnumerable FindImplementationsOf(Type baseType, Assembly assembly) { ArgumentNullException.ThrowIfNull(baseType); ArgumentNullException.ThrowIfNull(assembly); @@ -113,7 +113,7 @@ public IEnumerable FindImplementationsOf(Type baseT return assembly.ExportedTypes .Where(baseType.IsAssignableFrom) .Where(t => t.IsClass && !t.IsAbstract) - .Select(type => new InterfaceImplementationInfo(type.FullName!, type, baseType)); + .Select(type => new TypeImplementationInfo(type.FullName!, type)); } /// @@ -126,7 +126,7 @@ public IEnumerable FindImplementationsOf(Type baseT /// The list may contain several times the same if it implements the same interface /// multiple types with different generic types. /// - public IEnumerable FindImplementationsOfGeneric(Type baseType) + public IEnumerable FindImplementationsOfGeneric(Type baseType) { ArgumentNullException.ThrowIfNull(baseType); @@ -146,7 +146,7 @@ public IEnumerable FindImplementationsOfGene /// The list may contain several entries for the same implementation type /// if it implements several type the generic with different parameters. /// - public IEnumerable FindImplementationsOfGeneric( + public IEnumerable FindImplementationsOfGeneric( Type baseType, Assembly assembly) { @@ -163,7 +163,7 @@ public IEnumerable FindImplementationsOfGene .SelectMany(type => type.GetInterfaces() // A class may implement a generic interface multiple times .Where(ValidImplementationInterface) .Select(implementedInterface => - new GenericInterfaceImplementationInfo( + new GenericTypeImplementationInfo( type.FullName!, type, implementedInterface, diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs index 450b80f5..20ce50e4 100644 --- a/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs @@ -43,7 +43,7 @@ public void SourceAndDestinationInfoFromGenericTypes() [Test] public void CreateFromGenericInfo() { - var genericInfo = new GenericInterfaceImplementationInfo( + var genericInfo = new GenericTypeImplementationInfo( typeof(MyConverter).FullName!, typeof(MyConverter), typeof(IConverter), @@ -53,8 +53,8 @@ public void CreateFromGenericInfo() Assert.Multiple(() => { Assert.That(info.Name, Is.EqualTo(genericInfo.Name)); Assert.That(info.Type, Is.EqualTo(genericInfo.Type)); - Assert.That(info.InterfaceImplemented, Is.EqualTo(genericInfo.InterfaceImplemented)); - Assert.That(info.GenericTypes, Is.EquivalentTo(genericInfo.GenericTypes)); + Assert.That(info.GenericBaseType, Is.EqualTo(genericInfo.GenericBaseType)); + Assert.That(info.GenericTypeParameters, Is.EquivalentTo(genericInfo.GenericTypeParameters)); Assert.That(info.SourceType, Is.EqualTo(typeof(MySourceFormat))); Assert.That(info.DestinationType, Is.EqualTo(typeof(MyDestFormat))); @@ -67,13 +67,13 @@ public void CreateFromGenericInfo() [Test] public void CreateFromGenericInfoThrowsIfMoreThanTwoGenericTypes() { - var generic3Info = new GenericInterfaceImplementationInfo( + var generic3Info = new GenericTypeImplementationInfo( typeof(MyConverter).FullName!, typeof(MyConverter), typeof(IConverter), new[] { typeof(MySourceFormat), typeof(MyDestFormat), typeof(string) }); - var generic1Info = new GenericInterfaceImplementationInfo( + var generic1Info = new GenericTypeImplementationInfo( typeof(MyConverter).FullName!, typeof(MyConverter), typeof(IConverter), diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs index bbf23cf5..ef290fa2 100644 --- a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs @@ -73,18 +73,17 @@ public void InitializeWithCustomLoadContextProvidesIsolation() [Test] public void LocateFormatsWithTypeInfo() { - InterfaceImplementationInfo myFormat = ConverterLocator.Default.Formats + TypeImplementationInfo myFormat = ConverterLocator.Default.Formats .FirstOrDefault(i => i.Type == typeof(DerivedSourceFormat)); Assert.That(myFormat, Is.Not.Null); - Assert.That(myFormat.InterfaceImplemented, Is.EqualTo(typeof(IFormat))); Assert.That(myFormat.Name, Is.EqualTo(typeof(DerivedSourceFormat).FullName)); } [Test] public void FormatsAreNotDuplicated() { - InterfaceImplementationInfo[] formats = ConverterLocator.Default.Formats + TypeImplementationInfo[] formats = ConverterLocator.Default.Formats .Where(f => f.Type == typeof(MySourceFormat)) .ToArray(); @@ -110,7 +109,7 @@ public void LocateConvertersWithTypeInfo() .FirstOrDefault(i => i.Type == typeof(MyConverter)); Assert.That(result, Is.Not.Null); - Assert.That(result.InterfaceImplemented, Is.EqualTo(typeof(IConverter))); + Assert.That(result.GenericBaseType, Is.EqualTo(typeof(IConverter))); Assert.That(result.Name, Is.EqualTo(typeof(MyConverter).FullName)); Assert.That(result.SourceType, Is.EqualTo(typeof(MySourceFormat))); Assert.That(result.DestinationType, Is.EqualTo(typeof(MyDestFormat))); diff --git a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs index 2cf3b86e..2b718135 100644 --- a/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs +++ b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs @@ -68,7 +68,6 @@ public void FindImplementationOfInterface() Assert.Multiple(() => { Assert.That(extensions[index].Name, Is.EqualTo(typeof(ExistsClass).FullName)); - Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); }); } @@ -84,7 +83,6 @@ public void FindImplementationOfInterfaceWithAssembly() Assert.Multiple(() => { Assert.That(extensions[index].Name, Is.EqualTo(typeof(ExistsClass).FullName)); - Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IExistsInterface))); }); } @@ -99,7 +97,6 @@ public void FindImplementationOfClass() Assert.Multiple(() => { Assert.That(extensions[0].Name, Is.EqualTo(typeof(ExistsClass).FullName)); Assert.That(extensions[0].Type, Is.EqualTo(typeof(ExistsClass))); - Assert.That(extensions[0].InterfaceImplemented, Is.EqualTo(typeof(ExistsClass))); }); } @@ -173,7 +170,6 @@ public void FindImplementationCanFindConstructorWithException() Assert.Multiple(() => { Assert.That(results[0].Name, Is.EqualTo(typeof(ConstructorWithException).FullName)); Assert.That(results[0].Type, Is.EqualTo(typeof(ConstructorWithException))); - Assert.That(results[0].InterfaceImplemented, Is.EqualTo(typeof(ConstructorWithException))); }); } @@ -189,13 +185,13 @@ public void FindImplementationOfGenericInterface1() Assert.Multiple(() => { Assert.That(extensions[index].Name, Is.EqualTo(typeof(Generic1Class).FullName)); - Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IGenericInterface))); - Assert.That(extensions[index].GenericTypes, Has.Count.EqualTo(1)); - Assert.That(extensions[index].GenericTypes[0], Is.EqualTo(typeof(int))); + Assert.That(extensions[index].GenericBaseType, Is.EqualTo(typeof(IGenericInterface))); + Assert.That(extensions[index].GenericTypeParameters, Has.Count.EqualTo(1)); + Assert.That(extensions[index].GenericTypeParameters[0], Is.EqualTo(typeof(int))); }); // For code coverage -.- - GenericInterfaceImplementationInfo copyInfo = extensions[index] with { }; + GenericTypeImplementationInfo copyInfo = extensions[index] with { }; Assert.That(copyInfo, Is.EqualTo(extensions[index])); } @@ -211,10 +207,10 @@ public void FindImplementationOfGenericInterface2() Assert.Multiple(() => { Assert.That(extensions[index].Name, Is.EqualTo(typeof(Generic2Class).FullName)); - Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(IGenericInterface))); - Assert.That(extensions[index].GenericTypes, Has.Count.EqualTo(2)); - Assert.That(extensions[index].GenericTypes[0], Is.EqualTo(typeof(string))); - Assert.That(extensions[index].GenericTypes[1], Is.EqualTo(typeof(int))); + Assert.That(extensions[index].GenericBaseType, Is.EqualTo(typeof(IGenericInterface))); + Assert.That(extensions[index].GenericTypeParameters, Has.Count.EqualTo(2)); + Assert.That(extensions[index].GenericTypeParameters[0], Is.EqualTo(typeof(string))); + Assert.That(extensions[index].GenericTypeParameters[1], Is.EqualTo(typeof(int))); }); } @@ -230,7 +226,6 @@ public void FindImplementationOfGenericClass() Assert.Multiple(() => { Assert.That(extensions[index].Name, Is.EqualTo(typeof(Generic2Class).FullName)); - Assert.That(extensions[index].InterfaceImplemented, Is.EqualTo(typeof(Generic2Class))); }); } @@ -248,22 +243,22 @@ public void FindImplementationOfMultipleGenericInterfaces() Assert.That(extensions[0].Name, Is.EqualTo(typeof(GenericMultipleClass).FullName)); Assert.That(extensions[1].Name, Is.EqualTo(typeof(GenericMultipleClass).FullName)); - Assert.That(extensions[0].GenericTypes, Has.Count.EqualTo(2)); - Assert.That(extensions[1].GenericTypes, Has.Count.EqualTo(2)); + Assert.That(extensions[0].GenericTypeParameters, Has.Count.EqualTo(2)); + Assert.That(extensions[1].GenericTypeParameters, Has.Count.EqualTo(2)); int indexString2Int = extensions.FindIndex( - i => i.InterfaceImplemented == typeof(IGenericInterface)); - Assert.That(extensions[indexString2Int].GenericTypes[0], Is.EqualTo(typeof(string))); - Assert.That(extensions[indexString2Int].GenericTypes[1], Is.EqualTo(typeof(int))); + i => i.GenericBaseType == typeof(IGenericInterface)); + Assert.That(extensions[indexString2Int].GenericTypeParameters[0], Is.EqualTo(typeof(string))); + Assert.That(extensions[indexString2Int].GenericTypeParameters[1], Is.EqualTo(typeof(int))); Assert.That( - extensions[indexString2Int].InterfaceImplemented, + extensions[indexString2Int].GenericBaseType, Is.EqualTo(typeof(IGenericInterface))); int indexInt2String = indexString2Int == 0 ? 1 : 0; - Assert.That(extensions[indexInt2String].GenericTypes[0], Is.EqualTo(typeof(int))); - Assert.That(extensions[indexInt2String].GenericTypes[1], Is.EqualTo(typeof(string))); + Assert.That(extensions[indexInt2String].GenericTypeParameters[0], Is.EqualTo(typeof(int))); + Assert.That(extensions[indexInt2String].GenericTypeParameters[1], Is.EqualTo(typeof(string))); Assert.That( - extensions[indexInt2String].InterfaceImplemented, + extensions[indexInt2String].GenericBaseType, Is.EqualTo(typeof(IGenericInterface))); }); } From 04760de1ee2278f0d95b9019e694cdc0f9de4f05 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Thu, 30 Nov 2023 13:55:58 +0100 Subject: [PATCH 12/12] =?UTF-8?q?=F0=9F=93=9A=20Document=20finding=20conve?= =?UTF-8?q?rter=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/articles/plugins/locate-types.md | 71 ++++++++++++++++++- .../Plugins/LocateTypesExamples.cs | 52 ++++++++++++++ src/Yarhl.Examples/Yarhl.Examples.csproj | 1 + 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/Yarhl.Examples/Plugins/LocateTypesExamples.cs diff --git a/docs/articles/plugins/locate-types.md b/docs/articles/plugins/locate-types.md index 99fd78a6..7d112a4a 100644 --- a/docs/articles/plugins/locate-types.md +++ b/docs/articles/plugins/locate-types.md @@ -1,9 +1,76 @@ # Locate types +After [loading external .NET assemblies](./load-assembly.md) containing +implementation of _formats_ and _converters_, the application can get a list of +them via `ConverterLocator`. + +> [!NOTE] +> This is only needed if the application does not know in advance the converter +> to use. It can present the list to the user so it can choose. Or it can get +> the converter names from a configuration file and later find the actual type +> via reflection. For instance for generic Tinke-like applications. + ## TypeLocator -The `TypeLocator` is a _singleton_ class that p +The `TypeLocator` provides features to find types that implement or inherit a +given base type. It searches in the **loaded assemblies** of an +`AssemblyLoadContext` instance. The default _singleton_ instance is accesible +via `TypeLocator.Default` and it uses `AssemblyLoadContext.Default`. Normally +you don't need to create your own instance. + +> [!NOTE] +> .NET loads assemblies lazily, when a code to run needs them. If you need a +> deterministic search consider loading every assembly from the application +> path. See +> [Load from executing directory](./load-assembly.md#load-from-executing-directory) +> for more information. + +To find a list of types that inherit a given base class or implements an +interface use the method +[`FindImplementationsOf(Type)`](). +It searches for final types, that is: **classes that are public and not +abstract**. It returns information for each of these types in the _record_ +[`TypeImplementationInfo`](xref:Yarhl.Plugins.TypeImplementationInfo) + +For instance to find every _format_ in the loaded asssemblies use: + +[!code-csharp[FindFormats](../../../src/Yarhl.Examples/Plugins/LocateTypesExamples.cs?name=FindFormats)] + +The case of a _generic base type_ is special as types may implemented it +multiple. For instance a _class_ may implement `IConverter` +**and** `IConverter`. Using the _generic type definition_ +(`typeof(IConverter<,>)`) to find types will throw an exception. Use this method +if you are searching for a specific implementation, like +`typeof(IConverter)` + +Use the method +[`FindImplementationsOfGeneric(Type)`]() +to get a list of types implementing the **generic base type definition** with +any type arguments. For instance in the previous example calling +`FindImplementationsOfGeneric(typeof(IConverter<,>))` will return two results +for that class. One for `IConverter` and a second for +`IConverter`. The return type is the _record_ +[`GenericTypeImplementationInfo`](xref:Yarhl.Plugins.GenericTypeImplementationInfo) + +[!code-csharp[FindConverters](../../../src/Yarhl.Examples/Plugins/LocateTypesExamples.cs?name=FindConverters)] ## ConverterLocator -TODO +The [`ConverterLocator`](xref:Yarhl.Plugins.FileFormat.ConverterLocator) class +provides a cache of formats and converters found in the loaded assemblies. +During initialization (first use) it will use `TypeLocator` to find every format +and converter types. The `Default` singleton instance use `TypeLocator.Default`. +You can pass a custom `TypeLocator` via its public constructor. + +The properties +[`Converters`](xref:Yarhl.Plugins.FileFormat.ConverterLocator.Converters) and +[`Formats`](xref:Yarhl.Plugins.FileFormat.ConverterLocator.Formats) provides a +list of the types found, so there is no need to re-scan the assemblies each +time. + +> [!NOTE] +> If a new assembly is loaded in the `AssemblyLoadContext`, the +> `ConverterLocator` will need to performn a re-scan to find the new types. Make +> sure to call +> [`ConverterLocator.ScanAssemblies()`](xref:Yarhl.Plugins.FileFormat.ConverterLocator.ScanAssemblies) +> after loading new assemblies. diff --git a/src/Yarhl.Examples/Plugins/LocateTypesExamples.cs b/src/Yarhl.Examples/Plugins/LocateTypesExamples.cs new file mode 100644 index 00000000..f01dbc9c --- /dev/null +++ b/src/Yarhl.Examples/Plugins/LocateTypesExamples.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Plugins; + +using Yarhl.FileFormat; +using Yarhl.Plugins; + +public static class LocateTypesExamples +{ + public static void FindFormats() + { + #region FindFormats + TypeImplementationInfo[] formatsInfo = TypeLocator.Default + .FindImplementationsOf(typeof(IFormat)) + .ToArray(); + + Console.WriteLine(formatsInfo[0].Name); // e.g. Yarhl.IO.BinaryFormat + Console.WriteLine(formatsInfo[0].Type); // e.g. Type object for BinaryFormat + #endregion + } + + public static void FindConverters() + { + #region FindConverters + GenericTypeImplementationInfo[] convertersInfo = TypeLocator.Default + .FindImplementationsOfGeneric(typeof(IConverter<,>)) + .ToArray(); + + Console.WriteLine(convertersInfo[0].Name); // e.g. Yarhl.Media.Text.Binary2Po + Console.WriteLine(convertersInfo[0].Type); // e.g. Type object for Yarhl.Media.Text.Binary2Po + Console.WriteLine(convertersInfo[0].GenericBaseType); // e.g. Type IConverter + Console.WriteLine(convertersInfo[0].GenericTypeParameters); // e.g. [BinaryFormat, Po] + #endregion + } +} diff --git a/src/Yarhl.Examples/Yarhl.Examples.csproj b/src/Yarhl.Examples/Yarhl.Examples.csproj index 27c3761b..e950b62d 100644 --- a/src/Yarhl.Examples/Yarhl.Examples.csproj +++ b/src/Yarhl.Examples/Yarhl.Examples.csproj @@ -11,6 +11,7 @@ +