CoreCLR 测试配置决策流程:CLRTestKind 与兼容性开关的避坑清单

📅 发布时间:2026/10/16 5:45:22
CoreCLR 测试配置决策流程:CLRTestKind 与兼容性开关的避坑清单
CoreCLR 测试配置决策流程CLRTestKind 与兼容性开关的避坑清单【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime往src/tests里新增一个测试时你真正要回答的只有四个问题构建系统会不会编译它、会不会执行它、它会进入哪些执行模式、批量构建时它会被跑到哪一级为止CLRTestKind、CLRTestPriority以及一组Incompatible属性加上 stripe 条纹切分就是 CoreCLR 测试配置的完整决策流程。一个测试工程如何被构建系统识别默认推断链本节回答csproj 里什么都不写时构建系统会替你做出哪些决定RunOnly 工程又如何找到自己要运行的可执行文件。未声明 CLRTestKind 时会发生什么所有测试工程统一从 src/tests/Directory.Build.targets 进入文件开头的推断链第 19-21 行先按OutputType兜底CLRTestKind Condition$(CLRTestKind) and $(OutputType) ExeBuildAndRun/CLRTestKind CLRTestKind Condition$(CLRTestKind) SharedLibrary/CLRTestKind CLRTestPriority Condition$(CLRTestPriority) 0/CLRTestPriority解读可执行程序不写CLRTestKind就是BuildAndRun类库则是SharedLibrary优先级永远默认 0也就是说测试只会被你显式降级不存在升级。随后第 30-64 行的Choose块把四种种类映射为四个内部开关决定编译源码、构建可执行文件、生成执行脚本、是否需要运行这四个动作的组合CLRTestKind编译源码构建可执行文件生成执行脚本需要运行BuildOnly是是否否RunOnly否否是是BuildAndRun默认是是是是SharedLibrary是否否否BuildOnly适合只做编译期校验的工程SharedLibrary适合被多个测试共用的托管库BuildAndRun覆盖绝大多数普通测试。RunOnly 工程如何定位要运行的产物RunOnly的特殊之处在于它自己不编译任何东西只负责生成执行脚本。第 66-69 行先算出需要运行但不产出可执行文件的组合置位_CLRTestNeedsProjectToRun接着第 84-88 行把你用CLRTestProjectToRun指定的依赖工程转成一条ProjectReference保证被引用工程先构建完成第 564 行再导入 src/tests/Common/runonly.targets 走专用构建路径。真正拼接执行脚本的逻辑在 src/tests/Common/CLRTest.Execute.targets 的FetchExternalProperties目标第 90-103 行其中输入程序集的定位方式按种类分叉InputAssemblyName Condition$(CLRTestKind) RunOnly$([MSBuild]::MakeRelative($(OutputPath), $(_CLRTestToRunFileFullPath)))/InputAssemblyName InputAssemblyName Condition$(CLRTestKind) BuildAndRun$(AssemblyName).dll/InputAssemblyName解读BuildAndRun直接用自身$(AssemblyName).dllRunOnly则先跨进程查询被引用工程的脚本目标再用[MSBuild]::MakeRelative算出相对路径作为InputAssemblyName写进 BatchWindows或 BashUnix脚本。典型用法是同一个可执行文件配多组不同命令行参数各自建一个 RunOnly 工程避免重复编译。CLRTestPriority 如何控制构建到第几级优先级除了决定运行范围还参与选择性构建。第 94-99 行的判定逻辑是只有传入BuildAllProjectstrue时才启用批量模式此时仅当CLRTestPriority CLRTestPriorityToBuild的工程才把_WillCLRTestProjectBuild置为 true第 126 行还会对优先级超限的工程跳过 IL targets 导入。运行语义上则是包含式的你要跑 priority 2 的测试priority 0、1 的工程会一并执行——所以最重要的测试应当恰好落在最小集合里即 priority 0。工程文件里降级只需一行比如CLRTestPriority1/CLRTestPriority仓库里GC/API/Frozen/Frozen.csproj就是这样的真实用例。你的测试会出现在哪些执行模式里兼容性开关全清单本节回答怎样把测试从 GCStress、JIT 压力、NativeAOT 等特定执行模式里摘出去。所有开关都写在 csproj 中取值统一为true。执行模式、注入方式与排除属性对照执行模式触发/注入机制csproj 属性GCStress执行脚本注入环境变量DOTNET_GCStress值取自RunWithGcStressGCStressIncompatibleJIT stressDOTNET_JitStress/DOTNET_JitStressRegs等环境变量JitOptimizationSensitiveNativeAOTTestBuildModenativeaot的构建变体NativeAotIncompatibleilasm 往返执行脚本中的RunningIlasmRoundTrip开关反汇编后再汇编回 ILIlasmRoundTripIncompatible同时隐含ManagedIlasmRoundTripIncompatible可回收程序集unloadability独立测试通道UnloadabilityIncompatibleReadyToRunR2R 测试通道 crossgen2 编译R2RIncompatibleGCStress 的注入点可以在 src/tests/Common/CLRTest.Jit.targets 末尾看到第 497-498 行CLRTestEnvironmentVariable Condition$(RunWithGcStress) ! IncludeDOTNET_GCStress Value$(RunWithGcStress) /解读压力参数本身是运行期通过CLRTestEnvironmentVariable项拼进执行脚本的环境变量所以排除某种模式本质上是告诉脚本生成器该模式下不要生成这个测试的执行入口。两条会自动生效的联动规则第一条来自CLRTest.Jit.targets的GetDisasmCheckData目标第 108-118 行只要任一Compile项标记了HasDisasmChecktrue构建系统就会自动置GCStressIncompatibletrue。原因是反汇编检查断言的是特定代码形态GCStress 改变分配模式后 CHECK 断言必然失真与其跑挂了再排查不如直接屏蔽。第二条在 src/tests/Common/CLRTest.CrossGen.targets 第 26 行AlwaysUseCrossGen2true会连带置IlasmRoundTripIncompatibletrue因为 crossgen2 产物与 ilasm 往返流程不兼容。R2R 的两个高级属性AlwaysUseCrossGen2 与 CrossGenTestAlwaysUseCrossGen2true测试专门验证 crossgen2任何测试模式下都走 AOT 编译。执行脚本里对应export RunCrossGen21与export CompositeBuildMode1Bash 版脚本第 61-64 行进入复合composite编译流程。CrossGenTestfalse即便整轮是 R2R 测试通道该测试也不做标准 R2R 编译只按普通方式运行。两者同时出现会被直接拒绝第 47 行定义了一个 MSBuildError文案为 Setting both AlwaysUseCrossGen2 to true and CrossGenTest to false is non-sensical。若设了R2RIncompatibletrue又逢 R2R 通道生成的脚本会打印SKIPPING EXECUTION BECAUSE the test is incompatible with ReadyToRun并exit 0——这是主动跳过不是失败。从零建一个测试工程端到端检查单本节回答一个新测试要经过哪些步骤才能被基础设施完整接受。动笔之前的硬性规范每个测试源文件头部必须带这两行许可证横幅// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license.托管代码部分必须能在任何平台编译通过——CI 的做法是所有测试的托管部分先在 OSX 上统一构建各目标平台只运行其中子集。因此你的代码里不能出现三种东西依赖目标平台的条件编译、依赖目标平台的条件文件包含、依赖目标平台的DefineConstants/条件定义。平台差异只能放在运行期判断或交给下面的禁用开关处理。另外三条约定测试用到的System.Private.CoreLib类型和方法必须在所有平台上可编译不支持的平台用throw new PlatformNotSupportedException()的桩实现兜底因已知 bug 而失败的测试要挂[ActiveIssue]特性NuGet 依赖不写进各测试工程而是集中加到 src/tests/Common/test_dependencies/test_dependencies.csproj该工程自身就是BuildOnly通过AddLibrariesToCoreRoot目标把库运行时文件汇集到 Core_RootDirectory.Build.targets第 488-491 行会导入它的 targets 到每个测试工程。从模板到首次运行的八步把一个成熟测试例如src/tests/Exceptions/Finalization/Finalizer.csproj连同源文件复制到src/tests下的新目录。⚠️删掉AssemblyName属性。保留它会干扰构建系统对测试产物的命名与定位假设。声明CLRTestKind/CLRTestPriority显式表达意图别依赖推断。用 xunit 的[Fact]特性写测试方法。Directory.Build.targets第 537-543 行会把 src/tests/Common/XUnitWrapperGenerator 以 Analyzer 形式的ProjectReference注入每个需要运行的 C# 测试工程——这个源生成器扫描所有Fact方法并从生成的Main里依次调用它们。也可以自己写Main而不依赖源生成器此时遵循退出码约定成功返回 100任何非 100 值都算失败。这与脚本侧一致CLRTest.Execute.targets第 37 行定义了CLRTestExitCode默认值100。static public int Main(string[] notUsed) { try { // 测试场景 } catch (Exception e) { Console.WriteLine($Test Failure: {e}); return 101; } return 100; }解读执行脚本最终比对的是进程退出码与CLRTestExitCode自定义入口时只要守住100 通过这条线即可。按需声明依赖托管库用ProjectReference Include../ManagedDll.csproj /原生库用CMakeProjectReference Include../NativeDll/CMakeLists.txt /后者由Directory.Build.targets的ResolveCMakeNativeProjectReference识别后转为NativeProjectReference构建完把原生二进制复制进测试输出目录。执行构建。按重跑失败测试的标准步骤验证新测试确实能跑、能失败。需要在某些目标上整体禁用两个开关条件性禁用用CLRTestTargetUnsupported例如CLRTestTargetUnsupported Condition$(TargetArchitecture) arm64true/CLRTestTargetUnsupported效果是该目标下既不构建也不运行无条件禁用构建用DisableProjectBuildtrue/DisableProjectBuild。两者最终都汇入第 94-107 行的_WillCLRTestProjectBuild判定分别对应第 100、101 行判定为 false 后第 561 行导入 src/tests/Common/nobuild.targets让后续构建目标全部空转——这是被跳过的最常见根因。两类特殊的测试组织形态回归测试与合并运行器本节回答不加新 csproj 怎么投喂回归测试大批量测试组在 GC Stress 下超时怎么办。回归测试源码source-glob 直接并入还是独立工程JIT/Regression_ro_2/Regression_ro_2.csproj这类运行器会递归包含其目录下所有.cs文件所以往src/tests/JIT/Regression_ro_2/Runtime_issue_number.cs丢一个文件就是一次完整的新增——不改编程文件也不为单个源文件建目录。相关文件可以放子目录分组被多个运行器共享的源码放在某个运行器目录里由其他运行器显式包含。其他源码编译型回归运行器沿用同一约定并保留各自的分区partition与编译设置。反过来需要独立编译参数、独立程序集、原生依赖或进程隔离的测试必须拥有自己的工程放在src/tests/JIT/Regression_2/Runtime_issue_number/Regression_2.csproj采用递归发现工程的方式组织它们而不是直接编译源码Regression_3.csproj则类似地管辖Regression_3/下既有的GitHub_*、DevDiv_*等基于工程的测试。⚠️ 这两类形态的边界必须守住独立工程的源码不要塞进 source-glob 运行器的目录否则会被悄悄编译进错误的运行器。合并运行器与 stripe 切分大批量测试怎么在本地复现以 src/tests/JIT/Methodical/Methodical_d1.csproj 为模板工程用HasMergedInTeststrue/HasMergedInTests声明合并属性再用MergedWrapperProjectReference以通配符批量引入子工程例如MergedWrapperProjectReference Includea*/**/*_d.??proj / MergedWrapperProjectReference Includeb*/**/*_d.??proj /解读这两行把 a~e 开头分区里所有_d.??proj子工程聚合进同一个运行器程序集共享的公共源文件如common/localloc_common.ilproj用普通ProjectReference引入少数需要直接并入的源码用显式Compile Include列出。若这个运行器测试量巨大、GC Stress 下跑不完就设NumberOfStripesToUseInStress比如 10。生效逻辑在Directory.Build.targets的GenerateMarkerFiles目标合并运行器未显式指定时默认取 1第 319-321 行随后按 0..99 整数序列为前 N 个值各生成一个标记文件命名形如name.MergedTestAssembly.stripe.total.MergedTestAssemblyForStress第 426-429 行CI 压力测试据此把大组切成多条条纹并行超过 99 会直接报错 Attempt to split into too many stripes. Either reduce stripe count, or add more entries to SequenceOfIntegers第 431 行。本地手动复现时合并运行器接受一组标准命令行参数[testFilterString] [-stripe whichStripe totalStripes]testFilterString是除-stripe外的任意字符串目前仅支持dotnet test --filter的简单形式即测试完全限定名的子串匹配。条纹控制有等价两条路径命令行-stripe whichStripe totalStripes或环境变量TEST_HARNESS_STRIPE_TO_EXECUTE值必须写成.whichStripe.totalStripes的形式。whichStripe从 0 开始编号totalStripes是条纹总数——比如-stripe 2 10表示跑第 3 条0 起算共 10 条中的那一条。当测试被跳过或行为异常按症状定位 targets 的排查路径本节回答测试没构建、没跑、或者 R2R 行为怪异时从哪个文件的哪个判定分支查起。一张症状 → 去处的对照表观察到的症状先查的文件具体判定分支整个工程没构建产物目录为空src/tests/Directory.Build.targets_WillCLRTestProjectBuild94-107 行CLRTestTargetUnsupported、DisableProjectBuild、NativeAotIncompatible nativeaot、AlwaysUseCrossGen2 mono/sanitizer/nativeaot 等都会置 false随后导入 nobuild.targets 空转RunOnly 工程报找不到程序集src/tests/Common/CLRTest.Execute.targetsFetchExternalProperties90-103 行CLRTestProjectToRun是否指向可构建工程、MakeRelative路径是否正确应生成执行脚本却没有src/tests/Common/CLRTest.Execute.targetsGenerateExecutionScriptsInternal105 行的触发条件是_CLRTestNeedsToRun or _CLRTestBuildsExecutable两者皆 false 则静默跳过R2R 通道下脚本直接 exit 0src/tests/Common/CLRTest.CrossGen.targets第 50-55 行的跳过分支R2RIncompatible以及第 47 行的AlwaysUseCrossGen2与CrossGenTestfalse冲突Error反汇编断言意外失效src/tests/Common/CLRTest.Jit.targetsGetDisasmCheckData108-118 行HasDisasmCheck自动置GCStressIncompatibleGetDisasmCheck*Script还会在 JitStress/TailcallStress/TieredPGO 等环境下整体跳过 FileCheckGCStress 环境变量没进脚本src/tests/Common/CLRTest.Jit.targets末尾497-498 行的CLRTestEnvironmentVariable注入点应力测试切分不生效src/tests/Directory.Build.targetsGenerateMarkerFiles318-431 行IsMergedTestRunnerAssembly、NumberOfStripesToUseInStress与标记文件生成条件CI 上看到的失败页面通常会把原始输出与退出码一起给出排查时先对照退出码 100 才算通过的约定再回到上表定位分支这张截图来自仓库 CI 工作流文档展示了失败测试的典型呈现原始输出、期望/实际值对比、退出码判定。对照本文的机制退出码检查正是执行脚本末尾与CLRTestExitCode默认 100比对的结果而 SKIPPING EXECUTION BECAUSE ... 一类文案则意味着脚本在判定分支处主动退出属于被跳过而非被失败。收尾关键属性速查表把前面几章的决策压缩成一张表CLRTestKind决定构建形态CLRTestPriority决定运行/构建范围Incompatible系列决定执行模式准入stripe 属性决定压力测试切分退出码守住成功语义。属性默认值作用CLRTestKindBuildAndRunOutputTypeExe时构建形态BuildOnly/RunOnly/BuildAndRun/SharedLibraryCLRTestProjectToRun空RunOnly工程指向的真正被运行工程CLRTestPriority0优先级批量运行时 ≤ 该值的全部执行需显式降级CLRTestExitCode100视为成功的退出码CLRTestTargetUnsupported空按条件在特定目标上禁构建、禁运行DisableProjectBuild空无条件取消构建导入 nobuild.targetsGCStressIncompatible空带反汇编检查时自动置位排除 GCStress 通道JitOptimizationSensitive空排除 JIT 压力运行NativeAotIncompatible空排除 NativeAOT 构建变体IlasmRoundTripIncompatible空AlwaysUseCrossGen2时自动置位排除 ilasm 往返隐含ManagedIlasmRoundTripIncompatibleUnloadabilityIncompatible空排除可回收程序集测试R2RIncompatible空排除 ReadyToRun 通道脚本打印跳过文案后 exit 0AlwaysUseCrossGen2空恒走 crossgen2 复合编译与CrossGenTestfalse冲突会报 ErrorCrossGenTesttrue置false后 R2R 通道内也不做标准 R2R 编译HasMergedInTests空声明合并测试运行器NumberOfStripesToUseInStress1合并运行器GC Stress 条纹数上限 99掌握这张表之后配置问题的本质就回到了第一章的推断链每一个属性最终都汇入_WillCLRTestProjectBuild、四个种类开关或某段执行脚本生成逻辑。遇到测试被跳过、脚本未生成或R2R 行为异常沿着症状表反查对应的 targets 分支通常几分钟内就能定位根因。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考