XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

一致性套件与门禁 ​

这一页讲每个子系统的 <领域>.Conformance 包怎么用(继承抽象套件类、提供实现),插件协议的两份黑盒套件 Plugins.Conformance 与 xihan-conformance,以及框架仓库的架构测试与 CI 门禁。读完你能让自己写的 Provider 或插件跑完框架同一套用例,并知道向框架仓库提交代码要过哪些关。前置知识是单元与集成测试。

为什么需要 ​

「换 Provider 只改包引用与配置」这句承诺要有人兜底:同一个 ICacheStore 契约,内存实现、Redis 实现和 Go 插件实现对过期、批量、并发写入的语义必须一致,否则应用在开发机上跑得好、上线换了 Provider 就出问题。框架为每个子系统抽象配一个一致性套件:一组 xUnit 抽象测试类,Provider 的测试项目继承基类、实现工厂方法,套件里的全部用例随之执行。框架自己的每个默认实现与 Provider 都在仓库里跑同一套用例,自己写的 Provider 也应当继承它。插件协议同理,任何语言写的插件都用同一份黑盒套件验证。

套件一览 ​

每个套件包是一组 xUnit v3 抽象测试类,只引用 xUnit 的核心与断言包,不带运行器;测试项目要自己引用 xunit.v3。基类与要实现的工厂方法:

套件包基类实现什么
Caching.ConformanceCacheStoreConformanceCreateStore() 返回 ICacheStore;可覆写 ExpirationGrace
Security.ConformancePermissionGrantStoreConformanceCreateStoreAsync(grants) 返回装好授予的 IPermissionGrantStore;访问外部存储的 Provider 覆写 CreateUnavailableStoreAsync()
Messaging.ConformanceMessageBrokerConformance / MessageStreamConformance / OutboxConformanceCreateFeature() 返回提供 messaging.queue@1(或流、持久化)的特性;可覆写 Configuration、DeliveryTimeout、SupportsMultipleInstances
Persistence.ConformanceRepositoryConformance / DataSourceConformance / SchemaConformance / IdGeneratorConformance<TKey>CreateFeature();数据源与结构套件另实现 CreateDefinition(name) 等;标识生成器实现 CreateGenerator(clock)
MultiTenancy.ConformanceTenantStoreConformance / TenantShardResolverConformanceCreateStoreAsync(tenants) / CreateResolverAsync(tenants)
ObjectStorage.ConformanceBlobStoreConformance / BlobStoreProviderConformanceCreateStore() 与 PlantOutsideContractAsync(...);CreateProvider()、ValidSettings()、OtherValidSettings()
Localization.ConformanceMessageLocalizerConformanceCreateLocalizer(catalogs, options)
Scheduling.ConformanceJobSchedulerConformance / JobStoreConformance / LeaderElectionConformanceStartAsync(configure, configuration) 返回 JobSchedulerHost;CreateAsync();CreateElection(clock) 与 StartNodeAsync(clock, probe)
Workflow.ConformanceWorkflowStoreConformance / WorkflowEngineConformance / WorkflowTaskConformanceStartAsync() 或 StartAsync(configure, configuration) 返回套件的宿主句柄
Auditing.ConformanceAuditTrailConformanceCreateFixtureAsync() 返回 AuditTrailFixture
Configuration.ConformanceRemoteConfigurationClientConformance / SecretStoreConformanceCreateFixtureAsync() 返回带被测客户端与写入入口的夹具
AI.ConformanceKnowledgeStoreConformance / AIProviderConformanceStartAsync() 返回套件的宿主句柄
Plugins.ConformancePluginProtocolConformance / WasmPluginConformancePluginDirectory:插件构建产物目录,见下文

以特性为入口的套件(持久化、消息、调度、工作流)自己用 TestHost 装配并走完整管道,所以它们验证的是 Provider 在真实管道里的行为,不只是接口方法。各套件的用例覆盖面见 Conformance。

最小示例 ​

