215 lines
7.9 KiB
C#
215 lines
7.9 KiB
C#
#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
|
|
{
|
|
/// <summary>
|
|
/// ShrinkAppLoaderHost 端到端:
|
|
/// 启动激活与 Started 事件、运行中 disable/enable 不重启宿主、设置级初始禁用、
|
|
/// 未知模块报错、重复 ModuleId 构造期失败、Shutdown。
|
|
/// </summary>
|
|
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<ShrinkAppSettings>();
|
|
}
|
|
|
|
[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<InvalidOperationException>(() =>
|
|
TestAwait.Run(host.SetModuleDisabledAsync("app.no-such", true)));
|
|
}
|
|
|
|
[Test]
|
|
public void DuplicateModuleIds_ThrowAtConstruction()
|
|
{
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
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<NativeReplacementComponent>(fiber.Component);
|
|
}
|
|
|
|
/// <summary>替换安装器的原生测试组件(发布同一模块键)。</summary>
|
|
private sealed class NativeReplacementComponent : IShrinkComponent
|
|
{
|
|
public string Name => "app.a";
|
|
public System.Collections.Generic.IReadOnlyList<string> Inject => System.Array.Empty<string>();
|
|
public System.Collections.Generic.IReadOnlyList<string> 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<string> DependsOn { get; set; } = Array.Empty<string>();
|
|
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; }
|
|
}
|
|
}
|
|
}
|