#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Cysharp.Threading.Tasks; using NUnit.Framework; using ShrinkApp; using ShrinkContext; using ShrinkEventBus; using UnityEngine; using Object = UnityEngine.Object; namespace ShrinkContext.AppAdapter.Tests { /// /// ShrinkAppLoaderHost 端到端: /// 启动激活与 Started 事件、运行中 disable/enable 不重启宿主、设置级初始禁用、 /// 未知模块报错、重复 ModuleId 构造期失败、Shutdown。 /// public class ShrinkAppLoaderHostTests { [ShrinkEventSubscriber(DefaultBus = "game")] private sealed class AppStartedProbe { public ShrinkAppStartedEvent? Value { get; private set; } [ShrinkSubscribe] private void OnStarted(ShrinkAppStartedEvent value) => Value = value; } private ShrinkAppSettings _settings = null!; [SetUp] public void SetUp() { _settings = ScriptableObject.CreateInstance(); } [TearDown] public void TearDown() { ResetBootstrapperState(); SetSharedSettings(null); if (_settings != null) Object.DestroyImmediate(_settings); } private ShrinkAppLoaderHost CreateHost(params FakeInstaller[] installers) { return new ShrinkAppLoaderHost(_settings, installers); } [Test] public void StartAsync_ActivatesModules_AndPublishesStartedEvent() { var host = CreateHost( new FakeInstaller("app.a", "a"), new FakeInstaller("app.b", "b") { DependsOn = new[] { "app.a" } }); var probe = new AppStartedProbe(); using (EventBus.Attach(probe)) { TestAwait.Run(host.StartAsync()); } Assert.IsTrue(host.IsRunning); Assert.IsTrue(host.IsModuleActive("app.a")); Assert.IsTrue(host.IsModuleActive("app.b"), "依赖模块在提供者激活后自动激活"); Assert.IsNotNull(probe.Value); CollectionAssert.AreEquivalent(new[] { "app.a", "app.b" }, probe.Value!.ModuleIds); } [Test] public void WaitUntilStartedAsync_AllConcurrentWaitersCompleteTogether() { PrepareBootstrapperWaitState(); var first = ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync(); var second = ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync(); CompleteBootstrapperStartup(null); TestAwait.Run(UniTask.WhenAll(first, second)); Assert.IsNull(ShrinkAppLoaderBootstrapper.StartError); } [Test] public void WaitUntilStartedAsync_PropagatesObservableStartupError() { PrepareBootstrapperWaitState(); var expected = new InvalidOperationException("test startup failure"); CompleteBootstrapperStartup(expected); var actual = Assert.Throws(() => TestAwait.Run(ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync())); Assert.AreSame(expected, actual); Assert.AreSame(expected, ShrinkAppLoaderBootstrapper.StartError); } [Test] public void SetModuleDisabled_RetiresModuleAndDependents_EnableReloadsWithoutRestart() { var provider = new FakeInstaller("app.a", "a"); var dependent = new FakeInstaller("app.b", "b") { DependsOn = new[] { "app.a" } }; var bystander = new FakeInstaller("app.c", "c"); var host = CreateHost(provider, dependent, bystander); TestAwait.Run(host.StartAsync()); Assert.AreEqual(3, host.ActiveModuleIds.Count); TestAwait.Run(host.SetModuleDisabledAsync("app.a", true)); Assert.IsTrue(host.IsModuleDisabled("app.a")); Assert.IsFalse(host.IsModuleActive("app.a")); Assert.IsFalse(host.IsModuleActive("app.b"), "依赖者随提供者停用"); Assert.IsTrue(host.IsModuleActive("app.c"), "无关模块不受影响,宿主不重启"); Assert.IsTrue(host.IsRunning); TestAwait.Run(host.SetModuleDisabledAsync("app.a", false)); Assert.IsTrue(host.IsModuleActive("app.a")); Assert.IsTrue(host.IsModuleActive("app.b"), "重新启用后依赖链整体复活"); Assert.AreEqual(2, provider.InitCount, "重启用会重新执行安装器初始化(重建语义)"); Assert.AreEqual(1, bystander.InitCount, "未受影响的模块不重复初始化"); } [Test] public void DisabledViaSettings_ExcludedInitially_CanEnableAtRuntime() { _settings.disabledModuleIds = new[] { "app.a" }; var provider = new FakeInstaller("app.a", "a"); var dependent = new FakeInstaller("app.b", "b") { DependsOn = new[] { "app.a" } }; var host = CreateHost(provider, dependent); TestAwait.Run(host.StartAsync()); Assert.IsTrue(host.IsModuleDisabled("app.a")); Assert.IsFalse(host.TryGetModuleFiber("app.a", out _)); Assert.IsTrue(host.IsModuleWaiting("app.b"), "依赖被禁用模块的安装器保持等待而非报错"); CollectionAssert.AreEqual(new[] { "app.b" }, host.WaitingModuleIds); TestAwait.Run(host.SetModuleDisabledAsync("app.a", false)); Assert.IsTrue(host.IsModuleActive("app.a")); Assert.IsTrue(host.IsModuleActive("app.b")); } [Test] public void UnknownModuleId_Throws() { var host = CreateHost(new FakeInstaller("app.a", "a")); TestAwait.Run(host.StartAsync()); Assert.Throws(() => TestAwait.Run(host.SetModuleDisabledAsync("app.no-such", true))); } [Test] public void DuplicateModuleIds_ThrowAtConstruction() { Assert.Throws(() => CreateHost(new FakeInstaller("app.dup", "a"), new FakeInstaller("app.dup", "b"))); } [Test] public void ShutdownAsync_RetiresEverything() { var provider = new FakeInstaller("app.a", "a"); var host = CreateHost(provider); TestAwait.Run(host.StartAsync()); Assert.IsTrue(host.IsModuleActive("app.a")); TestAwait.Run(host.ShutdownAsync()); Assert.IsFalse(host.IsRunning); Assert.IsFalse(host.IsModuleActive("app.a")); } [Test] public void OverrideModuleComponent_ReplacesInstallerWrapper() { var installer = new FakeInstaller("app.a", "a"); var host = CreateHost(installer); host.OverrideModuleComponent("app.a", () => new NativeReplacementComponent()); TestAwait.Run(host.StartAsync()); Assert.IsTrue(host.IsModuleActive("app.a")); Assert.AreEqual(0, installer.InitCount, "原生组件替换后安装器不再被包装执行"); Assert.IsTrue(host.TryGetModuleFiber("app.a", out var fiber)); Assert.IsInstanceOf(fiber.Component); } /// 替换安装器的原生测试组件(发布同一模块键)。 private sealed class NativeReplacementComponent : IShrinkComponent { public string Name => "app.a"; public System.Collections.Generic.IReadOnlyList Inject => System.Array.Empty(); public System.Collections.Generic.IReadOnlyList Provide => new[] { ShrinkAppInstallerComponent.ModuleKeyPrefix + "app.a" }; public UniTask ApplyAsync(ShrinkCtx ctx, object? config) { ctx.Set(Provide[0], Name); return UniTask.CompletedTask; } } private sealed class FakeInstaller : IShrinkAppModuleInstaller { public FakeInstaller(string moduleId, string serviceValue) { ModuleId = moduleId; ServiceValue = serviceValue; } public string ModuleId { get; } public int Order => 0; public IReadOnlyList DependsOn { get; set; } = Array.Empty(); public int InitCount { get; private set; } private string ServiceValue { get; } public void RegisterServices(ShrinkAppContext context) { context.Services.Register(new FakeService(ServiceValue)); } public UniTask InitializeAsync(ShrinkAppContext context) { InitCount++; return UniTask.CompletedTask; } } private sealed class FakeService { public FakeService(string value) { Value = value; } public string Value { get; } } private void PrepareBootstrapperWaitState() { ResetBootstrapperState(); _settings.hostingMode = ShrinkAppHostingMode.ContextLoader; SetSharedSettings(_settings); } private static void CompleteBootstrapperStartup(Exception? error) { var method = typeof(ShrinkAppLoaderBootstrapper).GetMethod( "CompleteStartup", BindingFlags.NonPublic | BindingFlags.Static); Assert.IsNotNull(method); method!.Invoke(null, new object?[] { error }); } private static void ResetBootstrapperState() { var method = typeof(ShrinkAppLoaderBootstrapper).GetMethod( "ResetStaticState", BindingFlags.NonPublic | BindingFlags.Static); Assert.IsNotNull(method); method!.Invoke(null, null); } private static void SetSharedSettings(ShrinkAppSettings? settings) { var property = typeof(ShrinkAppSettings).GetProperty( nameof(ShrinkAppSettings.Instance), BindingFlags.Public | BindingFlags.Static); Assert.IsNotNull(property); property!.SetValue(null, settings); } } }