feat(plugins): complete Campaign LA5 cross-host hosting
This commit is contained in:
parent
6c4cd2bbc6
commit
95f4be94db
26 changed files with 1630 additions and 99 deletions
201
tests/AcDream.Core.Tests/Plugins/PluginSessionTests.cs
Normal file
201
tests/AcDream.Core.Tests/Plugins/PluginSessionTests.cs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
using System.Text.Json;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.Core.Tests.Plugins;
|
||||
|
||||
public sealed class PluginSessionTests
|
||||
{
|
||||
[Fact]
|
||||
public void AbsentAllowListLoadsEveryDiscoveredPlugin()
|
||||
{
|
||||
using var temporary = new TemporaryDirectory();
|
||||
InstallFixture(temporary.Path, "alpha", "acdream.test.alpha");
|
||||
InstallFixture(temporary.Path, "beta", "acdream.test.beta");
|
||||
var statuses = new List<PluginSessionStatus>();
|
||||
var plugins = new PluginSession(new StubHost(), statuses.Add);
|
||||
|
||||
plugins.Start([temporary.Path], allowList: null);
|
||||
|
||||
Assert.Equal(2, plugins.LoadedCount);
|
||||
Assert.Equal(
|
||||
["acdream.test.alpha", "acdream.test.beta"],
|
||||
plugins.LoadedPluginIds);
|
||||
Assert.All(
|
||||
statuses,
|
||||
status => Assert.Equal(PluginSessionStatusKind.Loaded, status.Kind));
|
||||
ReleaseAndCollect(plugins);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowListIsCaseInsensitiveAndOneFailureDoesNotBlockAnotherPlugin()
|
||||
{
|
||||
using var temporary = new TemporaryDirectory();
|
||||
InstallFixture(temporary.Path, "good", "acdream.test.good");
|
||||
InstallBroken(temporary.Path, "broken", "acdream.test.broken");
|
||||
var statuses = new List<PluginSessionStatus>();
|
||||
var plugins = new PluginSession(new StubHost(), statuses.Add);
|
||||
|
||||
plugins.Start(
|
||||
[temporary.Path],
|
||||
[
|
||||
"ACDREAM.TEST.BROKEN",
|
||||
"ACDREAM.TEST.GOOD",
|
||||
"acdream.test.missing",
|
||||
]);
|
||||
|
||||
Assert.Equal(["acdream.test.good"], plugins.LoadedPluginIds);
|
||||
Assert.Equal(
|
||||
[
|
||||
("ACDREAM.TEST.BROKEN", PluginSessionStatusKind.Failed),
|
||||
("acdream.test.good", PluginSessionStatusKind.Loaded),
|
||||
("acdream.test.missing", PluginSessionStatusKind.Failed),
|
||||
],
|
||||
statuses.Select(static status => (status.Plugin, status.Kind)));
|
||||
Assert.All(
|
||||
statuses.Where(static status => status.Kind == PluginSessionStatusKind.Failed),
|
||||
status => Assert.False(string.IsNullOrWhiteSpace(status.Error)));
|
||||
ReleaseAndCollect(plugins);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitEmptyAllowListLoadsNothing()
|
||||
{
|
||||
using var temporary = new TemporaryDirectory();
|
||||
InstallFixture(temporary.Path, "fixture", "acdream.test.fixture");
|
||||
var statuses = new List<PluginSessionStatus>();
|
||||
using var plugins = new PluginSession(new StubHost(), statuses.Add);
|
||||
|
||||
plugins.Start([temporary.Path], []);
|
||||
|
||||
Assert.Equal(0, plugins.LoadedCount);
|
||||
Assert.Empty(statuses);
|
||||
}
|
||||
|
||||
private static void ReleaseAndCollect(PluginSession plugins)
|
||||
{
|
||||
IReadOnlyList<WeakReference> contexts =
|
||||
plugins.CaptureLoadContextWeakReferences();
|
||||
plugins.Dispose();
|
||||
for (int attempt = 0;
|
||||
attempt < 10 && contexts.Any(static context => context.IsAlive);
|
||||
attempt++)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
}
|
||||
Assert.All(contexts, static context => Assert.False(context.IsAlive));
|
||||
}
|
||||
|
||||
private static void InstallFixture(string root, string folder, string id)
|
||||
{
|
||||
string source = FixturePluginPath();
|
||||
Assert.True(File.Exists(source), $"fixture DLL not found: {source}");
|
||||
string pluginDirectory = Path.Combine(root, folder);
|
||||
Directory.CreateDirectory(pluginDirectory);
|
||||
string fileName = Path.GetFileName(source);
|
||||
File.Copy(source, Path.Combine(pluginDirectory, fileName));
|
||||
WriteManifest(pluginDirectory, id, fileName);
|
||||
}
|
||||
|
||||
private static void InstallBroken(string root, string folder, string id)
|
||||
{
|
||||
string pluginDirectory = Path.Combine(root, folder);
|
||||
Directory.CreateDirectory(pluginDirectory);
|
||||
WriteManifest(pluginDirectory, id, "missing.dll");
|
||||
}
|
||||
|
||||
private static void WriteManifest(
|
||||
string directory,
|
||||
string id,
|
||||
string entryDll) =>
|
||||
File.WriteAllText(
|
||||
Path.Combine(directory, "plugin.json"),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
id,
|
||||
displayName = id,
|
||||
version = "1.0.0",
|
||||
entryDll,
|
||||
apiVersion = 1,
|
||||
}));
|
||||
|
||||
private static string FixturePluginPath()
|
||||
{
|
||||
string configuration = new DirectoryInfo(AppContext.BaseDirectory)
|
||||
.Parent!.Name;
|
||||
string root = FindRepoRoot(AppContext.BaseDirectory);
|
||||
return Path.Combine(
|
||||
root,
|
||||
"tests",
|
||||
"AcDream.Core.Tests.Fixtures.HelloPlugin",
|
||||
"bin",
|
||||
configuration,
|
||||
"net10.0",
|
||||
"AcDream.Core.Tests.Fixtures.HelloPlugin.dll");
|
||||
}
|
||||
|
||||
private static string FindRepoRoot(string start)
|
||||
{
|
||||
DirectoryInfo? directory = new(start);
|
||||
while (directory is not null
|
||||
&& !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
{
|
||||
directory = directory.Parent;
|
||||
}
|
||||
return directory?.FullName
|
||||
?? throw new InvalidOperationException("Repository root not found.");
|
||||
}
|
||||
|
||||
private sealed class StubHost : IPluginHost
|
||||
{
|
||||
public bool HasUi => false;
|
||||
public IPluginLogger Log { get; } = new StubLogger();
|
||||
public IGameState State { get; } = new StubState();
|
||||
public IEvents Events { get; } = new StubEvents();
|
||||
public ISelectionService Selection { get; } = new SelectionState();
|
||||
public IUiRegistry Ui => NoOpUiRegistry.Instance;
|
||||
}
|
||||
|
||||
private sealed class StubLogger : IPluginLogger
|
||||
{
|
||||
public void Info(string message) { }
|
||||
public void Warn(string message) { }
|
||||
public void Error(string message, Exception? exception = null) { }
|
||||
}
|
||||
|
||||
private sealed class StubState : IGameState
|
||||
{
|
||||
public IReadOnlyList<WorldEntitySnapshot> Entities => [];
|
||||
}
|
||||
|
||||
private sealed class StubEvents : IEvents
|
||||
{
|
||||
public event Action<WorldEntitySnapshot> EntitySpawned
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TemporaryDirectory : IDisposable
|
||||
{
|
||||
internal TemporaryDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(),
|
||||
$"acdream-plugin-session-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
internal string Path { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue