#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")); _catalog.Register("failing", () => new FailingComponent("failing", "svc")); _catalog.Register("policy.provider", () => new PolicyProviderComponent("policy.service")); _catalog.Register("policy.consumer", () => new PolicyConsumerComponent("policy.service")); _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 的绑定"); } [Test] public void IsolatedProviderReplacement_DoesNotReloadOtherRealmConsumer() { var isolateR1 = new Dictionary { ["svc"] = "r1" }; var isolateR2 = new Dictionary { ["svc"] = "r2" }; TestAwait.Run(_loader.ApplyAsync(new List { new("p1", "provider", "r1-v1", isolate: isolateR1), new("p2", "provider", "r2-v1", isolate: isolateR2), new("c2", "consumer", isolate: isolateR2), })); Assert.IsTrue(_loader.TryGetFiber("c2", out var consumerFiber)); var consumer = (ConsumerComponent)consumerFiber.Component; Assert.AreEqual(1, consumer.LoadCount); TestAwait.Run(_loader.ApplyAsync(new List { new("p1", "provider", "r1-v2", isolate: isolateR1), new("p2", "provider", "r2-v1", isolate: isolateR2), new("c2", "consumer", isolate: isolateR2), })); Assert.AreEqual(1, consumer.LoadCount, "r1 提供者替换不得触发只解析 r2 的消费者重载"); Assert.AreEqual("r2-v1", consumer.LastSeen); } [Test] public void InterceptUpdate_ChangesAccessWithoutReloadingFiber() { var suffixA = new Dictionary> { ["policy.service"] = new Dictionary { ["suffix"] = "-a" } }; var suffixB = new Dictionary> { ["policy.service"] = new Dictionary { ["suffix"] = "-b" } }; TestAwait.Run(_loader.ApplyAsync(new List { new("policy-provider", "policy.provider", "base"), new("policy-consumer", "policy.consumer", intercept: suffixA), })); Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var firstFiber)); var consumer = (PolicyConsumerComponent)firstFiber.Component; Assert.AreEqual("base-a", consumer.Read()); Assert.AreEqual(1, consumer.ApplyCount); TestAwait.Run(_loader.ApplyAsync(new List { new("policy-provider", "policy.provider", "base"), new("policy-consumer", "policy.consumer", intercept: suffixB), })); Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var sameFiber)); Assert.AreSame(firstFiber, sameFiber); Assert.AreEqual(1, consumer.ApplyCount, "intercept 元数据变化不应触发 reload"); Assert.AreEqual("base-b", consumer.Read(), "访问时应读取更新后的 metadata"); } [Test] public void FailedReplacement_RestoresPreviousLoaderCompositionAndReportsTransaction() { TestAwait.Run(_loader.ApplyAsync(new List { new("provider-entry", "provider", "stable") })); Assert.Throws(() => TestAwait.Run(_loader.ApplyAsync( new List { new("provider-entry", "failing") }))); Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var restored)); Assert.AreEqual(ShrinkFiberState.Active, restored.State); Assert.AreEqual("stable", restored.Config); Assert.IsNotNull(_loader.LastTransaction); Assert.AreEqual(ShrinkLoaderTransactionPhase.Failed, _loader.LastTransaction!.Phase); Assert.IsTrue(_loader.LastTransaction.RestoreAttempted); Assert.IsTrue(_loader.LastTransaction.PreviousCompositionRestored); StringAssert.Contains("failed during apply", _loader.LastTransaction.ErrorMessage); } } }