☰
gsd-core 受影响测试选择:如何在 PR 运行中彻底排除 install/slow 套件
2026/9/25 2:47:53 网站建设 项目流程

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

导读

本文以 gsd-core 仓库中的 changeset 370-affected-tests-exclude-install-slow.md 为主体,讲解该仓库的受影响测试(affected-tests)选择器如何在 PR(Pull Request)场景下将install/slow两个"仅推送(push-only)"套件彻底排除在运行集合之外。读完本文,你将掌握 gsd-core 的套件命名约定与suiteOf检测逻辑、pickAffectedTests单一咽喉点的三层过滤原理、空选择回退到unit的设计,以及resolveRunPlan的完整运行决策表,并能够在本地复现和验证这一行为。

一、背景:gsd-core 的套件体系与受影响测试选择

gsd-core 将全部测试文件按文件名后缀标记划分为若干套件(suite),约定为:标记位于.test.cjs之前。例如foo.security.test.cjs属于security套件;foo.test.cjs(无标记)属于unit套件。完整的合法套件列表定义在 scripts/run-tests.cjs:

const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow', 'qa'];

其中带标记的套件由 scripts/lib/suite-detection.cjs 中的常量统一登记:

const MARKED_SUITES = ['integration', 'install', 'security', 'slow', 'qa'];

对应的套件级 npm 脚本在 docs/TESTING-SUITES.md 中有完整说明:

npm test # everything(与旧行为一致) npm run test:unit # 仅 unit npm run test:integration # 仅 integration npm run test:install # 仅 install npm run test:security # 仅 security npm run test:slow # 仅 slow

受影响测试(affected-tests)机制的目标是:给定一次变更的文件集合,通过require()/import依赖图计算出"可能被这次变更影响"的测试文件,只运行这些文件,从而在 PR 上获得远比全量测试快的反馈。该机制有两个入口:

  • CI 侧:规则表驱动的 scripts/ci-test-scope.cjs(文档明确说明这是 CI 选测的权威映射);
  • 本地/PR 侧:依赖图驱动的 scripts/run-affected-tests.cjs,它只是一个薄 CLI 入口,核心逻辑全部位于 scripts/affected-tests-lib.cjs。

二、问题:install 与 slow 套件为什么不能出现在 PR 运行中

install套件的测试(如release-tarball-smoke.install.test.cjs)是 npm 打包 + 全局安装级的长耗时集成测试,在 gsd-core 的 CI 编排中由独立的install-smoke.ymlworkflow负责运行;slow套件则是标记为低频率、长耗时的测试。它们被设计为push-only:只应在推送/发布级流水线中运行,而不是在每一次 PR 上重复执行——否则会显著拖长 PR 反馈周期,甚至触发按 chunk 的 Windows 超时(tests/ci-test-scope.test.cjs 中明确记录了这一历史教训)。

