"""The class-file editing behind the patch build, checked against real javac output.""" import shutil import subprocess import sys import tempfile import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "tools")) from build_patch import TOO_NEW, retarget # noqa: E402 from classfile import JAVA_1_4_MAJOR, ClassFile # noqa: E402 SOURCE = """package pkg; public class Thing { public Thing next; public long big = 5000000000L; public double ratio = 2.5; public static Thing make(Thing other) { return new Thing(); } public static Integer boxed(int i) { return i; } public static String joined(String a, int b) { return a + b; } } """ # Written the way patch/src has to be: nothing newer than Java 1.4. CLEAN_SOURCE = """package pkg; public class Clean { public static String joined(String a, int b) { return new StringBuffer(a).append(b).toString(); } } """ @unittest.skipUnless(shutil.which("javac"), "needs a JDK on PATH") class ClassFileTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.dir = tempfile.TemporaryDirectory() root = Path(cls.dir.name) (root / "pkg").mkdir() (root / "pkg" / "Thing.java").write_text(SOURCE, encoding="utf-8") (root / "pkg" / "Clean.java").write_text(CLEAN_SOURCE, encoding="utf-8") subprocess.run(["javac", "--release", "8", "-d", str(root), str(root / "pkg" / "Thing.java"), str(root / "pkg" / "Clean.java")], check=True, capture_output=True) cls.data = (root / "pkg" / "Thing.class").read_bytes() cls.clean_data = (root / "pkg" / "Clean.class").read_bytes() @classmethod def tearDownClass(cls): cls.dir.cleanup() def test_round_trip_is_byte_identical(self): # Also proves longs and doubles (two pool slots each) are walked correctly. self.assertEqual(ClassFile(self.data).to_bytes(), self.data) def test_rename_covers_the_name_and_descriptors(self): parsed = ClassFile(self.data) self.assertGreaterEqual(parsed.replace_in_utf8({"pkg/Thing": "pkg/RenamedThing"}), 3) strings = ClassFile(parsed.to_bytes()).utf8_strings() self.assertIn("pkg/RenamedThing", strings) self.assertIn("(Lpkg/RenamedThing;)Lpkg/RenamedThing;", strings) self.assertFalse([s for s in strings if "pkg/Thing" in s]) def test_retarget_changes_only_the_version(self): self.assertEqual(ClassFile(self.data).major, 52) clean = Path(self.dir.name) / "Clean.class" clean.write_bytes(self.clean_data) retarget(clean) out = clean.read_bytes() self.assertEqual((ClassFile(out).major, ClassFile(out).minor), (JAVA_1_4_MAJOR, 0)) self.assertEqual(out[8:], self.clean_data[8:]) def test_retarget_refuses_post_1_4_code(self): modern = Path(self.dir.name) / "Modern.class" modern.write_bytes(self.data) with self.assertRaises(SystemExit): retarget(modern) self.assertEqual(modern.read_bytes(), self.data) # left untouched def test_build_guard_markers_really_appear_for_post_1_4_code(self): # SOURCE autoboxes and concatenates; the build must be able to see both. pool = "\n".join(ClassFile(self.data).utf8_strings()) found = [marker for marker in TOO_NEW if marker in pool] self.assertIn("(I)Ljava/lang/Integer;", found) self.assertIn("java/lang/StringBuilder", found) def test_rejects_non_class_data(self): with self.assertRaises(ValueError): ClassFile(b"not a class") if __name__ == "__main__": unittest.main()