blob: 68e87b9846dfb1e5ea0c137eb4fc7bb113dad082 [file] [log] [blame]
/*
* Copyright (C) 2012 The Guava Authors
*
* 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.google.auto.value.processor;
import com.google.common.base.Joiner;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import com.google.common.io.Files;
import junit.framework.TestCase;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.List;
import java.util.regex.Pattern;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
/**
* @author emcmanus@google.com (Éamonn McManus)
*/
public class CompilationErrorsTest extends TestCase {
// TODO(emcmanus): add tests for:
// - superclass in a different package with nonpublic abstract methods (this must fail but
// is it clean?)
private JavaCompiler javac;
private DiagnosticCollector<JavaFileObject> diagnosticCollector;
private StandardJavaFileManager fileManager;
private File tmpDir;
@Override
protected void setUp() {
javac = ToolProvider.getSystemJavaCompiler();
diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
fileManager = javac.getStandardFileManager(diagnosticCollector, null, null);
tmpDir = Files.createTempDir();
}
@Override
protected void tearDown() {
boolean deletedAll = deleteDirectory(tmpDir);
assertTrue(deletedAll);
}
// Files.deleteRecursively has been deprecated because Dr Evil could put a symlink in the
// temporary directory while this test is running and make you delete a bunch of unrelated stuff.
// That's surely not much of a problem here, but just in case, we check that anything we're going
// to delete is either a directory or ends with .java or .class.
// TODO(emcmanus): simplify now that we are only using this to test compilation failure.
// It should be straightforward to know exactly what files will be generated.
private boolean deleteDirectory(File dir) {
File[] files = dir.listFiles();
boolean deletedAll = true;
for (File file : files) {
if (file.isDirectory()) {
deletedAll &= deleteDirectory(file);
} else if (file.getName().endsWith(".java") || file.getName().endsWith(".class")) {
deletedAll &= file.delete();
} else {
fail("Not deleting unexpected file " + file);
}
}
return dir.delete() && deletedAll;
}
// Ensure that assertCompilationFails does in fact throw AssertionError when compilation succeeds.
public void testAssertCompilationFails() throws Exception {
String testSourceCode = Joiner.on('\n').join(
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue",
"public abstract class Baz {",
" public abstract int integer();",
" public static Baz create(int integer) {",
" return new AutoValue_Baz(integer);",
" }\n",
"}\n");
boolean compiled = false;
try {
assertCompilationFails(ImmutableList.of(testSourceCode));
compiled = true;
} catch (AssertionError expected) {
}
assertFalse(compiled);
}
public void testNoWarningsFromGenerics() throws Exception {
String testSourceCode = Joiner.on('\n').join(
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue",
"public abstract class Baz<T extends Number, U extends T> {",
" public abstract T t();",
" public abstract U u();",
" public static <T extends Number, U extends T> Baz<T, U> create(T t, U u) {",
" return new AutoValue_Baz<T, U>(t, u);",
" }",
"}");
assertCompilationSucceedsWithoutWarning(ImmutableList.of(testSourceCode));
}
private static final Pattern CANNOT_HAVE_NON_PROPERTIES = Pattern.compile(
"@AutoValue classes cannot have abstract methods other than property getters");
public void testAbstractVoid() throws Exception {
String testSourceCode = Joiner.on('\n').join(
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue",
"public abstract class Baz {",
" public abstract void foo();",
"}");
ImmutableTable<Diagnostic.Kind, Integer, Pattern> expectedDiagnostics =
new ImmutableTable.Builder<Diagnostic.Kind, Integer, Pattern>()
.put(Diagnostic.Kind.WARNING, 5, CANNOT_HAVE_NON_PROPERTIES)
.put(Diagnostic.Kind.ERROR, 0, Pattern.compile("AutoValue_Baz"))
.build();
assertCompilationResultIs(expectedDiagnostics, ImmutableList.of(testSourceCode));
}
public void testAbstractWithParams() throws Exception {
String testSourceCode = Joiner.on('\n').join(
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue",
"public abstract class Baz {",
" public abstract int foo(int bar);",
"}");
ImmutableTable<Diagnostic.Kind, Integer, Pattern> expectedDiagnostics =
new ImmutableTable.Builder<Diagnostic.Kind, Integer, Pattern>()
.put(Diagnostic.Kind.WARNING, 5, CANNOT_HAVE_NON_PROPERTIES)
.put(Diagnostic.Kind.ERROR, 0, Pattern.compile("AutoValue_Baz"))
.build();
assertCompilationResultIs(expectedDiagnostics, ImmutableList.of(testSourceCode));
}
public void testPrimitiveArrayWarning() throws Exception {
String testSourceCode = Joiner.on('\n').join(
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue",
"public abstract class Baz {",
" public abstract byte[] bytes();",
" public static Baz create(byte[] bytes) {",
" return new AutoValue_Baz(bytes);",
" }",
"}");
Pattern warningPattern = Pattern.compile(
"An @AutoValue property that is a primitive array returns the original array");
ImmutableTable<Diagnostic.Kind, Integer, Pattern> expectedDiagnostics = ImmutableTable.of(
Diagnostic.Kind.WARNING, 5, warningPattern);
assertCompilationResultIs(expectedDiagnostics, ImmutableList.of(testSourceCode));
}
public void testPrimitiveArrayWarningFromParent() throws Exception {
// If the array-valued property is defined by an ancestor then we shouldn't try to attach
// the warning to the method that defined it, but rather to the @AutoValue class itself.
String testSourceCode = Joiner.on('\n').join(
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"public abstract class Baz {",
" public abstract byte[] bytes();",
"",
" @AutoValue",
" public abstract static class BazChild extends Baz {",
" public static BazChild create(byte[] bytes) {",
" return new AutoValue_Baz_BazChild(bytes);",
" }",
" }",
"}");
Pattern warningPattern = Pattern.compile(
"An @AutoValue property that is a primitive array returns the original array"
+ ".*foo\\.bar\\.Baz\\.bytes");
ImmutableTable<Diagnostic.Kind, Integer, Pattern> expectedDiagnostics = ImmutableTable.of(
Diagnostic.Kind.WARNING, 7, warningPattern);
assertCompilationResultIs(expectedDiagnostics, ImmutableList.of(testSourceCode));
}
public void testPrimitiveArrayWarningSuppressed() throws Exception {
String testSourceCode = Joiner.on('\n').join(
"package foo.bar;",
"import com.google.auto.value.AutoValue;",
"@AutoValue",
"public abstract class Baz {",
" @SuppressWarnings(\"mutable\")",
" public abstract byte[] bytes();",
" public static Baz create(byte[] bytes) {",
" return new AutoValue_Baz(bytes);",
" }",
"}");
assertCompilationSucceedsWithoutWarning(ImmutableList.of(testSourceCode));
}
// We compile the test classes by writing the source out to our temporary directory and invoking
// the compiler on them. An earlier version of this test used an in-memory JavaFileManager, but
// that is probably overkill, and in any case led to a problem that I gave up trying to fix,
// where a bunch of classes were somehow failing to load, such as junit.framework.TestFailure
// and the local classes that are defined in the various test methods. The TestFailure class in
// particular worked fine if I instantiated it before running any test code, but something in the
// invocation of javac.getTask with the MemoryFileManager broke things. I don't know how to
// explain what I saw other than as a bug in the JDK and the simplest fix was just to use
// the standard JavaFileManager.
private void assertCompilationFails(List<String> testSourceCode) throws IOException {
assertCompilationResultIs(ImmutableTable.of(Diagnostic.Kind.ERROR, 0, Pattern.compile("")),
testSourceCode);
}
private void assertCompilationSucceedsWithoutWarning(List<String> testSourceCode)
throws IOException {
assertCompilationResultIs(ImmutableTable.<Diagnostic.Kind, Integer, Pattern>of(),
testSourceCode);
}
/**
* Assert that the result of compiling the source file whose lines are {@code testSourceCode}
* corresponds to the diagnostics in {@code expectedDiagnostics}. Each row of
* {@expectedDiagnostics} specifies a diagnostic kind (such as warning or error), a line number
* on which the diagnostic is expected, and a Pattern that is expected to match the diagnostic
* text. If the line number is 0 it is not checked.
*/
private void assertCompilationResultIs(
Table<Diagnostic.Kind, Integer, Pattern> expectedDiagnostics,
List<String> testSourceCode) throws IOException {
assertFalse(testSourceCode.isEmpty());
StringWriter compilerOut = new StringWriter();
List<String> options = ImmutableList.of(
"-sourcepath", tmpDir.getPath(),
"-d", tmpDir.getPath(),
"-processor", AutoValueProcessor.class.getName(),
"-Xlint");
javac.getTask(compilerOut, fileManager, diagnosticCollector, options, null, null);
// This doesn't compile anything but communicates the paths to the JavaFileManager.
// Convert the strings containing the source code of the test classes into files that we
// can feed to the compiler.
List<String> classNames = Lists.newArrayList();
List<JavaFileObject> sourceFiles = Lists.newArrayList();
for (String source : testSourceCode) {
ClassName className = ClassName.extractFromSource(source);
File dir = new File(tmpDir, className.sourceDirectoryName());
dir.mkdirs();
assertTrue(dir.isDirectory()); // True if we just made it, or it was already there.
String sourceName = className.simpleName + ".java";
Files.write(source, new File(dir, sourceName), Charset.forName("UTF-8"));
classNames.add(className.fullName());
JavaFileObject sourceFile = fileManager.getJavaFileForInput(
StandardLocation.SOURCE_PATH, className.fullName(), Kind.SOURCE);
sourceFiles.add(sourceFile);
}
assertEquals(classNames.size(), sourceFiles.size());
// Compile the classes.
JavaCompiler.CompilationTask javacTask = javac.getTask(
compilerOut, fileManager, diagnosticCollector, options, classNames, sourceFiles);
boolean compiledOk = javacTask.call();
// Check that there were no compilation errors unless we were expecting there to be.
// We ignore "notes", typically debugging output from the annotation processor
// when that is enabled.
Table<Diagnostic.Kind, Integer, String> diagnostics = HashBasedTable.create();
for (Diagnostic<?> diagnostic : diagnosticCollector.getDiagnostics()) {
boolean ignore = (diagnostic.getKind() == Diagnostic.Kind.NOTE
|| (diagnostic.getKind() == Diagnostic.Kind.WARNING
&& diagnostic.getMessage(null).contains(
"No processor claimed any of these annotations")));
if (!ignore) {
diagnostics.put(
diagnostic.getKind(), (int) diagnostic.getLineNumber(), diagnostic.getMessage(null));
}
}
assertEquals(diagnostics.containsRow(Diagnostic.Kind.ERROR), !compiledOk);
assertEquals("Diagnostic kinds should match: " + diagnostics,
expectedDiagnostics.rowKeySet(), diagnostics.rowKeySet());
for (Table.Cell<Diagnostic.Kind, Integer, Pattern> expectedDiagnostic :
expectedDiagnostics.cellSet()) {
boolean match = false;
for (Table.Cell<Diagnostic.Kind, Integer, String> diagnostic : diagnostics.cellSet()) {
if (expectedDiagnostic.getValue().matcher(diagnostic.getValue()).find()) {
int expectedLine = expectedDiagnostic.getColumnKey();
if (expectedLine != 0) {
int actualLine = diagnostic.getColumnKey();
if (actualLine != expectedLine) {
fail("Diagnostic matched pattern but on line " + actualLine
+ " not line " + expectedLine + ": " + diagnostic.getValue());
}
}
match = true;
break;
}
}
assertTrue("Diagnostics should contain " + expectedDiagnostic + ": " + diagnostics, match);
}
}
private static class ClassName {
final String packageName; // Package name with trailing dot. May be empty but not null.
final String simpleName;
private ClassName(String packageName, String simpleName) {
this.packageName = packageName;
this.simpleName = simpleName;
}
// Extract the package and simple name of the top-level class defined in the given string,
// which is a Java sourceUnit unit.
static ClassName extractFromSource(String sourceUnit) {
String pkg;
if (sourceUnit.contains("package ")) {
// (?s) means that . matches everything including \n
pkg = sourceUnit.replaceAll("(?s).*?package ([a-z.]+);.*", "$1") + ".";
} else {
pkg = "";
}
String cls = sourceUnit.replaceAll("(?s).*?(class|interface|enum) ([A-Za-z0-9_$]+).*", "$2");
assertTrue(cls, cls.matches("[A-Za-z0-9_$]+"));
return new ClassName(pkg, cls);
}
String fullName() {
return packageName + simpleName;
}
String sourceDirectoryName() {
return packageName.replace('.', '/');
}
}
}