157 lines
6.0 KiB
C#
157 lines
6.0 KiB
C#
#nullable enable
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using Cysharp.Threading.Tasks;
|
||
using NUnit.Framework;
|
||
using ShrinkContext;
|
||
|
||
namespace ShrinkContext.Tests
|
||
{
|
||
/// <summary>
|
||
/// 余效应操作与介导访问(论文 3.2 / 算法 2、6):
|
||
/// set 是可逆效应;代理介导的 get 拒绝未声明/未激活访问;隔离域派生独立绑定。
|
||
/// </summary>
|
||
public class CoeffectAccessTests
|
||
{
|
||
private ShrinkContextRuntime _runtime = null!;
|
||
private ShrinkCtx _ctx = null!;
|
||
|
||
[SetUp]
|
||
public void SetUp()
|
||
{
|
||
_runtime = new ShrinkContextRuntime();
|
||
_ctx = _runtime.RootContext;
|
||
}
|
||
|
||
[Test]
|
||
public void Set_IsReversibleEffect_EarlyWithdrawAndContextDispose()
|
||
{
|
||
var handle = _ctx.Set("cfg", 42);
|
||
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "cfg", out var v1));
|
||
Assert.AreEqual(42, v1);
|
||
|
||
// 早期手动撤回
|
||
TestAwait.Run(handle.DisposeAsync());
|
||
Assert.IsFalse(_runtime.TryGetRaw<int>(_ctx, "cfg", out _));
|
||
|
||
// 再次安装后随上下文卸载自动撤回
|
||
_ctx.Set("cfg", 43);
|
||
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "cfg", out var v2));
|
||
Assert.AreEqual(43, v2);
|
||
|
||
TestAwait.Run(_ctx.DisposeAsync());
|
||
Assert.IsFalse(_runtime.TryGetRaw<int>(_ctx, "cfg", out _), "set 的逆操作必须移除绑定");
|
||
}
|
||
|
||
[Test]
|
||
public void MediatedGet_RejectsUndeclaredKey_InApply()
|
||
{
|
||
var provider = new ProviderComponent("provider", "real", "v");
|
||
var fiber = _runtime.Use(provider);
|
||
Assert.AreEqual(ShrinkFiberState.Active, fiber.State);
|
||
|
||
var greedy = new GreedyConsumerComponent("greedy", "real", "undeclared");
|
||
var greedyFiber = _runtime.Use(greedy);
|
||
|
||
Assert.AreEqual(ShrinkFiberState.Inactive, greedyFiber.State);
|
||
Assert.IsInstanceOf<ShrinkUndeclaredAccessException>(greedyFiber.LastError,
|
||
"未声明的依赖访问在访问点被拒绝,并使装载失败(算法 6 UNDECLARED_ACCESS)");
|
||
}
|
||
|
||
[Test]
|
||
public void MediatedGet_ThrowsWhenOwnKeyNotActivated()
|
||
{
|
||
var consumer = new ConsumerComponent("consumer", "missing");
|
||
var fiber = _runtime.Use(consumer);
|
||
|
||
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State,
|
||
"依赖缺失时组件保持非活动,而不是抛错(响应式余效应)");
|
||
|
||
Assert.Throws<ShrinkInactiveAccessException>(() => fiber.Ctx.Get<object>("missing"));
|
||
}
|
||
|
||
[Test]
|
||
public void Isolate_DerivesIndependentRealm_RestoreByDiscard()
|
||
{
|
||
_ctx.Set("k", 1);
|
||
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "k", out var rootValue));
|
||
Assert.AreEqual(1, rootValue);
|
||
|
||
var isolated = _ctx.Isolate("k", "test-realm");
|
||
|
||
// 隔离域内初始为空:逻辑键相同但 realm 不同
|
||
Assert.IsFalse(_runtime.TryGetRaw<int>(isolated, "k", out _));
|
||
|
||
isolated.Set("k", 2);
|
||
Assert.IsTrue(_runtime.TryGetRaw<int>(isolated, "k", out var isolatedValue));
|
||
Assert.AreEqual(2, isolatedValue);
|
||
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "k", out var rootValueAfter));
|
||
Assert.AreEqual(1, rootValueAfter, "隔离域写入不得影响父上下文绑定");
|
||
|
||
// 派生上下文的恢复是隐式的:丢弃即恢复(论文定义 27 派生实现)
|
||
TestAwait.Run(isolated.DisposeAsync());
|
||
Assert.IsFalse(_runtime.TryGetRaw<int>(isolated, "k", out _));
|
||
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "k", out var rootValueFinal));
|
||
Assert.AreEqual(1, rootValueFinal);
|
||
}
|
||
|
||
[Test]
|
||
public void TypedKey_PreservesVersionedIdentityAcrossMediatedAccess()
|
||
{
|
||
var key = new ShrinkKey<string>("test.package", "typed-service", 2);
|
||
Assert.AreEqual("test.package/typed-service@v2", key.Id);
|
||
|
||
_runtime.Use(new PolicyProviderComponent(key.Id), "typed-value");
|
||
var consumer = _runtime.Use(new PolicyConsumerComponent(key.Id));
|
||
|
||
Assert.AreEqual(ShrinkFiberState.Active, consumer.State);
|
||
Assert.AreEqual("typed-value", consumer.Ctx.Get(key));
|
||
}
|
||
|
||
[Test]
|
||
public void Intercept_MergesRootToLeafAndChildMetadataWins()
|
||
{
|
||
const string key = "policy.service";
|
||
_runtime.Use(new PolicyProviderComponent(key), "value");
|
||
var consumer = _runtime.Use(new PolicyConsumerComponent(key));
|
||
Assert.AreEqual(ShrinkFiberState.Active, consumer.State);
|
||
|
||
var parent = consumer.Ctx.Intercept(key, new Dictionary<string, object?>
|
||
{
|
||
["prefix"] = "parent-",
|
||
["suffix"] = "-parent"
|
||
});
|
||
var child = parent.Intercept(key, new Dictionary<string, object?>
|
||
{
|
||
["suffix"] = "-child"
|
||
});
|
||
|
||
Assert.AreEqual("parent-value-child", child.Get<string>(key));
|
||
}
|
||
|
||
/// <summary>声明一个键却访问另一个未声明键的组件(验证能力介导纪律)。</summary>
|
||
private sealed class GreedyConsumerComponent : IShrinkComponent
|
||
{
|
||
private readonly string[] _inject;
|
||
|
||
public GreedyConsumerComponent(string name, string declaredKey, string undeclaredKey)
|
||
{
|
||
Name = name;
|
||
UndeclaredKey = undeclaredKey;
|
||
_inject = new[] { declaredKey };
|
||
}
|
||
|
||
public string Name { get; }
|
||
public string UndeclaredKey { get; }
|
||
public IReadOnlyList<string> Inject => _inject;
|
||
public IReadOnlyList<string> Provide => Array.Empty<string>();
|
||
|
||
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
|
||
{
|
||
ctx.Get<object>(UndeclaredKey);
|
||
return UniTask.CompletedTask;
|
||
}
|
||
}
|
||
}
|
||
}
|