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

132 lines
4.4 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.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);
}
}
}