.NET runtime CoreCLR 测试配置深度指南:CLRTestKind、CLRTestPriority 与测试工程编写规范
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
你在src/tests下新建一个 CoreCLR 测试工程后,CI 会替它决定:要不要编译、要不要生成执行脚本、GCStress 跑遍时跳不跳过、R2R 模式下用不用 crossgen2 预编译。这些决定全部来自工程里的几个 MSBuild 属性——CLRTestKind与CLRTestPriority是入口,其余不兼容开关是修正项。本文拆解这套配置体系:每个属性的定义、正确写法,以及底层判定发生在哪个 targets 文件的哪一行,让你配置一次到位、跳过得来也查得清。
机制总览:一个属性如何走完"判定 → 执行"
所有测试工程的属性声明都先汇入src/tests/Directory.Build.targets做统一判定,再分流到执行脚本生成。先记住这张分层图,后文每个概念都在其中占位:
csproj 中声明: CLRTestKind / CLRTestPriority / 各种 *Incompatible 属性 │ ▼ src/tests/Directory.Build.targets ├─ 推导默认值(未显式设置时按 OutputType 推断 Kind,Priority 默认 0) ├─ 构建门禁 _WillCLRTestProjectBuild ──false──▶ nobuild.targets(Build 空转,不生成任何产物) └─ true ▼ src/tests/Common/CLRTest.Execute.targets ├─ 生成 .cmd(Windows)/ .sh(Unix)执行脚本 ├─ CLRTest.Jit.targets ── ilasm 往返、反汇编检查、GCStress 环境变量 └─ CLRTest.CrossGen.targets ── R2R / crossgen2 预编译与 R2RDump 校验两个值得先记住的设计:
- 身份与门禁分离:
CLRTestKind回答"这个工程是什么形态",CLRTestPriority回答"它属于第几梯队",而CLRTestTargetUnsupported、NativeAotIncompatible一类属性回答"这次构建里要不要它"。三者最终都汇进同一个_WillCLRTestProjectBuild判定。 - 构建时决策 + 运行时自跳过:不兼容属性不是在 CI 脚本里过滤工程,而是把
SKIPPING EXECUTION ...逻辑直接写进执行脚本。同一个产物因此可以安全地跑在多种模式里——模式不匹配时脚本自己退出。
概念拆解:五个属性组,从身份到形态
1. CLRTestKind:工程是构建、运行还是两者都做
定义:每个测试工程必须归属四种形态之一,由<CLRTestKind>声明:
| CLRTestKind | 编译源码 | 构建可执行文件 | 生成执行脚本 | 典型用途 |
|---|---|---|---|---|
BuildOnly | 是 | 是 | 否 | 编译期校验、供 RunOnly 复用的产物 |
RunOnly | 否 | 否 | 是 | 同一可执行文件配不同命令行参数 |
BuildAndRun | 是 | 是 | 是 | 绝大多数普通测试(默认值) |
SharedLibrary | 是 | 否 | 否 | 多个测试共用的托管库 |
作用:Kind 是构建系统一切行为的总开关——它决定脚本是否生成、依赖工程是否先行构建、签名是否跳过。
配置写法:多数工程不需要显式设置,默认推导逻辑在 src/tests/Directory.Build.targets 第 19-20 行:
<CLRTestKind Condition="'$(CLRTestKind)' == '' and '$(OutputType)' == 'Exe'">BuildAndRun</CLRTestKind> <CLRTestKind Condition="'$(CLRTestKind)' == ''">SharedLibrary</SharedLibrary>即输出为Exe的工程默认BuildAndRun,类库默认SharedLibrary。需要特殊形态时才显式写<CLRTestKind>RunOnly</CLRTestKind>。
底层实现:第 30-64 行的Choose块把四种 Kind 翻译成_CLRTestCompilesSource/_CLRTestNeedsToRun/GenerateRunScript/_CLRTestBuildsExecutable四个内部开关。RunOnly的依赖工程机制在第 84-88 行:当"需要运行但不构建可执行文件"时,CLRTestProjectToRun被自动转换为ProjectReference,保证被引用的工程先构建。随后 src/tests/Common/CLRTest.Execute.targets 的FetchExternalProperties目标(第 100-101 行)用[MSBuild]::MakeRelative把被引用工程的输出算成InputAssemblyName,BuildAndRun则直接用$(AssemblyName).dll。
真实示例:src/tests/GC/Scenarios/GCSimulator/GCSimulator_100.csproj 是一个RunOnly工程,通过<CLRTestProjectToRun>GCSimulator.csproj</CLRTestProjectToRun>复用同一可执行文件,并用<CLRTestExecutionArguments>传入不同的模拟器参数。
2. CLRTestPriority:测试梯队与选择性构建
定义:测试按重要性分梯队,priority 0 必须是最重要且最小的子集。默认所有测试为 priority 0,想要"降级"必须显式声明。
作用:既控制运行范围,也控制构建范围。低优先级值总是与高优先级值一起跑——选 priority 2 的运行,0、1、2 全部执行。
配置写法:
<CLRTestPriority>2</CLRTestPriority> <!-- 显式降级;不写则默认 0 -->底层实现:默认值在 src/tests/Directory.Build.targets 第 21 行。选择性构建在第 94-107 行:BuildAllProjects=true时,只有CLRTestPriority <= CLRTestPriorityToBuild的工程才置_WillCLRTestProjectBuild=true,其余工程连同 IL 目标一起被跳过(第 126 行SkipImportILTargets)。这就是"只构建到某一优先级为止"的批量操作基础。
仓库里的现实示例:src/tests/GC/API/GC/Collect.csproj 显式写了<CLRTestPriority>1</CLRTestPriority>(因为该测试要求独占的进程级 GC 状态),而 src/tests/GC/API/GC/Collect_Aggressive.csproj 留在默认的 priority 0。
3. 构建门禁:这个工程本次要不要构建
定义:一组条件开关,命中任意一条,工程就不参与构建、不生成执行脚本。
作用:把"某平台跑不了 / 某构建模式不适用"从 CI 流水线配置里解耦出来,直接写在工程自身。
配置写法(条件写法按需选用):
<!-- 在特定目标平台不构建也不运行 --> <CLRTestTargetUnsupported Condition="'$(TargetArchitecture)' == 'arm64'">true</CLRTestTargetUnsupported> <!-- 无条件禁用构建 --> <DisableProjectBuild>true</DisableProjectBuild> <!-- NativeAOT 模式不适用 --> <NativeAotIncompatible>true</NativeAotIncompatible>底层实现:判定全部集中在 src/tests/Directory.Build.targets 第 94-107 行的_WillCLRTestProjectBuild链条中。除了上面三个属性,还自动处理了一批衍生场景:MonoAotIncompatible在 mono fullaot 变体下禁用(第 15 行)、IsGCSimulatorTest与TestBuildMode=nativeaot互斥(第 103 行)、AlwaysUseCrossGen2在 mono、nativeaot、sanitizer 模式下禁用构建(第 104-106 行,与概念 5 联动)。判定为不构建时,文件末尾(第 561 行)导入 src/tests/Common/nobuild.targets——它把Build、Compile等目标全部替换为空操作,构建"空转"通过,但不产出任何东西。
4. 压力模式不兼容属性:让执行脚本自己跳过
定义:一组把测试从特定运行模式排除的开关。它们不影响构建,只影响执行脚本内容。
作用:CI 会用 GCStress、JIT stress、ilasm 往返、可卸载程序集等模式反复跑同一批测试。有些测试与某种模式的代码形态根本不相容(比如反汇编检查遇上 GCStress 注入的分配点),强行跑只会误报。
配置写法:
| 属性 | 排除的模式 | 写法 |
|---|---|---|
GCStressIncompatible | GCStress 运行 | <GCStressIncompatible>true</GCStressIncompatible> |
JitOptimizationSensitive | JIT stress / TieredCompilation 开启 | <JitOptimizationSensitive>true</JitOptimizationSensitive> |
NativeAotIncompatible | NativeAOT 运行 | <NativeAotIncompatible>true</NativeAotIncompatible> |
IlasmRoundTripIncompatible | ilasm 往返测试 | <IlasmRoundTripIncompatible>true</IlasmRoundTripIncompatible> |
UnloadabilityIncompatible | 可卸载(collectible 程序集)测试 | <UnloadabilityIncompatible>true</UnloadabilityIncompatible> |
R2RIncompatible | ReadyToRun 运行 | <R2RIncompatible>true</R2RIncompatible> |
底层实现,按三个文件分述:
- GCStress:src/tests/Common/CLRTest.Jit.targets 第 496-499 行把
RunWithGcStress写进执行脚本的DOTNET_GCStress环境变量。同一文件第 108-117 行的GetDisasmCheckData目标会扫描带HasDisasmCheck元数据的源文件,有反汇编检查的工程被自动标记GCStressIncompatible(第 116 行)——你不需要、也不应该手写这一条。 - JIT 敏感:src/tests/Common/CLRTest.Execute.Bash.targets 第 105-115 行(Batch 版在第 98-119 行)在脚本前置环境兼容性检查:TieredCompilation 未禁用时输出
SKIPPING EXECUTION BECAUSE ... marked JitOptimizationSensitive并退出。 - ilasm 往返:
IlasmRoundTripIncompatible=true时构建会落一个标记文件(GenerateMarkerFiles目标,src/tests/Directory.Build.targets 第 445-447 行),ilasm 往返脚本(CLRTest.Jit.targets第 283 行起的IlasmRoundTripCommand)看到标记就跳过该程序集。
5. R2R 与 crossgen2:预编译测试的高级属性
定义:围绕 ReadyToRun 的四个属性:R2RIncompatible(整体排除)、AlwaysUseCrossGen2(强制以 crossgen2 方式编译)、CrossGenTest(默认true,置false表示 R2R 测试遍中也不做标准 R2R 编译)、CompositeCrossGen2TestExtraArguments/CrossGen2TestExtraArguments(附加 crossgen2 参数)。
作用:让"专测 crossgen2 行为"的工程在所有模式下都以 R2R 身份出场,同时避免与 ilasm 往返等不相容的模式冲突。
配置写法,仓库里最完整的范本是 src/tests/readytorun/crossgen2/crossgen2smoke.csproj:
<IlasmRoundTripIncompatible>true</IlasmRoundTripIncompatible> <AlwaysUseCrossGen2>true</AlwaysUseCrossGen2> <CrossGen2TestExtraArguments>$(CrossGen2TestExtraArguments) --generate-unboxing-stubs</CrossGen2TestExtraArguments>底层实现:src/tests/Common/CLRTest.CrossGen.targets 第 26 行规定AlwaysUseCrossGen2=true自动蕴含IlasmRoundTripIncompatible=true(crossgen2 产物与 ilasm 往返不相容,无需手动写)。执行脚本生成时的三分支:
R2RIncompatible=true且运行模式要求 R2R → 脚本直接输出SKIPPING EXECUTION BECAUSE the test is incompatible with ReadyToRun后退出(第 50-57 行);AlwaysUseCrossGen2=true→ 设置RunCrossGen2=1与CompositeBuildMode=1,走 composite 编译流程,产物还要过 R2RDump 的--header --sc --val校验;- 冲突规则:如果同时设置
AlwaysUseCrossGen2=true和CrossGenTest=false,构建直接报错Setting both AlwaysUseCrossGen2 to true and CrossGenTest to false is non-sensical(第 47 行)——两者语义互相矛盾。
6. 合并测试运行器:一个入口跑几百个测试
定义:merged test runner 把目录下成百上千个小测试聚合成单个可执行文件运行,由IsMergedTestRunnerAssembly(运行器自身声明)与HasMergedInTests(声明其吞并了子工程)标识。
作用:避免每个小测试各自起进程的开销,同时保留-stripe切分能力,让 GCStress 这类慢模式下大测试组也能切条并行。
配置写法:以 src/tests/JIT/Methodical/Methodical_d1.csproj 为模板,核心结构:
<HasMergedInTests>true</HasMergedInTests> <!-- 按通配符批量引入子工程 --> <MergedWrapperProjectReference Include="a*/**/*_d.??proj" /> <!-- GCStress 下耗时过长时,切分成 10 条 stripe --> <NumberOfStripesToUseInStress>10</NumberOfStripesToUseInStress>底层实现:src/tests/Directory.Build.targets 第 318-331 行的GenerateMarkerFiles目标:合并运行器未显式设置时默认 1 条 stripe(第 319-321 行),随后生成形如<name>.MergedTestAssembly.<stripe>.<total>.MergedTestAssemblyForStress的标记文件(第 426-431 行),CI 的压力测试按标记文件把测试组切开并行执行;stripe 数超过 99 会直接报构建错误。运行时的参数解析在 src/tests/Common/XUnitWrapperLibrary/TestFilter.cs 第 128-156 行。
实战场景
新增一个 C# 测试工程
- 以 src/tests/Exceptions/Finalization/Finalizer.csproj 为模板复制到
src/tests下的新目录; - 务必移除模板里的
<AssemblyName>属性——保留它会让构建系统幕后处理测试的方式产生混淆; - 按需设置
CLRTestKind/CLRTestPriority(普通测试两者都可以不写,走默认推导); - 加源文件。测试用例用 Xunit 的
[Fact]特性标记:构建系统通过 src/tests/Directory.Build.targets 第 537-543 行把XUnitWrapperGenerator源生成器注入为Analyzer,它会自动发现所有[Fact]方法并从生成的Main中调用。也可以手写Main——成功返回 100,失败返回任何非 100 值(100 这个默认值定义在 src/tests/Common/CLRTest.Execute.targets 第 37 行CLRTestExitCode); - 声明依赖:托管引用用
<ProjectReference Include="../ManagedDll.csproj" />,CMake 原生库用<CMakeProjectReference Include="../NativeDll/CMakeLists.txt" />——后者由ResolveCMakeNativeProjectReference目标识别并转成NativeProjectReference,构建后由CopyNativeProjectBinaries把原生二进制复制进测试输出目录; - 构建,并按"重跑失败测试"的既有流程验证新测试真的被执行。
新增一个 JIT 回归测试
- 常规情况:优化编译、无调试信息的回归测试直接往
src/tests/JIT/Regression_ro_2/目录放Runtime_<issue_number>.cs即可。src/tests/JIT/Regression_ro_2/Regression_ro_2.csproj 用<Compile Include="**\*.cs" />递归吞并目录下全部 C# 文件——不需要改工程文件,也不需要为单个源文件建目录,相关辅助文件可以放子目录分组。 - 进阶情况:需要不同编译设置、独立程序集、原生依赖或进程隔离的测试,放在
src/tests/JIT/Regression_2/Runtime_<issue_number>/下建独立工程。Regression_2.csproj以递归发现工程的方式组织它们,不直接编译源码;Regression_3.csproj类似地管理既有的GitHub_*、DevDiv_*系列工程。注意:不要把这类工程的源码放进源文件通配运行器的目录,否则会被直接编译进运行器。
本地运行合并测试运行器
合并运行器支持的标准命令行形态:
[testFilterString] [-stripe <whichStripe> <totalStripe>]testFilterString是除-stripe外的任意字符串,目前仅支持dotnet test --filter的简单子串形式。条纹控制有两种等价方式:-stripe 3 10(whichStripe从 0 计),或环境变量TEST_HARNESS_STRIPE_TO_EXECUTE=.3.10(必须带首尾点号)。
排查"测试被跳过"的三步法
- 先确认它到底构建了没有。被门禁拦下的工程走
nobuild.targets,Build是空转的——产物目录里没有 dll 与执行脚本,是"构建被拒"而非"运行被跳"。检查_WillCLRTestProjectBuild链条(src/tests/Directory.Build.targets 第 94-107 行)里的每个条件:CLRTestTargetUnsupported、DisableProjectBuild、NativeAotIncompatible、AlwaysUseCrossGen2与各模式的组合。 - 再看执行脚本里的自跳过逻辑。用
MSBuild -preprocess或直接读生成的.sh/.cmd,找SKIPPING EXECUTION BECAUSE文案:R2RIncompatible+ R2R 模式、JitOptimizationSensitive+ TieredCompilation 未禁用、GC 模拟器/长跑 GC 的开关不匹配,都在这里退出。 - 最后核对退出码约定。默认
CLRTestExitCode是 100;如果你给手写Main的测试设了别的成功码,要确认执行脚本侧(ExpectedExitCode标记文件,GenerateMarkerFiles第 454-456 行)同步修改,否则"跑对了"也会被判失败。
配置速查
| 属性 | 默认值 | 作用 | 适用场景 | 冲突规则 |
|---|---|---|---|---|
CLRTestKind | BuildAndRun(OutputType=Exe时);否则SharedLibrary | 决定编译/构建/脚本生成行为 | 特殊形态(RunOnly 等)才需显式设置 | — |
CLRTestPriority | 0 | 测试梯队,低值随高值一起运行 | 显式降级,不显式升级 | > CLRTestPriorityToBuild时不构建 |
CLRTestProjectToRun | 空 | RunOnly 工程指向真正运行的工程 | 同一可执行文件多种参数 | 仅CLRTestKind=RunOnly有意义 |
CLRTestTargetUnsupported | 空 | 条件禁用某目标的构建与运行 | 平台/架构不支持 | 置 true 即走 nobuild |
DisableProjectBuild | 空 | 无条件禁用构建 | 工程整体停用 | 优先级最高的门禁 |
GCStressIncompatible | 空 | 排除 GCStress | 依赖稳定 GC 行为的测试 | HasDisasmCheck时自动置 true,勿手写 |
JitOptimizationSensitive | 空 | 排除 JIT stress | 对寄存器分配/优化路径敏感 | 要求DOTNET_TieredCompilation禁用 |
NativeAotIncompatible | 空 | 排除 NativeAOT | 托管桩在 AOT 下不可用 | 置 true 即 nativeaot 模式不构建 |
IlasmRoundTripIncompatible | 空 | 排除 ilasm 往返 | 产物无法可靠往返 | 自动蕴含ManagedIlasmRoundTripIncompatible |
UnloadabilityIncompatible | 空 | 排除 collectible 程序集测试 | 依赖静态程序集生命期 | — |
R2RIncompatible | 空 | 排除 R2R 运行 | R2R 产物行为不相容 | R2R 模式下脚本自跳过 |
AlwaysUseCrossGen2 | 空 | 强制 crossgen2 编译 | 专测 crossgen2 的工程 | 与CrossGenTest=false同设直接报错 |
CrossGenTest | true | false时 R2R 遍中不做标准 R2R 编译 | 需绕过 R2R 的测试 | 见上行 |
NumberOfStripesToUseInStress | 1(仅合并运行器) | GCStress 下切分的 stripe 数 | 大测试组压力测试 | 上限 99,超出报构建错误 |
CLRTestExitCode | 100 | 成功退出码 | 手写Main的非标准约定 | 合并运行器可用ExpectedExitCode标记覆盖 |
源码导读
- src/tests/Directory.Build.targets —— 所有测试工程的统一入口:Kind/Priority 默认值推导、四形态开关、
_WillCLRTestProjectBuild构建判定、stripe 标记文件生成、XUnit 源生成器注入。 - src/tests/Common/CLRTest.Execute.targets —— 执行脚本生成核心:
InputAssemblyName决策(RunOnly 相对路径 vs BuildAndRun 直用 dll)、默认CLRTestExitCode=100。 - src/tests/Common/CLRTest.CrossGen.targets —— crossgen2/R2R 脚本:响应文件、composite 模式、R2RDump 校验、矛盾配置报错。
- src/tests/Common/CLRTest.Jit.targets —— ilasm 往返脚本、反汇编检查(SuperFileCheck 前后置命令)、GCStress 环境变量注入、SuperPMI 收集。
- src/tests/Common/nobuild.targets 与 src/tests/Common/runonly.targets —— 被门禁拦截时让构建空转;RunOnly 工程的"只构建依赖 + 建输出目录"特殊构建。
- src/tests/Common/XUnitWrapperLibrary/TestFilter.cs —— 合并运行器的测试过滤与
-stripe/TEST_HARNESS_STRIPE_TO_EXECUTE解析。 - src/tests/Common/test_dependencies/test_dependencies.csproj —— 测试公共 NuGet 依赖的集中承载工程(
BuildOnly),新增包引用改这里。 - 示例工程:src/tests/JIT/Methodical/Methodical_d1.csproj(合并运行器模板)、src/tests/JIT/Regression_ro_2/Regression_ro_2.csproj(源文件通配运行器)、src/tests/GC/API/GC/Collect.csproj(Priority 降级)、src/tests/readytorun/crossgen2/crossgen2smoke.csproj(crossgen2 属性全量用法)。
建议按Directory.Build.targets → CLRTest.Execute.targets → 你手头那个"行为异常"的 targets 文件的顺序精读:先看懂属性如何变成判定,再顺着判定落到脚本片段,"测试被跳过、脚本没生成、R2R 行为不对"这类问题基本都能在这条链路上对号入座。
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考