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
202
tests/AcDream.App.Tests/Plugins/GraphicalPluginSessionTests.cs
Normal file
202
tests/AcDream.App.Tests/Plugins/GraphicalPluginSessionTests.cs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
using System.Text.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AcDream.App.Plugins;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Platform;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Plugins;
|
||||
|
||||
public sealed class GraphicalPluginSessionTests
|
||||
{
|
||||
private const string FixtureId = "acdream.test.host-fixture";
|
||||
|
||||
[Fact]
|
||||
public void ConfiguredSetLoadsOnlyAllowedPluginAndReportsBothOutcomes()
|
||||
{
|
||||
using var temporary = new TemporaryDirectory();
|
||||
ApplicationPathSet paths = Paths(temporary.Path);
|
||||
InstallFixture(paths.PluginsDirectory, FixtureId);
|
||||
string statusPath = Path.Combine(temporary.Path, "status.jsonl");
|
||||
var logger = new CapturingLogger();
|
||||
var state = new WorldGameState();
|
||||
var events = new WorldEvents();
|
||||
var selection = new SelectionState();
|
||||
var ui = new BufferedUiRegistry();
|
||||
var host = new AppPluginHost(logger, state, events, selection, ui);
|
||||
|
||||
using GraphicalPluginSession plugins = GraphicalPluginSession.Start(
|
||||
paths,
|
||||
[FixtureId.ToUpperInvariant(), "acdream.test.missing"],
|
||||
"gui-session",
|
||||
host,
|
||||
new SessionStatusWriter(statusPath));
|
||||
|
||||
Assert.Equal(1, plugins.LoadedCount);
|
||||
Assert.True(host.HasUi);
|
||||
AssertPanelWasRegisteredAndReleaseBinding(ui);
|
||||
Assert.Contains(
|
||||
logger.Messages,
|
||||
message => message.Contains("fixture-enabled:hasUi=True", StringComparison.Ordinal));
|
||||
|
||||
JsonElement[] statuses = ReadStatuses(statusPath);
|
||||
Assert.Equal(["pluginLoaded", "pluginFailed"], EventNames(statuses));
|
||||
Assert.Equal(FixtureId, statuses[0].GetProperty("plugin").GetString());
|
||||
Assert.Equal(
|
||||
"acdream.test.missing",
|
||||
statuses[1].GetProperty("plugin").GetString());
|
||||
Assert.Contains(
|
||||
"not found",
|
||||
statuses[1].GetProperty("error").GetString(),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
WeakReference context = Assert.Single(
|
||||
plugins.CaptureLoadContextWeakReferences());
|
||||
plugins.Dispose();
|
||||
Collect(context);
|
||||
Assert.False(context.IsAlive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitEmptyConfiguredSetLoadsNone()
|
||||
{
|
||||
using var temporary = new TemporaryDirectory();
|
||||
ApplicationPathSet paths = Paths(temporary.Path);
|
||||
InstallFixture(paths.PluginsDirectory, FixtureId);
|
||||
string statusPath = Path.Combine(temporary.Path, "status.jsonl");
|
||||
var ui = new BufferedUiRegistry();
|
||||
var host = new AppPluginHost(
|
||||
new CapturingLogger(),
|
||||
new WorldGameState(),
|
||||
new WorldEvents(),
|
||||
new SelectionState(),
|
||||
ui);
|
||||
|
||||
using GraphicalPluginSession plugins = GraphicalPluginSession.Start(
|
||||
paths,
|
||||
[],
|
||||
"gui-session",
|
||||
host,
|
||||
new SessionStatusWriter(statusPath));
|
||||
|
||||
Assert.Equal(0, plugins.LoadedCount);
|
||||
Assert.Empty(ui.Drain());
|
||||
Assert.False(File.Exists(statusPath));
|
||||
}
|
||||
|
||||
private static ApplicationPathSet Paths(string root) => new(
|
||||
Path.Combine(root, "config"),
|
||||
Path.Combine(root, "data"),
|
||||
Path.Combine(root, "cache"),
|
||||
LegacyConfigDirectory: null);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void AssertPanelWasRegisteredAndReleaseBinding(
|
||||
BufferedUiRegistry ui)
|
||||
{
|
||||
BufferedUiRegistry.Pending panel = Assert.Single(ui.Drain());
|
||||
Assert.EndsWith(
|
||||
"fixture-panel.xml",
|
||||
panel.MarkupPath,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Equal(
|
||||
"AcDream.Plugin.Tests.Fixtures.HostPlugin",
|
||||
panel.Binding.GetType().Assembly.GetName().Name);
|
||||
}
|
||||
|
||||
private static JsonElement[] ReadStatuses(string path) =>
|
||||
File.ReadAllLines(path)
|
||||
.Select(static line => JsonDocument.Parse(line).RootElement.Clone())
|
||||
.ToArray();
|
||||
|
||||
private static string[] EventNames(IEnumerable<JsonElement> events) =>
|
||||
events.Select(static item => item.GetProperty("e").GetString()!).ToArray();
|
||||
|
||||
private static void InstallFixture(string root, string id)
|
||||
{
|
||||
string source = FixtureAssemblyPath();
|
||||
Assert.True(File.Exists(source), $"fixture DLL not found: {source}");
|
||||
string pluginDirectory = Path.Combine(root, "host-fixture");
|
||||
Directory.CreateDirectory(pluginDirectory);
|
||||
string fileName = Path.GetFileName(source);
|
||||
File.Copy(source, Path.Combine(pluginDirectory, fileName));
|
||||
File.WriteAllText(
|
||||
Path.Combine(pluginDirectory, "plugin.json"),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
id,
|
||||
displayName = "Host fixture",
|
||||
version = "1.0.0",
|
||||
entryDll = fileName,
|
||||
apiVersion = 1,
|
||||
}));
|
||||
}
|
||||
|
||||
private static string FixtureAssemblyPath()
|
||||
{
|
||||
string configuration = new DirectoryInfo(AppContext.BaseDirectory)
|
||||
.Parent!.Name;
|
||||
string root = FindRepoRoot(AppContext.BaseDirectory);
|
||||
return Path.Combine(
|
||||
root,
|
||||
"tests",
|
||||
"AcDream.Plugin.Tests.Fixtures.HostPlugin",
|
||||
"bin",
|
||||
configuration,
|
||||
"net10.0",
|
||||
"AcDream.Plugin.Tests.Fixtures.HostPlugin.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 static void Collect(WeakReference reference)
|
||||
{
|
||||
for (int attempt = 0; attempt < 10 && reference.IsAlive; attempt++)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingLogger : IPluginLogger
|
||||
{
|
||||
internal List<string> Messages { get; } = [];
|
||||
|
||||
public void Info(string message) => Messages.Add(message);
|
||||
public void Warn(string message) => Messages.Add(message);
|
||||
public void Error(string message, Exception? exception = null) =>
|
||||
Messages.Add(exception is null ? message : $"{message}: {exception.Message}");
|
||||
}
|
||||
|
||||
private sealed class TemporaryDirectory : IDisposable
|
||||
{
|
||||
internal TemporaryDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(),
|
||||
$"acdream-graphical-plugins-{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