Sparkdo.Runtime.Testing 0.0.1-preview.3

This is a prerelease version of Sparkdo.Runtime.Testing.
dotnet add package Sparkdo.Runtime.Testing --version 0.0.1-preview.3
                    
NuGet\Install-Package Sparkdo.Runtime.Testing -Version 0.0.1-preview.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sparkdo.Runtime.Testing" Version="0.0.1-preview.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sparkdo.Runtime.Testing" Version="0.0.1-preview.3" />
                    
Directory.Packages.props
<PackageReference Include="Sparkdo.Runtime.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sparkdo.Runtime.Testing --version 0.0.1-preview.3
                    
#r "nuget: Sparkdo.Runtime.Testing, 0.0.1-preview.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sparkdo.Runtime.Testing@0.0.1-preview.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sparkdo.Runtime.Testing&version=0.0.1-preview.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Sparkdo.Runtime.Testing&version=0.0.1-preview.3&prerelease
                    
Install as a Cake Tool

Sparkdo.Runtime.Testing

<code>Sparkdo.Runtime.Testing</code> 是 Sparkdo 统一运行时的框架无关测试与合规验证工具包。它把真实的 <code>RuntimeEngine</code> 放入可控的时间、调度、观察和生命周期环境中,帮助测试代码稳定验证以下问题:

  • 重协调、停止、排空、取消和隔离等生命周期路径;
  • 并发准备、待处理工作项与 Observation 队列是否保持有界;
  • Scope、导出租约、版本租约和临时 Hold 的所有权是否按预期收敛;
  • Native AOT、trimming 和包消费者所需的显式注册表路径;
  • 已发布 Snapshot 的 Scope 与导出获取热路径是否满足固定查找不变量。

该包不是应用运行时扩展包,也不负责 Hosting、依赖注入、配置装载或生产诊断。生产应用应组合 <code>Sparkdo.Runtime.Contracts</code>、<code>Sparkdo.Runtime</code> 及所需的 Hosting 或 Configuration 包;本包仅应出现在测试、验证或合规门禁项目中。

适用场景

当测试需要真实运行时行为,但又不能依赖墙钟、线程调度偶然性或程序集扫描时,使用 <code>Sparkdo.Runtime.Testing</code>。典型场景包括:

  • 为自定义 <code>IRuntimeRegistrationTable</code> 编写集成测试;
  • 固定重协调到某个检查点,验证并发窗口或停止竞争;
  • 推进虚拟时间,触发排空、取消确认或其他基于 <code>TimeProvider</code> 的定时逻辑;
  • 验证 Observation 接收、拒绝、异常和阻塞时的运行时行为;
  • 审计 Snapshot、Scope pin、消费者租约、版本租约和 Hold;
  • 在不使用反射发现的前提下执行 AOT 或 trimming 冒烟测试;
  • 用 Core 采集的计数器验证有界并发与热路径不变量。

它不替代业务能力实现的单元测试。业务测试仍应针对自己的 <code>ICapabilityActivator<TPlan></code>、<code>ICapabilityPreparationAdapter<TPlan></code>、<code>ICapabilityReconciler<TPlan></code> 和导出实现编写明确的断言。

安装

测试项目的目标框架应与运行时兼容;当前仓库构建目标为 <code>net10.0</code>。从 NuGet 安装时,选择与其他 Sparkdo Runtime 包一致的版本:

dotnet add package Sparkdo.Runtime.Testing --version <版本>

或在测试项目中加入引用:

<ItemGroup>
  <PackageReference Include="Sparkdo.Runtime.Testing" Version="<版本>" />
</ItemGroup>

若测试项目自己实现注册表和能力适配器,也应引用其中使用到的 Sparkdo Runtime 合同包。不要把此包加入应用启动项目、Web 服务项目或生产 worker 项目。

快速开始

<code>StaticRuntimeRegistrationTable</code> 是最小、可直接使用的显式注册表。它提供一个固定静态能力、一个字符串导出,并且只接受 <code>CatalogInputs.Empty</code>。它适合验证 Runtime 创建、发布、Scope、导出租约和停止这一条完整路径,也适合 AOT、trimming 与包消费者冒烟测试。

下面是可直接放入 xUnit 测试项目的最小示例:

using System.Threading;
using Sparkdo.Runtime;
using Sparkdo.Runtime.Testing;
using Xunit;

