☰
.NET runtime CoreCLR 测试配置深度指南:CLRTestKind、CLRTestPriority 与测试工程编写规范
2026/10/16 3:24:31 网站建设 项目流程

.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 注入的分配点),强行跑只会误报。

配置写法:

属性排除的模式写法
GCStressIncompatibleGCStress 运行<GCStressIncompatible>true</GCStressIncompatible>
JitOptimizationSensitiveJIT stress / TieredCompilation 开启<JitOptimizationSensitive>true</JitOptimizationSensitive>
NativeAotIncompatibleNativeAOT 运行<NativeAotIncompatible>true</NativeAotIncompatible>
IlasmRoundTripIncompatibleilasm 往返测试<IlasmRoundTripIncompatible>true</IlasmRoundTripIncompatible>
UnloadabilityIncompatible可卸载(collectible 程序集)测试<UnloadabilityIncompatible>true</UnloadabilityIncompatible>
R2RIncompatibleReadyToRun 运行<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# 测试工程

  1. 以 src/tests/Exceptions/Finalization/Finalizer.csproj 为模板复制到src/tests下的新目录;
  2. 务必移除模板里的<AssemblyName>属性——保留它会让构建系统幕后处理测试的方式产生混淆;
  3. 按需设置CLRTestKind/CLRTestPriority(普通测试两者都可以不写,走默认推导);
  4. 加源文件。测试用例用 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);
  5. 声明依赖:托管引用用<ProjectReference Include="../ManagedDll.csproj" />,CMake 原生库用<CMakeProjectReference Include="../NativeDll/CMakeLists.txt" />——后者由ResolveCMakeNativeProjectReference目标识别并转成NativeProjectReference,构建后由CopyNativeProjectBinaries把原生二进制复制进测试输出目录;
  6. 构建,并按"重跑失败测试"的既有流程验证新测试真的被执行。

新增一个 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(必须带首尾点号)。

排查"测试被跳过"的三步法

  1. 先确认它到底构建了没有。被门禁拦下的工程走nobuild.targets,Build是空转的——产物目录里没有 dll 与执行脚本,是"构建被拒"而非"运行被跳"。检查_WillCLRTestProjectBuild链条(src/tests/Directory.Build.targets 第 94-107 行)里的每个条件:CLRTestTargetUnsupported、DisableProjectBuild、NativeAotIncompatible、AlwaysUseCrossGen2与各模式的组合。
  2. 再看执行脚本里的自跳过逻辑。用MSBuild -preprocess或直接读生成的.sh/.cmd,找SKIPPING EXECUTION BECAUSE文案:R2RIncompatible+ R2R 模式、JitOptimizationSensitive+ TieredCompilation 未禁用、GC 模拟器/长跑 GC 的开关不匹配,都在这里退出。
  3. 最后核对退出码约定。默认CLRTestExitCode是 100;如果你给手写Main的测试设了别的成功码,要确认执行脚本侧(ExpectedExitCode标记文件,GenerateMarkerFiles第 454-456 行)同步修改,否则"跑对了"也会被判失败。

配置速查

属性默认值作用适用场景冲突规则
CLRTestKindBuildAndRun(OutputType=Exe时);否则SharedLibrary决定编译/构建/脚本生成行为特殊形态(RunOnly 等)才需显式设置—
CLRTestPriority0测试梯队,低值随高值一起运行显式降级,不显式升级> 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同设直接报错
CrossGenTesttruefalse时 R2R 遍中不做标准 R2R 编译需绕过 R2R 的测试见上行
NumberOfStripesToUseInStress1(仅合并运行器)GCStress 下切分的 stripe 数大测试组压力测试上限 99,超出报构建错误
CLRTestExitCode100成功退出码手写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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询