新建测试项目,引用被测 Provider、对应的 <领域>.Conformance 包与 xunit.v3,写一个 public sealed class 继承基类。Caching.InMemory 的测试项目就这样跑完整套缓存用例:

csharp
using XiHan.Framework.Caching.Abstractions.Stores;
using XiHan.Framework.Caching.Conformance;
using XiHan.Framework.Caching.InMemory;

public sealed class InMemoryCacheStoreConformanceTests : CacheStoreConformance
{
    protected override ICacheStore CreateStore() => new InMemoryCacheStore(new InMemoryCachingOptions(), TimeProvider.System);
}

以特性为入口的套件实现 CreateFeature();内存代理每个宿主一个、两个宿主不共用,所以把多实例用例关掉:

csharp
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Messaging.Conformance;
using XiHan.Framework.Messaging.InMemory;

public sealed class InMemoryMessageBrokerConformanceTests : MessageBrokerConformance
{
    protected override bool SupportsMultipleInstances => false;

    protected override IFeature CreateFeature() => new InMemoryMessagingFeature();
}

运行 dotnet test --project <测试项目>,套件的每个 [Fact] 都作为这个类的用例出现:缓存套件会列出读写往返、键区分大小写、批量保持顺序、不存在才写只有一个赢家、滑动过期、分布式锁互斥等十几条,全部通过才算与默认实现语义一致。某条失败时,失败名就是语义差异所在,按它修 Provider,不要在派生类里跳过。

依赖真实服务的套件 ​

依赖真实服务的 Provider(Redis、RabbitMQ、Kafka、PostgreSQL、SQL Server、MySQL、Oracle、S3、Nacos、Qdrant 等)在框架仓库里只在设置了对应环境变量时运行,没有就用 Assert.SkipWhen 跳过:XIHAN_TEST_REDIS、XIHAN_TEST_RABBITMQ、XIHAN_TEST_KAFKA、XIHAN_TEST_POSTGRES、XIHAN_TEST_SQLSERVER、XIHAN_TEST_MYSQL、XIHAN_TEST_ORACLE、XIHAN_TEST_S3、XIHAN_TEST_NACOS、XIHAN_TEST_QDRANT。CI 以服务容器提供这些服务。自己的 Provider 照这个做法:工厂方法里读环境变量,没有就跳过,这样本机没有服务也能跑其余用例,CI 再接真实服务。给不可达的存储做用例时只让被测存储的连接不可达,不要让整个宿主的库不可达:宿主里别的子系统的后台循环会如实报不可用,那是真实的故障信号而不是测试噪音。

Plugins.Conformance 与 xihan-conformance ​

插件协议有两份黑盒套件,任何语言写的插件都用它们验证:.NET 的 XiHan.Framework.Plugins.Conformance,和规范仓库 framework-spec 里语言中立的 Go 运行器 xihan-conformance。两者都以宿主身份启动插件、按规范核对,用例相同。

.NET 测试项目以插件的构建产物目录继承 PluginProtocolConformance(进程外)或 WasmPluginConformance(Wasm):

csharp
using XiHan.Framework.Plugins.Conformance;

public sealed class EchoPluginConformanceTests : PluginProtocolConformance
{
    protected override string PluginDirectory => Path.Combine(AppContext.BaseDirectory, "plugins", "echo");

    // 故意崩溃的操作不调用
    protected override IReadOnlySet<string> ExcludedOperations => new HashSet<string>(StringComparer.Ordinal) { "echoes.crash" };

    protected override IReadOnlyDictionary<string, string> SampleRequests => new Dictionary<string, string>(StringComparer.Ordinal) { ["echoes.create"] = """{"text":"conformance"}""" };
}

public sealed class EchoWasmPluginConformanceTests : WasmPluginConformance
{
    protected override string PluginDirectory => Path.Combine(AppContext.BaseDirectory, "plugins", "echo-wasm");

    protected override IReadOnlySet<string> ExcludedOperations => new HashSet<string>(StringComparer.Ordinal) { "echoes.trap", "echoes.spin" };

