feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
#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
|
||||
{
|
||||
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" } });
|
||||
|
||||
ShrinkAppStartedEvent? started = null;
|
||||
using (var subscription = EventBus.SubscribeEvent<ShrinkAppStartedEvent>(e => started = e))
|
||||
{
|
||||
TestAwait.Run(host.StartAsync());
|
||||
}
|
||||
|
||||
Assert.IsTrue(host.IsRunning);
|
||||
Assert.IsTrue(host.IsModuleActive("app.a"));
|
||||
Assert.IsTrue(host.IsModuleActive("app.b"), "依赖模块在提供者激活后自动激活");
|
||||
Assert.IsNotNull(started);
|
||||
CollectionAssert.AreEquivalent(new[] { "app.a", "app.b" }, started!.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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user