874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
171 lines
8.9 KiB
Python
171 lines
8.9 KiB
Python
import struct
|
|
import unittest
|
|
|
|
from tools import class_index, vanilla_map
|
|
|
|
|
|
class Pool:
|
|
"""A constant pool built in the order entries are asked for; `pad` shifts every index."""
|
|
|
|
def __init__(self, pad=0):
|
|
self.entries, self.index = [], {}
|
|
for i in range(pad):
|
|
self.utf8(f"padding{i}")
|
|
|
|
def add(self, key, data):
|
|
if key not in self.index:
|
|
self.entries.append(data)
|
|
self.index[key] = len(self.entries)
|
|
return self.index[key]
|
|
|
|
def utf8(self, text):
|
|
raw = text.encode()
|
|
return self.add(("u", text), b"\x01" + struct.pack(">H", len(raw)) + raw)
|
|
|
|
def cls(self, name):
|
|
return self.add(("c", name), b"\x07" + struct.pack(">H", self.utf8(name)))
|
|
|
|
def string(self, text):
|
|
return self.add(("s", text), b"\x08" + struct.pack(">H", self.utf8(text)))
|
|
|
|
def method(self, owner, name, descriptor):
|
|
nat = self.add(("nt", name, descriptor),
|
|
b"\x0c" + struct.pack(">HH", self.utf8(name), self.utf8(descriptor)))
|
|
return self.add(("m", owner, name, descriptor), b"\x0a" + struct.pack(">HH", self.cls(owner), nat))
|
|
|
|
def bytes(self):
|
|
return struct.pack(">H", len(self.entries) + 1) + b"".join(self.entries)
|
|
|
|
|
|
def class_file(methods, name="a/B", pad=0, wide=False):
|
|
"""A class with the given methods: {name: (descriptor, [instructions])}, each instruction
|
|
("ldc", text) | ("call", owner, name, descriptor) | ("ifnull", +instructions) | (opcode,)."""
|
|
pool = Pool(pad)
|
|
this, parent = pool.cls(name), pool.cls("java/lang/Object")
|
|
code_name = pool.utf8("Code")
|
|
encoded = []
|
|
for method, (descriptor, instructions) in methods.items():
|
|
sizes = [3 if i[0] in ("call", "ifnull") or (i[0] == "ldc" and wide) else 2 if i[0] == "ldc" else 1
|
|
for i in instructions]
|
|
body = b""
|
|
for at, instruction in enumerate(instructions):
|
|
if instruction[0] == "ldc":
|
|
index = pool.string(instruction[1])
|
|
body += bytes([19]) + struct.pack(">H", index) if wide else bytes([18, index])
|
|
elif instruction[0] == "call":
|
|
body += bytes([184]) + struct.pack(">H", pool.method(*instruction[1:]))
|
|
elif instruction[0] == "ifnull":
|
|
offset = sum(sizes[at:at + instruction[1]])
|
|
body += bytes([198]) + struct.pack(">h", offset)
|
|
else:
|
|
body += bytes([instruction[0]])
|
|
code = struct.pack(">HHI", 2, 1, len(body)) + body + struct.pack(">HH", 0, 0)
|
|
encoded.append(struct.pack(">HHHH", 0x0009, pool.utf8(method), pool.utf8(descriptor), 1)
|
|
+ struct.pack(">HI", code_name, len(code)) + code)
|
|
return (b"\xca\xfe\xba\xbe" + struct.pack(">HH", 0, 69) + pool.bytes()
|
|
+ struct.pack(">HHHH", 0x0021, this, parent, 0) + struct.pack(">H", 0)
|
|
+ struct.pack(">H", len(encoded)) + b"".join(encoded) + struct.pack(">H", 0))
|
|
|
|
|
|
def index(methods, **options):
|
|
return class_index.ClassFile(class_file(methods, **options)).index()
|
|
|
|
|
|
GREET = {"greet": ("()Ljava/lang/String;", [("ldc", "hi"), ("ifnull", 2), (1,), (176,)])}
|
|
|
|
|
|
class ClassIndexTests(unittest.TestCase):
|
|
def test_constant_pool_layout_and_ldc_width_do_not_change_hashes(self):
|
|
plain = index(GREET)
|
|
self.assertEqual(index(GREET, pad=3), plain)
|
|
self.assertEqual(index(GREET, pad=300, wide=True), plain)
|
|
|
|
def test_a_changed_literal_or_call_changes_the_hash(self):
|
|
plain = index(GREET)["methods"]
|
|
other = index({"greet": ("()Ljava/lang/String;", [("ldc", "hello"), ("ifnull", 2), (1,), (176,)])})
|
|
self.assertNotEqual(other["methods"], plain)
|
|
call = {"run": ("()V", [("call", "a/C", "go", "()V"), (177,)])}
|
|
moved = {"run": ("()V", [("call", "b/C", "go", "()V"), (177,)])}
|
|
self.assertNotEqual(index(call)["methods"], index(moved)["methods"])
|
|
|
|
def test_lambdas_fold_into_their_declaring_method(self):
|
|
body = ("()V", [(177,)])
|
|
first = index({"run": body, "lambda$run$0": body, "lambda$run$1": ("()V", [(1,), (87,), (177,)])})
|
|
renumbered = index({"run": body, "lambda$run$4": ("()V", [(1,), (87,), (177,)]), "lambda$run$5": body})
|
|
self.assertEqual(set(first["methods"]), {"run()V", "lambda$run"})
|
|
self.assertEqual(first["methods"], renumbered["methods"])
|
|
|
|
def test_diff_with_moves(self):
|
|
def entry(**methods):
|
|
return {"access": 33, "super": "java/lang/Object", "interfaces": [], "fields": {}, "methods": methods}
|
|
old = {"a/Same": entry(f="1"), "a/Edit": entry(f="1", g="2"), "a/Gone": entry(),
|
|
"a/Moved": entry(f="1"), "a/x/Twin": entry(), "a/y/Twin": entry()}
|
|
new = {"a/Same": entry(f="1"), "a/Edit": entry(f="9", h="3"), "b/Moved": entry(f="2"), "a/New": entry(),
|
|
"b/Twin": entry()}
|
|
result = class_index.diff(old, new)
|
|
self.assertEqual(result["moved"], {"a/Moved": "b/Moved"})
|
|
self.assertEqual(result["added"], ["a/New", "b/Twin"])
|
|
self.assertEqual(result["removed"], ["a/Gone", "a/x/Twin", "a/y/Twin"])
|
|
self.assertEqual(result["changed"]["a/Edit"],
|
|
{"methods": {"added": ["h"], "removed": ["g"], "changed": ["f"]}})
|
|
self.assertEqual(result["changed"]["b/Moved"]["moved_from"], "a/Moved")
|
|
self.assertEqual(class_index.changed_methods(result["changed"]["a/Edit"]), 3)
|
|
|
|
|
|
CLASSES = ["net/minecraft/world/food/FoodData", "net/minecraft/world/level/Level",
|
|
"com/mojang/realmsclient/Level", "net/minecraft/world/entity/Entity",
|
|
"net/minecraft/world/entity/Entity$RemovalReason", "net/minecraft/a/Thing$1",
|
|
"net/minecraft/server/level/ServerLevel"]
|
|
|
|
|
|
class VanillaMapTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.names = vanilla_map.Names(CLASSES)
|
|
|
|
def test_resolve(self):
|
|
self.assertEqual(self.names.resolve("FoodData"), "net/minecraft/world/food/FoodData")
|
|
self.assertEqual(self.names.resolve("Level"), "net/minecraft/world/level/Level")
|
|
self.assertEqual(self.names.resolve("RemovalReason"), "net/minecraft/world/entity/Entity$RemovalReason")
|
|
self.assertEqual(self.names.resolve("Entity.RemovalReason"), "net/minecraft/world/entity/Entity$RemovalReason")
|
|
self.assertIsNone(self.names.resolve("Missing"))
|
|
self.assertEqual(self.names.java("net.minecraft.world.entity.Entity.RemovalReason"),
|
|
"net/minecraft/world/entity/Entity$RemovalReason")
|
|
|
|
def test_rust_comments(self):
|
|
files = {"crates/x/src/food.rs": "/// FoodData.tick, as vanilla\nfn tick() {} // ServerLevel too\n"
|
|
"let FoodData = 1;\n// a Level, a Level.tick() and Entity.RemovalReason\n"}
|
|
found = vanilla_map.rust_references(files, self.names, read=files.get)
|
|
self.assertEqual(found["net/minecraft/world/food/FoodData"], {"crates/x/src/food.rs": ["tick"]})
|
|
self.assertEqual(found["net/minecraft/server/level/ServerLevel"], {"crates/x/src/food.rs": []})
|
|
# A one-word name counts only with a member: "a Level" alone is English.
|
|
self.assertEqual(found["net/minecraft/world/level/Level"], {"crates/x/src/food.rs": ["tick"]})
|
|
self.assertIn("net/minecraft/world/entity/Entity$RemovalReason", found)
|
|
|
|
def test_java_references(self):
|
|
text = ("import net.minecraft.world.food.*;import net.minecraft.world.entity.Entity;\n"
|
|
"import static net.minecraft.world.level.Level.OVERWORLD;\n"
|
|
"class X { FoodData f; net.minecraft.server.level.ServerLevel l; }")
|
|
self.assertEqual(vanilla_map.java_references(text, self.names), {
|
|
"net/minecraft/world/food/FoodData", "net/minecraft/world/entity/Entity",
|
|
"net/minecraft/world/level/Level", "net/minecraft/server/level/ServerLevel"})
|
|
|
|
def test_probe_modules(self):
|
|
sources = {"crates/horizon-world/src/food.rs", "crates/horizon-world/src/level/mod.rs",
|
|
"crates/horizon-world/src/level/tick.rs"}
|
|
text = "use horizon_world::food::FoodData;\nuse horizon_world::level::tick::{a, b};\nuse crate::level;\n"
|
|
self.assertEqual(vanilla_map.module_files("crates/horizon-world/examples/p.rs", text, sources),
|
|
{"crates/horizon-world/src/food.rs", "crates/horizon-world/src/level/tick.rs",
|
|
"crates/horizon-world/src/level/mod.rs"})
|
|
self.assertEqual(vanilla_map.probe_files("run(['food_fixture'])", {"food_fixture": "e.rs", "food": "f.rs"}),
|
|
{"food_fixture": "e.rs"})
|
|
self.assertEqual(vanilla_map.harness_name("tools/test_food_parity.py"), "food")
|
|
|
|
def test_lookup_falls_back_to_the_outer_class(self):
|
|
mapping = {"classes": {"a/B": {"rust": {"x.rs": []}, "harnesses": ["h"]}}}
|
|
self.assertEqual(vanilla_map.lookup(mapping, "a/B$C")["harnesses"], ["h"])
|
|
self.assertEqual(vanilla_map.lookup(mapping, "a/D"), {"rust": {}, "harnesses": []})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|