#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
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()
{
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 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; }
}
}
}