Files
ShrinkContext.AppAdapter/Tests/ShrinkAppInstallerComponentTests.cs
cneicy 2e8cc588c4
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:49:53 +08:00

180 lines
6.7 KiB
C#

#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();
}
public static T Run<T>(UniTask<T> task)
{
return task.GetAwaiter().GetResult();
}
}
}