132 lines
4.4 KiB
C#
132 lines
4.4 KiB
C#
#nullable enable
|
||
using System.Collections.Generic;
|
||
using Cysharp.Threading.Tasks;
|
||
using NUnit.Framework;
|
||
using ShrinkContext;
|
||
|
||
namespace ShrinkContext.Tests
|
||
{
|
||
/// <summary>
|
||
/// 可逆效应语义(论文 3.1 / 算法 1):
|
||
/// 定理 7(恢复到初始上下文)、定理 16(LIFO 逐步还原)、armed 单次执行、异步前向等待。
|
||
/// </summary>
|
||
public class EffectTrackingTests
|
||
{
|
||
private ShrinkContextRuntime _runtime = null!;
|
||
private ShrinkCtx _ctx = null!;
|
||
|
||
[SetUp]
|
||
public void SetUp()
|
||
{
|
||
_runtime = new ShrinkContextRuntime();
|
||
_ctx = _runtime.RootContext;
|
||
}
|
||
|
||
[Test]
|
||
public void DisposeAsync_RestoresInitialState_Theorem7()
|
||
{
|
||
var shared = new Dictionary<string, int>();
|
||
_ctx.Effect(() => shared["a"] = 1, () => shared.Remove("a"));
|
||
_ctx.Effect(() => shared["b"] = 2, () => shared.Remove("b"));
|
||
_ctx.Effect(() => shared["c"] = 3, () => shared.Remove("c"));
|
||
|
||
Assert.AreEqual(3, shared.Count);
|
||
|
||
TestAwait.Run(_ctx.DisposeAsync());
|
||
|
||
Assert.IsEmpty(shared, "累加器回滚后上下文必须恢复到初始状态(定理 7)");
|
||
}
|
||
|
||
[Test]
|
||
public void ManualDispose_UndoesOnlyItsOwnEffect()
|
||
{
|
||
var shared = new Dictionary<string, int>();
|
||
_ctx.Effect(() => shared["a"] = 1, () => shared.Remove("a"));
|
||
var effectB = _ctx.Effect(() => shared["b"] = 2, () => shared.Remove("b"));
|
||
_ctx.Effect(() => shared["c"] = 3, () => shared.Remove("c"));
|
||
|
||
TestAwait.Run(effectB.DisposeAsync());
|
||
|
||
Assert.IsFalse(shared.ContainsKey("b"));
|
||
Assert.IsTrue(shared.ContainsKey("a"));
|
||
Assert.IsTrue(shared.ContainsKey("c"), "选择性撤销不得影响其它效应(𝔈Γ 提升)");
|
||
|
||
TestAwait.Run(_ctx.DisposeAsync());
|
||
Assert.IsEmpty(shared);
|
||
}
|
||
|
||
[Test]
|
||
public void ManualDispose_IsIdempotent_ArmedSemantics()
|
||
{
|
||
var undoCount = 0;
|
||
var handle = _ctx.Effect(() => { }, () => undoCount++);
|
||
|
||
TestAwait.Run(handle.DisposeAsync());
|
||
TestAwait.Run(handle.DisposeAsync());
|
||
TestAwait.Run(_ctx.DisposeAsync());
|
||
|
||
Assert.AreEqual(1, undoCount, "逆操作至多执行一次(armed 语义)");
|
||
}
|
||
|
||
[Test]
|
||
public void AsyncInverses_RunInLifoOrder()
|
||
{
|
||
var log = new List<string>();
|
||
|
||
var h1 = TestAwait.Run(_ctx.EffectAsync(async () =>
|
||
{
|
||
log.Add("f1");
|
||
return () =>
|
||
{
|
||
log.Add("i1");
|
||
return UniTask.CompletedTask;
|
||
};
|
||
}));
|
||
var h2 = TestAwait.Run(_ctx.EffectAsync(async () =>
|
||
{
|
||
log.Add("f2");
|
||
return async () =>
|
||
{
|
||
log.Add("i2");
|
||
await UniTask.CompletedTask;
|
||
};
|
||
}));
|
||
|
||
TestAwait.Run(h2.DisposeAsync());
|
||
CollectionAssert.AreEqual(new[] { "f1", "f2", "i2" }, log);
|
||
|
||
TestAwait.Run(_ctx.DisposeAsync());
|
||
CollectionAssert.AreEqual(new[] { "f1", "f2", "i2", "i1" }, log, "后注册的逆操作先执行(LIFO)");
|
||
}
|
||
|
||
[Test]
|
||
public void DisposeAsync_WaitsForInflightForwardBeforeRecovering()
|
||
{
|
||
var log = new List<string>();
|
||
var gate = new UniTaskCompletionSource();
|
||
|
||
var effectTask = _ctx.EffectAsync(async () =>
|
||
{
|
||
await gate.Task;
|
||
log.Add("fwd");
|
||
return () =>
|
||
{
|
||
log.Add("inv");
|
||
return UniTask.CompletedTask;
|
||
};
|
||
});
|
||
|
||
// 前向仍在进行中时启动卸载:dispose 应先等待前向完成,再执行其产出的逆操作
|
||
var disposeTask = _ctx.DisposeAsync();
|
||
Assert.IsEmpty(log, "前向未完成时不得提前执行恢复");
|
||
|
||
gate.TrySetResult();
|
||
TestAwait.Run(disposeTask);
|
||
var handle = TestAwait.Run(effectTask);
|
||
|
||
CollectionAssert.AreEqual(new[] { "fwd", "inv" }, log, "dispose = await task; recover()");
|
||
Assert.IsFalse(handle.IsArmed);
|
||
}
|
||
}
|
||
}
|