diff --git a/scripts/check-changed.mjs b/scripts/check-changed.mjs index 3414885d7f8..f9321ff0209 100644 --- a/scripts/check-changed.mjs +++ b/scripts/check-changed.mjs @@ -8,8 +8,7 @@ import { } from "./changed-lanes.mjs"; import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs"; import { printTimingSummary } from "./lib/check-timing-summary.mjs"; - -const ROUTABLE_TEST_PATH_RE = /^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u; +import { resolveChangedTestTargetPlan } from "./test-projects.test-support.mjs"; export function createChangedCheckPlan(result) { const commands = []; @@ -25,6 +24,7 @@ export function createChangedCheckPlan(result) { return { commands, testTargets: [], + runChangedTestsBroad: false, runFullTests: false, runExtensionTests: false, summary: "docs-only", @@ -41,6 +41,7 @@ export function createChangedCheckPlan(result) { return { commands, testTargets: [], + runChangedTestsBroad: false, runFullTests: true, runExtensionTests: false, summary: "all", @@ -82,10 +83,12 @@ export function createChangedCheckPlan(result) { add("pairing account guard", ["lint:auth:pairing-account-scope"]); } - const testTargets = result.paths.filter((changedPath) => ROUTABLE_TEST_PATH_RE.test(changedPath)); + const testPlan = resolveChangedTestTargetPlan(result.paths); + const runChangedTestsBroad = testPlan.mode === "broad"; return { commands, - testTargets, + testTargets: testPlan.targets, + runChangedTestsBroad, runFullTests: false, runExtensionTests: result.extensionImpactFromCore, summary: Object.entries(lanes) @@ -118,6 +121,21 @@ export async function runChangedCheck(result, options = {}) { printSummary(timings, options); return status; } + } else if (plan.runChangedTestsBroad) { + const testArgs = options.explicitPaths + ? ["scripts/test-projects.mjs"] + : ["scripts/test-projects.mjs", "--changed", options.base ?? "origin/main"]; + const status = await runNode( + { + name: options.explicitPaths ? "tests all" : "tests changed broad", + args: testArgs, + }, + timings, + ); + if (status !== 0) { + printSummary(timings, options); + return status; + } } else if (plan.testTargets.length > 0) { const status = await runNode( { @@ -154,6 +172,9 @@ function printPlan(result, plan, options) { if (result.extensionImpactFromCore) { console.error(`${prefix} core contract changed; extension tests included`); } + if (plan.runChangedTestsBroad) { + console.error(`${prefix} broad changed tests included`); + } for (const reason of result.reasons) { console.error(`${prefix} ${reason}`); } @@ -246,5 +267,8 @@ if (isDirectRun()) { ? listStagedChangedPaths() : listChangedPathsFromGit({ base: args.base, head: args.head }); const result = detectChangedLanes(paths); - process.exitCode = await runChangedCheck(result, args); + process.exitCode = await runChangedCheck(result, { + ...args, + explicitPaths: args.paths.length > 0, + }); } diff --git a/scripts/test-projects.test-support.d.mts b/scripts/test-projects.test-support.d.mts index 04fa5b5fe2d..e81d36d50a9 100644 --- a/scripts/test-projects.test-support.d.mts +++ b/scripts/test-projects.test-support.d.mts @@ -35,6 +35,11 @@ export function resolveChangedTargetArgs( listChangedPaths?: (baseRef: string, cwd: string) => string[], ): string[] | null; +export function resolveChangedTestTargetPlan(changedPaths: string[]): { + mode: "none" | "broad" | "targets"; + targets: string[]; +}; + export function listFullExtensionVitestProjectConfigs(): string[]; export function createVitestRunSpecs( diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 4c6e97828e1..896486e1e5d 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -168,9 +168,27 @@ const BROAD_CHANGED_RERUN_PATTERNS = [ /^vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u, /^test\/vitest\/vitest\.(?:config|shared\.config|scoped-config|performance-config)\.ts$/u, /^test\/helpers\//u, - /^scripts\/run-vitest\.mjs$/u, - /^scripts\/test-projects(?:\.test-support)?\.mjs$/u, ]; +const TOOLING_SOURCE_TEST_TARGETS = new Map([ + ["scripts/changed-lanes.mjs", ["test/scripts/changed-lanes.test.ts"]], + ["scripts/check-changed.mjs", ["test/scripts/changed-lanes.test.ts"]], + ["scripts/lib/vitest-local-scheduling.mjs", ["test/scripts/vitest-local-scheduling.test.ts"]], + [ + "scripts/run-vitest.mjs", + ["test/scripts/test-projects.test.ts", "test/scripts/vitest-local-scheduling.test.ts"], + ], + ["scripts/test-projects.mjs", ["test/scripts/test-projects.test.ts"]], + ["scripts/test-projects.test-support.d.mts", ["test/scripts/test-projects.test.ts"]], + ["scripts/test-projects.test-support.mjs", ["test/scripts/test-projects.test.ts"]], +]); +const TOOLING_TEST_TARGETS = new Map([ + ["test/scripts/changed-lanes.test.ts", ["test/scripts/changed-lanes.test.ts"]], + ["test/scripts/test-projects.test.ts", ["test/scripts/test-projects.test.ts"]], + [ + "test/scripts/vitest-local-scheduling.test.ts", + ["test/scripts/vitest-local-scheduling.test.ts"], + ], +]); const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map( Object.entries(VITEST_CONFIG_BY_KIND).map(([kind, config]) => [config, kind]), ); @@ -239,7 +257,29 @@ function isVitestConfigTargetForKind(kind, targetArg, cwd) { } function listChangedPathsFromGit(baseRef, cwd) { - return execFileSync("git", ["diff", "--name-only", `${baseRef}...HEAD`], { + return [ + ...new Set([ + ...runGitNameOnlyDiff(cwd, [`${baseRef}...HEAD`]), + ...runGitNameOnlyDiff(cwd, ["--cached", "--diff-filter=ACMR"]), + ...runGitNameOnlyDiff(cwd, ["--diff-filter=ACMR"]), + ...runGitLsFiles(cwd, ["--others", "--exclude-standard"]), + ]), + ].toSorted((left, right) => left.localeCompare(right)); +} + +function runGitNameOnlyDiff(cwd, extraArgs) { + return execFileSync("git", ["diff", "--name-only", ...extraArgs], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + .split("\n") + .map((line) => normalizePathPattern(line.trim())) + .filter((line) => line.length > 0); +} + +function runGitLsFiles(cwd, extraArgs) { + return execFileSync("git", ["ls-files", ...extraArgs], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], @@ -290,10 +330,45 @@ function shouldKeepBroadChangedRun(changedPaths) { ); } +function resolveToolingChangedTestTargets(changedPaths) { + const targets = []; + for (const changedPath of changedPaths) { + const testTargets = + TOOLING_SOURCE_TEST_TARGETS.get(changedPath) ?? TOOLING_TEST_TARGETS.get(changedPath); + if (!testTargets) { + return null; + } + targets.push(...testTargets); + } + return [...new Set(targets)]; +} + function isRoutableChangedTarget(changedPath) { return /^(?:src|test|extensions|ui|packages)(?:\/|$)/u.test(changedPath); } +export function resolveChangedTestTargetPlan(changedPaths) { + if (changedPaths.length === 0) { + return { mode: "none", targets: [] }; + } + const toolingTargets = resolveToolingChangedTestTargets(changedPaths); + if (toolingTargets) { + return { mode: "targets", targets: toolingTargets }; + } + if (shouldKeepBroadChangedRun(changedPaths)) { + return { mode: "broad", targets: [] }; + } + const changedLanes = detectChangedLanes(changedPaths); + if (changedLanes.lanes.all) { + return { mode: "broad", targets: [] }; + } + const targets = changedPaths.filter(isRoutableChangedTarget); + if (changedLanes.extensionImpactFromCore) { + targets.push("extensions"); + } + return { mode: "targets", targets: [...new Set(targets)] }; +} + export function listFullExtensionVitestProjectConfigs() { return ( fullSuiteVitestShards.find((shard) => shard.config === FULL_EXTENSIONS_VITEST_CONFIG) @@ -311,19 +386,11 @@ export function resolveChangedTargetArgs( return null; } const changedPaths = listChangedPaths(baseRef, cwd); - if (changedPaths.length === 0 || shouldKeepBroadChangedRun(changedPaths)) { + const plan = resolveChangedTestTargetPlan(changedPaths); + if (plan.mode === "broad") { return null; } - const changedLanes = detectChangedLanes(changedPaths); - if (changedLanes.lanes.all) { - return null; - } - const routablePaths = changedPaths.filter(isRoutableChangedTarget); - const targets = [...routablePaths]; - if (changedLanes.extensionImpactFromCore) { - targets.push("extensions"); - } - return [...new Set(targets)]; + return plan.targets; } function classifyTarget(arg, cwd) {