2026/10/16 2:13:18

.NET runtime 的 CoreCLR 测试工程为什么不跑?CLRTestKind、优先级与构建判定背后的 MSBuild 机制一次讲透

.NET runtime 的 CoreCLR 测试工程为什么不跑?CLRTestKind、优先级与构建判定背后的 MSBuild 机制一次讲透 .NET runtime 的 CoreCLR 测试工程为什么不跑CLRTestKind、优先级与构建判定背后的 MSBuild 机制一次讲透【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime往src/tests下丢了一个新的.csproj本地dotnet build一切正常CI 上却连影子都没有或者更糟某些配置下显示 SKIPPED另一些配置下直接 FAILED。先说结论每一个 CoreCLR 测试工程的命运——构建不构建、生成不生成执行脚本、脚本里跑什么——由一组 MSBuild 布尔属性决定它们全部在编译任何源码之前就算完了。这些属性的主场在src/tests/Directory.Build.targets。三个名字值得先记住_WillCLRTestProjectBuild这个工程到底进不进构建CLRTestKind四种基本形态之一决定编译 / 构建可执行文件 / 生成运行脚本三件事的组合CLRTestPriority优先级分层低值先跑批量构建时还负责砍尾。下面按真实排查顺序走一遍工程没构建 → 构建了却被跳过 → 执行脚本从哪里来 → 合并运行器 → 最后给一份新增测试的工程清单。工程根本没进构建从 CLRTestKind 的四种形态查起CLRTestKind不是一个风格标记它直接对应编译源码、构建可执行文件、生成运行脚本这三个开关的组合。如果工程没有显式声明它src/tests/Directory.Build.targets第 19-20 行会按OutputType兜底推断CLRTestKind Condition$(CLRTestKind) and $(OutputType) ExeBuildAndRun/CLRTestKind CLRTestKind Condition$(CLRTestKind) SharedLibrary/CLRTestKind也就是说输出为Exe的工程默认BuildAndRun类库默认SharedLibrary。四种形态各自的开关组合在紧接着的Choose块第 30-64 行里定义核心差异如下CLRTestKind编译源码产出可执行文件生成运行脚本典型场景BuildAndRun是是是绝大多数普通测试BuildOnly是是否编译期校验、被复用的可执行文件RunOnly否否是同一个可执行文件的多种运行形态SharedLibrary是否否公共托管测试库第一个排查动作就是确认CLRTestKind没被意外推断成SharedLibrary——那种形态GenerateRunScriptfalse你根本不会得到执行脚本CI 自然看不到这个测试。RunOnly形态有个容易踩的依赖机制它不编译任何东西靠CLRTestProjectToRun指向真正要跑的工程。第 66-69 行计算出_CLRTestNeedsProjectToRun需要运行但不自己产出可执行文件第 84-88 行则把CLRTestProjectToRun转成一个ProjectReference保证依赖先被构建。生成脚本时src/tests/Common/CLRTest.Execute.targets的FetchExternalProperties目标第 90-103 行会再去调用被引用工程的脚本路径目标并用[MSBuild]::MakeRelative把它的输出路径换算成相对路径塞进InputAssemblyName而BuildAndRun工程直接用$(AssemblyName).dll。所以 RunOnly 工程的 csproj 里CLRTestProjectToRun少写一个字母症状就是脚本生成了但指向了不存在的 dll。构建了却被跳过压力模式兼容性属性与脚本内跳过第二类问题测试在普通配置下绿GCStress、JIT Stress 或 R2R 配置下要么被排除、要么显示跳过。这里要区分两种跳过——构建期排除工程不构建_WillCLRTestProjectBuildfalse下一节细讲和运行期跳过脚本照样生成但脚本内部判断后直接exit 0。环境变量的注入点GCStress 与 JIT 压力GCStress 不是构建参数而是运行环境变量。src/tests/Common/CLRTest.Jit.targets末尾第 496-499 行把 CI 传入的RunWithGcStress翻译成环境变量项CLRTestEnvironmentVariable Condition$(RunWithGcStress) ! IncludeDOTNET_GCStress Value$(RunWithGcStress) /因此GCStressIncompatible的含义是该工程生成的脚本不会注入这个环境变量。类似地JitOptimizationSensitive用于把测试排除出 JIT 压力运行UnloadabilityIncompatible排除可回收程序集场景。这里有一个自动联动值得注意如果你的某个源文件标了反汇编检查Compile项带HasDisasmChecktrue元数据GetDisasmCheckData目标第 108-118 行会自动把工程置为GCStressIncompatibleGCStressIncompatible Condition$(HasDisasmCheck) truetrue/GCStressIncompatible原因很直接GCStress 注入会改变代码形态与反汇编文本检查SuperFileCheck / FileCheck 风格的CHECK:模式不相容。另一个隐式联动在CLRTest.Execute.targets第 30-33 行非托管宿主入口CLRTestIsHosted ! true的工程会自动同时标记UnloadabilityIncompatible和TieringTestIncompatible。R2R 的优雅跳过脚本里预埋的 exit 0ReadyToRun 相关的跳过最隐蔽因为它发生在生成的脚本内部。src/tests/Common/CLRTest.CrossGen.targets第 50-57 行当工程声明R2RIncompatibletrue时生成的 Bash 脚本开头就是这段if [ ! -z ${RunCrossGen2x} ]; then echo SKIPPING EXECUTION BECAUSE the test is incompatible with ReadyToRun exit 0 fi注意这个设计CI 日志里出现的 SKIPPING EXECUTION BECAUSE the test is incompatible with ReadyToRun 不是测试代码打印的而是 targets 文件模板的一部分且退出码是 0——在 CI 上它不算失败。如果你看到的是这句话说明一切正常只是这个测试被正确地排除了真正异常的是脚本在 crossgen2 阶段报错、或 R2RDump 校验失败。crossgen2 脚本的主体第 45 行起Bash 版做的事比看上去多它先把参数写进响应文件.rsp追加--targetarch、--targetos、--method-layout:random、--verify-type-and-field-layout再按开关追加--composite、--hot-cold-splitting、--synthesize-random-mibc合成 PGO 数据等随后调用$CORE_ROOT/crossgen2/crossgen2成功后用 R2RDump 对产物做--header --sc --val校验。过程中还会export -n掉DOTNET_GCStress、DOTNET_GCName等一批变量避免压力环境变量污染交叉编译。两个属性组合的坑AlwaysUseCrossGen2true的测试会同时被置RunCrossGen21与CompositeBuildMode1进入 composite 编译流程并且CLRTest.CrossGen.targets第 26 行会自动标记IlasmRoundTripIncompatibletrue——crossgen2 产物没法再走 ildasm/ilasm 往返。如果同时写了CrossGenTestfalse和AlwaysUseCrossGen2true第 47 行的Error会直接让构建失败报错文本是 Setting both AlwaysUseCrossGen2 to true and CrossGenTest to false is non-sensical。这两个语义互斥一个是必须用 crossgen2另一个是R2R 通过中也不要标准 R2R 编译。其余模式排除属性剩下的排除开关都是同一思路——把工程从某类运行形态中摘出去属性排除的运行形态NativeAotIncompatibleNativeAOT 运行同时触发构建期排除见下节IlasmRoundTripIncompatibleildasm/ilasm 反汇编-重汇编往返MonoAotIncompatibleMono 的 minifullaot / llvmfullaot 变体这些属性落到构建系统的方式除了脚本片段外还通过标记文件传递GenerateMarkerFiles目标第 318 行起会把IlasmRoundTripIncompatible、MonoAotIncompatible等状态写成中间目录里的AssemblyName.IlasmRoundTripIncompatible、AssemblyName.NoMonoAot之类的小文件并复制到输出目录往返脚本Python 生成器IlasmRoundTripCommand目标在遍历*.dll时读到同名标记就跳过对应程序集。所以合并运行器目录下那些看起来莫名其妙的.IlasmRoundTripIncompatible文件是这个机制的产物不是垃圾文件。谁决定不构建_WillCLRTestProjectBuild 的判定链前两类问题查到一定程度都会归到同一个布尔量上。src/tests/Directory.Build.targets第 94-107 行是它的完整判定链BuildAllProjects Condition$(BuildAllProjects) false/BuildAllProjects _WillCLRTestProjectBuild Condition$(_WillCLRTestProjectBuild) false/_WillCLRTestProjectBuild _WillCLRTestProjectBuild Condition$(BuildAllProjects) ! truetrue/_WillCLRTestProjectBuild _WillCLRTestProjectBuild Condition$(BuildAllProjects) true And $(CLRTestPriority) lt; $(CLRTestPriorityToBuild)true/_WillCLRTestProjectBuild _WillCLRTestProjectBuild Condition$(CLRTestBuildAllTargets) ! allTargets And $(CLRTestTargetUnsupported) truefalse/_WillCLRTestProjectBuild _WillCLRTestProjectBuild Condition$(DisableProjectBuild) truefalse/_WillCLRTestProjectBuild _WillCLRTestProjectBuild Condition$(NativeAotIncompatible) true and $(TestBuildMode) nativeaotfalse/_WillCLRTestProjectBuild注意 MSBuild 属性求值的语义后写的覆盖先写的条件都满足时才覆盖。所以这段链的实际读法是——先按优先级放行再被一系列条件追杀为 falseBuildAllProjects ! true默认的单工程构建直接放行优先级不参与BuildAllProjects true批量构建CLRTestPriority CLRTestPriorityToBuild才放行。这就是优先级分层落到构建层的方式——要跑 priority 2priority 0、1 的全部先被构建。真实的降级示例src/tests/GC/API/GC/Collect.csproj 声明CLRTestPriority1而同目录的 Collect_Aggressive.csproj 显式声明 0。优先级未声明时默认是 0第 21 行。之后的 false 条件依次是CLRTestTargetUnsupportedtrue且不是allTargets全量构建、DisableProjectBuildtrue、NativeAotIncompatible撞上TestBuildModenativeaot、GC 模拟器测试撞上 nativeaot、AlwaysUseCrossGen2撞上 mono flavor / nativeaot / 原生 sanitizer 等组合。CLRTestTargetUnsupported是按平台/配置条件性禁用的标准做法典型写法CLRTestTargetUnsupported Condition$(RuntimeFlavor) ! coreclrtrue/CLRTestTargetUnsupported判定为不构建之后第 561 行导入Common/nobuild.targets把构建目标整体置空转——这就是CI 上测试消失而非报错的原因。定位技巧对可疑工程执行 msbuild 时打印_WillCLRTestProjectBuild再看上面哪条 false 条件命中。另外GetProjectsWithDisabledBuild目标第 507-511 行会把这些工程汇总上报给上层调度供测试框架统一识别。还有个细节第 126 行CLRTestPriority超过阈值时还会置SkipImportILTargetstrue连 IL 工程的 targets 都跳过导入进一步省掉无用功。执行脚本从哪里来脚本片段拼接、退出码 100 与 Main 入口确认工程会构建之后执行脚本的生成逻辑在src/tests/Common/CLRTest.Execute.targets。它的架构是片段拼接GenerateExecutionScriptsInternal目标第 105-107 行按平台选择脚本种类——Windows 生成 Batch、其他平台生成 Bash第 69-72 行的ExecutionScriptKind项然后依次调用GenerateBatchExecutionScript/GenerateBashExecutionScript各个特性 targets 不直接写完整脚本而是向BashScriptSnippetGen/BatchScriptSnippetGen注册自己的片段生成目标。比如CLRTest.Jit.targets第 24-25 行注册了GetIlasmRoundTripBashScript;GetDisasmCheckBashScriptCLRTest.CrossGen.targets第 22-23 行注册 crossgen2 片段反汇编检查的逻辑则是通过CLRTestBashPreCommands/CLRTestBashPostCommands挂在主命令前后。所以一个带反汇编检查的测试脚本长什么样、GCStress 环境变量在哪里 export、crossgen2 在哪个阶段插入都是这套注册-拼接机制拼出来的。想改某个行为先找注册片段的 targets而不是去翻生成出来的.sh/.cmd。关于测试代码侧的约定有两点成功退出码是 100默认值写在CLRTest.Execute.targets第 37 行CLRTestExitCode。如果你自己写Main成功路径返回 100、失败路径返回任意非 100 值static public int Main(string[] notUsed) { try { // 测试场景 } catch (Exception e) { Console.WriteLine($Test Failure: {e}); return 101; } return 100; }不想手写Main就用 xunit 的[Fact]基础设施通过源生成器src/tests/Common/XUnitWrapperGenerator扫描所有Fact方法并合成入口。它的注入条件是Directory.Build.targets第 537-543 行——C# 语言、需要运行且满足以下任一BuildAsStandalonetrue、RequiresProcessIsolationtrue、InMergedTestDirectorytrue或IsMergedTestRunnerAssemblytrue。命中后以OutputItemTypeAnalyzer的方式引用该源生成器工程。注意这个条件普通独立BuildAndRun工程不在注入名单里要么写Main要么把工程放进合并运行器目录结构。原生依赖走另一条路csproj 里写CMakeProjectReference Include../NativeDll/CMakeLists.txt /ResolveCMakeNativeProjectReference目标第 223-230 行把文件名是CMakeLists的项转成NativeProjectReference随后CopyNativeProjectBinaries目标按平台过滤出动态库/可执行文件/符号文件Copy到测试输出目录。如果这一步报 The native project files are missing通常是没从仓库根跑过完整构建第 208-209 行专门有这条Error。合并测试运行器glob 批量引用与条纹切分JIT 目录下大量历史回归测试采用合并运行器形态一个 Exe 工程把成百上千个小组合并进来跑。以src/tests/JIT/Methodical/Methodical_d1.csproj为模板看它的骨架是PropertyGroup HasMergedInTeststrue/HasMergedInTests /PropertyGroup ItemGroup MergedWrapperProjectReference Includea*/**/*_d.??proj / MergedWrapperProjectReference Includeb*/**/*_d.??proj / ... /ItemGroupMergedWrapperProjectReference用 glob 批量引入子工程真正的测试单元再显式Compile包含若干共享源文件最后导入MergedTestRunner.targets完成装配。这里有个硬约束CheckForTestOutsideOfGroup目标第 134-142 行会在 Build 前报错——合并目录里出现OutputTypeExe且需要运行的工程除非它声明了RequiresProcessIsolation、BuildAsStandalone或本身就是IsMergedTestRunnerAssembly。也就是说合并目录下的子工程不能自己当 Exe 跑它们只能被 wrapper 合并、或者显式声明独立运行。GC Stress 下大合并组会慢到超时NumberOfStripesToUseInStress就是为此设计的GenerateMarkerFiles目标第 318-321 行对合并运行器默认取 1你在 csproj 里显式设置比如 10后会按0..N-1生成 N 个标记文件命名形如$(MSBuildProjectName).MergedTestAssembly.stripe.total.MergedTestAssemblyForStressCI 的压力测试据这些文件把大测试组切成分条并行执行。上限是 99第 431 行的Error会拦住超量设置。本地手动跑合并运行器时命令行支持[testFilterString] [-stripe whichStripe totalStripes]testFilterString目前只支持dotnet test --filter那类完全限定名子串形式条纹下标从 0 开始。等价的另一种指定方式是环境变量TEST_HARNESS_STRIPE_TO_EXECUTE取值形如.whichStripe.totalStripes。新增一个测试工程的操作清单把前面的机制串起来新增一个独立测试工程以 docs/workflow/testing/coreclr/test-configuration.md 的官方指引为准实际操作链是复制一个现有简单工程如 Exceptions 下的某测试到新目录删掉模板里遗留的AssemblyName——它会干扰构建系统的后台处理源文件头部加 MIT 许可证横幅托管代码必须能在任意平台编译通过CI 统一在 OSX 上构建全部托管测试再分平台运行子集所以 csproj 里不能出现依赖目标平台的条件编译、条件文件包含或DefineConstants按需要声明CLRTestKind/CLRTestPriority/ 各类*Incompatible/CLRTestTargetUnsupported——对照上面三节的判定链选择而不是凭感觉全加一遍测试代码用[Fact]配合源生成器或自写Main成功返回 100依赖托管库用ProjectReferenceCMake 原生库用CMakeProjectReference公共 NuGet 依赖统一加到 src/tests/Common/test_dependencies/test_dependencies.csproj它本身是BuildOnly由test_dependencies的 targets 集中分发给各测试测试用到的System.Private.CoreLibAPI 必须在所有平台可用——不支持的平台上提供throw new PlatformNotSupportedException()的桩实现即可因活跃 bug 失败时加[ActiveIssue]。对于 JIT 回归测试有额外的目录约定优化编译无调试信息类回归直接丢src/tests/JIT/Regression_ro_2/Runtime_issue.cs运行器递归包含目录下所有.cs不用改工程文件但需要独立编译设置、独立程序集、原生依赖或进程隔离的则放src/tests/JIT/Regression_2/Runtime_issue/下建独立工程由Regression_2.csproj以工程递归发现的方式管理。两条线不要混共享给多个运行器的源码放在某个运行器目录中由对方显式包含而源文件通配目录中的源码会被直接编进运行器本身。排查路径与源码阅读顺序把整条链路压成一张排查表症状先看哪里CI 上测试完全没构建_WillCLRTestProjectBuild判定链Directory.Build.targets第 94-107 行逐条核对 false 条件构建了但没有执行脚本CLRTestKind是否被推断成SharedLibrary/BuildOnly第 19-20 行 Choose块日志出现 SKIPPING EXECUTION BECAUSE ...脚本内预埋的跳过片段CLRTest.CrossGen.targets第 50-57 行属正常排除执行阶段失败打开生成的.sh/.cmd反查各 targets 注册的片段来源R2R/crossgen2 行为异常CLRTest.CrossGen.targets重点看AlwaysUseCrossGen2/CrossGenTest/R2RIncompatible组合如果想把机制读透建议按这个顺序走源码src/tests/Directory.Build.targets所有默认值与判定链的总入口→src/tests/Common/CLRTest.Execute.targets脚本种类选择与片段拼接框架→src/tests/Common/CLRTest.CrossGen.targets与src/tests/Common/CLRTest.Jit.targetsR2R 与压力测试的具体片段→src/tests/Common/test_dependencies/test_dependencies.csproj依赖集中点→src/tests/JIT/Methodical/Methodical_d1.csproj合并运行器样板。把这几个文件和你手头一个行为异常的工程属性对着读一遍绝大多数它为什么这样跑的问题都能落到某一条具体的条件表达式上。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考