Skip to content

Commit

Permalink
Add Test for Loading Plugins in non-default ALC.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sewer56 committed Dec 25, 2019
1 parent 9d46b5d commit e87229e
Show file tree
Hide file tree
Showing 14 changed files with 918 additions and 625 deletions.
1,323 changes: 699 additions & 624 deletions DotNetCorePlugins.sln

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions test/Plugins.Tests/McMaster.NETCore.Plugins.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Plugins\McMaster.NETCore.Plugins.csproj" />
<ProjectReference Include="..\TestProjects\WithOwnPluginsContract\WithOwnPluginsContract.csproj" />
<ProjectReference Include="..\TestProjects\ReferencedLibv1\ReferencedLibv1.csproj" />
<ProjectReference Include="..\TestProjects\SharedAbstraction.v2\SharedAbstraction.v2.csproj" />
<TestProject Include="..\TestProjects\ReferencedLibv2\ReferencedLibv2.csproj" />
Expand All @@ -29,6 +30,7 @@
<TestProject Include="..\TestProjects\SqlClientApp\SqlClientApp.csproj" />
<TestProject Include="..\TestProjects\TransitivePlugin\TransitivePlugin.csproj" />
<PublishedTestProject Include="..\TestProjects\PowerShellPlugin\PowerShellPlugin.csproj" />
<MultitargetTestProject Include="..\TestProjects\WithOwnPlugins\WithOwnPlugins.csproj" />
</ItemGroup>

<Import Project="TestProjectRefs.targets" />
Expand Down
51 changes: 51 additions & 0 deletions test/Plugins.Tests/SharedTypesTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Test.Referenced.Library;
using Test.Shared.Abstraction;
using WithOwnPluginsContract;
using Xunit;

namespace McMaster.NETCore.Plugins.Tests
Expand Down Expand Up @@ -51,5 +53,54 @@ public void TransitiveAssembliesOfSharedTypesAreResolved()
var transitiveInstance = configType.GetMethod("GetTransitiveType")?.Invoke(config, null);
Assert.IsType<Test.Transitive.TransitiveSharedType>(transitiveInstance);
}

/// <summary>
/// This is a carefully crafted example which tests
/// whether the library can be used outside of the default load context
/// (<see cref="AssemblyLoadContext.Default"/>).
///
/// It works by loading a plugin (that gets loaded into another ALC)
/// which in turn loads its own plugins using the library. If said plugin
/// can successfully share its own types, the test should work.
/// </summary>
[Fact]
public void NonDefaultLoadContextsAreSupported()
{
/* The loaded plugin here will be in its own ALC.
* It will load its own plugins, which are not known to this ALC.
* Then this ALC will ask that ALC if it managed to successfully its own plugins.
*/

using var loader = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("WithOwnPlugins"), new[] { typeof(IWithOwnPlugins) });
var assembly = loader.LoadDefaultAssembly();
var configType = assembly.GetType("WithOwnPlugins.WithOwnPlugins", throwOnError: true)!;
var config = (IWithOwnPlugins?)Activator.CreateInstance(configType);

/*
* Here, we have made sure that neither WithOwnPlugins or its own plugins have any way to be
* accidentally unified with the default (current for our tests) ALC. We did this by ensuring they are
* not loaded in the default ALC in the first place, hence the use of the `IWithOwnPlugins` interface.
*
* We are simulating a real use case scenario where the plugin host is 100% unaware of the
* plugin's own plugins.
*
* An important additional note:
* - Although the assembly of WithOurPlugins is not directly referenced thanks to the
* ReferenceOutputAssembly = false property, its contents will still be copied to the output.
* - This is problematic because the test runner seems to load all of the Assemblies present in the same
* directory as the test assembly, regardless of whether referenced or not.
* - Therefore we store the plugins of `WithOwnPlugins` are output in a `Plugins` directory.
* (see csproj of WithOwnPlugins, Link property)
*
* You can ensure that WithOwnPlugins or its plugins are not loaded by inspecting the following:
* AssemblyLoadContext.Default.Assemblies
*
* Even if it was loaded, there's an extra check on the other side to ensure no unification could happen.
* Nothing wrong with being extra careful ;).
*/

var callingContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
Assert.True(config?.TryLoadPluginsInCustomContext(callingContext));
}
}
}
7 changes: 6 additions & 1 deletion test/Plugins.Tests/TestProjectRefs.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<Project>

<ItemDefinitionGroup>
<MultitargetTestProject>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<OutputItemType>_ResolvedTestProjectReference</OutputItemType>
</MultitargetTestProject>
<TestProject>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
Expand All @@ -16,7 +21,7 @@
</ItemDefinitionGroup>

<ItemGroup>
<ProjectReference Include="@(TestProject);@(PublishedTestProject)" />
<ProjectReference Include="@(TestProject);@(MultitargetTestProject);@(PublishedTestProject)" />
</ItemGroup>

