324 lines
14 KiB
C#
324 lines
14 KiB
C#
#nullable enable
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Cysharp.Threading.Tasks;
|
||
using NUnit.Framework;
|
||
using ShrinkContext;
|
||
|
||
namespace ShrinkContext.Tests
|
||
{
|
||
/// <summary>
|
||
/// 纤程生命周期(论文算法 4、5 与元理论 4.4 的可观察行为):
|
||
/// 依赖驱动激活/停用、提供者替换重载、drain-before-inverse 次序、
|
||
/// 父子级联、供给冲突、循环依赖静止、惯性链式转换、分步部分回滚、错误路径回滚。
|
||
/// </summary>
|
||
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<string>();
|
||
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<ShrinkSupplyConflictException>(secondFiber.LastError,
|
||
"两个活跃纤程供给同一 realm 必须在生成阶段报错(供给不相交)");
|
||
|
||
Assert.IsTrue(_runtime.TryGetRaw<string>(_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<ShrinkUndeclaredSupplyException>(fiber.LastError);
|
||
Assert.IsFalse(_runtime.TryGetRaw<int>(_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<string>(_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<string>(_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<InvalidOperationException>(fiber.LastError);
|
||
Assert.IsFalse(_runtime.TryGetRaw<string>(_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 后注册表必须为空");
|
||
}
|
||
}
|
||
}
|