From 190e58391c85b17b45aa1b0c0c6ffb9027235c5c Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:59:13 -0600 Subject: [PATCH 1/4] Run the OpenJDK tests on pull requests and pushes, split over parallel jobs, picking the tests a pull request's changes need. --- .github/workflows/jtreg.yml | 65 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 21 +++++------ .github/workflows/test.yml | 5 +++ scripts/jtreg/run.ts | 14 +++++--- scripts/jtreg/select.ts | 56 ++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/jtreg.yml create mode 100644 scripts/jtreg/select.ts diff --git a/.github/workflows/jtreg.yml b/.github/workflows/jtreg.yml new file mode 100644 index 0000000..3e9dc59 --- /dev/null +++ b/.github/workflows/jtreg.yml @@ -0,0 +1,65 @@ +name: jtreg + +# The OpenJDK regression tests in test/jtreg/passing.txt, split over parallel +# jobs. A pull request runs the tests its changes need; `full` runs them all. +on: + workflow_call: + inputs: + full: + type: boolean + default: false + +permissions: + contents: read + +jobs: + plan: + runs-on: ubuntu-latest + outputs: + filters: ${{ steps.select.outputs.filters }} + shards: ${{ steps.select.outputs.shards }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - id: select + run: node scripts/jtreg/select.ts ${{ inputs.full && '--full' || github.event.pull_request.base.sha || '--full' }} >> "$GITHUB_OUTPUT" + + run: + needs: plan + if: needs.plan.outputs.shards != '[]' + runs-on: ubuntu-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + shard: ${{ fromJSON(needs.plan.outputs.shards) }} + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + with: + version: 12 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + - uses: actions/cache@v6 + with: + path: ~/.cache/java-web + key: jdk-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('scripts/build-jdk.sh') }} + - run: pnpm install --frozen-lockfile + - run: pnpm jdk + - name: OpenJDK regression tests + env: + FILTERS: ${{ needs.plan.outputs.filters }} + run: | + release=$(sed -n 's/^release=//p' scripts/build-jdk.sh) + git clone -q --filter=blob:none --sparse --depth 1 --branch "jdk-${release%+*}-ga" https://github.com/openjdk/jdk25u.git build/ref/jdk25u + dirs=$(grep -v '^#' test/jtreg/passing.txt | cut -d/ -f1-2 | sort -u | sed 's|^|test/jdk/|') + git -C build/ref/jdk25u sparse-checkout set test/lib test/jdk/lib test/jdk/tools/lib $dirs + set -f + node scripts/jtreg/run.ts build/ref/jdk25u --only-passing --jobs 4 --timeout 600 \ + --shard ${{ matrix.shard }}/${{ strategy.job-total }} $FILTERS diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d75d8c..bf1f1fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,14 +6,22 @@ on: permissions: contents: read - id-token: write jobs: - publish: + jtreg: # prereleases are not published, so npm's latest tag only gets stable versions if: ${{ !github.event.release.prerelease }} + uses: ./.github/workflows/jtreg.yml + with: + full: true + + publish: + needs: jtreg runs-on: ubuntu-latest - timeout-minutes: 240 + timeout-minutes: 30 + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v7 - name: Check the tag matches package.json @@ -29,13 +37,6 @@ jobs: - run: pnpm jdk - run: pnpm typecheck - run: pnpm test - - name: OpenJDK regression tests - run: | - release=$(sed -n 's/^release=//p' scripts/build-jdk.sh) - git clone -q --filter=blob:none --sparse --depth 1 --branch "jdk-${release%+*}-ga" https://github.com/openjdk/jdk25u.git build/ref/jdk25u - dirs=$(grep -v '^#' test/jtreg/passing.txt | cut -d/ -f1-2 | sort -u | sed 's|^|test/jdk/|') - git -C build/ref/jdk25u sparse-checkout set test/lib test/jdk/lib test/jdk/tools/lib $dirs - node scripts/jtreg/run.ts build/ref/jdk25u --only-passing --jobs 4 --timeout 600 - run: pnpm build - name: Node entry on the built dist/ run: node --expose-gc --test test/image.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a48c45c..ca8872f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,3 +36,8 @@ jobs: - run: pnpm build - name: Node entry on the built dist/ run: node --expose-gc --test test/image.test.ts + + jtreg: + uses: ./.github/workflows/jtreg.yml + with: + full: ${{ github.event_name == 'push' }} diff --git a/scripts/jtreg/run.ts b/scripts/jtreg/run.ts index e62542e..5571041 100644 --- a/scripts/jtreg/run.ts +++ b/scripts/jtreg/run.ts @@ -1,12 +1,14 @@ // Runs OpenJDK regression tests (the `@run main`, `testng` and `junit` kinds) on java-web. // // node scripts/jtreg/run.ts [dir or file filters...] -// [--jobs 1] [--timeout 120] [--rerun] [--only-passing] [--vm path/to/node.ts] [--cool 75] +// [--jobs 1] [--timeout 120] [--rerun] [--only-passing] [--shard 1/4] [--vm path/to/node.ts] [--cool 75] // // Each test is compiled the way jtreg does it, run on HotSpot first (only // tests that pass there count), then on java-web in a child process. // Results go to build/jtreg/results.jsonl; --only-passing reruns the tests -// listed in test/jtreg/passing.txt and fails if any of them regress. +// listed in test/jtreg/passing.txt (those under the filters, if any) on +// java-web alone, and fails if any of them regress. --shard k/n runs every +// nth of the selected tests, for parallel CI jobs. import { createHash } from "node:crypto"; import { spawn, spawnSync } from "node:child_process"; @@ -36,6 +38,7 @@ const rerun = has("--rerun"); // pause new tests while the CPU is at or above this many degrees C (for small boards) const cool = Number(flag("--cool", "0")); const onlyPassing = has("--only-passing"); +const [shardK, shardN] = flag("--shard", "1/1").split("/").map(Number); // node entry to test; point it at a copy of src/ to keep editing during a long run const vm = resolve(flag("--vm", join(root, "src", "node.ts"))); const src = resolve(argv.shift() ?? ""); @@ -363,7 +366,8 @@ async function runCase(tc: TestCase): Promise> { // the whole sequence on one VM, then the other: later runs may depend on files earlier ones left let refMs = 0; let work = scratch("hotspot"); - for (const a of tc.runs) { + // tests in passing.txt already pass on HotSpot + for (const a of onlyPassing ? [] : tc.runs) { if (a.kind === "compile") { const err = await javacInto(tc, path, a.opts, a.args.map((f) => resolve(tc.dir, f))); if (err) return { status: "skip", why: `javac: ${err}` }; @@ -413,7 +417,8 @@ function listed(): string[] { mkdirSync(out, { recursive: true }); let selected: TestCase[]; if (onlyPassing) { - const ids = new Set(listed()); + const under = (id: string) => !argv.length || argv.some((f) => id === f || id.startsWith(f + "/") || id.startsWith(f + "#")); + const ids = new Set(listed().filter(under)); const files = [...new Set([...ids].map((id) => join(testRoot, id.split("#")[0])))]; selected = files.flatMap(parseFile).filter((tc) => ids.has(tc.id)); } else { @@ -427,6 +432,7 @@ if (onlyPassing) { return ids.has(tc.id) || whole.has(file) || !narrowed.has(file); }); } +selected = selected.filter((_, i) => i % shardN === shardK - 1); const done = new Set(); if (!rerun && !onlyPassing && existsSync(resultsFile)) { for (const l of readFileSync(resultsFile, "utf8").split("\n")) if (l) done.add(JSON.parse(l).id); diff --git a/scripts/jtreg/select.ts b/scripts/jtreg/select.ts new file mode 100644 index 0000000..cd5adde --- /dev/null +++ b/scripts/jtreg/select.ts @@ -0,0 +1,56 @@ +// Picks the OpenJDK tests a change needs, for CI. +// +// node scripts/jtreg/select.ts [ | --full] +// +// Prints GitHub Actions outputs: `filters`, the test areas to pass to run.ts +// (empty for all of them), and `shards`, a JSON list of shard numbers ([] when +// no test is needed). A changed file this does not know runs every test. + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const root = join(import.meta.dirname, "..", ".."); +const passingFile = "test/jtreg/passing.txt"; + +// first match wins; null runs no test +const areas: [RegExp, string[] | null][] = [ + [/\.md$|^docs\/|^LICENSE|^renovate\.json$|^\.coderabbit/, null], + [/^src\/browser\/|^src\/(bridge|library|image|http-proxy)\.ts$|^types\/|^scripts\/(build-web|serve)\.ts$/, null], + [/^test\/(?!jtreg\/)/, null], + [/^src\/natives\/zip\.ts$|^src\/inflate\.ts$/, ["java/util/zip", "java/util/jar"]], + [/^src\/natives\/(io|nio)\.ts$|^src\/fs\.ts$/, ["java/io", "java/nio", "java/util/zip", "java/util/jar"]], + [/^src\/natives\/(net|sockets)\.ts$|^src\/(sockets|net-node)\.ts$|^scripts\/tls\//, ["java/net", "java/nio/channels"]], + [/^src\/natives\/(font|java2d)[\w-]*\.ts$/, ["java/awt", "java/beans"]], + [/^src\/natives\/management\.ts$/, ["java/lang/management"]], +]; + +const shardsFor = (n: number) => Array.from({ length: Math.min(4, 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(); + for (const file of git("diff", "--name-only", `${base}...HEAD`).split("\n").filter(Boolean)) { + if (file === passingFile) { + // 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; + } + const hit = areas.find(([re]) => re.test(file)); + if (!hit) return "all"; + for (const f of hit[1] ?? []) 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(" ") : ""}`); From 0d6fa2b322f1bf04ce6d5a6349e1c74f674a32a3 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:06:23 -0600 Subject: [PATCH 2/4] Drop checkout credentials in the jtreg jobs, reject bad shard values, and map both paths of a renamed file. --- .github/workflows/jtreg.yml | 3 +++ scripts/jtreg/run.ts | 4 ++++ scripts/jtreg/select.ts | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/jtreg.yml b/.github/workflows/jtreg.yml index 3e9dc59..8e65795 100644 --- a/.github/workflows/jtreg.yml +++ b/.github/workflows/jtreg.yml @@ -22,6 +22,7 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 + persist-credentials: false - uses: actions/setup-node@v7 with: node-version: 24 @@ -39,6 +40,8 @@ jobs: shard: ${{ fromJSON(needs.plan.outputs.shards) }} steps: - uses: actions/checkout@v7 + with: + persist-credentials: false - uses: pnpm/action-setup@v6 with: version: 12 diff --git a/scripts/jtreg/run.ts b/scripts/jtreg/run.ts index 5571041..ea891b8 100644 --- a/scripts/jtreg/run.ts +++ b/scripts/jtreg/run.ts @@ -39,6 +39,10 @@ const rerun = has("--rerun"); const cool = Number(flag("--cool", "0")); const onlyPassing = has("--only-passing"); const [shardK, shardN] = flag("--shard", "1/1").split("/").map(Number); +if (!Number.isInteger(shardK) || !Number.isInteger(shardN) || shardK < 1 || shardK > shardN) { + console.error("--shard takes k/n with 1 <= k <= n"); + process.exit(2); +} // node entry to test; point it at a copy of src/ to keep editing during a long run const vm = resolve(flag("--vm", join(root, "src", "node.ts"))); const src = resolve(argv.shift() ?? ""); diff --git a/scripts/jtreg/select.ts b/scripts/jtreg/select.ts index cd5adde..37f29d1 100644 --- a/scripts/jtreg/select.ts +++ b/scripts/jtreg/select.ts @@ -33,7 +33,7 @@ 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(); - for (const file of git("diff", "--name-only", `${base}...HEAD`).split("\n").filter(Boolean)) { + for (const file of git("diff", "--name-only", "--no-renames", `${base}...HEAD`).split("\n").filter(Boolean)) { if (file === passingFile) { // the tests it adds for (const l of git("diff", "-U0", `${base}...HEAD`, "--", file).split("\n")) { From 46ff1c09f11e82f67b39a102f7a9fc34266c0648 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:34:08 -0600 Subject: [PATCH 3/4] Choose the jtreg selection argument in shell rather than a nested expression. --- .github/workflows/jtreg.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/jtreg.yml b/.github/workflows/jtreg.yml index 8e65795..435b9d6 100644 --- a/.github/workflows/jtreg.yml +++ b/.github/workflows/jtreg.yml @@ -27,7 +27,13 @@ jobs: with: node-version: 24 - id: select - run: node scripts/jtreg/select.ts ${{ inputs.full && '--full' || github.event.pull_request.base.sha || '--full' }} >> "$GITHUB_OUTPUT" + env: + FULL: ${{ inputs.full }} + BASE: ${{ github.event.pull_request.base.sha }} + # a pull request runs what its diff needs; anything else runs everything + run: | + if [ "$FULL" = true ] || [ -z "$BASE" ]; then arg=--full; else arg=$BASE; fi + node scripts/jtreg/select.ts "$arg" >> "$GITHUB_OUTPUT" run: needs: plan From 88c49ebedeade3ee577ba3219038f3eae1b80308 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:41:09 -0600 Subject: [PATCH 4/4] Read the jtreg filters into a bash array. --- .github/workflows/jtreg.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/jtreg.yml b/.github/workflows/jtreg.yml index 435b9d6..d2e33dd 100644 --- a/.github/workflows/jtreg.yml +++ b/.github/workflows/jtreg.yml @@ -69,6 +69,6 @@ jobs: git clone -q --filter=blob:none --sparse --depth 1 --branch "jdk-${release%+*}-ga" https://github.com/openjdk/jdk25u.git build/ref/jdk25u dirs=$(grep -v '^#' test/jtreg/passing.txt | cut -d/ -f1-2 | sort -u | sed 's|^|test/jdk/|') git -C build/ref/jdk25u sparse-checkout set test/lib test/jdk/lib test/jdk/tools/lib $dirs - set -f + read -ra filters <<< "$FILTERS" node scripts/jtreg/run.ts build/ref/jdk25u --only-passing --jobs 4 --timeout 600 \ - --shard ${{ matrix.shard }}/${{ strategy.job-total }} $FILTERS + --shard ${{ matrix.shard }}/${{ strategy.job-total }} "${filters[@]}"