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..7eedc259 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: Find converters + 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..637b84b6 --- /dev/null +++ b/docs/articles/plugins/load-assembly.md @@ -0,0 +1,89 @@ +# Loading .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 new file mode 100644 index 00000000..7d112a4a --- /dev/null +++ b/docs/articles/plugins/locate-types.md @@ -0,0 +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` 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 + +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/docs/articles/plugins/overview.md b/docs/articles/plugins/overview.md index 6a1cfb1c..63893c04 100644 --- a/docs/articles/plugins/overview.md +++ b/docs/articles/plugins/overview.md @@ -1,3 +1,33 @@ # 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. + +The main APIs are: + +- [`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 + 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/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" } 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/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.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 @@ + diff --git a/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs b/src/Yarhl.IntegrationTests/AssemblyLoadContextExtensionsTests.cs new file mode 100644 index 00000000..fd1541e8 --- /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 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(GetProgramDirectory(), "Yarhl.Media.Text.dll"))); + } + + [Test] + public void LoadingYarhlMediaFromPath() + { + string assemblyPath = Path.Combine(GetPluginsDirectory(), "Yarhl.Media.Text.dll"); + 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.Default.LoadContext.Assemblies.Select(a => a.GetName().Name), + Does.Contain("Yarhl.Media.Text")); + } + + [Test] + public void LoadingInvalidAssemblyReturnsNull() + { + string assemblyPath = Path.Combine(GetPluginsDirectory(), "MyBadPlugin.dll"); + Assembly? loaded = TypeLocator.Default.LoadContext.TryLoadFromAssemblyPath(assemblyPath); + + Assert.That(loaded, Is.Null); + } + + [Test] + public void LoadingIgnoreSystemLibraries() + { + IEnumerable loaded = TypeLocator.Default.LoadContext.TryLoadFromBaseLoadDirectory(); + + 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.Default.LoadContext.TryLoadFromBaseLoadDirectory(); + + Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl")); + + Assert.That( + TypeLocator.Default.LoadContext.Assemblies.Select(a => a.GetName().Name), + Does.Contain("Yarhl")); + } + + [Test] + public void LoadingPluginsDirGetsYarhlMedia() + { + string pluginDir = GetPluginsDirectory(); + IEnumerable loaded = TypeLocator.Default.LoadContext + .TryLoadFromDirectory(pluginDir, false); + + Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl.Media.Text")); + + Assert.That( + TypeLocator.Default.LoadContext.Assemblies.Select(a => a.GetName().Name), + Does.Contain("Yarhl.Media.Text")); + } + + [Test] + public void LoadingPluginsDirRecursiveGetsYarhlMedia() + { + string programDir = GetProgramDirectory(); + IEnumerable loaded = TypeLocator.Default.LoadContext + .TryLoadFromDirectory(programDir, true); + + Assert.That(loaded.Select(a => a.GetName().Name), Does.Contain("Yarhl.Media.Text")); + + Assert.That( + TypeLocator.Default.LoadContext.Assemblies.Select(a => a.GetName().Name), + Does.Contain("Yarhl.Media.Text")); + } + + [Test] + public void FindFormatFromPluginsDir() + { + string pluginDir = GetPluginsDirectory(); + TypeLocator.Default.LoadContext.TryLoadFromDirectory(pluginDir, false); + + var formats = ConverterLocator.Default.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 pluginDir = GetPluginsDirectory(); + TypeLocator.Default.LoadContext.TryLoadFromDirectory(pluginDir, false); + + Type poType = ConverterLocator.Default.Formats + .Single(f => f.Name == "Yarhl.Media.Text.Po") + .Type; + + var converters = ConverterLocator.Default.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")); + } + + private static string GetProgramDirectory() => + AppDomain.CurrentDomain.BaseDirectory; + + private static string GetPluginsDirectory() => + Path.Combine(GetProgramDirectory(), "Plugins"); +} diff --git a/src/Yarhl.IntegrationTests/PluginDiscovery.cs b/src/Yarhl.IntegrationTests/PluginDiscovery.cs deleted file mode 100644 index ab5ebfa2..00000000 --- a/src/Yarhl.IntegrationTests/PluginDiscovery.cs +++ /dev/null @@ -1,68 +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; - - [TestFixture] - public class PluginDiscovery - { - [Test] - public void YarhlMediaIsInPluginsFolder() - { - string programDir = AppDomain.CurrentDomain.BaseDirectory; - string pluginDir = Path.Combine(programDir, PluginManager.PluginDirectory); - 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() - { - var formats = PluginManager.Instance.GetFormats(); - Assert.That(formats, Is.Not.Empty); - Assert.That( - formats.Select(t => t.Metadata.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; - - var converters = PluginManager.Instance.GetConverters() - .Where(f => f.Metadata.CanConvert(poType)); - Assert.That(converters, Is.Not.Empty); - Assert.That( - converters.Select(t => t.Metadata.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 new file mode 100644 index 00000000..642cfaa7 --- /dev/null +++ b/src/Yarhl.Plugins/AssemblyLoadContextExtensions.cs @@ -0,0 +1,127 @@ +// 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. + /// + /// 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) + { + // 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. + /// + /// 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) + { + 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 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. + /// + /// 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 TryLoadFromBaseLoadDirectory(this AssemblyLoadContext loader) + { + string programDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) ?? + 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/ConverterLocator.cs b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs new file mode 100644 index 00000000..1225f67b --- /dev/null +++ b/src/Yarhl.Plugins/FileFormat/ConverterLocator.cs @@ -0,0 +1,107 @@ +// 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 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. + /// + /// The type locator to use internally. + public ConverterLocator(TypeLocator locator) + { + this.locator = locator; + + formatsMetadata = new List(); + Formats = formatsMetadata; + + convertersMetadata = new List(); + Converters = convertersMetadata; + + ScanAssemblies(); + } + + /// + /// 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 Default { + get { + if (singleInstance == null) { + lock (LockObj) { + singleInstance ??= new ConverterLocator(); + } + } + + 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. + /// + /// + /// This method is already called when the instance is created. Only needed + /// after loading additional assemblies. + /// + public void ScanAssemblies() + { + formatsMetadata.Clear(); + convertersMetadata.Clear(); + + formatsMetadata.AddRange( + locator.FindImplementationsOf(typeof(IFormat))); + + convertersMetadata.AddRange( + locator.FindImplementationsOfGeneric(typeof(IConverter<,>)) + .Select(x => new ConverterTypeInfo(x))); + } +} 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..f5deb96f --- /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) + : GenericTypeImplementationInfo(Name, Type, InterfaceImplemented, GenericTypes) +{ + /// + /// Initializes a new instance of the class. + /// + /// The generic implementor information. + public ConverterTypeInfo(GenericTypeImplementationInfo info) + : this(info.Name, info.Type, info.GenericBaseType, info.GenericTypeParameters) + { + if (info.GenericTypeParameters.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/FormatMetadata.cs b/src/Yarhl.Plugins/GenericTypeImplementationInfo.cs similarity index 50% rename from src/Yarhl.Plugins/FileFormat/FormatMetadata.cs rename to src/Yarhl.Plugins/GenericTypeImplementationInfo.cs index 6ba8f748..ef286b77 100644 --- a/src/Yarhl.Plugins/FileFormat/FormatMetadata.cs +++ b/src/Yarhl.Plugins/GenericTypeImplementationInfo.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,20 @@ // 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 base type. +/// +/// The name of the implementation type. Shortcut for Type.FullName. +/// 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 GenericBaseType, + IReadOnlyList GenericTypeParameters) + : TypeImplementationInfo(Name, Type); 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/FileFormat/IExportMetadata.cs b/src/Yarhl.Plugins/TypeImplementationInfo.cs similarity index 62% rename from src/Yarhl.Plugins/FileFormat/IExportMetadata.cs rename to src/Yarhl.Plugins/TypeImplementationInfo.cs index 6727d1a0..164960b1 100644 --- a/src/Yarhl.Plugins/FileFormat/IExportMetadata.cs +++ b/src/Yarhl.Plugins/TypeImplementationInfo.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,11 @@ // 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 a base type. +/// +/// The name of the implementation type. Shortcut for Type.FullName. +/// The implementation type. +public record TypeImplementationInfo(string Name, Type Type); diff --git a/src/Yarhl.Plugins/TypeLocator.cs b/src/Yarhl.Plugins/TypeLocator.cs new file mode 100644 index 00000000..51b10973 --- /dev/null +++ b/src/Yarhl.Plugins/TypeLocator.cs @@ -0,0 +1,172 @@ +// 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. + /// + /// The load context to search assemblies. + public TypeLocator(AssemblyLoadContext loadContext) + { + LoadContext = loadContext; + } + + /// + /// Initializes a new instance of the class. + /// + private TypeLocator() + { + LoadContext = AssemblyLoadContext.Default; + } + + /// + /// Gets a singleton instance that use the default AssemblyLoadContext. + /// + /// + /// It initializes the type if needed on the first call. + /// + public static TypeLocator Default { + 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); + + 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) + .Select(type => new TypeImplementationInfo(type.FullName!, type)); + } + + /// + /// 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); + + return assembly.ExportedTypes + .Where(t => t.IsClass && !t.IsAbstract) + .Where(t => Array.Exists(t.GetInterfaces(), ValidImplementationInterface)) + .SelectMany(type => type.GetInterfaces() // A class may implement a generic interface multiple times + .Where(ValidImplementationInterface) + .Select(implementedInterface => + new GenericTypeImplementationInfo( + type.FullName!, + type, + implementedInterface, + implementedInterface.GenericTypeArguments))); + } +} 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..4b3b3940 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 = ConverterLocator.Default.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 = ConverterLocator.Default.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..20ce50e4 --- /dev/null +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConverterTypeInfoTests.cs @@ -0,0 +1,163 @@ +// 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 NUnit.Framework; +using Yarhl.FileFormat; +using Yarhl.Plugins; +using Yarhl.Plugins.FileFormat; + +[TestFixture] +public class ConverterTypeInfoTests +{ + private readonly ConverterTypeInfo converterInfo = new( + typeof(MyConverter).FullName!, + typeof(MyConverter), + typeof(IConverter), + new[] { typeof(MySourceFormat), typeof(MyDestFormat) }); + + [Test] + public void SourceAndDestinationInfoFromGenericTypes() + { + Assert.That(converterInfo.SourceType, Is.EqualTo(typeof(MySourceFormat))); + Assert.That(converterInfo.DestinationType, Is.EqualTo(typeof(MyDestFormat))); + } + + [Test] + public void CreateFromGenericInfo() + { + var genericInfo = new GenericTypeImplementationInfo( + typeof(MyConverter).FullName!, + typeof(MyConverter), + typeof(IConverter), + new[] { typeof(MySourceFormat), typeof(MyDestFormat) }); + + 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.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))); + }); + + var copy = info with { }; + Assert.That(copy, Is.EqualTo(info)); + } + + [Test] + public void CreateFromGenericInfoThrowsIfMoreThanTwoGenericTypes() + { + var generic3Info = new GenericTypeImplementationInfo( + typeof(MyConverter).FullName!, + typeof(MyConverter), + typeof(IConverter), + new[] { typeof(MySourceFormat), typeof(MyDestFormat), typeof(string) }); + + var generic1Info = new GenericTypeImplementationInfo( + 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() + { + Assert.That( + () => converterInfo.CanConvert(null), + Throws.ArgumentNullException); + } + + [Test] + public void CanConvertReturnsTrueForExactType() + { + Assert.That(converterInfo.CanConvert(typeof(MySourceFormat)), Is.True); + } + + [Test] + public void CanConvertReturnsTrueForDerivedTypes() + { + Assert.That(converterInfo.CanConvert(typeof(DerivedSourceFormat)), Is.True); + } + + [Test] + public void CanConvertReturnsFalseForBaseTypes() + { + Assert.That(converterInfo.CanConvert(typeof(IFormat)), Is.False); + } + + [Test] + public void CanConvertReturnsFalseForDifferentTypes() + { + Assert.That(converterInfo.CanConvert(typeof(MyDestFormat)), Is.False); + } + + [Test] + public void CanConvertReturnsForExactSourceAndDest() + { + Assert.That( + converterInfo.CanConvert(typeof(MySourceFormat), typeof(MyDestFormat)), + Is.True); + Assert.That( + converterInfo.CanConvert(typeof(MyDestFormat), typeof(MySourceFormat)), + Is.False); + } + + [Test] + public void CanConvertReturnsForSourceAndDestDerived() + { + // Source is a derived format + Assert.That( + converterInfo.CanConvert(typeof(DerivedSourceFormat), typeof(MyDestFormat)), + Is.True); + + // Destination is a base type + Assert.That( + converterInfo.CanConvert(typeof(MySourceFormat), typeof(IFormat)), + Is.True); + + // Cannot convert base type + Assert.That( + converterInfo.CanConvert(typeof(IFormat), typeof(MyDestFormat)), + Is.False); + + // Cannot convert to derived type + Assert.That( + converterInfo.CanConvert(typeof(MySourceFormat), typeof(DerivedDestFormat)), + Is.False); + } + + [Test] + public void CanConvertSourceDestThrowsExceptionIfNullArgument() + { + Assert.That( + () => converterInfo.CanConvert(null, typeof(int)), + Throws.ArgumentNullException); + Assert.That( + () => converterInfo.CanConvert(typeof(int), null), + Throws.ArgumentNullException); + } +} diff --git a/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs new file mode 100644 index 00000000..ef290fa2 --- /dev/null +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/ConvertersLocatorTests.cs @@ -0,0 +1,215 @@ +// 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 System.Runtime.Loader; +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 InstanceIsSingleton() + { + ConverterLocator instance1 = ConverterLocator.Default; + ConverterLocator instance2 = ConverterLocator.Default; + + Assert.That(instance1, Is.SameAs(instance2)); + } + + [Test] + public void InstanceIsInitialized() + { + 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() + { + TypeImplementationInfo myFormat = ConverterLocator.Default.Formats + .FirstOrDefault(i => i.Type == typeof(DerivedSourceFormat)); + + Assert.That(myFormat, Is.Not.Null); + Assert.That(myFormat.Name, Is.EqualTo(typeof(DerivedSourceFormat).FullName)); + } + + [Test] + public void FormatsAreNotDuplicated() + { + TypeImplementationInfo[] formats = ConverterLocator.Default.Formats + .Where(f => f.Type == typeof(MySourceFormat)) + .ToArray(); + + Assert.That(formats, Has.Length.EqualTo(1)); + } + + [Test] + public void LocateFormatsFindYarhlBaseFormats() + { + Assert.That( + ConverterLocator.Default.Formats.Select(f => f.Type), + Does.Contain(typeof(BinaryFormat))); + + Assert.That( + ConverterLocator.Default.Formats.Select(f => f.Type), + Does.Contain(typeof(NodeContainerFormat))); + } + + [Test] + public void LocateConvertersWithTypeInfo() + { + ConverterTypeInfo result = ConverterLocator.Default.Converters + .FirstOrDefault(i => i.Type == typeof(MyConverter)); + + Assert.That(result, Is.Not.Null); + 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))); + } + + [Test] + public void ConvertersAreNotDuplicated() + { + ConverterTypeInfo[] results = ConverterLocator.Default.Converters + .Where(f => f.Type == typeof(MyConverter)) + .ToArray(); + + Assert.That(results, Has.Length.EqualTo(1)); + } + + [Test] + public void ScanAssembliesDoesNotDuplicateFindings() + { + ConverterLocator.Default.ScanAssemblies(); + + FormatsAreNotDuplicated(); + ConvertersAreNotDuplicated(); + } + + [Test] + public void LocateConverterWithParameters() + { + ConverterTypeInfo[] results = ConverterLocator.Default.Converters + .Where(f => f.Type == typeof(MyConverterParametrized)) + .ToArray(); + + Assert.That(results.Length, Is.EqualTo(1)); + } + + [Test] + public void LocateSingleInnerConverter() + { + ConverterTypeInfo converter = ConverterLocator.Default.Converters + .FirstOrDefault(c => c.Type == typeof(SingleOuterConverter.SingleInnerConverter)); + + Assert.That(converter, Is.Not.Null); + } + + [Test] + public void LocateSingleOuterConverter() + { + ConverterTypeInfo converter = ConverterLocator.Default.Converters + .FirstOrDefault(c => c.Type == typeof(SingleOuterConverter)); + + Assert.That(converter, Is.Not.Null); + } + + [Test] + public void LocateTwoConvertersInSameClass() + { + ConverterTypeInfo[] converters = ConverterLocator.Default.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( + 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( + Array.Exists( + converters, + c => c.SourceType == typeof(MyDestFormat) && c.DestinationType == typeof(MySourceFormat)), + Is.True); + } + + [Test] + public void LocateDerivedConverter() + { + ConverterTypeInfo[] converters = ConverterLocator.Default.Converters + .Where(c => c.Type == typeof(DerivedConverter)) + .ToArray(); + + Assert.That(converters.Length, Is.EqualTo(1)); + } + + [Test] + public void LocateConvertsWithOtherInterfaces() + { + ConverterTypeInfo[] converters = ConverterLocator.Default.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 new file mode 100644 index 00000000..1effa185 --- /dev/null +++ b/src/Yarhl.UnitTests/Plugins/FileFormat/TestConvertersDefinition.cs @@ -0,0 +1,100 @@ +namespace Yarhl.UnitTests.Plugins.FileFormat; + +using System; +using Yarhl.FileFormat; + +#pragma warning disable SA1649 // File name match type name + +public class MySourceFormat : IFormat +{ +} + +public class MyDestFormat : IFormat +{ +} + +public class DerivedSourceFormat : MySourceFormat +{ +} + +public class DerivedDestFormat : IFormat +{ +} + +public class MyConverter : IConverter +{ + public MyDestFormat Convert(MySourceFormat source) + { + return new MyDestFormat(); + } +} + +public class MyConverterParametrized : IConverter +{ + public MyConverterParametrized(bool ignoreMe) + { + } + + public MyDestFormat Convert(MySourceFormat source) + { + return new MyDestFormat(); + } +} + +public class SingleOuterConverter : IConverter +{ + public MyDestFormat Convert(MySourceFormat source) + { + return new MyDestFormat(); + } + + public class SingleInnerConverter : IConverter + { + public MyDestFormat Convert(MySourceFormat source) + { + return new MyDestFormat(); + } + } +} + +public sealed class ConverterAndOtherInterface : + IConverter, + IDisposable +{ + public MyDestFormat Convert(MySourceFormat source) + { + return new MyDestFormat(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} + +public class TwoConverters : + IConverter, + IConverter +{ + public MyDestFormat Convert(MySourceFormat source) + { + return new MyDestFormat(); + } + + public MySourceFormat Convert(MyDestFormat source) + { + return new MySourceFormat(); + } +} + +public abstract class BaseAbstractConverter : IConverter +{ + public MyDestFormat Convert(MySourceFormat source) + { + return new MyDestFormat(); + } +} + +public class DerivedConverter : BaseAbstractConverter +{ +} 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.UnitTests/Plugins/TestConvertersDefinition.cs b/src/Yarhl.UnitTests/Plugins/TestConvertersDefinition.cs deleted file mode 100644 index ebf52355..00000000 --- a/src/Yarhl.UnitTests/Plugins/TestConvertersDefinition.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace Yarhl.UnitTests.Plugins; - -using System; -using Yarhl.FileFormat; - -#pragma warning disable SA1649 // File name match type name - -public class BasicConverter : IConverter -{ - public byte Convert(string source) - { - return System.Convert.ToByte(source); - } -} - -public class SingleOuterConverterExample : IConverter -{ - public uint Convert(string source) - { - return System.Convert.ToUInt32(source); - } - - public class SingleInnerConverterExample : IConverter - { - public ulong Convert(string source) - { - return System.Convert.ToUInt64(source); - } - } -} - -public sealed class ConverterAndOtherInterface : - IConverter, - IDisposable -{ - public short Convert(string source) - { - return System.Convert.ToInt16(source); - } - - 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(); - } -} - -public class DuplicatedConverter1 : - IConverter -{ - public sbyte Convert(string source) - { - return System.Convert.ToSByte(source); - } -} - -public class DuplicatedConverter2 : - IConverter -{ - public sbyte Convert(string source) - { - return System.Convert.ToSByte(source); - } -} - -public abstract class BaseAbstractConverter : IConverter -{ - public long Convert(string source) - { - return System.Convert.ToInt64(source); - } -} - -public class DerivedConverter : BaseAbstractConverter -{ -} 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..d287eafd 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,63 @@ // 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 +{ +} + +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 class ExistsClass : IExistsInterface +{ +} + +public class Generic1Class : IGenericInterface +{ +} + +public class Generic2Class : IGenericInterface +{ +} + +public class GenericMultipleClass : + IGenericInterface, + IGenericInterface +{ +} + +public interface ISecondInterface : IExistsInterface +{ +} + +public abstract class AbstractClass : IExistsInterface +{ +} + +public interface ISecondGenericInterface : IGenericInterface +{ +} + +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..2b718135 --- /dev/null +++ b/src/Yarhl.UnitTests/Plugins/TypeLocatorTests.cs @@ -0,0 +1,285 @@ +// 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.UnitTests.Plugins; + +using System.Linq; +using System.Runtime.Loader; +using NUnit.Framework; +using Yarhl.FileFormat; +using Yarhl.IO; +using Yarhl.Plugins; + +[TestFixture] +public class TypeLocatorTests +{ + [Test] + public void InstanceInitializePluginManager() + { + TypeLocator instance = TypeLocator.Default; + Assert.That(instance, Is.Not.Null); + Assert.That(instance.LoadContext, Is.Not.Null); + } + + [Test] + public void InstanceIsCreatedOnce() + { + 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.Default + .FindImplementationsOf(typeof(IExistsInterface)) + .ToList(); + + int index = extensions.FindIndex(i => i.Type == typeof(ExistsClass)); + Assert.That(index, Is.Not.EqualTo(-1)); + + Assert.Multiple(() => { + Assert.That(extensions[index].Name, Is.EqualTo(typeof(ExistsClass).FullName)); + }); + } + + [Test] + public void FindImplementationOfInterfaceWithAssembly() + { + var extensions = TypeLocator.Default + .FindImplementationsOf(typeof(IExistsInterface), typeof(IExistsInterface).Assembly) + .ToList(); + + int index = extensions.FindIndex(i => i.Type == typeof(ExistsClass)); + Assert.That(index, Is.Not.EqualTo(-1)); + + Assert.Multiple(() => { + Assert.That(extensions[index].Name, Is.EqualTo(typeof(ExistsClass).FullName)); + }); + } + + [Test] + public void FindImplementationOfClass() + { + var extensions = TypeLocator.Default + .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))); + }); + } + + [Test] + public void FindImplementationOfInterfaceDifferentAssemblyReturnsEmpty() + { + var extensions = TypeLocator.Default + .FindImplementationsOf(typeof(IExistsInterface), typeof(IBinary).Assembly) + .ToList(); + + Assert.That(extensions, Has.Count.EqualTo(0)); + } + + [Test] + public void FindImplementationWithNullTypeThrowsException() + { + Assert.That( + () => TypeLocator.Default.FindImplementationsOf(null), + Throws.ArgumentNullException); + Assert.That( + () => TypeLocator.Default.FindImplementationsOf(null, typeof(IExistsInterface).Assembly), + Throws.ArgumentNullException); + Assert.That( + () => TypeLocator.Default.FindImplementationsOf(typeof(IExistsInterface), null), + Throws.ArgumentNullException); + } + + [Test] + public void FindImplementationThrowsWithGenericTypeDefinitions() + { + Assert.That( + () => TypeLocator.Default + .FindImplementationsOf(typeof(IGenericInterface<,>)) + .ToArray(), + Throws.ArgumentException); + Assert.That( + () => TypeLocator.Default + .FindImplementationsOf(typeof(IGenericInterface<,>), typeof(IGenericInterface<,>).Assembly) + .ToArray(), + Throws.ArgumentException); + } + + [Test] + public void FindImplementationIgnoresAbstractClasses() + { + var results = TypeLocator.Default + .FindImplementationsOf(typeof(IExistsInterface)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(AbstractClass)), Is.Null); + } + + [Test] + public void FindImplementationIgnoresInterfaces() + { + var results = TypeLocator.Default + .FindImplementationsOf(typeof(IExistsInterface)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(ISecondInterface)), Is.Null); + } + + [Test] + public void FindImplementationCanFindConstructorWithException() + { + var results = TypeLocator.Default + .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))); + }); + } + + [Test] + public void FindImplementationOfGenericInterface1() + { + var extensions = TypeLocator.Default + .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].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 -.- + GenericTypeImplementationInfo copyInfo = extensions[index] with { }; + Assert.That(copyInfo, Is.EqualTo(extensions[index])); + } + + [Test] + public void FindImplementationOfGenericInterface2() + { + var extensions = TypeLocator.Default + .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].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))); + }); + } + + [Test] + public void FindImplementationOfGenericClass() + { + var extensions = TypeLocator.Default + .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)); + }); + } + + [Test] + public void FindImplementationOfMultipleGenericInterfaces() + { + var extensions = TypeLocator.Default + .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].GenericTypeParameters, Has.Count.EqualTo(2)); + Assert.That(extensions[1].GenericTypeParameters, Has.Count.EqualTo(2)); + + int indexString2Int = extensions.FindIndex( + 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].GenericBaseType, + Is.EqualTo(typeof(IGenericInterface))); + + int indexInt2String = indexString2Int == 0 ? 1 : 0; + Assert.That(extensions[indexInt2String].GenericTypeParameters[0], Is.EqualTo(typeof(int))); + Assert.That(extensions[indexInt2String].GenericTypeParameters[1], Is.EqualTo(typeof(string))); + Assert.That( + extensions[indexInt2String].GenericBaseType, + Is.EqualTo(typeof(IGenericInterface))); + }); + } + + [Test] + public void FindImplementationOfGenericIgnoresInterfaces() + { + var results = TypeLocator.Default + .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(ISecondGenericInterface)), Is.Null); + } + + [Test] + public void FindImplementationOfGenericIgnoresAbstractClasses() + { + var results = TypeLocator.Default + .FindImplementationsOfGeneric(typeof(IGenericInterface<,>)) + .ToList(); + + Assert.That(results.Find(i => i.Type == typeof(AbstractGenericClass)), Is.Null); + } +} 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 @@ - +