public sealed class RuntimeSmokeTests
{
    [Fact(DisplayName = "静态注册表可发布、获取导出并停止")]
    public async Task StaticRegistrationCanPublishAcquireAndStopAsync()
    {
        var registrations = StaticRuntimeRegistrationTable.Create();
        var catalog = registrations.CreateCatalog(CatalogInputs.Empty);

        Assert.True(catalog.Succeeded);
        Assert.NotNull(catalog.Catalog);

        await using var kit = RuntimeTestKit.Create(registrations);
        var published = await kit.Runtime.SubmitReconciliationAsync(
            new ReconciliationRequest(catalog.Catalog!),
            CancellationToken.None);

        Assert.Equal(ReconciliationOutcome.Published, published.Outcome);

        var opened = kit.Runtime.OpenScope();
        Assert.True(opened.Succeeded);
        Assert.NotNull(opened.Scope);

        await using (var scope = opened.Scope!)
        {
            await using var lease = await scope.AcquireAsync(
                registrations.CreateExport(),
                CancellationToken.None);

            Assert.Equal("aot-static", lease.Value);
        }

        var ownership = kit.CaptureOwnership();
        Assert.Equal(0, ownership.RootPins);
        Assert.Equal(0, ownership.ConsumerLeases);

        var stopped = await kit.Runtime.StopAsync(
            cancellationToken: CancellationToken.None);
        Assert.Equal(RuntimeStopStatus.Stopped, stopped.Status);
    }
}

<code>RuntimeTestKit</code> 持有 Runtime 并实现 <code>IAsyncDisposable</code>。始终使用 <code>await using</code>,以便测试结束时调用 Runtime 的异步释放路径。

RuntimeTestKit 与选项

<code>RuntimeTestKit.Create(IRuntimeRegistrationTable, RuntimeTestOptions)</code> 构造真实的 Core Runtime,并为该实例注入:

成员 用途
<code>Runtime</code> 真实的 <code>IRuntime</code>,可调用 <code>SubmitReconciliationAsync</code>、<code>OpenScope</code>、<code>StopAsync</code> 等公开运行时 API。
<code>Time</code> <code>ControlledTimeProvider</code>,从 Unix Epoch 起步,由测试显式推进。
<code>Scheduler</code> <code>ControlledScheduler</code>,用于观察并解除指定检查点的暂停。
<code>Observations</code> <code>ControlledObservationSink</code>,用于检查 Observation 或模拟接收器行为。
<code>CaptureOwnership()</code> 返回当前 Snapshot、租约、Hold 和重协调状态的测试视图。

<code>RuntimeTestOptions</code> 可控制以下测试条件:

选项 含义
<code>MaximumPreparationConcurrency</code> 传给 Runtime 的准备并发上限。默认值为 <code>1</code>。
<code>CancellationConfirmationTimeout</code> 传给 Runtime 的取消确认窗口;未设置时使用 Runtime 默认值。
<code>PausedCheckpoints</code> 指定哪些 <code>RuntimeCheckpoint</code> 会在受控执行探针处暂停。
<code>FailSuccessorEntryHoldRelease</code> 注入后继 Entry Hold 释放失败,用于负向路径测试。
<code>FailSuccessorEntryHoldTransferAfter</code> 在指定次数的后继 Entry Hold 转移时注入失败。
<code>FailSuccessorEntryHoldRestoreAfterTransfer</code> 注入已转移后恢复 Hold 的失败。

后三项是故障注入开关,只应用于测试预期的失败、隔离或恢复路径。它们不是生产容错配置。

<code>Create</code> 会先校验注册表和测试选项;若 Runtime 无法创建,会抛出 <code>InvalidOperationException</code>,而不是返回一个空的测试对象。对于生产式注册表,应先独立测试 <code>RuntimeFactory.Create</code> 的结构化创建结果;对于受控集成测试,再使用 <code>RuntimeTestKit</code>。

可控时间

<code>ControlledTimeProvider</code> 是 <code>TimeProvider</code> 的实现。它不读取墙钟,初始时间为 Unix Epoch,并且只能通过 <code>Advance(TimeSpan)</code> 向前推进。

var stopping = kit.Runtime.StopAsync(
    new RuntimeStopOptions(TimeSpan.FromSeconds(1)),
    CancellationToken.None).AsTask();

kit.Time.Advance(TimeSpan.FromSeconds(1));

var result = await stopping;

推进时间会在更新虚拟时钟后同步执行到期定时器的回调。时间不能倒退;负时长会抛出 <code>ArgumentOutOfRangeException</code>。因此,测试应只推进刚好覆盖目标分支的时长,并将超时、排空和取消确认的断言放在每一步推进之后。

