313 lines
14 KiB
C#
313 lines
14 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using NUnit.Framework;
|
|
using ShrinkContext;
|
|
|
|
namespace ShrinkContext.Tests
|
|
{
|
|
/// <summary>
|
|
/// 声明式加载器(论文 5.2 的原型子集):
|
|
/// 增量协调、幂等、退役、disabled 开关、组件/配置变化重建、依赖缺失等待、隔离域条目。
|
|
/// </summary>
|
|
public class LoaderTests
|
|
{
|
|
private ShrinkContextRuntime _runtime = null!;
|
|
private ShrinkComponentCatalog _catalog = null!;
|
|
private ShrinkContextLoader _loader = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_runtime = new ShrinkContextRuntime();
|
|
_catalog = new ShrinkComponentCatalog();
|
|
_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);
|
|
}
|
|
|
|
[Test]
|
|
public void Apply_CreatesEntriesAndActivates()
|
|
{
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v1"),
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var providerFiber));
|
|
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
|
|
Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State);
|
|
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
|
|
}
|
|
|
|
[Test]
|
|
public void Apply_IsIdempotent_SecondApplyDoesNotReload()
|
|
{
|
|
var config = new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v1"),
|
|
new("consumer-entry", "consumer"),
|
|
};
|
|
|
|
TestAwait.Run(_loader.ApplyAsync(config));
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber));
|
|
|
|
TestAwait.Run(_loader.ApplyAsync(config));
|
|
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var secondFiber));
|
|
Assert.AreSame(firstFiber, secondFiber, "无变化的条目不得重建纤程");
|
|
var providerComponent = (ConfigProviderComponent)secondFiber.Component;
|
|
Assert.AreEqual(1, providerComponent.ApplyCount, "幂等协调不触发重载");
|
|
}
|
|
|
|
[Test]
|
|
public void RemovedEntry_RetiresFiber_DependentDeactivatesButStaysManaged()
|
|
{
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v1"),
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
|
|
Assert.IsFalse(_loader.TryGetFiber("provider-entry", out _), "消失的条目不再被管理");
|
|
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
|
|
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State,
|
|
"提供者退役后依赖者被响应式停用,但条目仍被管理、等待依赖回归");
|
|
|
|
// 依赖回归后无需重建条目即可重新激活
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v1"),
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
|
|
}
|
|
|
|
[Test]
|
|
public void DisabledEntry_UnloadsAndReloadsOnEnable()
|
|
{
|
|
var enabled = new List<ShrinkLoaderEntry> { new("provider-entry", "provider", "v1") };
|
|
TestAwait.Run(_loader.ApplyAsync(enabled));
|
|
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v1", disabled: true),
|
|
}));
|
|
Assert.IsFalse(_loader.TryGetFiber("provider-entry", out _),
|
|
"disabled 条目不持有纤程,但条目本身仍被管理");
|
|
CollectionAssert.Contains(_loader.ManagedEntryIds, "provider-entry");
|
|
|
|
TestAwait.Run(_loader.ApplyAsync(enabled));
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var reloadedFiber));
|
|
Assert.AreEqual(ShrinkFiberState.Active, reloadedFiber.State, "取消 disabled 后重新加载");
|
|
}
|
|
|
|
[Test]
|
|
public void ComponentChange_RebuildsEntry()
|
|
{
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v1"),
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber));
|
|
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider.alt", "v2"),
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var rebuiltFiber));
|
|
Assert.AreNotSame(firstFiber, rebuiltFiber, "组件名变化必须重建条目(论文 id/url → 重建)");
|
|
Assert.AreEqual(ShrinkFiberState.Active, rebuiltFiber.State);
|
|
Assert.AreEqual("provider.alt", rebuiltFiber.Component.Name);
|
|
|
|
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
|
|
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
|
|
var consumer = (ConsumerComponent)consumerFiber.Component;
|
|
Assert.AreEqual("v2", consumer.LastSeen, "依赖者切换到重建后的新提供者");
|
|
}
|
|
|
|
[Test]
|
|
public void ConfigChange_ReloadsEntryWithNewConfig()
|
|
{
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v1"),
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber));
|
|
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("provider-entry", "provider", "v2"),
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
|
|
// 原型语义:配置变化走重建(组件自决 diff 属后续阶段)
|
|
Assert.AreEqual(ShrinkFiberState.Inactive, firstFiber.State);
|
|
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var rebuiltFiber));
|
|
Assert.AreNotSame(firstFiber, rebuiltFiber);
|
|
Assert.AreEqual("v2", rebuiltFiber.Config, "重建后的纤程携带新配置");
|
|
|
|
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
|
|
var consumer = (ConsumerComponent)consumerFiber.Component;
|
|
Assert.AreEqual("v2", consumer.LastSeen, "依赖者看到新配置的值");
|
|
}
|
|
|
|
[Test]
|
|
public void MissingDependency_WaitsWithoutError_ThenActivatesWhenProvided()
|
|
{
|
|
// 首轮只有消费者:不抛“依赖缺失”异常,条目保持被管理、纤程 Inactive
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("consumer-entry", "consumer"),
|
|
}));
|
|
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
|
|
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State);
|
|
|
|
// 次轮补上提供者:依赖者被响应式激活,无需重发消费者条目
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("consumer-entry", "consumer"),
|
|
new("provider-entry", "provider", "late"),
|
|
}));
|
|
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
|
|
Assert.AreEqual("late", ((ConsumerComponent)consumerFiber.Component).LastSeen);
|
|
}
|
|
|
|
[Test]
|
|
public void DuplicateEntryIds_Throw()
|
|
{
|
|
Assert.Throws<ShrinkLoaderException>(() => TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("dup", "provider", "v1"),
|
|
new("dup", "provider", "v2"),
|
|
})));
|
|
}
|
|
|
|
[Test]
|
|
public void UnknownComponent_ThrowsWithEntryContext()
|
|
{
|
|
var ex = Assert.Throws<ShrinkLoaderException>(() => TestAwait.Run(_loader.ApplyAsync(
|
|
new List<ShrinkLoaderEntry> { new("entry", "no.such.component") })));
|
|
StringAssert.Contains("no.such.component", ex!.Message);
|
|
StringAssert.Contains("entry", ex.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void IsolatedEntries_SameKeyCoexistInDifferentRealms()
|
|
{
|
|
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
|
|
{
|
|
new("p1", "provider", "from-r1", isolate: new Dictionary<string, string> { ["svc"] = "r1" }),
|
|
new("p2", "provider", "from-r2", isolate: new Dictionary<string, string> { ["svc"] = "r2" }),
|
|
new("c2", "consumer", isolate: new Dictionary<string, string> { ["svc"] = "r2" }),
|
|
}));
|
|
|
|
Assert.IsTrue(_loader.TryGetFiber("p1", out var f1));
|
|
Assert.IsTrue(_loader.TryGetFiber("p2", out var f2));
|
|
Assert.AreEqual(ShrinkFiberState.Active, f1.State, "不同隔离域的同键供给互不冲突");
|
|
Assert.AreEqual(ShrinkFiberState.Active, f2.State);
|
|
|
|
Assert.IsTrue(_loader.TryGetFiber("c2", out var consumerFiber));
|
|
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
|
|
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);
|
|
}
|
|
}
|
|
}
|