From 55f21ef7dfeda7adf98c363a2040de62fcb563bd Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 4 Sep 2026 21:05:48 +0200 Subject: [PATCH] test: characterize method symbol full names --- src/test/fixtures/README.md | 14 ++ .../ReferenceScalaMethodSymbolFull.scala | 7 + .../ScalaSignatureMethodSymbolFullTest.java | 213 ++++++++++++++++++ .../ReferenceScalaMethodSymbolFull.class.b64 | 21 ++ 4 files changed, 255 insertions(+) create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMethodSymbolFull.scala create mode 100644 src/test/java/codechicken/multipart/asm/ScalaSignatureMethodSymbolFullTest.java create mode 100644 src/test/resources/compat/ReferenceScalaMethodSymbolFull.class.b64 diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 812d30f..0de8ee2 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -360,3 +360,17 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouche | --- | --- | | `ReferenceScalaClassSymbolFull` | `77498a049a18080a67b557b1751c7f8ab905b9f832ebd680db822e5447ea0799` | | `ReferenceScalaClassSymbolFull$$anon$1` | `df9d4c907de922b6e917ea848e40f746ef446bf601dbda4b8d9f9761403b5f46` | + +### `ReferenceScalaMethodSymbolFull` + +`scala/codechicken/multipart/compat/ReferenceScalaMethodSymbolFull.scala` freezes a Scala caller of +`MethodSymbol.full`. Tests combine it with virtual JVM accessor overrides to preserve read order and failures. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouched dev jar at `64f5abc` +(SHA-256 `39dcdec0f93d8d475b326b70abb0c8afd8c76f45d175e0d4fd032d6d61e6dc07`). Source SHA-256 is +`7a82e44389fe45d9aaf0b3a830ba554074b2f947e634a9ae5417163e9e969b33`. The frozen class file is under +`src/test/resources/compat/`; do not regenerate it against the port. + +| Class | SHA-256 | +| --- | --- | +| `ReferenceScalaMethodSymbolFull` | `82aad2c3f67b61337d7f9daf853ccdd5e68bd30ccf82abd084195e7b66e0846d` | diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMethodSymbolFull.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMethodSymbolFull.scala new file mode 100644 index 0000000..9bf364e --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMethodSymbolFull.scala @@ -0,0 +1,7 @@ +package codechicken.multipart.compat + +import codechicken.multipart.asm.ScalaSignature + +class ReferenceScalaMethodSymbolFull { + def full(ref: ScalaSignature#MethodSymbol): String = ref.full +} diff --git a/src/test/java/codechicken/multipart/asm/ScalaSignatureMethodSymbolFullTest.java b/src/test/java/codechicken/multipart/asm/ScalaSignatureMethodSymbolFullTest.java new file mode 100644 index 0000000..430a692 --- /dev/null +++ b/src/test/java/codechicken/multipart/asm/ScalaSignatureMethodSymbolFullTest.java @@ -0,0 +1,213 @@ +package codechicken.multipart.asm; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.Scanner; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import codechicken.multipart.asm.ScalaSignature.Bytes; +import codechicken.multipart.asm.ScalaSignature.MethodSymbol; +import codechicken.multipart.asm.ScalaSignature.SymbolRef; +import codechicken.multipart.asm.ScalaSignature.TMethodType; + +class ScalaSignatureMethodSymbolFullTest { + + private static final Fixture FIXTURE = new Fixture(); + + @Test + void methodSymbolsJoinTheirOwnerAndName() { + ScalaSignature sig = signature(); + assertEquals( + "pkg.Owner.method", + sig.new MethodSymbol("method", sig.new ExternalSymbol("pkg.Owner"), 0, 0).full()); + assertEquals(".", sig.new MethodSymbol("", sig.NoSymbol(), 0, 0).full()); + assertEquals("..a,b(c)", sig.new MethodSymbol("a,b(c)", sig.new ExternalSymbol("."), 0, 0).full()); + } + + @Test + void nullFullNameAndNameAreFormattedLiterally() { + ScalaSignature sig = signature(); + SymbolRef nullFull = sig.new ExternalSymbol("unused") { + + @Override + public String full() { + return null; + } + }; + assertEquals("null.method", sig.new MethodSymbol("method", nullFull, 0, 0).full()); + assertEquals("pkg.Owner.null", sig.new MethodSymbol(null, sig.new ExternalSymbol("pkg.Owner"), 0, 0).full()); + } + + @Test + void virtualOwnerFullAndNameAreReadInOrderOnEveryFrozenCallerQuery() throws Throwable { + ScalaSignature sig = signature(); + List calls = new ArrayList<>(); + int[] query = { 0 }; + SymbolRef owner = sig.new ExternalSymbol("unused") { + + @Override + public String full() { + calls.add("owner.full"); + return "Owner" + query[0]; + } + }; + MethodSymbol ref = ref(sig, () -> { + calls.add("owner"); + return owner; + }, () -> { + calls.add("name"); + return "method" + query[0]++; + }); + assertEquals("Owner0.method0", full(ref)); + assertEquals("Owner1.method1", full(ref)); + assertEquals(Arrays.asList("owner", "owner.full", "name", "owner", "owner.full", "name"), calls); + } + + @Test + void failuresStopAtTheSelectedOwnerOrNameRead() throws Throwable { + ScalaSignature sig = signature(); + List order = Arrays.asList("owner", "owner.full", "name"); + RuntimeException failure = new IllegalStateException("method symbol full getter"); + for (String failing : order) { + List calls = new ArrayList<>(); + SymbolRef owner = sig.new ExternalSymbol("unused") { + + @Override + public String full() { + return read(calls, "owner.full", "Owner", failing, failure); + } + }; + MethodSymbol ref = ref( + sig, + () -> read(calls, "owner", owner, failing, failure), + () -> read(calls, "name", "method", failing, failure)); + assertSame(failure, assertThrows(RuntimeException.class, () -> full(ref))); + assertEquals(order.subList(0, order.indexOf(failing) + 1), calls); + } + } + + @Test + void nullOwnerFailsBeforeReadingTheName() throws Throwable { + ScalaSignature sig = signature(); + int[] nameReads = { 0 }; + MethodSymbol ref = ref(sig, () -> null, () -> { + nameReads[0]++; + return "method"; + }); + assertThrows(NullPointerException.class, () -> full(ref)); + assertEquals(0, nameReads[0]); + } + + @Test + void fullDoesNotReadOwnerStringOrDerivedMethodMembers() { + ScalaSignature sig = signature(); + SymbolRef owner = sig.new ExternalSymbol("Owner") { + + @Override + public String toString() { + throw new AssertionError("owner.toString"); + } + }; + MethodSymbol ref = sig.new MethodSymbol("method", owner, 0, 0) { + + @Override + public int flags() { + throw new AssertionError("flags"); + } + + @Override + public int infoId() { + throw new AssertionError("infoId"); + } + + @Override + public TMethodType info() { + throw new AssertionError("info"); + } + + @Override + public String jDesc() { + throw new AssertionError("jDesc"); + } + + @Override + public String toString() { + throw new AssertionError("toString"); + } + }; + assertEquals("Owner.method", ref.full()); + } + + @Test + void frozenScalaCallerKeepsItsDescriptorAndNullReceiverFailure() throws Throwable { + ScalaSignature sig = signature(); + MethodSymbol ref = sig.new MethodSymbol("method", sig.new ExternalSymbol("Owner"), 0, 0); + assertEquals("Owner.method", full(ref)); + assertThrows(NullPointerException.class, () -> full(null)); + } + + private static T read(List calls, String name, T value, String failing, RuntimeException failure) { + calls.add(name); + if (name.equals(failing)) throw failure; + return value; + } + + private static MethodSymbol ref(ScalaSignature sig, Supplier owners, Supplier names) + throws Throwable { + return sig.new MethodSymbol(null, null, 0, 0) { + + @Override + public String name() { + return names.get(); + } + + @Override + public SymbolRef owner() { + return owners.get(); + } + }; + } + + private static String full(MethodSymbol ref) throws Throwable { + return (String) FIXTURE.full.invoke(ref); + } + + private static ScalaSignature signature() { + return new ScalaSignature(new Bytes(new byte[] { 5, 0, 0 }, 0, 3)); + } + + private static final class Fixture extends ClassLoader { + + final MethodHandle full; + + Fixture() { + super(ScalaSignatureMethodSymbolFullTest.class.getClassLoader()); + try { + String base = "ReferenceScalaMethodSymbolFull"; + byte[] bytes; + try (InputStream stream = Objects.requireNonNull(getResourceAsStream("compat/" + base + ".class.b64")); + Scanner scanner = new Scanner(stream, StandardCharsets.UTF_8.name()).useDelimiter("\\A")) { + bytes = Base64.getMimeDecoder().decode(scanner.next()); + } + defineClass(null, bytes, 0, bytes.length); + Class type = loadClass("codechicken.multipart.compat." + base); + Object instance = type.getConstructor().newInstance(); + full = MethodHandles.publicLookup().unreflect(type.getMethod("full", MethodSymbol.class)) + .bindTo(instance); + } catch (ReflectiveOperationException | java.io.IOException e) { + throw new AssertionError(e); + } + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaMethodSymbolFull.class.b64 b/src/test/resources/compat/ReferenceScalaMethodSymbolFull.class.b64 new file mode 100644 index 0000000..3e49d4c --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaMethodSymbolFull.class.b64 @@ -0,0 +1,21 @@ +yv66vgAAADQAIgEAO2NvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFN +ZXRob2RTeW1ib2xGdWxsBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAJFJlZmVyZW5jZVNjYWxh +TWV0aG9kU3ltYm9sRnVsbC5zY2FsYQEAHkxzY2FsYS9yZWZsZWN0L1NjYWxhU2lnbmF0dXJlOwEA +BWJ5dGVzAQEyBgEpMkEhAQIBEwlxIisaNGZlFnQ3LVoqZEMyDFcqGjtpXxIcFjAcMnBZGitIDhwG +AwcRCWFhWThuYQYkKEJBAwcDJWlXDxw7amEGFEhPQwEIAy0Zdw5aM2RRJhw3Lho4BAFNEQFBAwkD +FzlpEQEEBgIbBSkxb1kxbUMmEXECBAIHAzpMKCsaNAkLRQFBEQEKAg1xSmcuGzs/KQUZAkMBCwEb +BREBIgIMAQkDORIBAjR2WTIkIgFHEBEFZWFiQkEGGxMJWUIiAQRRZRYkV01aBQM7eRFhYRU7c1M6 +PCdCQQ4NERUBUwMxASIDDRFYTRoJA0UhAiJhCRQOAxFSISEKAwIHBRxYLgMCKEkJcTFrWTFtQ05L +d01cMXVrSiwXQkEVJwUxaVUNHjVwSU5LWE5ZOG0BAQAEZnVsbAEASyhMY29kZWNoaWNrZW4vbXVs +dGlwYXJ0L2FzbS9TY2FsYVNpZ25hdHVyZSRNZXRob2RTeW1ib2w7KUxqYXZhL2xhbmcvU3RyaW5n +OwEANWNvZGVjaGlja2VuL211bHRpcGFydC9hc20vU2NhbGFTaWduYXR1cmUkTWV0aG9kU3ltYm9s +BwALAQAUKClMamF2YS9sYW5nL1N0cmluZzsMAAkADQoADAAOAQAEdGhpcwEAPUxjb2RlY2hpY2tl +bi9tdWx0aXBhcnQvY29tcGF0L1JlZmVyZW5jZVNjYWxhTWV0aG9kU3ltYm9sRnVsbDsBAANyZWYB +ADdMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L2FzbS9TY2FsYVNpZ25hdHVyZSRNZXRob2RTeW1ib2w7 +AQAGPGluaXQ+AQADKClWDAAUABUKAAQAFgEAKGNvZGVjaGlja2VuL211bHRpcGFydC9hc20vU2Nh +bGFTaWduYXR1cmUHABgBAAxNZXRob2RTeW1ib2wBAARDb2RlAQASTG9jYWxWYXJpYWJsZVRhYmxl +AQAPTGluZU51bWJlclRhYmxlAQAKU291cmNlRmlsZQEADElubmVyQ2xhc3NlcwEAGVJ1bnRpbWVW +aXNpYmxlQW5ub3RhdGlvbnMBAAhTY2FsYVNpZwAhAAIABAAAAAAAAgABAAkACgABABsAAAA5AAEA +AgAAAAUrtgAPsAAAAAIAHAAAABYAAgAAAAUAEAARAAAAAAAFABIAEwABAB0AAAAGAAEAAAAGAAEA +FAAVAAEAGwAAAC8AAQABAAAABSq3ABexAAAAAgAcAAAADAABAAAABQAQABEAAAAdAAAABgABAAAA +BwAEAB4AAAACAAUAHwAAAAoAAQAMABkAGgABACAAAAALAAEABgABAAdzAAgAIQAAAAMFAAA= \ No newline at end of file