feat(cordis): 完成阶段5访问介导与诊断
This commit is contained in:
@@ -95,6 +95,40 @@ namespace ShrinkContext.Tests
|
||||
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
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkContext;
|
||||
@@ -155,6 +156,57 @@ namespace ShrinkContext.Tests
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace ShrinkContext.Tests
|
||||
_catalog.Register("provider", () => new ConfigProviderComponent("provider", "svc"));
|
||||
_catalog.Register("provider.alt", () => new ConfigProviderComponent("provider.alt", "svc"));
|
||||
_catalog.Register("consumer", () => new ConsumerComponent("consumer", "svc"));
|
||||
_catalog.Register("failing", () => new FailingComponent("failing", "svc"));
|
||||
_catalog.Register("policy.provider", () => new PolicyProviderComponent("policy.service"));
|
||||
_catalog.Register("policy.consumer", () => new PolicyConsumerComponent("policy.service"));
|
||||
_loader = new ShrinkContextLoader(_runtime, _catalog);
|
||||
}
|
||||
|
||||
@@ -223,5 +226,87 @@ namespace ShrinkContext.Tests
|
||||
Assert.AreEqual("from-r2", ((ConsumerComponent)consumerFiber.Component).LastSeen,
|
||||
"消费者按自身隔离域解析到 r2 的绑定");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsolatedProviderReplacement_DoesNotReloadOtherRealmConsumer()
|
||||
{
|
||||
var isolateR1 = new Dictionary<string, string> { ["svc"] = "r1" };
|
||||
var isolateR2 = new Dictionary<string, string> { ["svc"] = "r2" };
|
||||
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
||||
{
|
||||
new("p1", "provider", "r1-v1", isolate: isolateR1),
|
||||
new("p2", "provider", "r2-v1", isolate: isolateR2),
|
||||
new("c2", "consumer", isolate: isolateR2),
|
||||
}));
|
||||
Assert.IsTrue(_loader.TryGetFiber("c2", out var consumerFiber));
|
||||
var consumer = (ConsumerComponent)consumerFiber.Component;
|
||||
Assert.AreEqual(1, consumer.LoadCount);
|
||||
|
||||
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
||||
{
|
||||
new("p1", "provider", "r1-v2", isolate: isolateR1),
|
||||
new("p2", "provider", "r2-v1", isolate: isolateR2),
|
||||
new("c2", "consumer", isolate: isolateR2),
|
||||
}));
|
||||
|
||||
Assert.AreEqual(1, consumer.LoadCount,
|
||||
"r1 提供者替换不得触发只解析 r2 的消费者重载");
|
||||
Assert.AreEqual("r2-v1", consumer.LastSeen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InterceptUpdate_ChangesAccessWithoutReloadingFiber()
|
||||
{
|
||||
var suffixA = new Dictionary<string, IReadOnlyDictionary<string, object?>>
|
||||
{
|
||||
["policy.service"] = new Dictionary<string, object?> { ["suffix"] = "-a" }
|
||||
};
|
||||
var suffixB = new Dictionary<string, IReadOnlyDictionary<string, object?>>
|
||||
{
|
||||
["policy.service"] = new Dictionary<string, object?> { ["suffix"] = "-b" }
|
||||
};
|
||||
|
||||
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
||||
{
|
||||
new("policy-provider", "policy.provider", "base"),
|
||||
new("policy-consumer", "policy.consumer", intercept: suffixA),
|
||||
}));
|
||||
Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var firstFiber));
|
||||
var consumer = (PolicyConsumerComponent)firstFiber.Component;
|
||||
Assert.AreEqual("base-a", consumer.Read());
|
||||
Assert.AreEqual(1, consumer.ApplyCount);
|
||||
|
||||
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
||||
{
|
||||
new("policy-provider", "policy.provider", "base"),
|
||||
new("policy-consumer", "policy.consumer", intercept: suffixB),
|
||||
}));
|
||||
|
||||
Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var sameFiber));
|
||||
Assert.AreSame(firstFiber, sameFiber);
|
||||
Assert.AreEqual(1, consumer.ApplyCount, "intercept 元数据变化不应触发 reload");
|
||||
Assert.AreEqual("base-b", consumer.Read(), "访问时应读取更新后的 metadata");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FailedReplacement_RestoresPreviousLoaderCompositionAndReportsTransaction()
|
||||
{
|
||||
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
||||
{
|
||||
new("provider-entry", "provider", "stable")
|
||||
}));
|
||||
|
||||
Assert.Throws<ShrinkLoaderException>(() => TestAwait.Run(_loader.ApplyAsync(
|
||||
new List<ShrinkLoaderEntry> { new("provider-entry", "failing") })));
|
||||
|
||||
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var restored));
|
||||
Assert.AreEqual(ShrinkFiberState.Active, restored.State);
|
||||
Assert.AreEqual("stable", restored.Config);
|
||||
Assert.IsNotNull(_loader.LastTransaction);
|
||||
Assert.AreEqual(ShrinkLoaderTransactionPhase.Failed, _loader.LastTransaction!.Phase);
|
||||
Assert.IsTrue(_loader.LastTransaction.RestoreAttempted);
|
||||
Assert.IsTrue(_loader.LastTransaction.PreviousCompositionRestored);
|
||||
StringAssert.Contains("failed during apply", _loader.LastTransaction.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +250,81 @@ namespace ShrinkContext.Tests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>故意写入未声明键,验证 Provide 运行时契约。</summary>
|
||||
public sealed class UndeclaredProviderComponent : IShrinkComponent
|
||||
{
|
||||
public string Name => "undeclared-provider";
|
||||
public IReadOnlyList<string> Inject => Array.Empty<string>();
|
||||
public IReadOnlyList<string> Provide => Array.Empty<string>();
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
|
||||
{
|
||||
ctx.Set("not-declared", 1);
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MetadataSuffixPolicy : IShrinkCoeffectAccessPolicy
|
||||
{
|
||||
public object? Resolve(ShrinkCoeffectAccessContext context, object? value)
|
||||
{
|
||||
var text = value?.ToString() ?? string.Empty;
|
||||
if (context.TryGetMetadata<string>("prefix", out var prefix))
|
||||
text = prefix + text;
|
||||
return context.TryGetMetadata<string>("suffix", out var suffix) ? text + suffix : text;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PolicyProviderComponent : IShrinkComponent
|
||||
{
|
||||
private readonly string[] _provide;
|
||||
|
||||
public PolicyProviderComponent(string key)
|
||||
{
|
||||
Key = key;
|
||||
_provide = new[] { key };
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
public string Name => "policy-provider";
|
||||
public IReadOnlyList<string> Inject => Array.Empty<string>();
|
||||
public IReadOnlyList<string> Provide => _provide;
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
|
||||
{
|
||||
ctx.Set(Key, config?.ToString() ?? "value", new MetadataSuffixPolicy());
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PolicyConsumerComponent : IShrinkComponent
|
||||
{
|
||||
private readonly string[] _inject;
|
||||
private ShrinkCtx? _ctx;
|
||||
|
||||
public PolicyConsumerComponent(string key)
|
||||
{
|
||||
Key = key;
|
||||
_inject = new[] { key };
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
public string Name => "policy-consumer";
|
||||
public int ApplyCount { get; private set; }
|
||||
public IReadOnlyList<string> Inject => _inject;
|
||||
public IReadOnlyList<string> Provide => Array.Empty<string>();
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
|
||||
{
|
||||
_ctx = ctx;
|
||||
ApplyCount++;
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public string Read() => (_ctx ?? throw new InvalidOperationException("Consumer is not active."))
|
||||
.Get<string>(Key);
|
||||
}
|
||||
|
||||
/// <summary>测试辅助:同步等待 UniTask(测试保证全部延续同步完成,无死锁风险)。</summary>
|
||||
public static class TestAwait
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user