feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkApp;
|
||||
using ShrinkContext;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace ShrinkContext.AppAdapter.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// ShrinkApp 安装器适配器:
|
||||
/// 依赖缺失等待而非抛错(对照 ShrinkAppHost.SortInstallers 的行为差异)、
|
||||
/// 依赖就绪顺序保证、重复 ModuleId 退化为供给冲突而非宿主级失败、服务注册可见性。
|
||||
/// </summary>
|
||||
public class ShrinkAppInstallerComponentTests
|
||||
{
|
||||
private ShrinkContextRuntime _runtime = null!;
|
||||
private ShrinkAppServices _services = null!;
|
||||
private ShrinkAppSettings _settings = null!;
|
||||
private static long _sequence;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_runtime = new ShrinkContextRuntime();
|
||||
_services = new ShrinkAppServices();
|
||||
_settings = ScriptableObject.CreateInstance<ShrinkAppSettings>();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_settings != null)
|
||||
Object.DestroyImmediate(_settings);
|
||||
}
|
||||
|
||||
private ShrinkFiber UseInstaller(FakeInstaller installer)
|
||||
{
|
||||
return _runtime.Use(new ShrinkAppInstallerComponent(installer, _settings), _services);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MissingDependency_WaitsInsteadOfThrowing()
|
||||
{
|
||||
var dependent = new FakeInstaller("app.b", "b-service") { DependsOn = new[] { "app.a" } };
|
||||
var fiber = UseInstaller(dependent);
|
||||
|
||||
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State,
|
||||
"依赖模块未就绪时安装器保持非活动(对照:ShrinkAppHost 在排序期直接抛依赖缺失)");
|
||||
Assert.AreEqual(0, dependent.RegisterCount);
|
||||
Assert.AreEqual(0, dependent.InitCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DependencyArrives_DependentActivates_AfterProvider()
|
||||
{
|
||||
var provider = new FakeInstaller("app.a", "a-service");
|
||||
var dependent = new FakeInstaller("app.b", "b-service") { DependsOn = new[] { "app.a" } };
|
||||
|
||||
var dependentFiber = UseInstaller(dependent);
|
||||
var providerFiber = UseInstaller(provider);
|
||||
|
||||
Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State);
|
||||
Assert.AreEqual(ShrinkFiberState.Active, dependentFiber.State,
|
||||
"依赖键由提供者发布后,依赖安装器被响应式激活");
|
||||
|
||||
// 激活次序由依赖关系保证:提供者先完成 Register+Init
|
||||
Assert.AreEqual(1, provider.InitCount);
|
||||
Assert.AreEqual(1, dependent.InitCount);
|
||||
Assert.Less(provider.CompletedAt, dependent.CompletedAt,
|
||||
"依赖者的初始化必须晚于其依赖的提供者(空间可组合性排序)");
|
||||
|
||||
Assert.IsTrue(_services.TryGet<FakeService>(out var service));
|
||||
Assert.AreEqual("b-service", service!.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateModuleId_SecondFailsAsSupplyConflict_FirstStaysActive()
|
||||
{
|
||||
var first = new FakeInstaller("app.dup", "first");
|
||||
var second = new FakeInstaller("app.dup", "second");
|
||||
|
||||
var firstFiber = UseInstaller(first);
|
||||
var secondFiber = UseInstaller(second);
|
||||
|
||||
Assert.AreEqual(ShrinkFiberState.Active, firstFiber.State);
|
||||
Assert.AreEqual(ShrinkFiberState.Inactive, secondFiber.State);
|
||||
Assert.IsInstanceOf<ShrinkSupplyConflictException>(secondFiber.LastError,
|
||||
"重复 ModuleId 退化为该纤程的供给冲突失败,而不是宿主级异常(对照 ShrinkHost 的 Duplicate installer 抛错)");
|
||||
Assert.AreEqual(0, second.InitCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RetireProvider_DependentInstallerDeactivates()
|
||||
{
|
||||
var provider = new FakeInstaller("app.a", "a-service");
|
||||
var dependent = new FakeInstaller("app.b", "b-service") { DependsOn = new[] { "app.a" } };
|
||||
|
||||
var providerFiber = UseInstaller(provider);
|
||||
var dependentFiber = UseInstaller(dependent);
|
||||
Assert.AreEqual(ShrinkFiberState.Active, dependentFiber.State);
|
||||
|
||||
TestAwait.Run(_runtime.RetireAsync(providerFiber));
|
||||
|
||||
Assert.AreEqual(ShrinkFiberState.Inactive, dependentFiber.State,
|
||||
"提供者退役后依赖安装器自动停用(服务注销为已知边界,见组件注释)");
|
||||
}
|
||||
[Test]
|
||||
public void MissingServicesConfig_FailsWithClearError()
|
||||
{
|
||||
var installer = new FakeInstaller("app.x", "x");
|
||||
var component = new ShrinkAppInstallerComponent(installer, _settings);
|
||||
var fiber = _runtime.Use(component, config: null!);
|
||||
|
||||
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State);
|
||||
Assert.IsInstanceOf<InvalidOperationException>(fiber.LastError);
|
||||
StringAssert.Contains("ShrinkAppServices", fiber.LastError!.Message);
|
||||
}
|
||||
|
||||
// ---- 测试替身 ----
|
||||
|
||||
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 RegisterCount { get; private set; }
|
||||
public int InitCount { get; private set; }
|
||||
public long CompletedAt { get; private set; }
|
||||
private string ServiceValue { get; }
|
||||
|
||||
public void RegisterServices(ShrinkAppContext context)
|
||||
{
|
||||
RegisterCount++;
|
||||
context.Services.Register(new FakeService(ServiceValue));
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync(ShrinkAppContext context)
|
||||
{
|
||||
InitCount++;
|
||||
CompletedAt = ++_sequence;
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeService
|
||||
{
|
||||
public FakeService(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>跨程序集测试辅助(与 ShrinkContext.Core.Tests.TestAwait 同语义)。</summary>
|
||||
public static class TestAwait
|
||||
{
|
||||
public static void Run(UniTask task)
|
||||
{
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user