<Target Name="GeneratePathToTestProjects"
Expand Down
10 changes: 10 additions & 0 deletions test/TestProjects/WithOurPluginsPluginA/Class1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using WithOurPluginsPluginContract;

namespace WithOurPluginsPluginA
{
public class Class1 : ISayHello
{
public string SayHello() => $"Hello from {nameof(WithOurPluginsPluginA)}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\WithOurPluginsPluginContract\WithOurPluginsPluginContract.csproj" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions test/TestProjects/WithOurPluginsPluginB/Class1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using WithOurPluginsPluginContract;

namespace WithOurPluginsPluginB
{
public class Class1 : ISayHello
{
public string SayHello() => $"Hello from {nameof(WithOurPluginsPluginB)}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\WithOurPluginsPluginContract\WithOurPluginsPluginContract.csproj" />
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions test/TestProjects/WithOurPluginsPluginContract/ISayHello.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace WithOurPluginsPluginContract
{
public interface ISayHello
{
string SayHello();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

</Project>
69 changes: 69 additions & 0 deletions test/TestProjects/WithOwnPlugins/WithOwnPlugins.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using McMaster.NETCore.Plugins;
using WithOurPluginsPluginContract;
using WithOwnPluginsContract;

namespace WithOwnPlugins
{
public class WithOwnPlugins : IWithOwnPlugins
{
public bool TryLoadPluginsInCustomContext(AssemblyLoadContext? callingContext)
{
var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
if (currentContext == callingContext)
{
throw new ArgumentException("The context of the caller is the context of this assembly. This invalidates the test.");
}

#if NETCOREAPP3_0
/*
Ensure the source calling context does not have our plugin's interfaces loaded.
This guarantees that the Assembly cannot possibly unify with the default load context.
Note:
The code below this check would fail anyway if the assembly does unify with the default context.
This is more of a safety check to ensure "correctness" as opposed to anything else.
*/
var sayHelloAssembly = typeof(ISayHello).Assembly;
if (callingContext?.Assemblies.Contains(sayHelloAssembly) == true) // .Assemblies API not available in Core 2.X
{
throw new ArgumentException("The context of the caller has this plugin's interface to interact with its own plugins loaded. Test is void.");
}
#endif

// Load our own plugins: Remember, we are in an isolated, non-default ALC.
var plugins = new List<ISayHello?>();
string[] assemblyNames = { "Plugins/WithOurPluginsPluginA.dll", "Plugins/WithOurPluginsPluginB.dll" };

foreach (var assemblyName in assemblyNames)
{
var currentAssemblyFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Unable to get folder path for currently executing assembly.");
var pluginPath = Path.Combine(currentAssemblyFolderPath, assemblyName);

using var loader = PluginLoader.CreateFromAssemblyFile(pluginPath, new[] { typeof(ISayHello) });
var assembly = loader.LoadDefaultAssembly();
var configType = assembly.GetTypes().First(x => typeof(ISayHello).IsAssignableFrom(x) && !x.IsAbstract);
var plugin = (ISayHello?)Activator.CreateInstance(configType);
if (plugin == null)
{
throw new Exception($"Failed to load instance of {nameof(ISayHello)} from plugin.");
}

plugins.Add(plugin);
}

// Shouldn't need to check for this but just in case to absolutely make sure.
if (plugins.Any(plugin => String.IsNullOrEmpty(plugin?.SayHello())))
{
throw new Exception("No value returned from plugin.");
}

return true;
}
}
}
15 changes: 15 additions & 0 deletions test/TestProjects/WithOwnPlugins/WithOwnPlugins.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;netcoreapp2.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Plugins\McMaster.NETCore.Plugins.csproj" />
<ProjectReference Include="..\WithOurPluginsPluginA\WithOurPluginsPluginA.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" OutputItemType="Content" CopyToOutputDirectory="Always" Link="Plugins/%(RecursiveDir)%(Filename).dll" />
<ProjectReference Include="..\WithOurPluginsPluginB\WithOurPluginsPluginB.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" OutputItemType="Content" CopyToOutputDirectory="Always" Link="Plugins/%(RecursiveDir)%(Filename).dll" />
<ProjectReference Include="..\WithOurPluginsPluginContract\WithOurPluginsPluginContract.csproj" />
<ProjectReference Include="..\WithOwnPluginsContract\WithOwnPluginsContract.csproj" />
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions test/TestProjects/WithOwnPluginsContract/IWithOwnPlugins.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Runtime.Loader;

namespace WithOwnPluginsContract
{
public interface IWithOwnPlugins
{
bool TryLoadPluginsInCustomContext(AssemblyLoadContext? callingContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;netcoreapp2.1</TargetFrameworks>
</PropertyGroup>

</Project>

0 comments on commit e87229e

Please sign in to comment.