#nullable enable using System; using System.Collections.Generic; using NUnit.Framework; using ShrinkContext; namespace ShrinkContext.Tests { /// /// 声明式加载器(论文 5.2 的原型子集): /// 增量协调、幂等、退役、disabled 开关、组件/配置变化重建、依赖缺失等待、隔离域条目。 /// public class LoaderTests { private ShrinkContextRuntime _runtime = null!; private ShrinkComponentCatalog _catalog = null!; private ShrinkContextLoader _loader = null!; [SetUp] public void SetUp() { _runtime = new ShrinkContextRuntime(); _catalog = new ShrinkComponentCatalog(); _catalog.Register("provider", () => new ConfigProviderComponent("provider", "svc")); _catalog.Register("provider.alt", () => new ConfigProviderComponent("provider.alt", "svc")); _catalog.Register("consumer", () => new ConsumerComponent("consumer", "svc")); _loader = new ShrinkContextLoader(_runtime, _catalog); } [Test] public void Apply_CreatesEntriesAndActivates() { TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "v1"), new("consumer-entry", "consumer"), })); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var providerFiber)); Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber)); Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); } [Test] public void Apply_IsIdempotent_SecondApplyDoesNotReload() { var config = new List { new("provider-entry", "provider", "v1"), new("consumer-entry", "consumer"), }; TestAwait.Run(_loader.ApplyAsync(config)); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber)); TestAwait.Run(_loader.ApplyAsync(config)); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var secondFiber)); Assert.AreSame(firstFiber, secondFiber, "无变化的条目不得重建纤程"); var providerComponent = (ConfigProviderComponent)secondFiber.Component; Assert.AreEqual(1, providerComponent.ApplyCount, "幂等协调不触发重载"); } [Test] public void RemovedEntry_RetiresFiber_DependentDeactivatesButStaysManaged() { TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "v1"), new("consumer-entry", "consumer"), })); TestAwait.Run(_loader.ApplyAsync(new List { new("consumer-entry", "consumer"), })); Assert.IsFalse(_loader.TryGetFiber("provider-entry", out _), "消失的条目不再被管理"); Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber)); Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State, "提供者退役后依赖者被响应式停用,但条目仍被管理、等待依赖回归"); // 依赖回归后无需重建条目即可重新激活 TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "v1"), new("consumer-entry", "consumer"), })); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); } [Test] public void DisabledEntry_UnloadsAndReloadsOnEnable() { var enabled = new List { new("provider-entry", "provider", "v1") }; TestAwait.Run(_loader.ApplyAsync(enabled)); TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "v1", disabled: true), })); Assert.IsFalse(_loader.TryGetFiber("provider-entry", out _), "disabled 条目不持有纤程,但条目本身仍被管理"); CollectionAssert.Contains(_loader.ManagedEntryIds, "provider-entry"); TestAwait.Run(_loader.ApplyAsync(enabled)); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var reloadedFiber)); Assert.AreEqual(ShrinkFiberState.Active, reloadedFiber.State, "取消 disabled 后重新加载"); } [Test] public void ComponentChange_RebuildsEntry() { TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "v1"), new("consumer-entry", "consumer"), })); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber)); TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider.alt", "v2"), new("consumer-entry", "consumer"), })); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var rebuiltFiber)); Assert.AreNotSame(firstFiber, rebuiltFiber, "组件名变化必须重建条目(论文 id/url → 重建)"); Assert.AreEqual(ShrinkFiberState.Active, rebuiltFiber.State); Assert.AreEqual("provider.alt", rebuiltFiber.Component.Name); Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber)); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); var consumer = (ConsumerComponent)consumerFiber.Component; Assert.AreEqual("v2", consumer.LastSeen, "依赖者切换到重建后的新提供者"); } [Test] public void ConfigChange_ReloadsEntryWithNewConfig() { TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "v1"), new("consumer-entry", "consumer"), })); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber)); TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "v2"), new("consumer-entry", "consumer"), })); // 原型语义:配置变化走重建(组件自决 diff 属后续阶段) Assert.AreEqual(ShrinkFiberState.Inactive, firstFiber.State); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var rebuiltFiber)); Assert.AreNotSame(firstFiber, rebuiltFiber); Assert.AreEqual("v2", rebuiltFiber.Config, "重建后的纤程携带新配置"); Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber)); var consumer = (ConsumerComponent)consumerFiber.Component; Assert.AreEqual("v2", consumer.LastSeen, "依赖者看到新配置的值"); } [Test] public void MissingDependency_WaitsWithoutError_ThenActivatesWhenProvided() { // 首轮只有消费者:不抛“依赖缺失”异常,条目保持被管理、纤程 Inactive TestAwait.Run(_loader.ApplyAsync(new List { new("consumer-entry", "consumer"), })); Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber)); Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State); // 次轮补上提供者:依赖者被响应式激活,无需重发消费者条目 TestAwait.Run(_loader.ApplyAsync(new List { new("consumer-entry", "consumer"), new("provider-entry", "provider", "late"), })); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); Assert.AreEqual("late", ((ConsumerComponent)consumerFiber.Component).LastSeen); } [Test] public void DuplicateEntryIds_Throw() { Assert.Throws(() => TestAwait.Run(_loader.ApplyAsync(new List { new("dup", "provider", "v1"), new("dup", "provider", "v2"), }))); } [Test] public void UnknownComponent_ThrowsWithEntryContext() { var ex = Assert.Throws(() => TestAwait.Run(_loader.ApplyAsync( new List { new("entry", "no.such.component") }))); StringAssert.Contains("no.such.component", ex!.Message); StringAssert.Contains("entry", ex.Message); } [Test] public void IsolatedEntries_SameKeyCoexistInDifferentRealms() { TestAwait.Run(_loader.ApplyAsync(new List { new("p1", "provider", "from-r1", isolate: new Dictionary { ["svc"] = "r1" }), new("p2", "provider", "from-r2", isolate: new Dictionary { ["svc"] = "r2" }), new("c2", "consumer", isolate: new Dictionary { ["svc"] = "r2" }), })); Assert.IsTrue(_loader.TryGetFiber("p1", out var f1)); Assert.IsTrue(_loader.TryGetFiber("p2", out var f2)); Assert.AreEqual(ShrinkFiberState.Active, f1.State, "不同隔离域的同键供给互不冲突"); Assert.AreEqual(ShrinkFiberState.Active, f2.State); Assert.IsTrue(_loader.TryGetFiber("c2", out var consumerFiber)); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); Assert.AreEqual("from-r2", ((ConsumerComponent)consumerFiber.Component).LastSeen, "消费者按自身隔离域解析到 r2 的绑定"); } } }