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(); 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(); 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(); 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 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 Entities => []; } private sealed class StubEvents : IEvents { public event Action 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); } } }