diff --git a/scripts/lib/extension-test-plan.mjs b/scripts/lib/extension-test-plan.mjs index 9b658706931..5a6d687f3e7 100644 --- a/scripts/lib/extension-test-plan.mjs +++ b/scripts/lib/extension-test-plan.mjs @@ -1,3 +1,4 @@ +import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { channelTestRoots } from "../../test/vitest/vitest.channel-paths.mjs"; @@ -64,7 +65,46 @@ function normalizeRelative(inputPath) { return inputPath.split(path.sep).join("/"); } +function isPathInsideRepo(relativePath) { + return relativePath !== ".." && !relativePath.startsWith("../") && !path.isAbsolute(relativePath); +} + +function isSkippedTrackedTestFile(relativePath) { + return relativePath.split("/").some((segment) => segment === "dist" || segment === "node_modules"); +} + +function listTrackedTestFiles(rootPath) { + const relativeRoot = normalizeRelative(path.relative(repoRoot, rootPath)); + if (!isPathInsideRepo(relativeRoot)) { + return null; + } + + const result = spawnSync("git", ["ls-files", "--", relativeRoot], { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) { + return null; + } + + return result.stdout + .split("\n") + .map((line) => line.trim().replaceAll("\\", "/")) + .filter( + (line) => + line.length > 0 && + !isSkippedTrackedTestFile(line) && + (line.endsWith(".test.ts") || line.endsWith(".test.tsx")), + ); +} + function countTestFiles(rootPath) { + const trackedFiles = listTrackedTestFiles(rootPath); + if (trackedFiles) { + return trackedFiles.length; + } + let total = 0; const stack = [rootPath]; diff --git a/test/scripts/test-extension.test.ts b/test/scripts/test-extension.test.ts index 8329c89b813..3926012b549 100644 --- a/test/scripts/test-extension.test.ts +++ b/test/scripts/test-extension.test.ts @@ -424,6 +424,52 @@ describe("scripts/test-extension.mjs", () => { ]); }); + it("counts tracked extension tests without walking extension directories", () => { + const output = execFileSync( + process.execPath, + [ + "--input-type=module", + "--eval", + ` + import fs from "node:fs"; + import { syncBuiltinESMExports } from "node:module"; + const counts = { readdirSync: 0 }; + const originalReaddirSync = fs.readdirSync; + fs.readdirSync = (...args) => { + counts.readdirSync += 1; + return originalReaddirSync(...args); + }; + syncBuiltinESMExports(); + const { createExtensionTestShards, resolveExtensionBatchPlan } = await import("./scripts/lib/extension-test-plan.mjs"); + const extensionIds = ["matrix", "openai", "slack", "telegram"]; + const batch = resolveExtensionBatchPlan({ cwd: process.cwd(), extensionIds }); + const shards = createExtensionTestShards({ cwd: process.cwd(), extensionIds, shardCount: 2 }); + console.log(JSON.stringify({ + batchTests: batch.testFileCount, + counts, + shards: shards.length, + shardTests: shards.reduce((total, shard) => total + shard.testFileCount, 0), + })); + `, + ], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + const payload = JSON.parse(output) as { + batchTests: number; + counts: { readdirSync: number }; + shards: number; + shardTests: number; + }; + expect(payload.batchTests).toBeGreaterThan(0); + expect(payload.shards).toBe(2); + expect(payload.shardTests).toBe(payload.batchTests); + expect(payload.counts.readdirSync).toBe(0); + }); + it("balances extension test shards by estimated CI cost", () => { const shards = createExtensionTestShards({ cwd: process.cwd(),