不要把 <code>ControlledTimeProvider</code> 当作性能计时器或生产时间源。它的职责是消除时间相关测试的不确定性。

可控调度、检查点与轨迹

<code>RuntimeCheckpoint</code> 描述 Core 可观察的协调、发布、排空、停止和 Observation 阶段。把某个检查点加入 <code>RuntimeTestOptions.PausedCheckpoints</code> 后,运行时到达相应受控探针时会停住,直到测试调用 <code>Scheduler.Release(...)</code> 或 <code>Scheduler.RunNextAsync()</code>。

典型并发测试的顺序是:先启动异步操作,再等待它实际到达检查点,断言中间状态,最后解除检查点并等待操作完成。

using System.Collections.Immutable;

await using var kit = RuntimeTestKit.Create(
    registrations,
    new RuntimeTestOptions
    {
        PausedCheckpoints = ImmutableArray.Create(
            RuntimeCheckpoint.SnapshotPublishing)
    });

var reconciliation = kit.Runtime.SubmitReconciliationAsync(
    new ReconciliationRequest(catalog),
    CancellationToken.None).AsTask();

await kit.Scheduler.WaitForCheckpointAsync(
    RuntimeCheckpoint.SnapshotPublishing,
    CancellationToken.None);

Assert.False(reconciliation.IsCompleted);

kit.Scheduler.Release(RuntimeCheckpoint.SnapshotPublishing);
var result = await reconciliation;

几个容易混淆的点:

  • <code>WaitForCheckpointAsync</code> 只等待“已到达”的事实,不会自动解除暂停。
  • <code>Release(checkpoint)</code> 解除该检查点所有当前等待者;若尚无等待者,会保留一个释放令牌供后续等待者消费。
  • <code>RunNextAsync()</code> 按全局等待顺序释放一个尚未完成的等待者,适合探索交错顺序。
  • <code>HasPendingWork</code> 只反映调度器中等待释放的工作。
  • <code>CaptureTrace()</code> 返回 <code>RuntimeTrace</code>,其中包含检查点和门事件;<code>ReplayAsync</code> 仅重放轨迹中 <code>GateOpened</code> 的检查点事件,不能替代完整的并发重放或线程调度模拟。

仅为 Runtime 实际经过的检查点配置暂停。等待一个不会被当前路径触达的检查点会导致测试自身无限等待,应始终传入测试框架提供的取消令牌或额外的测试超时。

受控生命周期与导出夹具

包内包含以下受控生命周期类型:

类型 表示的受控行为
<code>ControlledGate</code> 代表一个 <code>ControlledStage</code> 的一次性门;<code>Open()</code> 后不可再次关闭。
<code>ControlledCandidate</code> Candidate 的 Prepare、Discard、Retire 与 Dispose 生命周期。
<code>ControlledVersion</code> Version 的 <code>ReleaseAsync</code> 和对应导出提供方。
<code>ControlledReload</code> Reload 的 Prepare、Commit、Rollback 与 Release 生命周期。
<code>ControlledPreparationAdapter<TPlan></code> 将 Prepare 调用转交给 <code>ControlledCandidate</code>。
<code>ControlledReconciler<TPlan></code> 可设置 <code>ReconciliationDecision</code>,并控制 Reload Prepare 的完成。
<code>ControlledExportProvider</code> 控制导出获取门和按导出标识完成的获取结果。
<code>ControlledExportAcquisition<T></code> 控制单个导出租约的 Release 结果和 Release 门。

这些夹具的完成语义是“双条件”:先调用 <code>Complete...</code> 写入结果、再打开对应门,或先开门、再完成结果,都会得到相同的最终行为。测试可以据此区分“回调已进入”“结果已准备”“允许回调返回”三个状态。

<code>ControlledStage</code> 包括 Prepare、Discard、Retire、Dispose、VersionRelease、ExportAcquire、ExportRelease 以及 Reload 的 Assess、Prepare、Commit、Rollback 阶段。测试应只打开和完成当前断言所需的阶段,避免无关门被提前放行而掩盖竞态。

外部测试项目的构造边界

<code>ControlledCandidate</code>、<code>ControlledVersion</code>、<code>ControlledReload</code>、<code>ControlledPreparationAdapter<TPlan></code>、<code>ControlledReconciler<TPlan></code>、<code>ControlledExportProvider</code> 和 <code>ControlledExportAcquisition<T></code> 的构造函数是 <code>internal</code>。它们虽然作为公开类型出现在 API 表面,但包外测试项目不能直接使用 <code>new</code> 创建实例。

