Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/jtreg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
- id: select
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
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
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
with:
persist-credentials: false
- 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
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
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
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[@]}"
21 changes: 11 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
18 changes: 14 additions & 4 deletions scripts/jtreg/run.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Runs OpenJDK regression tests (the `@run main`, `testng` and `junit` kinds) on java-web.
//
// node scripts/jtreg/run.ts <jdk-source-checkout> [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";
Expand Down Expand Up @@ -36,6 +38,11 @@ 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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() ?? "");
Expand Down Expand Up @@ -363,7 +370,8 @@ async function runCase(tc: TestCase): Promise<Record<string, unknown>> {
// 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
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
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}` };
Expand Down Expand Up @@ -413,7 +421,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 {
Expand All @@ -427,6 +436,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<string>();
if (!rerun && !onlyPassing && existsSync(resultsFile)) {
for (const l of readFileSync(resultsFile, "utf8").split("\n")) if (l) done.add(JSON.parse(l).id);
Expand Down
56 changes: 56 additions & 0 deletions scripts/jtreg/select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Picks the OpenJDK tests a change needs, for CI.
//
// node scripts/jtreg/select.ts [<base> | --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);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
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<string>();
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")) {
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(" ") : ""}`);
Loading