// Picks the OpenJDK tests a change needs, for CI. // // node scripts/jtreg/select.ts [ | --full] // // Prints GitHub Actions outputs: `filters`, the tests to pass to run.ts (empty // for all of them), and `shards`, a JSON list of shard numbers ([] when no test // is needed). Tests added to passing.txt or locale-data.txt run alone. A change // to a loaded file with a table entry runs the test directories it maps to, // plus every tenth listed test; a change to any other loaded file runs them // all. A change to times.tsv alone runs none. import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; const root = join(import.meta.dirname, "..", ".."); const passingFile = "test/jtreg/passing.txt"; const localeFile = "test/jtreg/locale-data.txt"; const timesFile = "test/jtreg/times.tsv"; // TypeScript the tests load: everything src/node.ts (the VM each test runs on), // the web build that bundles it into dist/node.js, the runner and this // selection import, followed through their imports. Other .ts files under // src/ and scripts/ (the page API) cannot change a test's result. function loaded(): Set { const seen = new Set(); const visit = (file: string) => { if (seen.has(file)) return; seen.add(file); const text = readFileSync(join(root, file), "utf8"); for (const m of text.matchAll(/(?:from|import)\s*\(?\s*["'](\.{1,2}\/[^"']+\.ts)["']/g)) visit(join(dirname(file), m[1])); }; visit("src/node.ts"); visit("scripts/build-web.ts"); visit("scripts/jtreg/run.ts"); visit("scripts/jtreg/select.ts"); return seen; } const notLoaded = /\.md$|^docs\/|^LICENSE|^renovate\.json$|^\.coderabbit|^types\/|^test\/(?!jtreg\/)/; // The test directories a change to a loaded file can affect. A file not here // runs every test, so only add one when its effect is known and narrow. const zipTests = ["java/util/zip", "java/util/jar", "jdk/nio/zipfs"]; const netTests = ["java/net", "java/nio/channels", "javax/net", "sun/net"]; const ioTests = ["java/io", "java/nio", ...zipTests, "java/util/prefs", "java/lang/ProcessBuilder", "java/lang/Process", "java/lang/RuntimeTests"]; const processTests = ["java/lang/ProcessBuilder", "java/lang/Process", "java/lang/ProcessHandle", "java/lang/RuntimeTests"]; const awtTests = ["java/awt", "java/beans", "javax/imageio"]; const cryptoTests = ["java/security", "javax/crypto", "javax/net", "java/math", "sun/security"]; const managementTests = ["java/lang/management", "java/util/concurrent"]; const affects: [string, string[]][] = [ ["src/natives/zip.ts", zipTests], ["src/inflate.ts", zipTests], ["src/deflate.ts", zipTests], ["src/natives/net.ts", netTests], ["src/sockets.ts", netTests], ["src/net-node.ts", netTests], ["src/natives/sockets.ts", netTests], ["src/http-proxy.ts", netTests], ["src/natives/nio.ts", ioTests], ["src/natives/io.ts", ioTests], ["src/fs.ts", ioTests], ["src/process-node.ts", processTests], ["src/natives/java2d*.ts", awtTests], ["src/natives/font*.ts", awtTests], ["src/natives/crypto.ts", cryptoTests], ["src/natives/management.ts", managementTests], ]; const patterns = affects.map(([file, dirs]) => [new RegExp(`^${file.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*")}$`), dirs] as const); const dirsFor = (file: string) => patterns.find(([re]) => re.test(file))?.[1]; const shardsFor = (n: number) => Array.from({ length: Math.min(8, Math.ceil(n / 400)) }, (_, i) => i + 1); const listed = readFileSync(join(root, passingFile), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")); const under = (id: string, f: string) => id === f || id.startsWith(f + "/") || id.startsWith(f + "#"); function select(base: string | undefined): string[] | "all" { if (!base) return "all"; const git = (...args: string[]) => execFileSync("git", args, { cwd: root, encoding: "utf8" }); const filters = new Set(); let graph: Set | undefined; let dirs: string[] | undefined; for (const file of git("diff", "--name-only", "--no-renames", `${base}...HEAD`).split("\n").filter(Boolean)) { if (file === passingFile || file === localeFile) { // the tests it adds for (const l of git("diff", "-U0", `${base}...HEAD`, "--", file).split("\n")) { if (l.startsWith("+") && !l.startsWith("+++") && !l.startsWith("+#") && l.slice(1).trim()) filters.add(l.slice(1).trim()); } continue; } // it only balances shards if (file === timesFile) continue; if (/^(src|scripts)\/.*\.ts$/.test(file)) { if (!(graph ??= loaded()).has(file)) continue; const mapped = dirsFor(file); if (!mapped) return "all"; dirs = dirs ? [...dirs, ...mapped] : mapped; } else if (!notLoaded.test(file)) return "all"; } if (dirs) { // every tenth listed test, for a regression in shared code a mapped file calls into for (const f of [...dirs, ...listed.filter((_, i) => i % 10 === 0)]) filters.add(f); } return [...filters]; } const arg = process.argv[2]; const picked = select(arg === "--full" ? undefined : arg); const count = picked === "all" ? listed.length : listed.filter((id) => picked.some((f) => under(id, f))).length; console.log(`filters=${picked === "all" ? "" : picked.join(" ")}`); console.log(`shards=${JSON.stringify(count ? shardsFor(count) : [])}`); console.error(`${count} tests${picked === "all" ? " (all)" : picked.length ? ": " + picked.join(" ") : ""}`);