Files
cneicy 6eebb8e8e4
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:49:58 +08:00

324 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 后注册表必须为空");
}
}
}