因此,外部项目的直接入口是 <code>RuntimeTestKit</code> 与 <code>StaticRuntimeRegistrationTable</code>。如需为自己的能力注册表制造可控生命周期,请在测试项目中实现对应 Runtime 合同,或使用项目内已有的测试注册夹具;不要假设这些类型提供了公开构造工厂。

受控 Observation

<code>RuntimeTestKit.Observations</code> 返回 <code>ControlledObservationSink</code>。它实现 <code>IObservationSink</code>,并提供以下能力:

成员或模式 行为
<code>Items</code> 返回已接受 Observation 的不可变快照。
<code>WaitForCodeAsync(code, token)</code> 返回首个匹配代码的 Observation;未出现时异步等待。
<code>Accept</code> 接受并记录 Observation。
<code>Reject</code> 返回 <code>false</code>,不记录 Observation。
<code>Throw</code> 写入时抛出 <code>InvalidOperationException</code>。
<code>Block</code> 阻塞写入,直到调用 <code>ReleaseBlockedWrites()</code>。

使用 <code>Block</code> 时必须在 <code>finally</code> 中调用 <code>ReleaseBlockedWrites()</code>,避免测试留下等待中的 Observation 写入任务:

kit.Observations.Mode = ControlledObservationMode.Block;
try
{
    // 触发需要验证的运行时操作,并断言其与 Observation 投递的关系。
}
finally
{
    kit.Observations.ReleaseBlockedWrites();
}

<code>WaitForCodeAsync</code> 支持取消。对等待 Observation 的测试应传入测试框架的取消令牌,防止断言目标未产生时无限挂起。

所有权快照与状态审计

<code>RuntimeTestKit.CaptureOwnership()</code> 返回 <code>RuntimeOwnershipSnapshot</code>。它是测试时刻的观测快照,不是可修改的运行时控制面。

字段 检查重点
<code>Revision</code> 当前活动 Snapshot 的 Catalog 修订;没有活动 Snapshot 时为 <code>null</code>。
<code>Capabilities</code> 每个能力的实例、版本、Entry 引用数、入站版本租约和出站版本租约。
<code>RootPins</code> 活动 Snapshot 的 Scope pin 数量。
<code>ConsumerLeases</code> 活动 Snapshot 中消费者保留的租约数量。
<code>Holds</code> PreparedVersion、SuccessorEntry 或 Quarantine Hold 的标识、能力、实例、版本和状态。
<code>Admission</code> 当前 Entry 准入状态。
<code>Running</code> 与 <code>Pending</code> 运行中或待处理重协调的身份与修订。
<code>RunningBarrierReached</code>、<code>PendingBarrierReached</code> 是否越过相应的协调屏障。
<code>RunningCancellationRequested</code>、<code>PendingCancellationRequested</code> 是否已请求取消。

<code>CapabilityOwnership.EntryReferences</code> 是 Core 对该 Entry 的测试可见引用数,包含后继 Entry Hold 的影响;不要把它误解为业务调用次数。<code>RuntimeHoldStatus</code> 可取 <code>Held</code>、<code>Transferred</code>、<code>Releasing</code>、<code>Released</code> 或 <code>Quarantined</code>,适合断言资源在成功、失败和隔离路径上的归属。

推荐在每个竞争窗口的前后各采集一次快照,并断言 pin、租约和 Hold 是否收敛。例如,释放 Scope 后应检查 <code>RootPins</code> 是否回到零;退出导出租约后应检查 <code>ConsumerLeases</code> 是否回到零。

有界性与热路径合规夹具

RuntimeBoundedReconciliationFixture

<code>RuntimeBoundedReconciliationFixture</code> 在真实重协调之后读取 Core 计数器,返回 <code>RuntimeBoundedReconciliationResult</code>。结果包含:

  • 声明的准备并发上限;
  • 实际最大准备 worker 数与最大活动 Prepare 数;
  • 最大保留重协调工作项数;
  • Observation 队列容量、最大排队数、当前排队数和丢弃数;
  • 重协调结果。

<code>Satisfied</code> 是固定合规谓词:要求结果为 <code>Published</code>、准备相关最大值不超过声明上限、保留工作项不超过两个,并且 Observation 队列不超过其容量。它用于门禁断言,不是吞吐量或延迟基准。

