fix(plugins): close LA5 host lifecycle review

This commit is contained in:
Erik 2026-08-14 19:05:13 +02:00
parent 95f4be94db
commit fbe9c8a288
25 changed files with 1043 additions and 120 deletions

View file

@ -1,17 +1,20 @@
using System.Text.Json;
using System.Runtime.CompilerServices;
using AcDream.App.Configuration;
using AcDream.App.Plugins;
using AcDream.Core.Plugins;
using AcDream.Core.Selection;
using AcDream.Platform;
using AcDream.Plugin.Abstractions;
using AcDream.Runtime.Session;
using AcDream.Tests.Fixtures.CampaignLa;
namespace AcDream.App.Tests.Plugins;
public sealed class GraphicalPluginSessionTests
{
private const string FixtureId = "acdream.test.host-fixture";
private const string ThrowingId = "acdream.test.throwing-fixture";
[Fact]
public void ConfiguredSetLoadsOnlyAllowedPluginAndReportsBothOutcomes()
@ -27,12 +30,13 @@ public sealed class GraphicalPluginSessionTests
var ui = new BufferedUiRegistry();
var host = new AppPluginHost(logger, state, events, selection, ui);
using GraphicalPluginSession plugins = GraphicalPluginSession.Start(
using GraphicalPluginSession plugins = GraphicalPluginSession.Create(
paths,
[FixtureId.ToUpperInvariant(), "acdream.test.missing"],
"gui-session",
host,
new SessionStatusWriter(statusPath));
plugins.Start();
Assert.Equal(1, plugins.LoadedCount);
Assert.True(host.HasUi);
@ -42,19 +46,20 @@ public sealed class GraphicalPluginSessionTests
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(["started", "pluginLoaded", "pluginFailed"], EventNames(statuses));
Assert.Equal(FixtureId, statuses[1].GetProperty("plugin").GetString());
Assert.Equal(
"acdream.test.missing",
statuses[1].GetProperty("plugin").GetString());
statuses[2].GetProperty("plugin").GetString());
Assert.Contains(
"not found",
statuses[1].GetProperty("error").GetString(),
statuses[2].GetProperty("error").GetString(),
StringComparison.OrdinalIgnoreCase);
WeakReference context = Assert.Single(
plugins.CaptureLoadContextWeakReferences());
plugins.Dispose();
Assert.Equal(0, ui.RegistrationCount);
Collect(context);
Assert.False(context.IsAlive);
}
@ -66,6 +71,12 @@ public sealed class GraphicalPluginSessionTests
ApplicationPathSet paths = Paths(temporary.Path);
InstallFixture(paths.PluginsDirectory, FixtureId);
string statusPath = Path.Combine(temporary.Path, "status.jsonl");
string configPath = Path.Combine(temporary.Path, "session.json");
File.WriteAllText(
configPath,
LauncherCoreSessionConfigFixture.ComposeEmptyPlugins());
(_, SessionDescriptor descriptor) =
SessionConfigurationLoader.Load(configPath);
var ui = new BufferedUiRegistry();
var host = new AppPluginHost(
new CapturingLogger(),
@ -74,16 +85,70 @@ public sealed class GraphicalPluginSessionTests
new SelectionState(),
ui);
using GraphicalPluginSession plugins = GraphicalPluginSession.Start(
using GraphicalPluginSession plugins = GraphicalPluginSession.Create(
paths,
[],
descriptor.Plugins,
"gui-session",
host,
new SessionStatusWriter(statusPath));
plugins.Start();
Assert.Equal(0, plugins.LoadedCount);
Assert.NotNull(descriptor.Plugins);
Assert.Empty(descriptor.Plugins);
Assert.Empty(ui.Drain());
Assert.Equal(["started"], EventNames(ReadStatuses(statusPath)));
}
[Fact]
public void ThrowAfterRegistrationRollsBackUiAndEventsAndCollectsContext()
{
using var temporary = new TemporaryDirectory();
ApplicationPathSet paths = Paths(temporary.Path);
string pluginDirectory = InstallFixture(
paths.PluginsDirectory,
ThrowingId,
"throwing-fixture");
File.WriteAllText(
Path.Combine(pluginDirectory, "throw-after-register"),
string.Empty);
string statusPath = Path.Combine(temporary.Path, "status.jsonl");
var events = new WorldEvents();
var ui = new BufferedUiRegistry();
var host = new AppPluginHost(
new CapturingLogger(),
new WorldGameState(),
events,
new SelectionState(),
ui);
using GraphicalPluginSession plugins = GraphicalPluginSession.Create(
paths,
[ThrowingId],
"gui-session",
host,
new SessionStatusWriter(statusPath));
plugins.Start();
Assert.Equal(0, plugins.LoadedCount);
Assert.Empty(ui.Drain());
Assert.False(File.Exists(statusPath));
Assert.Equal(0, ui.RegistrationCount);
events.FireEntitySpawned(new WorldEntitySnapshot(
1u,
2u,
default,
System.Numerics.Quaternion.Identity));
Assert.False(File.Exists(
Path.Combine(pluginDirectory, "unexpected-callback")));
Assert.Equal(
["started", "pluginFailed"],
EventNames(ReadStatuses(statusPath)));
WeakReference context = Assert.Single(
plugins.CaptureLoadContextWeakReferences());
plugins.Dispose();
Collect(context);
Assert.False(context.IsAlive);
}
private static ApplicationPathSet Paths(string root) => new(
@ -114,11 +179,14 @@ public sealed class GraphicalPluginSessionTests
private static string[] EventNames(IEnumerable<JsonElement> events) =>
events.Select(static item => item.GetProperty("e").GetString()!).ToArray();
private static void InstallFixture(string root, string id)
private static string InstallFixture(
string root,
string id,
string directoryName = "host-fixture")
{
string source = FixtureAssemblyPath();
Assert.True(File.Exists(source), $"fixture DLL not found: {source}");
string pluginDirectory = Path.Combine(root, "host-fixture");
string pluginDirectory = Path.Combine(root, directoryName);
Directory.CreateDirectory(pluginDirectory);
string fileName = Path.GetFileName(source);
File.Copy(source, Path.Combine(pluginDirectory, fileName));
@ -132,6 +200,7 @@ public sealed class GraphicalPluginSessionTests
entryDll = fileName,
apiVersion = 1,
}));
return pluginDirectory;
}
private static string FixtureAssemblyPath()
@ -195,8 +264,22 @@ public sealed class GraphicalPluginSessionTests
public void Dispose()
{
if (Directory.Exists(Path))
Directory.Delete(Path, recursive: true);
for (int attempt = 0; Directory.Exists(Path); attempt++)
{
try
{
Directory.Delete(Path, recursive: true);
return;
}
catch (Exception error)
when (error is IOException or UnauthorizedAccessException
&& attempt < 9)
{
GC.Collect();
GC.WaitForPendingFinalizers();
Thread.Sleep(10);
}
}
}
}
}