/* * 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 retroweibo.processor; import com.google.common.base.Charsets; import com.google.common.base.Predicates; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.io.Files; import junit.framework.TestCase; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.util.Collection; import java.util.List; import java.util.Map; 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(user): 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(user): 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 = "package foo.bar;\n" + "import retroweibo.RetroWeibo;\n" + "@RetroWeibo\n" + "public abstract class Baz {\n" + " public abstract int integer();\n" + " public static Baz create(int integer) {\n" + " return new RetroWeibo_Baz(integer);\n" + " }\n" + "}\n"; boolean compiled = false; try { assertCompilationFails(ImmutableList.of(testSourceCode)); compiled = true; } catch (AssertionError expected) { } assertFalse(compiled); } public void testNoWarningsFromGenerics() throws Exception { String testSourceCode = "package foo.bar;\n" + "import retroweibo.RetroWeibo;\n" + "@RetroWeibo\n" + "public abstract class Baz<T extends Number, U extends T> {\n" + " public abstract T t();\n" + " public abstract U u();\n" + " public static <T extends Number, U extends T> Baz<T, U> create(T t, U u) {\n" + " return new RetroWeibo_Baz<T, U>(t, u);\n" + " }\n" + "}\n"; assertCompilationSucceedsWithoutWarning(ImmutableList.of(testSourceCode)); } private static final Pattern CANNOT_HAVE_NON_PROPERTIES = Pattern.compile( "@RetroWeibo classes cannot have abstract methods other than property getters"); public void testAbstractVoid() throws Exception { String testSourceCode = "package foo.bar;\n" + "import retroweibo.RetroWeibo;\n" + "@RetroWeibo\n" + "public abstract class Baz {\n" + " public abstract void foo();\n" + "}\n"; ImmutableMultimap<Diagnostic.Kind, Pattern> expectedDiagnostics = ImmutableMultimap.of( Diagnostic.Kind.WARNING, CANNOT_HAVE_NON_PROPERTIES, Diagnostic.Kind.ERROR, Pattern.compile("RetroWeibo_Baz") ); assertCompilationResultIs(expectedDiagnostics, ImmutableList.of(testSourceCode)); } public void testAbstractWithParams() throws Exception { String testSourceCode = "package foo.bar;\n" + "import retroweibo.RetroWeibo;\n" + "@RetroWeibo\n" + "public abstract class Baz {\n" + " public abstract int foo(int bar);\n" + "}\n"; ImmutableMultimap<Diagnostic.Kind, Pattern> expectedDiagnostics = ImmutableMultimap.of( Diagnostic.Kind.WARNING, CANNOT_HAVE_NON_PROPERTIES, Diagnostic.Kind.ERROR, Pattern.compile("RetroWeibo_Baz") ); assertCompilationResultIs(expectedDiagnostics, 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(ImmutableMultimap.of(Diagnostic.Kind.ERROR, Pattern.compile("")), testSourceCode); } private void assertCompilationSucceedsWithoutWarning(List<String> testSourceCode) throws IOException { assertCompilationResultIs(ImmutableMultimap.<Diagnostic.Kind, Pattern>of(), testSourceCode); } private void assertCompilationResultIs( Multimap<Diagnostic.Kind, 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", RetroWeiboProcessor.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), Charsets.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. Multimap<Diagnostic.Kind, String> diagnostics = ArrayListMultimap.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(), diagnostic.getMessage(null)); } } assertEquals(diagnostics.containsKey(Diagnostic.Kind.ERROR), !compiledOk); assertEquals("Diagnostic kinds should match: " + diagnostics, expectedDiagnostics.keySet(), diagnostics.keySet()); for (Map.Entry<Diagnostic.Kind, Pattern> expectedDiagnostic : expectedDiagnostics.entries()) { Collection<String> actualDiagnostics = diagnostics.get(expectedDiagnostic.getKey()); assertTrue("Diagnostics should contain " + expectedDiagnostic + ": " + diagnostics, Iterables.any(actualDiagnostics, Predicates.contains(expectedDiagnostic.getValue()))); } } 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('.', '/'); } } }