#nullable enable using System; using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; using NUnit.Framework; using ShrinkContext; namespace ShrinkContext.Tests { /// /// 纤程生命周期(论文算法 4、5 与元理论 4.4 的可观察行为): /// 依赖驱动激活/停用、提供者替换重载、drain-before-inverse 次序、 /// 父子级联、供给冲突、循环依赖静止、惯性链式转换、分步部分回滚、错误路径回滚。 /// public class FiberLifecycleTests { private ShrinkContextRuntime _runtime = null!; [SetUp] public void SetUp() { _runtime = new ShrinkContextRuntime(); } [Test] public void ProviderReady_ConsumerActivates() { var provider = new ProviderComponent("provider", "svc", "v1"); var consumer = new ConsumerComponent("consumer", "svc"); var providerFiber = _runtime.Use(provider); var consumerFiber = _runtime.Use(consumer); Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); Assert.AreEqual("v1", consumer.LastSeen); Assert.AreEqual(1, consumer.LoadCount); } [Test] public void MissingDependency_StaysInactiveThenActivatesOnLateProvider() { var consumer = new ConsumerComponent("consumer", "svc"); var consumerFiber = _runtime.Use(consumer); Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State, "依赖缺失时静默保持非活动,不抛错、不加载"); Assert.AreEqual(0, consumer.LoadCount); var provider = new ProviderComponent("provider", "svc", "late"); var providerFiber = _runtime.Use(provider); Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State, "提供者出现后依赖者被响应式激活(activating 转换)"); Assert.AreEqual("late", consumer.LastSeen); } [Test] public void RetireProvider_DrainsDependentsBeforeOwnInverses() { var log = new List(); var provider = new OrderProviderComponent("provider", "svc", "v1", log); var consumer = new OrderConsumerComponent("consumer", "svc", log); var providerFiber = _runtime.Use(provider); var consumerFiber = _runtime.Use(consumer); CollectionAssert.AreEqual(new[] { "provider-effect", "consumer-load" }, log); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); TestAwait.Run(_runtime.RetireAsync(providerFiber)); CollectionAssert.AreEqual( new[] { "provider-effect", "consumer-load", "consumer-undo", "provider-undo" }, log, "依赖者必须先于提供者的逆操作完成卸载(L-Unload 守卫:drain dependents)"); Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State); Assert.AreEqual(ShrinkFiberState.Inactive, providerFiber.State); } [Test] public void ProviderReplacement_ReloadsDependent() { var first = new ProviderComponent("p1", "svc", "v1"); var consumer = new ConsumerComponent("consumer", "svc"); var firstFiber = _runtime.Use(first); var consumerFiber = _runtime.Use(consumer); Assert.AreEqual("v1", consumer.LastSeen); TestAwait.Run(_runtime.RetireAsync(firstFiber)); Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State); var second = new ProviderComponent("p2", "svc", "v2"); var secondFiber = _runtime.Use(second); Assert.AreEqual(ShrinkFiberState.Active, secondFiber.State); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); Assert.AreEqual("v2", consumer.LastSeen); Assert.AreEqual(2, consumer.LoadCount, "提供者替换必须触发依赖者重载"); } [Test] public void ProviderReplacement_SameValueStillReloads_UidSemantics() { var first = new ProviderComponent("p1", "svc", "same"); var consumer = new ConsumerComponent("consumer", "svc"); var firstFiber = _runtime.Use(first); var consumerFiber = _runtime.Use(consumer); TestAwait.Run(_runtime.RetireAsync(firstFiber)); var second = new ProviderComponent("p2", "svc", "same"); _runtime.Use(second); Assert.AreEqual(2, consumer.LoadCount, "目标按提供者 uid 而非值比较:同值不同提供者的替换也要重载(定义 46)"); Assert.AreEqual("same", consumer.LastSeen); } [Test] public void ParentRetire_CascadesToChildFiber() { var parent = new ProviderComponent("parent", "parent.svc", "pv"); var parentFiber = _runtime.Use(parent); var child = new ConsumerComponent("child", "parent.svc"); var childFiber = _runtime.Use(child, null, parentFiber.Ctx); Assert.AreEqual(ShrinkFiberState.Active, childFiber.State); Assert.AreSame(parentFiber, childFiber.Parent); TestAwait.Run(_runtime.RetireAsync(parentFiber)); Assert.AreEqual(ShrinkFiberState.Inactive, parentFiber.State); Assert.AreEqual(ShrinkFiberState.Inactive, childFiber.State, "父纤程卸载必须级联卸载子纤程(算法 4:实例化是父的可逆效应)"); } [Test] public void SupplyConflict_SecondProviderFailsAndKeepsFirst() { var first = new ProviderComponent("p1", "svc", "v1"); var firstFiber = _runtime.Use(first); Assert.AreEqual(ShrinkFiberState.Active, firstFiber.State); var second = new ProviderComponent("p2", "svc", "v2"); var secondFiber = _runtime.Use(second); Assert.AreEqual(ShrinkFiberState.Inactive, secondFiber.State); Assert.IsInstanceOf(secondFiber.LastError, "两个活跃纤程供给同一 realm 必须在生成阶段报错(供给不相交)"); Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, "svc", out var value)); Assert.AreEqual("v1", value, "冲突失败后第一个提供者的绑定必须保持完好"); Assert.AreEqual(ShrinkFiberState.Active, firstFiber.State); } [Test] public void UndeclaredSupply_FailsFiberAndLeavesNoBinding() { var fiber = _runtime.Use(new UndeclaredProviderComponent()); Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State); Assert.IsInstanceOf(fiber.LastError); Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, "not-declared", out _)); var snapshot = _runtime.CaptureDiagnostic(); var diagnostic = snapshot.Fibers.Single(item => item.Uid == fiber.Uid); Assert.AreEqual(ShrinkFiberDiagnosticStatus.Failed, diagnostic.Status); StringAssert.Contains("not-declared", diagnostic.ErrorMessage); } [Test] public void DiagnosticSnapshot_ExplainsWaitingDependencyAndPotentialProvider() { var waiting = _runtime.Use(new ConsumerComponent("waiting", "svc")); var blockedProvider = _runtime.Use(new ProviderComponent("blocked-provider", "svc") { Inject = new[] { "missing" } }); var snapshot = _runtime.CaptureDiagnostic(); var waitingDiagnostic = snapshot.Fibers.Single(item => item.Uid == waiting.Uid); Assert.AreEqual(ShrinkFiberDiagnosticStatus.Waiting, waitingDiagnostic.Status); Assert.AreEqual(1, waitingDiagnostic.Dependencies.Count); Assert.IsFalse(waitingDiagnostic.Dependencies[0].IsSatisfied); CollectionAssert.Contains(waitingDiagnostic.Dependencies[0].PotentialProviderUids, blockedProvider.Uid, "等待链应显示声明了该键、但自身依赖未满足的潜在提供者"); } [Test] public void NotifyIndex_VisitsOnlyConsumersOfChangedKey() { var target = new ConsumerComponent("target", "target-key"); _runtime.Use(target); for (var i = 0; i < 40; i++) _runtime.Use(new ConsumerComponent("unrelated-" + i, "other-" + i)); _runtime.Use(new ProviderComponent("provider", "target-key", "value")); var notifications = _runtime.CaptureDiagnostic().Notifications; Assert.AreEqual(1, notifications.LastCandidateCount, "notify 应从 key 倒排索引取得候选,不扫描 40 个无关 fiber"); Assert.AreEqual(41, notifications.IndexedKeyCount); Assert.AreEqual("value", target.LastSeen); } [Test] public void CircularDependencies_BothStayInactiveWithoutError() { var a = new ProviderComponent("a", "ka") { Inject = new[] { "kb" } }; var b = new ProviderComponent("b", "kb") { Inject = new[] { "ka" } }; var fiberA = _runtime.Use(a); var fiberB = _runtime.Use(b); Assert.AreEqual(ShrinkFiberState.Inactive, fiberA.State); Assert.AreEqual(ShrinkFiberState.Inactive, fiberB.State); Assert.IsNull(fiberA.LastError); Assert.IsNull(fiberB.LastError, "依赖循环表现为可预测的永久非活动,而不是死锁或异常(论文 6.5)"); } [Test] public void Inertia_MidLoadRetire_NeverActivatesAndChainsToUnload() { var gate1 = new UniTaskCompletionSource(); var gate2 = new UniTaskCompletionSource(); var first = new ProviderComponent("p1", "svc", "v1") { OnApply = () => gate1.Task }; var consumer = new ConsumerComponent("consumer", "svc"); var firstFiber = _runtime.Use(first); Assert.AreEqual(ShrinkFiberState.Loading, firstFiber.State); var consumerFiber = _runtime.Use(consumer); Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State, "提供者仍在装载(未 Active)时,依赖者不可见其绑定"); // 装载中途退役:目标翻转 ⊥,但进行中的转换继续跑到惯性边界 var retireTask = _runtime.RetireAsync(firstFiber); Assert.AreEqual(ShrinkFiberState.Loading, firstFiber.State, "惯性:已进入的转换继续运行,之后才响应新目标"); var second = new ProviderComponent("p2", "svc", "v2") { OnApply = () => gate2.Task }; var secondFiber = _runtime.Use(second); gate2.TrySetResult(); Assert.AreEqual(ShrinkFiberState.Active, secondFiber.State); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); Assert.AreEqual("v2", consumer.LastSeen, "依赖者切换到新提供者"); gate1.TrySetResult(); TestAwait.Run(retireTask); Assert.AreEqual(ShrinkFiberState.Inactive, firstFiber.State); Assert.IsFalse(firstFiber.EverActive, "装载中途目标变化的组件不得进入 Active,必须链式转入卸载并回滚"); Assert.IsNotNull(firstFiber.LastError, "旧提供者补装时撞上活跃的新提供者,按供给冲突失败"); Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, "svc", out var value)); Assert.AreEqual("v2", value, "失败路径不得破坏新提供者的绑定"); Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State); } [Test] public void IterativeApply_TargetChangeInterruptsAndRollsBackExecutedSteps() { var component = new IterativeComponent("iterative", "svc", 3); var fiber = _runtime.Use(component); Assert.AreEqual(ShrinkFiberState.Loading, fiber.State); component.Gates[0].TrySetResult(); Assert.AreEqual(1, component.ExecutedSteps); // 转换仍在步进器等待中时翻转目标(不阻塞:延续由后续闸门驱动) var retireTask = _runtime.RetireAsync(fiber); // 放行第 2 步:执行完该步后,步进边界守卫发现目标已变 → 中断 + 回滚已执行步骤 component.Gates[1].TrySetResult(); TestAwait.Run(retireTask); Assert.AreEqual(2, component.ExecutedSteps, "守卫在第 3 步之前中断迭代"); CollectionAssert.AreEqual( new[] { "step1", "step2", "undo2", "undo1" }, component.Steps, "部分回滚:仅已执行步骤的逆操作按 LIFO 运行(定理 64 转换内中断)"); Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State); Assert.IsFalse(fiber.EverActive); Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, "svc", out _), "步骤中的 set 效应必须随回滚撤回"); } [Test] public void ApplyFailure_RollsBackPartialEffects() { var failing = new FailingComponent("failing", "svc"); var fiber = _runtime.Use(failing); Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State); Assert.IsFalse(fiber.EverActive); Assert.IsInstanceOf(fiber.LastError); Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, "svc", out _), "apply 抛异常前注册的效应必须全部回滚(L-Raise → 卸载路径)"); } [Test] public void DomainReset_DefaultRuntimeIsRebuilt() { ShrinkContextDefaults.ResetForTesting(); var runtime = ShrinkContextDefaults.Default; runtime.Use(new ProviderComponent("p", "k", "v")); ShrinkContextDefaults.ResetForTesting(); var rebuilt = ShrinkContextDefaults.Default; Assert.AreNotSame(runtime, rebuilt); Assert.AreEqual(0, rebuilt.Fibers.Count, "Domain Reload 后注册表必须为空"); } } }