blob: 3ed07f0bcae45642781948deddd724c3e5535969 [file] [log] [blame]
/*
* Copyright 2013-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 com.google.common.base.Charsets.UTF_8;
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.testutil.integration.TestDataHelper;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.SortedSet;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class AbiWriterTest {
@Rule public TemporaryFolder temp = new TemporaryFolder();
@Test
public void abiKeyForEmptySourcesIsStable() {
assertEquals(
"The ABI key used when a java_library() has no srcs should be constant across platforms.",
AbiWriterProtocol.EMPTY_ABI_KEY,
AbiWriter.computeAbiKey(ImmutableSortedSet.<String>of()));
}
@Test
public void willCaptureClassName() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {}"));
assertTrue(summary, summary.contains("public class com.facebook.buck.example.A"));
}
@Test
public void willCaptureTypeParametersOnClass() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A<T> {}"));
assertTrue(summary, summary.contains("com.facebook.buck.example.A<T>"));
}
@Test
public void classesDefaultToExtendingJavaLangObject() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {}"));
assertTrue(summary, summary.contains("extends java.lang.Object"));
}
@Test
public void classesCanImplementManyInterfaces() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"import java.io.Serializable;",
"public class A implements Cloneable, Serializable {}"));
assertTrue("Ordering of interfaces is alphabetical based on fully qualified name" + summary,
summary.contains("implements java.io.Serializable, java.lang.Cloneable"));
}
@Test
public void classesCanImplementInterfacesThatHaveAGenericType() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A implements Comparable<A> {",
" public int compareTo(A o) {",
" return 1;",
" }",
"}"));
assertTrue(summary,
summary.contains("implements java.lang.Comparable<com.facebook.buck.example.A>"));
}
@Test
public void runtimeAnnotationsArePreservedAtTheClassLevel() throws IOException{
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"@Deprecated",
"public class A {}"));
assertTrue(summary, summary.contains("@java.lang.Deprecated"));
}
@Test
public void classesCanHaveMethods() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public int doSomething(String foo) {",
" return 0;",
" }",
"}"));
assertTrue(summary, summary.contains("public int doSomething(java.lang.String)"));
// Make sure we've not output the method body.
assertFalse(summary, summary.contains("return"));
}
@Test
public void genericTypesOnClassMethodsAreCorrectlyReported() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A<Y> {",
" public <X> X doSomething(Y foo, A bar, String baz) {",
" return null;",
" }",
"}"));
assertTrue(summary, summary.contains(
"<X> X doSomething(Y, com.facebook.buck.example.A, java.lang.String)"));
}
@Test
public void retainedAnnotationsOnClassMethodsAreKept() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" @Deprecated public void foo() {}",
"}"));
assertTrue(summary, summary.contains("@java.lang.Deprecated"));
}
@Test
public void interfacesDoNotExtendAnything() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"interface A {}"));
assertTrue(summary, summary.contains("interface com.facebook.buck.example.A"));
}
@Test
public void interfacesCanExtendOtherInterfaces() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"interface A extends java.io.Serializable {}"));
assertTrue(summary, summary.contains("example.A extends java.io.Serializable"));
}
@Test
public void interfacesRetainClassLevelAnnotations() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"@Deprecated public interface A {}"));
assertTrue(summary, summary.contains("@java.lang.Deprecated"));
}
@Test
public void interfacesCanBeTyped() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public interface A<T> {}"));
assertEquals("public abstract interface com.facebook.buck.example.A<T>\n", summary);
}
@Test
public void interfaceMethodsAreReported() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public interface A {",
" public int doSomething(String foo);",
" void sitQuietly();",
"}"));
assertTrue(summary, summary.contains("public abstract int doSomething(java.lang.String)"));
assertTrue(summary, summary.contains("public abstract void sitQuietly()"));
}
@Test
public void interfaceMethodsWithTypeSignaturesAreCorrectlyKept() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public interface A<Y> {",
" public <X> X doSomething(Y foo, A bar, String baz);",
"}"));
assertTrue(summary, summary.contains(
"<X> X doSomething(Y, com.facebook.buck.example.A, java.lang.String)"));
}
@Test
public void shouldNotIncludePrivateMethods() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" private void doSomething() {}",
"}"));
assertFalse(summary, summary.contains("doSomething"));
}
// Fields
@Test
public void visibleFieldsAreIncluded() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public int number = 42;",
" private String magic = \"Now you see me\";",
"}"));
assertTrue(summary, summary.contains("public int number"));
assertFalse(summary, summary.contains("42"));
assertFalse(summary, summary.contains("magic"));
}
@Test
public void runTimeAnnotationsOnFieldsAreRetained() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" @Deprecated",
" public int number;",
"}"));
assertTrue(summary, summary.contains("@java.lang.Deprecated"));
}
@Test
public void genericTypesOnFieldsAreKept() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"import java.util.Set;",
"public class A {",
" public Set<String> strings;",
"}"));
assertTrue(summary, summary.contains("Set<java.lang.String> strings"));
}
@Test
public void shouldMaintainWildCardGenerics() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"import java.util.Set;",
"public class A {",
" public Set<?> question;",
"}"));
assertTrue(summary, summary.contains("Set<?> question"));
}
@Test
public void shouldComplexGenerics() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"import java.util.Collection;",
"import java.util.Set;",
"public class A<T> {",
" public Set<? extends Collection> extension;",
" public Set<? super T> supered;",
"}"));
assertTrue(summary, summary.contains("Set<? extends java.util.Collection> extension"));
assertTrue(summary, summary.contains("Set<? super T> supered"));
}
@Test
public void nestedGenericsAreOkay() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"import java.util.Collection;",
"import java.util.Set;",
"public class A<T> {",
" public Set<Collection<String>> nested;",
"}"));
assertTrue(summary,
summary.contains("java.util.Set<java.util.Collection<java.lang.String>> nested"));
}
// Constants
@Test
public void compileTimeConstantExpressionsAreRetainedOnFields() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public final static int NUMBER = 42;",
" final static String MAGIC = \"Now you see me\" + 3;",
" public static int foo = 37;",
" public static final long NOW = System.currentTimeMillis();",
"}"));
assertTrue(summary, summary.contains("public static final int NUMBER = 42"));
assertTrue(summary, summary.contains("static final java.lang.String MAGIC = Now you see me3"));
assertFalse(summary, summary.contains("public static int foo = 37"));
assertTrue(summary, summary.contains("public static final long NOW\n"));
}
@Test
public void checkThatDefinedInOtherClassesThatCanBeInlinedAreInlined() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public final static int NUMBER = 42;",
" final static String MAGIC = \"Now you see me\" + 3;",
" public static int foo = 37;",
" public static final long NOW = System.currentTimeMillis();",
"}"),
"B.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class B {",
" public final static int KEY = A.NUMBER + 3;",
"}"
));
assertTrue(summary, summary.contains("KEY = 45"));
}
// Enums
@Test
public void enumsAreHandled() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public enum A {",
" ONE, TWO, THREE;",
" public void doSomething() {} ",
"}"));
assertTrue(summary, summary.contains("ONE"));
assertTrue(summary, summary.contains("TWO"));
assertTrue(summary, summary.contains("THREE"));
assertTrue(summary, summary.contains("public void doSomething()"));
}
@Test
public void orderingOfEnumConstantsMatters() throws IOException {
String first = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public enum A {",
" ONE, TWO;",
" public void doSomething() {} ",
"}"));
String second = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public enum A {",
" TWO, ONE;",
" public void doSomething() {} ",
"}"));
assertNotEquals(first, second);
}
@Test
public void canHandleNewAnnotationTypes() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"import java.lang.annotation.*;",
"import static java.lang.annotation.ElementType.*;",
"@Retention(RetentionPolicy.RUNTIME)",
"@Target(value={CONSTRUCTOR, FIELD})",
"public @interface A {}"));
assertTrue(summary, summary.contains("@interface com.facebook.buck.example.A"));
assertTrue(summary, summary.contains(
"@java.lang.annotation.Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)"));
assertTrue(summary, summary.contains(
"@java.lang.annotation.Target(value={java.lang.annotation.ElementType.CONSTRUCTOR,"));
}
@Test
public void innerClassesCanBeNested() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public static class B {",
" public void foo() {}",
" }",
"}"));
assertTrue(summary, summary.contains(
"public static class com.facebook.buck.example.A.B extends java.lang.Object"));
}
@Test
public void innerClassesCanBeDeeplyNested() throws IOException {
String summary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public static class B {",
" public class C {",
" public void foo() {}",
" }",
" }",
"}"));
assertTrue(summary,
summary.contains("public class com.facebook.buck.example.A.B.C extends java.lang.Object"));
}
@Test
public void abisOfAClassAndAInterfaceWithSharedMethodsAreNotTheSame() throws IOException {
String classSummary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public abstract class A {",
" public abstract void doSomething();",
"}"));
String interfaceSummary = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public interface A {",
" void doSomething();",
"}"));
assertNotEquals(classSummary, interfaceSummary);
}
@Test
public void reorderingFieldsDoesNotModifyTheAbi() throws IOException {
String original = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public int first;",
" public int second;",
"}"));
String abiCompatible = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public int second;",
" public int first;",
"}"));
assertEquals(original, abiCompatible);
}
@Test
public void reorderingMethodsDoesNotModifyTheAbi() throws IOException {
String original = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public void doSomething() {}",
" public int aLongRunningComputation() { return 42; }",
"}"));
String abiCompatible = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public int aLongRunningComputation() { return 2345; }",
" public void doSomething() {}",
"}"));
assertEquals(original, abiCompatible);
}
@Test
public void renamingMethodArgumentsDoesNotModifyTheAbi() throws IOException {
String original = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public void doSomething(int count) {}",
"}"));
String abiCompatible = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public void doSomething(int numberOfTimes) {}",
"}"));
assertEquals(original, abiCompatible);
}
@Test
public void varargsAreNotTheSameAsArrays() throws IOException {
String original = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public void doSomething(String... args) {}",
"}"));
String amended = compile("A.java", Joiner.on("\n").join(
"package com.facebook.buck.example;",
"public class A {",
" public void doSomething(String[] args) {}",
"}"));
assertNotEquals(original, amended);
}
@Test
public void generateSampleOutput() throws IOException {
File testDataDir = TestDataHelper.getTestDataScenario(this, "compute_abi");
String source = Files.toString(
new File(testDataDir, "generateSampleOutput.src"), UTF_8);
String original = compile("A.java", source);
String expected = Files.toString(
new File(testDataDir, "generateSampleOutput.expected"), UTF_8);
assertEquals(expected, original);
}
@Test
public void generatedHashMustBeZeroPaddedToFortyCharacters() {
String hex = AbiWriter.computeAbiKey(ImmutableSortedSet.of("abcdefghi"));
assertEquals('0', hex.charAt(0));
assertEquals(40, hex.length());
}
@Test
public void shouldIgnoreAnnotatedPackageDeclarations() throws IOException {
Optional<String> compiled = getOptionalSummary(
"package-info.java",
Joiner.on("\n").join(
"/** This is a documented pacakge */",
"@Deprecated", // It's legit to deprecate an entire package
"package com.facebook.buck.example;"));
// If we get this far, then we know that everything is just fine. We're just preventing an
// exception being thrown, and it's impossible to accidentally have a java class called
// "package-info" so we're not expecting this to contribute to the API.
assertFalse(compiled.isPresent());
}
@Test
public void shouldIgnoreUnannotatedPackageDeclarations() throws IOException {
Optional<String> compiled = getOptionalSummary(
"package-info.java",
Joiner.on("\n").join(
"/** This is a documented pacakge without an annotation */",
"package com.facebook.buck.example;"));
assertFalse(compiled.isPresent());
}
@Test
public void emptySummariesLeadToAnEmptyAbiKeyBeingMade() throws IOException {
File outDir = temp.newFolder();
File onlyDocs = new File(outDir, "package-info.java");
SortedSet<String> summaries = generateSummary(
outDir,
new FileAndSource(onlyDocs,
Joiner.on("\n").join(
"/** This is a package */",
"package com.facebook.buck.example;")),
ImmutableSet.<File>of());
assertTrue(summaries.isEmpty());
String computed = AbiWriter.computeAbiKey(summaries);
assertEquals(AbiWriterProtocol.EMPTY_ABI_KEY, computed);
}
@Test
public void ensureHashMatchesGuavaEquivalent() {
String key = AbiWriter.computeAbiKey(ImmutableSortedSet.<String>of());
String guava = hashWithGuava(ImmutableSortedSet.<String>of());
assertEquals(guava, key);
String javaCode = Joiner.on('\n').join(
"public class Example",
"public void cheese(java.lang.String)");
SortedSet<String> summaries = ImmutableSortedSet.of(javaCode);
key = AbiWriter.computeAbiKey(summaries);
guava = hashWithGuava(summaries);
assertEquals(guava, key);
}
private String hashWithGuava(SortedSet<String> summaries) {
Hasher hasher = Hashing.sha1().newHasher();
for (String summary : summaries) {
hasher.putUnencodedChars(summary);
}
return hasher.hash().toString();
}
/**
* Files are compiled in the order that they're fed to this method. As each file is compiled its
* output directory is added to a classpath that lives for the duration of the method call. As
* each file is compiled, the summaries generated by AbiWriter for the last compilation are
* stored.
*
* @return the summary of the last file to be compiled.
*/
private String compile(
String fileName, String source, String... fileNamesAndSources) throws IOException {
return getOptionalSummary(fileName, source, fileNamesAndSources).get();
}
private Optional<String> getOptionalSummary(
String fileName, String source, String... fileNamesAndSources) throws IOException {
List<FileAndSource> targets = Lists.newArrayList();
targets.add(new FileAndSource(new File(temp.newFolder(), fileName), source));
for (int i = 0; i < fileNamesAndSources.length; i++) {
targets.add(new FileAndSource(
new File(temp.newFolder(), fileNamesAndSources[i++]), fileNamesAndSources[i]));
}
ImmutableSet.Builder<File> classpath = ImmutableSet.builder();
SortedSet<String> lastSummary = null;
for (FileAndSource target : targets) {
File outputDir = temp.newFolder();
lastSummary = generateSummary(outputDir, target, classpath.build());
classpath.add(outputDir);
}
if (lastSummary.isEmpty()) {
return Optional.absent();
}
return Optional.of(Iterables.getOnlyElement(lastSummary));
}
private SortedSet<String> generateSummary(
File outputDir,
FileAndSource target,
ImmutableSet<File> classpath) throws IOException{
File file = target.file;
Files.write(target.source, file, UTF_8);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> sourceObjects =
fileManager.getJavaFileObjectsFromFiles(ImmutableSet.of(file));
List<String> args = Lists.newArrayList("-g", "-d", outputDir.getAbsolutePath());
if (!classpath.isEmpty()) {
args.add("-classpath");
args.add(Joiner.on(File.pathSeparator).join(classpath));
}
AbiWriter processor = new AbiWriter();
JavaCompiler.CompilationTask compilation =
compiler.getTask(null, fileManager, null, args, null, sourceObjects);
compilation.setProcessors(ImmutableSet.of(processor));
Boolean result = compilation.call();
assertNotNull(result);
assertTrue(result);
return processor.getSummaries();
}
private static class FileAndSource {
public File file;
public String source;
public FileAndSource(File file, String source) {
this.file = file;
this.source = source;
}
}
}