| /* |
| * Copyright 2014-present Facebook, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may |
| * not use this file except in compliance with the License. You may obtain |
| * a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package com.facebook.buck.java.abi; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.facebook.buck.io.ProjectFilesystem; |
| import com.facebook.buck.zip.Unzip; |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.FluentIterable; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.ByteStreams; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TemporaryFolder; |
| import org.objectweb.asm.Opcodes; |
| import org.objectweb.asm.tree.AnnotationNode; |
| import org.objectweb.asm.tree.ClassNode; |
| import org.objectweb.asm.tree.FieldNode; |
| import org.objectweb.asm.tree.MethodNode; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.List; |
| import java.util.SortedSet; |
| import java.util.jar.JarOutputStream; |
| import java.util.zip.ZipEntry; |
| |
| import javax.tools.JavaCompiler; |
| import javax.tools.JavaFileObject; |
| import javax.tools.StandardJavaFileManager; |
| import javax.tools.ToolProvider; |
| |
| |
| public class MirrorTest { |
| |
| private static final ImmutableSortedSet<Path> EMPTY_CLASSPATH = ImmutableSortedSet.of(); |
| |
| @Rule |
| public TemporaryFolder temp = new TemporaryFolder(); |
| |
| private ProjectFilesystem filesystem; |
| private Path stubJar; |
| |
| @Before |
| public void createStubJar() throws IOException { |
| File out = temp.newFolder(); |
| filesystem = new ProjectFilesystem(out.toPath()); |
| stubJar = Paths.get("stub.jar"); |
| } |
| |
| @Test |
| public void emptyClass() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| "package com.example.buck; public class A {}"); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| // Verify that the stub jar works by compiling some code that depends on A. |
| compileToJar( |
| ImmutableSortedSet.of(stubJar), |
| "B.java", |
| "package com.example.buck; public class B extends A {}"); |
| } |
| |
| @Test |
| public void emptyClassWithAnnotation() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| "package com.example.buck; @Deprecated public class A {}"); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| // Examine the jar to see if the "A" class is deprecated. |
| ClassNode classNode = readClass(stubJar, "com/example/buck/A.class").getClassNode(); |
| assertNotEquals(0, classNode.access & Opcodes.ACC_DEPRECATED); |
| } |
| |
| @Test |
| public void classWithTwoMethods() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join(ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " public String toString() { return null; }", |
| " public void eatCake() {}", |
| "}"))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| // Verify that both methods are present and given in alphabetical order. |
| ClassNode classNode = readClass(stubJar, "com/example/buck/A.class").getClassNode(); |
| List<MethodNode> methods = classNode.methods; |
| // Index 0 is the <init> method. Skip that. |
| assertEquals("eatCake", methods.get(1).name); |
| assertEquals("toString", methods.get(2).name); |
| } |
| |
| @Test |
| public void genericClassSignaturesShouldBePreserved() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A<T> {", |
| " public T get(String key) { return null; }", |
| "}" |
| ))); |
| |
| // With generic classes, there are typically two interesting things we want to keep an eye on. |
| // First is the "descriptor", which is the signature of the method with type erasure complete. |
| // Optionally, compilers (and the OpenJDK, Oracle and Eclipse compilers all do this) can also |
| // include a "signature", which is the signature of the method before type erasure. See |
| // http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 for more. |
| AbiClass original = readClass(jar, "com/example/buck/A.class"); |
| String classSig = original.getClassNode().signature; |
| MethodNode originalGet = original.findMethod("get"); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| assertEquals(classSig, stubbed.getClassNode().signature); |
| |
| MethodNode stubbedGet = stubbed.findMethod("get"); |
| assertMethodEquals(originalGet, stubbedGet); |
| } |
| |
| @Test |
| public void shouldIgnorePrivateMethods() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " private void privateMethod() {}", |
| " void packageMethod() {}", |
| " protected void protectedMethod() {}", |
| " public void publicMethod() {}", |
| "}" |
| ))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| for (MethodNode method : stubbed.getClassNode().methods) { |
| assertFalse(method.name.contains("private")); |
| } |
| } |
| |
| @Test |
| public void shouldPreserveAField() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " protected String protectedField;", |
| "}" |
| ))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| FieldNode field = stubbed.findField("protectedField"); |
| assertEquals("protectedField", field.name); |
| assertTrue((field.access & Opcodes.ACC_PROTECTED) > 0); |
| } |
| |
| @Test |
| public void shouldIgnorePrivateFields() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " private String privateField;", |
| "}" |
| ))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| assertEquals(0, stubbed.getClassNode().fields.size()); |
| } |
| |
| @Test |
| public void shouldPreserveGenericTypesOnFields() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A<T> {", |
| " public T theField;", |
| "}"))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass original = readClass(jar, "com/example/buck/A.class"); |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| |
| FieldNode originalField = original.findField("theField"); |
| FieldNode stubbedField = stubbed.findField("theField"); |
| |
| assertFieldEquals(originalField, stubbedField); |
| } |
| |
| @Test |
| public void shouldPreserveGenericTypesOnMethods() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A<T> {", |
| " public T get(String key) { return null; }", |
| " public <X extends Comparable<T>> X compareWith(T other) { return null; }", |
| "}"))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass original = readClass(jar, "com/example/buck/A.class"); |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| |
| MethodNode originalGet = original.findMethod("get"); |
| MethodNode stubbedGet = stubbed.findMethod("get"); |
| |
| assertEquals(originalGet.signature, stubbedGet.signature); |
| assertEquals(originalGet.desc, stubbedGet.desc); |
| |
| MethodNode originalCompare = original.findMethod("compareWith"); |
| MethodNode stubbedCompare = stubbed.findMethod("compareWith"); |
| |
| assertEquals(originalCompare.signature, stubbedCompare.signature); |
| assertEquals(originalCompare.desc, stubbedCompare.desc); |
| } |
| |
| @Test |
| public void preservesAnnotationsOnMethods() throws IOException { |
| Path annotations = buildAnnotationJar(); |
| Path jar = compileToJar( |
| ImmutableSortedSet.of(annotations), |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " @Foo", |
| " public void cheese(String key) {}", |
| "}"))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| MethodNode method = stubbed.findMethod("cheese"); |
| |
| List<AnnotationNode> seen = method.visibleAnnotations; |
| assertEquals(1, seen.size()); |
| assertEquals("Lcom/example/buck/Foo;", seen.get(0).desc); |
| } |
| |
| @Test |
| public void preservesAnnotationsOnFields() throws IOException { |
| Path annotations = buildAnnotationJar(); |
| Path jar = compileToJar( |
| ImmutableSortedSet.of(annotations), |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " @Foo", |
| " public String name;", |
| "}"))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| FieldNode field = stubbed.findField("name"); |
| |
| List<AnnotationNode> seen = field.visibleAnnotations; |
| assertEquals(1, seen.size()); |
| assertEquals("Lcom/example/buck/Foo;", seen.get(0).desc); |
| } |
| |
| @Test |
| public void preservesAnnotationsOnParameters() throws IOException { |
| Path annotations = buildAnnotationJar(); |
| Path jar = compileToJar( |
| ImmutableSortedSet.of(annotations), |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " public void peynir(@Foo String very, int tasty) {}", |
| "}"))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| MethodNode method = stubbed.findMethod("peynir"); |
| |
| List<AnnotationNode>[] parameterAnnotations = method.visibleParameterAnnotations; |
| assertEquals(2, parameterAnnotations.length); |
| } |
| |
| @Test |
| public void stubsInnerClasses() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " public class B {", |
| " public int count;", |
| " public void foo() {}", |
| " }", |
| "}" |
| ))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| |
| AbiClass original = readClass(jar, "com/example/buck/A$B.class"); |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A$B.class"); |
| |
| MethodNode originalFoo = original.findMethod("foo"); |
| MethodNode stubbedFoo = stubbed.findMethod("foo"); |
| assertMethodEquals(originalFoo, stubbedFoo); |
| |
| FieldNode originalCount = original.findField("count"); |
| FieldNode stubbedCount = stubbed.findField("count"); |
| assertFieldEquals(originalCount, stubbedCount); |
| } |
| |
| @Test |
| public void abiSafeChangesResultInTheSameOutputJar() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " protected final static int count = 42;", |
| " public String getGreeting() { return \"hello\"; }", |
| " Class<?> clazz;", |
| " public int other;", |
| "}" |
| ))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| String originalHash = filesystem.computeSha1(stubJar); |
| |
| Path jar2 = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " Class<?> clazz = String.class;", |
| " public String getGreeting() { return \"merhaba\"; }", |
| " protected final static int count = 42;", |
| " public int other = 32;", |
| "}" |
| ))); |
| filesystem.deleteFileAtPath(stubJar); |
| new StubJar(jar2).writeTo(filesystem, stubJar); |
| String secondHash = filesystem.computeSha1(stubJar); |
| |
| assertEquals(originalHash, secondHash); |
| } |
| |
| @Test |
| public void shouldIncludeStaticFields() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " public static String foo;", |
| " public final static int count = 42;", |
| " protected static void method() {}", |
| "}"))); |
| |
| new StubJar(jar).writeTo(filesystem, stubJar); |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| |
| stubbed.findMethod("method"); // Presence is enough |
| stubbed.findField("foo"); // Presence is enough |
| FieldNode count = stubbed.findField("count"); |
| assertEquals(42, count.value); |
| } |
| |
| @Test |
| public void innerClassesInStubsCanBeCompiledAgainst() throws IOException { |
| Path original = compileToJar( |
| EMPTY_CLASSPATH, |
| "Outer.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class Outer {", |
| " public class Inner {", |
| " public String getGreeting() { return \"hola\"; }", |
| " }", |
| "}"))); |
| |
| new StubJar(original).writeTo(filesystem, stubJar); |
| |
| compileToJar( |
| ImmutableSortedSet.of(stubJar), |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck2;", // Note: different package |
| "import com.example.buck.Outer;", // Inner class becomes available |
| "public class A {", |
| " private Outer.Inner field;", // Reference the inner class |
| "}"))); |
| } |
| |
| @Test |
| public void shouldPreserveSynchronizedKeywordOnMethods() throws IOException { |
| Path original = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " public synchronized void doMagic() {}", |
| "}"))); |
| |
| new StubJar(original).writeTo(filesystem, stubJar); |
| |
| AbiClass stub = readClass(stubJar, "com/example/buck/A.class"); |
| MethodNode magic = stub.findMethod("doMagic"); |
| assertTrue((magic.access & Opcodes.ACC_SYNCHRONIZED) > 0); |
| } |
| |
| @Test |
| public void shouldKeepMultipleFieldsWithSameDescValue() throws IOException { |
| Path original = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " public static final A SEVERE = new A();", |
| " public static final A NOT_SEVERE = new A();", |
| " public static final A QUITE_MILD = new A();", |
| "}"))); |
| |
| new StubJar(original).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| stubbed.findField("SEVERE"); |
| stubbed.findField("NOT_SEVERE"); |
| stubbed.findField("QUITE_MILD"); |
| } |
| |
| @Test |
| public void stubJarIsEquallyAtHomeWalkingADirectoryOfClassFiles() throws IOException { |
| Path jar = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on("\n").join( |
| ImmutableList.of( |
| "package com.example.buck;", |
| "public class A {", |
| " public String toString() { return null; }", |
| " public void eatCake() {}", |
| "}"))); |
| |
| Path classDir = temp.newFolder().toPath(); |
| Unzip.extractZipFile(jar, classDir, true); |
| |
| new StubJar(classDir).writeTo(filesystem, stubJar); |
| |
| // Verify that both methods are present and given in alphabetical order. |
| AbiClass classNode = readClass(stubJar, "com/example/buck/A.class"); |
| List<MethodNode> methods = classNode.getClassNode().methods; |
| // Index 0 is the <init> method. Skip that. |
| assertEquals("eatCake", methods.get(1).name); |
| assertEquals("toString", methods.get(2).name); |
| } |
| |
| @Test |
| public void shouldIncludeBridgeMethods() throws IOException { |
| Path original = compileToJar( |
| EMPTY_CLASSPATH, |
| "A.java", |
| Joiner.on('\n').join(ImmutableList.of( |
| "package com.example.buck;", |
| "public class A implements Comparable<A> {", |
| " public int compareTo(A other) {", |
| " return 0;", |
| " }", |
| "}"))); |
| |
| new StubJar(original).writeTo(filesystem, stubJar); |
| |
| AbiClass stubbed = readClass(stubJar, "com/example/buck/A.class"); |
| int count = 0; |
| for (MethodNode method : stubbed.getClassNode().methods) { |
| if ("compareTo".equals(method.name)) { |
| count++; |
| } |
| } |
| // One for the generics method, one for the bridge method from Comparable |
| assertEquals(2, count); |
| } |
| |
| private Path compileToJar( |
| SortedSet<Path> classpath, |
| String fileName, |
| String source) throws IOException { |
| File inputs = temp.newFolder(); |
| |
| File file = new File(inputs, fileName); |
| |
| Files.write(file.toPath(), source.getBytes(StandardCharsets.UTF_8)); |
| |
| JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); |
| StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); |
| Iterable<? extends JavaFileObject> sourceObjects = |
| fileManager.getJavaFileObjectsFromFiles(ImmutableSet.of(file)); |
| |
| final File outputDir = temp.newFolder(); |
| List<String> args = Lists.newArrayList("-g", "-d", outputDir.getAbsolutePath()); |
| |
| if (!classpath.isEmpty()) { |
| args.add("-classpath"); |
| args.add(Joiner.on(File.pathSeparator).join(FluentIterable.from(classpath) |
| .transform(filesystem.getAbsolutifier()))); |
| } |
| |
| JavaCompiler.CompilationTask compilation = |
| compiler.getTask(null, fileManager, null, args, null, sourceObjects); |
| |
| Boolean result = compilation.call(); |
| |
| fileManager.close(); |
| assertNotNull(result); |
| assertTrue(result); |
| |
| File jar = new File(outputDir, "output.jar"); |
| |
| try ( |
| FileOutputStream fos = new FileOutputStream(jar); |
| final JarOutputStream os = new JarOutputStream(fos)) { |
| SimpleFileVisitor<Path> visitor = new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { |
| if (file.getFileName().toString().endsWith(".class")) { |
| ZipEntry entry = new ZipEntry(outputDir.toPath().relativize(file).toString()); |
| os.putNextEntry(entry); |
| ByteStreams.copy(Files.newInputStream(file), os); |
| os.closeEntry(); |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| }; |
| |
| Files.walkFileTree(outputDir.toPath(), visitor); |
| } |
| |
| return jar.toPath().toAbsolutePath(); |
| } |
| |
| private AbiClass readClass(Path pathToJar, String className) throws IOException { |
| return AbiClass.extract(filesystem.getPathForRelativePath(pathToJar), className); |
| } |
| |
| private Path buildAnnotationJar() throws IOException { |
| return compileToJar( |
| EMPTY_CLASSPATH, |
| "Foo.java", |
| Joiner.on("\n").join(ImmutableList.of( |
| "package com.example.buck;", |
| "import java.lang.annotation.*;", |
| "import static java.lang.annotation.ElementType.*;", |
| "@Retention(RetentionPolicy.RUNTIME)", |
| "@Target(value={CONSTRUCTOR, FIELD, METHOD, PARAMETER, TYPE})", |
| "public @interface Foo {}" |
| ))); |
| } |
| |
| private void assertMethodEquals(MethodNode expected, MethodNode seen) { |
| assertEquals(expected.access, seen.access); |
| assertEquals(expected.desc, seen.desc); |
| assertEquals(expected.signature, seen.signature); |
| } |
| |
| private void assertFieldEquals(FieldNode expected, FieldNode seen) { |
| assertEquals(expected.name, seen.name); |
| assertEquals(expected.desc, seen.desc); |
| assertEquals(expected.signature, seen.signature); |
| } |
| } |