Add junit-6-migration module: JUnit 5.14.4 vs 6.1.3, API diff and behaviour probes

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YPgGH6aMbSLANL3E2HHmS6
This commit is contained in:
2026-10-25 18:46:52 +00:00
co-authored by Claude Sonnet 5.5
parent 8fc8bc934f
commit 820318be7d
22 changed files with 1018 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""Compare the public API of JUnit 5.14.4 / Platform 1.14.4 against JUnit 6.1.3 straight from the jars.
Writes output/01-versions-and-baseline.txt and output/02-removed-api.txt.
Needs a JDK (javap) on PATH or via JAVA_HOME, and access to Maven Central.
"""
import collections
import os
import pathlib
import re
import subprocess
import time
import urllib.error
import urllib.request
import zipfile
ROOT = pathlib.Path(__file__).resolve().parent.parent
JARS = ROOT / "target" / "jars"
OUT = ROOT / "output"
JAVAP = os.path.join(os.environ["JAVA_HOME"], "bin", "javap") if os.environ.get("JAVA_HOME") else "javap"
OLD, NEW, PLATFORM_OLD = "5.14.4", "6.1.3", "1.14.4"
ARTIFACTS = [
("jupiter", "junit-jupiter-api"), ("jupiter", "junit-jupiter-engine"), ("jupiter", "junit-jupiter-params"),
("platform", "junit-platform-commons"), ("platform", "junit-platform-engine"), ("platform", "junit-platform-launcher"),
("platform", "junit-platform-suite-api"), ("platform", "junit-platform-suite-engine"), ("platform", "junit-platform-testkit"),
("vintage", "junit-vintage-engine"),
]
def version_for(group, which):
if which == "new":
return NEW
return PLATFORM_OLD if group == "platform" else OLD
def fetch(url, dest):
if dest.exists():
return
for _ in range(8):
try:
with urllib.request.urlopen(url) as r:
dest.write_bytes(r.read())
return
except urllib.error.HTTPError as e:
if e.code != 429:
raise
time.sleep(4)
raise RuntimeError("rate limited: " + url)
def jar(group, artifact, which):
v = version_for(group, which)
dest = JARS / f"{artifact}-{v}.jar"
fetch(f"https://repo1.maven.org/maven2/org/junit/{group}/{artifact}/{v}/{artifact}-{v}.jar", dest)
return dest
def classes(path):
with zipfile.ZipFile(path) as z:
return sorted(n[:-6].replace("/", ".") for n in z.namelist()
if n.endswith(".class") and not n.endswith("module-info.class") and "META-INF" not in n)
def major(path):
with zipfile.ZipFile(path) as z:
name = next(n for n in z.namelist()
if n.endswith(".class") and not n.endswith("module-info.class") and "META-INF" not in n)
b = z.read(name)
return int.from_bytes(b[6:8], "big")
MEMBER = re.compile(r"^ ((?:public|protected)[^;]*);$")
def dump(path):
"""One javap call per jar. Returns {class: (is_public, class_deprecated, {member signature: deprecated?})}."""
names = [c for c in classes(path) if ".internal." not in c]
result = {}
# javap takes many class names at once; split the list to keep the command line short
for i in range(0, len(names), 150):
chunk = names[i:i + 150]
p = subprocess.run([JAVAP, "-v", "-protected", "-cp", str(path)] + chunk, capture_output=True, text=True)
cur_cls = None
for block in re.split(r"(?m)^Classfile ", p.stdout)[1:]:
lines = block.splitlines()
header = lines[0]
cls = re.search(r"!/(.*)\.class", header).group(1).replace("/", ".")
public = False
cls_dep = False
members, cur, in_pool, seen_flags = {}, None, False, False
for line in lines[1:]:
if not seen_flags and line.startswith(" flags:"):
public = "ACC_PUBLIC" in line
seen_flags = True
if line.startswith("Constant pool:"):
in_pool = True
continue
if in_pool:
if line.startswith("{"):
in_pool = False
continue
m = MEMBER.match(line)
if m:
cur = " ".join(m.group(1).split())
members[cur] = False
continue
if line.strip() == "Deprecated: true":
if cur is None:
cls_dep = True
else:
members[cur] = True
if line.startswith("}"):
cur = None
if re.search(r"^Deprecated: true", block, re.M):
cls_dep = True
result[cls] = (public, cls_dep, members)
return result
def main():
JARS.mkdir(parents=True, exist_ok=True)
OUT.mkdir(parents=True, exist_ok=True)
versions = [f"{'artifact':<34} {'5.x jar':<28} {'6.x jar':<28}"]
removed_classes = collections.defaultdict(list)
removed_members = collections.defaultdict(list)
added = collections.Counter()
for group, art in ARTIFACTS:
old, new = jar(group, art, "old"), jar(group, art, "new")
mo, mn = major(old), major(new)
versions.append(f"{art:<34} {version_for(group, 'old')} (Java {mo - 44}){'':<9} {NEW} (Java {mn - 44})")
do, dn = dump(old), dump(new)
co, cn = set(classes(old)), set(classes(new))
for c in sorted(co - cn):
if c in do and do[c][0]:
removed_classes[art].append((c, do[c][1]))
added[art] = len(cn - co)
for c in sorted(do):
if c in dn and do[c][0]:
for sig, dep in do[c][2].items():
if sig not in dn[c][2]:
removed_members[art].append((c, sig, dep))
(OUT / "01-versions-and-baseline.txt").write_text("\n".join(versions) + "\n")
lines = []
for _, art in ARTIFACTS:
lines.append(f"== {art}: public classes removed: {len(removed_classes[art])}, "
f"members removed from classes that remain: {len(removed_members[art])}, "
f"new classes: {added[art]}")
for c, dep in removed_classes[art]:
lines.append(f" class {c} [{'deprecated in 5.x' if dep else 'NOT deprecated in 5.x'}]")
for c, sig, dep in removed_members[art]:
lines.append(f" member {c}: {sig} [{'deprecated in 5.x' if dep else 'NOT deprecated in 5.x'}]")
(OUT / "02-removed-api.txt").write_text("\n".join(lines) + "\n")
print("\n".join(versions))
print("\n".join(l for l in lines if l.startswith("==")))
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Compiles src/compat/OldApis.java against the JUnit 5.14.4 jars and then the 6.1.3 jars and records the compiler's answer.
# Run scripts/api_diff.py first: it downloads the jars into target/jars.
set -uo pipefail
cd "$(dirname "$0")/.."
unset JAVA_TOOL_OPTIONS
mkdir -p output target/compat
J=target/jars
[ -f "$J/apiguardian-api-1.1.2.jar" ] || curl -sSf --retry 5 -o "$J/apiguardian-api-1.1.2.jar" \
https://repo1.maven.org/maven2/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar
compile() { # $1 = label, $2 = jupiter version, $3 = platform version
for src in OldApis:05-old-apis FixedApis:06-fixed-apis; do
{
echo "JUnit $2, compiling ${src%%:*}.java"
javac -Xlint:deprecation --release 17 -d "target/compat/$1" \
-cp "$J/junit-jupiter-api-$2.jar:$J/junit-platform-commons-$3.jar:$J/apiguardian-api-1.1.2.jar" "src/compat/${src%%:*}.java" 2>&1 \
| sed -e '/^Picked up/d'
echo "javac exit status: ${PIPESTATUS[0]}"
} > "output/${src##*:}-$2.txt"
done
}
compile junit5 5.14.4 1.14.4
compile junit6 6.1.3 6.1.3
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Regenerates every file in output/. Needs JDK 17+ (built with 25), Maven 3.9 and access to Maven Central.
set -euo pipefail
cd "$(dirname "$0")/.."
unset JAVA_TOOL_OPTIONS
python3 scripts/api_diff.py >/dev/null # 01, 02: jar-to-jar API diff (also downloads the jars)
mvn -B -q clean test -Pjunit5 # 03, 04 on JUnit 5.14.4
mvn -B -q clean test -Pjunit6 # 03, 04 on JUnit 6.1.3
./scripts/compile-old-apis.sh # 05, 06
ls -1 output