var fixture = new RuntimeBoundedReconciliationFixture(kit);
var evidence = await fixture.ExerciseAsync(
    new ReconciliationRequest(catalog),
    declaredPreparationConcurrency: 2,
    CancellationToken.None);

Assert.True(evidence.Satisfied);

如果测试自行编排并发与检查点,可在操作完成后调用 <code>Capture(outcome, declaredPreparationConcurrency)</code> 获取同一类证据。

RuntimeHotPathInvariantFixture

<code>RuntimeHotPathInvariantFixture</code> 只用于已发布 Snapshot。它会打开 Scope、获取一次指定 <code>Export<T></code>,并读取 Core 热路径计数器的增量。<code>RuntimeHotPathInvariantResult.Satisfied</code> 要求:

  • 仅进行一次 Snapshot 定位与两次导出 Entry 定位;
  • 不发生 Catalog 验证、Plan 构建、AST 解释或程序集扫描;
  • 已发布的 Snapshot、Catalog 与 Plan 引用保持不变。
var hotPath = new RuntimeHotPathInvariantFixture(kit);
var evidence = await hotPath.ExerciseAsync(
    registrations.CreateExport(),
    CancellationToken.None);

Assert.True(evidence.Satisfied);

调用前必须先完成一次发布;没有活动 Snapshot 或无法打开 Scope 时,夹具会抛出 <code>InvalidOperationException</code>。这项检查验证固定的运行时路径不变量,不代替 BenchmarkDotNet、压力测试或生产容量评估。

静态注册表的边界

<code>StaticRuntimeRegistrationTable</code> 是固定测试夹具,不是业务注册表模板。它的行为是确定的:

  • 注册一个名为 <code>aot.static</code> 的静态能力;
  • 提供名为 <code>aot.static.value</code> 的 <code>string</code> 导出,获取值为 <code>"aot-static"</code>;
  • 仅接受空的 <code>CatalogInputs</code>;
  • 声明 AOT 与 trimming 支持,用于验证显式注册和无程序集扫描的路径。

不要在生产中复用该表,也不要根据它推导多能力拓扑、动态 Catalog 输入、业务依赖、配置绑定或失败恢复的行为。真实产品应提供自己的 <code>IRuntimeRegistrationTable</code> 和 Catalog 构建逻辑。

另外,<code>InvalidRegistrationTableFactory.Create(...)</code> 当前会抛出 <code>NotSupportedException</code>,不提供无效注册表构造行为;不要把它作为负向验证的可用工厂。负向验证应由测试项目自行构造目标输入,或使用已实现的项目内夹具。

使用边界

请遵守以下边界:

  • 不将 <code>RuntimeTestKit</code>、受控时间、受控调度器或受控 Observation 接收器注入生产服务。
  • 不把 <code>StaticRuntimeRegistrationTable</code> 作为应用的默认能力目录。
  • 不将 <code>RuntimeOwnershipSnapshot</code> 或 <code>RuntimeTrace</code> 当作生产监控协议;它们是测试观察模型。
  • 不用 <code>RuntimeBoundedReconciliationResult.Satisfied</code> 代替性能压测、可用性演练或安全审计。
  • 不依赖受控夹具的内部构造函数、内部身份生成或非公开 Core 类型。
  • 不在没有取消令牌、<code>finally</code> 清理或明确释放检查点的情况下编写并发测试。

本地验证

在仓库根目录执行以下命令可验证该包可还原、可构建,并运行与静态注册表和热路径夹具直接相关的规格测试:

dotnet restore src/runtime/src/Sparkdo.Runtime.Testing/Sparkdo.Runtime.Testing.csproj

dotnet build src/runtime/src/Sparkdo.Runtime.Testing/Sparkdo.Runtime.Testing.csproj --no-restore

dotnet test src/runtime/test/Sparkdo.Runtime.Specification.Tests/Sparkdo.Runtime.Specification.Tests.csproj --no-restore --filter "FullyQualifiedName~StaticRuntimeRegistrationTableTests|FullyQualifiedName~RuntimeHotPathInvariantFixtureTests"

dotnet pack src/runtime/src/Sparkdo.Runtime.Testing/Sparkdo.Runtime.Testing.csproj --configuration Release --no-restore

这些命令仅描述验证入口,不代表任何预先声明的测试结果。提交前还应运行当前变更范围要求的完整测试和包验证流程。

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.1-preview.3 45 8/26/2026
0.0.1-preview.2 50 8/25/2026
0.0.1-preview.1 54 8/25/2026