    // 陷阱后重建用例用它触发陷阱
    protected override string? TrapOperation => "echoes.trap";
}

可覆写的成员:Timeout(每次协议调用,缺省 15 秒,进程外)、ConfigurationJson(Configure 下发的配置节,缺省 {})、SampleRequests(没有样例的操作以 {} 调用)、ExcludedOperations(故意崩溃或依赖宿主回调的操作)、TrapOperation(Wasm,给出时才跑陷阱后重建用例)。跑 Wasm 套件的测试项目在 Windows 上要设 <CETCompat>false</CETCompat>:套件会故意触发陷阱,影子栈强制时整个测试进程 fail-fast(Wasm 插件)。

非 .NET 的插件用 Go 运行器,不需要 .NET 环境:

bash
go install github.com/xihanfun/framework-spec/conformance/cmd/xihan-conformance@latest
xihan-conformance plugin --dir ./bin --exclude echoes.crash --sample 'echoes.create={"text":"hi"}'
xihan-conformance plugin --dir ./bin --json
xihan-conformance vectors
选项含义
--dir插件目录(含 xihan.manifest.json 与 entry 指向的可执行文件),必填
--timeout每一步的截止时间,缺省 15s
--configConfigure 下发的配置节 JSON,缺省 {}
--sample op=json某个操作的请求样例,可重复;没有样例的操作以 {} 调用
--exclude op不调用的操作,可重复
--case name只跑指定用例,可重复
--json结果输出为 JSON
--quiet不转发插件的 stderr

用例:handshake(握手行、版本协商、原语层版本、清单一致、声明在清单范围内、目录至少一种区域)、refusal(没有共同版本时以 FailedPrecondition 拒绝)、lifecycle(Configure / Start / Health / WatchHealth / Stop,进程在宽限内退出)、unknown-operation(Unimplemented)、declared-operations(每个声明的操作都返回 status)、event(订阅投递得到确认,没有订阅时跳过)、stdin(关闭 stdin 后进程自行退出)。每个用例单独启动一次插件进程。退出码:0 全部通过(含跳过),1 有用例失败,2 参数或目录错误。xihan-conformance vectors 自检规范仓库 conformance/vectors/ 下的数据向量(错误码、Baggage 键、握手行、版本协商等的合法与非法样例),各语言 SDK 的单元测试也直接读这些文件。

两份套件都不提供宿主回调服务,Host.Invoke / Host.Publish 的令牌与权限、崩溃重启由宿主实现的测试覆盖。

架构测试与门禁 ​

这一节面向向框架仓库提交扩展的人(贡献指南):framework/ 下的每个正式包都要通过下列门禁,CI 全部是必过项,不放宽、不绕过、不加 NoWarn。

门禁位置检查什么
编译framework/Directory.Build.propsTreatWarningsAsErrors、Nullable、AnalysisLevel=latest-recommended、EnforceCodeStyleInBuild(命名 IDE1006 与未使用 using IDE0005 在构建期即错误)、NuGet 高危 / 严重漏洞中断构建
公开面与包framework/src/Directory.Build.propsPublicAPI.Shipped.txt / PublicAPI.Unshipped.txt 必须与公开面一致、包验证、AOT 分析(IsAotCompatible 缺省为真)
分析器Kernel.AnalyzersXH0001–XH0005:[ApiLevel]、XiHan 前缀、禁用词、Internal API、Ring 0/1 零反射(Kernel.Analyzers)
架构测试framework/test/XiHan.Framework.Architecture.Tests见下表
冒烟CIdotnet pack 触发包验证;samples/SimpleApp 以 Native AOT 发布

架构测试扫描源码目录与项目文件,按包 ID 推出环层再判定:

