feat(core): add PluginLoader with collectible ALC

This commit is contained in:
Erik 2026-04-10 09:51:16 +02:00
parent 9dfbc05052
commit a7f0732026
8 changed files with 226 additions and 0 deletions

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- Private=false + ExcludeAssets=runtime is CRITICAL: prevents the fixture
from copying AcDream.Plugin.Abstractions.dll next to itself, which
would cause type-identity mismatch when the ALC loads the fixture
and the host already has the abstractions loaded. -->
<ProjectReference Include="..\..\src\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,20 @@
using AcDream.Plugin.Abstractions;
namespace AcDream.Core.Tests.Fixtures.HelloPlugin;
public sealed class HelloPlugin : IAcDreamPlugin
{
public int InitializeCount { get; private set; }
public int EnableCount { get; private set; }
public int DisableCount { get; private set; }
public IPluginHost? ReceivedHost { get; private set; }
public void Initialize(IPluginHost host)
{
ReceivedHost = host;
InitializeCount++;
}
public void Enable() => EnableCount++;
public void Disable() => DisableCount++;
}

View file

@ -22,4 +22,13 @@
<ProjectReference Include="..\..\src\AcDream.Core\AcDream.Core.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Build ordering only — do NOT reference at runtime. The tests locate the
fixture DLL by path and load it into a separate ALC. -->
<ProjectReference Include="..\AcDream.Core.Tests.Fixtures.HelloPlugin\AcDream.Core.Tests.Fixtures.HelloPlugin.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,102 @@
using AcDream.Core.Plugins;
using AcDream.Plugin.Abstractions;
namespace AcDream.Core.Tests.Plugins;
public class PluginLoaderTests
{
private static string FixturePluginPath()
{
// walk up from the test bin dir to the repo root, then into the fixture's build output
var baseDir = AppContext.BaseDirectory;
var configuration = new DirectoryInfo(baseDir).Parent!.Name; // Debug / Release
var repoRoot = FindRepoRoot(baseDir);
return Path.Combine(
repoRoot,
"tests", "AcDream.Core.Tests.Fixtures.HelloPlugin", "bin", configuration, "net10.0",
"AcDream.Core.Tests.Fixtures.HelloPlugin.dll");
}
private static string FindRepoRoot(string startDir)
{
var dir = new DirectoryInfo(startDir);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "AcDream.slnx")))
dir = dir.Parent;
return dir?.FullName ?? throw new InvalidOperationException("repo root not found");
}
private sealed class StubHost : IPluginHost
{
public IPluginLogger Log { get; } = new StubLogger();
}
private sealed class StubLogger : IPluginLogger
{
public void Info(string message) { }
public void Warn(string message) { }
public void Error(string message, Exception? exception = null) { }
}
[Fact]
public void Load_FixtureDll_InstantiatesPluginAndCallsInitialize()
{
var dllPath = FixturePluginPath();
Assert.True(File.Exists(dllPath), $"fixture dll not found: {dllPath}");
var host = new StubHost();
var manifest = new PluginManifest(
Id: "acdream.test.hello",
DisplayName: "Hello",
Version: "0.0.1",
EntryDll: Path.GetFileName(dllPath),
ApiVersion: 1,
Dependencies: Array.Empty<string>());
var loaded = PluginLoader.Load(
pluginDirectory: Path.GetDirectoryName(dllPath)!,
manifest: manifest,
host: host);
Assert.True(loaded.Success);
Assert.NotNull(loaded.Plugin);
Assert.Equal("HelloPlugin", loaded.Plugin!.GetType().Name);
}
[Fact]
public void Load_MissingDll_ReturnsFailure()
{
var host = new StubHost();
var manifest = new PluginManifest(
Id: "x",
DisplayName: "X",
Version: "0.0.1",
EntryDll: "nope.dll",
ApiVersion: 1,
Dependencies: Array.Empty<string>());
var loaded = PluginLoader.Load("/does/not/exist", manifest, host);
Assert.False(loaded.Success);
Assert.NotNull(loaded.Error);
}
[Fact]
public void Load_DllWithNoPluginImpl_ReturnsFailure()
{
// Use AcDream.Core.dll itself — it has no IAcDreamPlugin impl
var coreDllDir = AppContext.BaseDirectory;
var host = new StubHost();
var manifest = new PluginManifest(
Id: "x",
DisplayName: "X",
Version: "0.0.1",
EntryDll: "AcDream.Core.dll",
ApiVersion: 1,
Dependencies: Array.Empty<string>());
var loaded = PluginLoader.Load(coreDllDir, manifest, host);
Assert.False(loaded.Success);
Assert.Contains("IAcDreamPlugin", loaded.Error ?? "");
}
}