在修复(PR #370)之前,受影响测试选择存在三处漏洞,导致 PR 仍可能选中甚至运行这些 push-only 套件:

  1. pickAffectedTests在 direct-change(直接变更的测试文件)、reverse-index(依赖图反查命中)、stem-match(文件名词干模糊匹配)三条选择路径上都可能把install/slow文件选入集合;
  2. 当选择结果为空时,存在一个DEFAULT_SMOKE_TESTS安装文件回退注入逻辑,会把 install 套件的 smoke 测试强行塞进 PR 运行;
  3. 当变更触及关键路径(critical path)时,旧实现调用runAllSuites——它会把包括install/slow在内的所有套件都运行一遍。

三、修复核心一:pickAffectedTests的单一咽喉点过滤

修复后的过滤逻辑集中在 scripts/affected-tests-lib.cjs,源码注释明确称之为"单一咽喉点(single chokepoint)":

// Drop any file whose suite is push-only. This is the single chokepoint — // it catches direct-change, reverse-index, AND stem-match selections. for (const file of selected) { const suite = suiteOf(path.basename(file)); if (PR_EXCLUDED_SUITES.has(suite)) selected.delete(file); }

这段代码在pickAffectedTests完成三类选测(direct-change、reverse-index 反查、stem 启发式匹配)之后、返回之前统一执行:对集合中每个文件调用suiteOf判断其套件,凡命中PR_EXCLUDED_SUITES的立即从集合中删除。

PR_EXCLUDED_SUITES的定义在 scripts/affected-tests-lib.cjs:

// Suites that are push-only. PRs must never select or run these. const PR_EXCLUDED_SUITES = new Set(['install', 'slow']);

把过滤收敛到"一个咽喉点"而非散落在三条选择路径里各自判断,带来的工程收益是单点可验证:只要保证咽喉点的删除逻辑正确,无论未来新增第几种选择路径,都不会再次出现 push-only 套件泄漏。

3.1 三类选择路径各自如何被覆盖

从 scripts/affected-tests-lib.cjs 的pickAffectedTests实现可以看到三条路径:

  • direct-change:遍历变更文件,凡tests/下且仍存在(未被删除)的.test.cjs直接加入集合(L300-L313);
  • reverse-index 反查:对非测试文件的变更,从传递性反查索引reverseIndex中取出所有可能依赖它的测试(L307-L312);
  • stem-match:对每个变更文件取 basename 去扩展名的词干,与全部测试文件路径做不区分大小写的子串包含匹配(L315-L322)。

咽喉点过滤位于三者之后,因此一条路径都不漏。

四、修复核心二:移除DEFAULT_SMOKE_TESTS回退注入,空选择运行 unit

旧的空选择回退逻辑会在"没有匹配到任何测试"时无条件注入一份DEFAULT_SMOKE_TESTS(其中包含 install 套件文件),这等于绕过了过滤规则。修复将其整体移除,新的行为是:空选择时不再注入任何 install 文件,而是以unit套件作为 smoke 回退。

这一契约变化在 tests/affected-tests-lib.test.cjs 中有直接断言:

test('pickAffectedTests returns empty (no install smoke) when nothing maps', () => { const allTests = [ 'tests/release-tarball-smoke.install.test.cjs', 'tests/some-unit.test.cjs', ]; const selected = pickAffectedTests( ['docs/CONTRIBUTING.md'], allTests, new Map(), ); assert.ok( !selected.includes('tests/release-tarball-smoke.install.test.cjs'), 'install smoke test must not be injected as fallback', ); assert.deepEqual(selected, []); });

对应的运行侧行为由resolveRunPlan落地(见下一节):当选择为空时返回{ mode: 'suite', suite: 'unit' },运行器随后以unit套件作为 smoke 兜底。CI 侧的同类修复(bug-408:移除无条件 smoke 列表注入、改为 unit 回退)在 tests/ci-test-scope.test.cjs 中有独立测试覆盖,可见"空选择 → unit 回退"是仓库两侧一致的策略。

五、修复核心三:critical-path 分支从runAllSuites改为PR_FULL_SUITES

第三个修复点是 critical-path 分支。旧实现调用runAllSuites会运行包括install/slow在内的全部套件;修复后替换为PR_FULL_SUITES常量(scripts/affected-tests-lib.cjs):

// Suites run on every PR cell when the critical-path fallback fires. const PR_FULL_SUITES = ['unit', 'integration', 'security'];

PR_FULL_SUITES恰好覆盖了所有 PR 合法套件(unit + integration + security),同时天然排除 install/slow。这一点在resolveRunPlan的注释中有更进一步的说明(scripts/affected-tests-lib.cjs):

Invariant: when widenRequired is true the executed set is ALWAYS ⊇ selected, because PR_FULL_SUITES covers every PR-eligible suite (unit + integration + security), so every concrete match that pickAffectedTests put intoselectedbelongs to one of those suites and will be exercised by running all three.

也就是说,当出现 widen(宽化)回退时,运行三套 PR 套件是"严格包含"咽喉点过滤后所选集合的超集——不存在过滤掉的选择会通过 widen 路径被重新放行的空子。

5.1resolveRunPlan完整决策表

从 scripts/affected-tests-lib.cjs 的实现可以得到一张完整的运行决策表:

输入状态决策结果
noChanges(相对 base 无变更){ mode: 'suite', suite: 'unit' }
criticalPath(关键路径变更){ mode: 'suites', suites: PR_FULL_SUITES }
widenRequired(孤儿源文件无静态依赖测试){ mode: 'suites', suites: PR_FULL_SUITES }
选择结果为空(widen=false){ mode: 'suite', suite: 'unit' }
有具体选择{ mode: 'files', files: selected }

六、suiteOf的导出机制:复用规范检测逻辑而非重复实现

changeset 中提到的"suiteOf从run-tests.cjs导出(由require.main守卫)"这一细节,在代码中有两层实现依据。

第一层:run-tests.cjs通过"仅当作为主入口运行时才执行 CLI 主流程"的守卫,使其既可被 require 复用、又不会在被导入时误触发命令执行(scripts/run-tests.cjs):

if (require.main === module) { runMain(main); } module.exports = { suiteOf, ensureBuiltArtifacts, // ... parseArgs、selectFiles、setupRunTempRoot 等 };

第二层:suiteOf的实际实现已经进一步下沉到独立模块scripts/lib/suite-detection.cjs,run-tests.cjs只是原样再导出以保持向后兼容。该文件头部的注释解释了这一拆分的缘由(#4591 packaging fix):run-tests.cjs本身不随发布包分发(它是仓库开发期工具),如果已发布的脚本(如scripts/gen-platform-conformance-tier.cjs)直接 require 它,会在安装后遇到MODULE_NOT_FOUND(#2858);因此把suiteOf及其背后的MARKED_SUITES提取到可发布的scripts/lib/下,再经由run-tests.cjs导出。核心实现(scripts/lib/suite-detection.cjs):

function suiteOf(filename) { const name = basename(filename); if (!name.endsWith('.test.cjs')) return null; const base = name.slice(0, -'.test.cjs'.length); const lastDot = base.lastIndexOf('.'); if (lastDot === -1) return null; const marker = base.slice(lastDot + 1); return MARKED_SUITES.includes(marker) ? marker : null; }

要点:无标记文件(如foo.test.cjs)属于unit套件,这里返回null;带标记文件(如foo.install.test.cjs)返回其标记字符串。affected-tests-lib.cjs在文件顶部以require('./run-tests.cjs')引入suiteOf(scripts/affected-tests-lib.cjs),从而与run-tests.cjs的套件过滤行为共享同一份规范实现,避免两处套件判定逻辑漂移。

七、测试验证:行为契约的落点

修复后的行为契约由以下测试固定,便于在仓库中直接复现验证:

  • 直接变更 + 反查命中,install 套件被排除:tests/affected-tests-lib.test.cjs 断言普通tests/install.test.cjs(无套件标记,属 unit)保留、tests/tarball.install.test.cjs(install 标记)被剔除;
  • 直接变更 install 测试文件本身也被排除:tests/affected-tests-lib.test.cjs;
  • stem-match 拉入的 install/slow 被排除:tests/affected-tests-lib.test.cjs;
  • 空选择不再注入 install smoke:tests/affected-tests-lib.test.cjs;
  • 常量契约:PR_EXCLUDED_SUITES含install、slow,PR_FULL_SUITES含unit/integration/security且不含二者:tests/affected-tests-lib.test.cjs;
  • CI 侧 smoke 列表移除:bug-408 用例断言命令类变更不再无条件注入core.test.cjs、package-manifest.test.cjs,无规则命中时回退['unit']:tests/ci-test-scope.test.cjs。

八、本地验证与使用方式

  1. 运行受影响测试选择相关测试:
    node --test tests/affected-tests-lib.test.cjs
  2. 运行 CI 测试范围规则相关测试:
    node --test tests/ci-test-scope.test.cjs
  3. 本地触发受影响测试(local-only 便捷入口,CI 不使用它):
    npm run test:affected

    该命令对应 scripts/run-affected-tests.cjs,实际执行runAffectedTests:先以GSD_AFFECTED_BASE→GITHUB_BASE_REF→origin/main的顺序解析对比基准(scripts/affected-tests-lib.cjs),再走"关键路径 → 反查选测 → widen 兜底 → unit smoke"的完整决策链。

九、总结

PR #370 的修复在 gsd-core 的受影响测试机制上确立了一个清晰且可验证的边界:PR 运行集合中永远不允许出现install/slow套件。实现上通过三个相互独立又彼此衔接的手段保证——pickAffectedTests单一咽喉点过滤覆盖全部选择路径、DEFAULT_SMOKE_TESTS注入移除后以unit作为空选择回退、critical-path 分支改用PR_FULL_SUITES;而suiteOf的提取与再导出机制(require.main守卫 +scripts/lib/suite-detection.cjs)保证了套件判定逻辑的唯一来源与向后兼容。这套设计既缩短了 PR 反馈周期,也通过单点收敛和测试契约避免了同类问题在未来回归。

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

相关推荐

上一篇:Spike模拟器与真实硬件对比:功能差异与适用场景终极指南
下一篇:解决Hoppscotch CLI 0.10.0版本Docker安装依赖的终极方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询