测试规则
ProjectReferences_PointToLowerRingsOrSameRingAbstractions依赖方向:只能引用更低环层的包;同环只能引用对方的 .Abstractions;工具链与组合包例外
Primitives_HasNoPackageOrProjectReferences、KernelAndAbstractions_ReferenceOnlyMicrosoftExtensionsAbstractions原语层零依赖;Ring 0/1 与抽象包只引用 Microsoft.Extensions.* 抽象包或白名单里的包
PrimitivesAndKernel_DoNotUseReflection零反射:Ring 0/1 源码不使用反射
PublicSignatures_DoNotLeakThirdPartyTypes公开签名只出现 BCL、Microsoft.Extensions 与框架自身的类型
PackageIds_FollowGrammar、Vocabulary_IsWellFormed包 ID 文法:由术语表与规范的文法推出,见能力包与中间件
Namespaces_MatchFolders、Namespaces_EqualPackageIdWithAtMostOneSegment命名空间等于包 ID,子命名空间最多一层且与目录同名
PublicTypes_DoNotCarryXiHanPrefix公开类型名不带 XiHan 前缀
SourceFiles_DeclareOneTopLevelType_NamedLikeTheFile、SourceFiles_StartWithCopyrightHeader一个文件一个顶层类型、文件名与类型名一致(泛型加 OfT);版权头
XmlSummaries_UseTheThreeLineForm、XmlSummaries_AreOneConciseSentence<summary> 三行写法、一句话,细节放 <remarks>
EverySourceProject_HasReadmeAndPublicApiFiles、EverySourceProject_IsReferencedByArchitectureTests每个正式包都有 README.md 与两个 PublicAPI 文件,并加进架构测试项目的引用
EveryLocalizedPackage_HasOneFilePerLocale_WithConsistentKeys、LocaleFiles_AreEmbeddedWithTheConventionalLogicalName、MessageConstants_MatchTheEnglishLocaleFile、ErrorCallSites_UseMessageKeyConstants本地化:10 种区域的语言文件齐全、占位符一致、按约定嵌入、消息键常量与 en-US.json 一一对应、错误调用点只用常量(全球化和本地化)
ErrorCodes_AreRegisteredOnce错误码全仓库唯一
DeclaredHealthItems_AreImplementedInTheSamePackage、PackagesRegisteringContributors_DeclareHealthItems清单 contributes.health 的条目由同一个包实现,登记了 IHealthContributor 的包必须声明条目(插件宿主例外);samples/ 下的示例项目同样受检
SourceProjects_DoNotCallSingleItemMethodsInsideLoops批量优先:不在循环里调用仓储、缓存存储、消息发布的单条方法,改用批量方法
DatabaseRecords_DeclareEveryStringColumnLength数据库记录的字符串列都声明 [StringLength(n)] 或 [LongText]
RecordsCarryingCredentials_MaskThemInToString携带凭据的记录在 ToString 里遮蔽凭据

常见问题 ​

套件里有一条用例我的后端做不到,能跳过吗 ​

先看基类有没有对应的虚成员:多实例、不可达存储、命名连接、直接写入入口这类差异都有开关,不覆写时相关用例自动跳过。没有开关的用例代表契约语义,做不到说明实现与契约不一致,要改实现或在应用里不声明这个能力。

套件要连真实服务,本机没有怎么办 ​

工厂方法里读 XIHAN_TEST_* 环境变量,没有就 Assert.SkipWhen 跳过;CI 用服务容器提供。本机要跑时用 Docker 起一个服务并设环境变量即可。

插件套件和宿主测试有什么区别 ​

Plugins.Conformance 与 xihan-conformance 验证插件是否遵守协议(握手、生命周期、操作应答、事件确认、stdin 退出),不提供宿主回调。回调权限、会话令牌、崩溃重启、代理操作经管道这些是宿主行为,由宿主包自己的测试覆盖;写插件只需过前者。

我的应用不向框架仓库提交,架构门禁与我有关吗 ​

包 ID 文法只约束 XiHan.Framework.*,但清单 ID、能力 ID、错误码与健康项的文法由内核在运行期校验,照样适用;本地化与批量优先的规则是框架默认实现的约定,自己的 Provider 遵守它们才能通过一致性套件。想让编译器执行同样的类型命名规则,启用 Kernel.Analyzers。

相关包与下一步 ​

Released under The MIT License