From 9550c1e080b4147c781ccc7fd40d2f2178e130f6 Mon Sep 17 00:00:00 2001 From: dpb Date: Tue, 11 Oct 2016 09:44:36 -0700 Subject: [PATCH 1/3] Allow Java8 in compile-testing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=135806147 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9fe873b9..8358a17a 100644 --- a/pom.xml +++ b/pom.xml @@ -85,8 +85,8 @@ maven-compiler-plugin 3.1 - 1.6 - 1.6 + 1.8 + 1.8 -Xlint:all true true From 5a07ad5ca65866d8bb5c8aadf7c0bdcf5f4b1c7f Mon Sep 17 00:00:00 2001 From: dpb Date: Wed, 19 Oct 2016 12:49:43 -0700 Subject: [PATCH 2/3] A new API for compile testing. Introduces an immutable object representing a compiler with options and annotation processors (Compiler) and an immutable object representing the results of compiling source files (Compilation). Introduces a Truth subject for Compilation, in combination with one for JavaFileObjects, that together allow users to make all the assertions that JavaSource(s)Subject does (except for parsesAs(JavaFileObject...), which is not directly supported in the new API), but without requiring chaining ("and()"). JavaFileObjectSubject adds the ability to make arbitrary assertions on the string contents of a JavaFileObject. JavaSourcesSubject now delegates to CompilationSubject for most of its features. Fix a small bug where files generated into the default package had an extra slash character in their URI (e.g., "/CLASS_OUTPUT//Foo.class"). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136629427 --- pom.xml | 8 +- .../google/testing/compile/Compilation.java | 372 +++++---- .../compile/CompilationFailureException.java | 10 +- .../testing/compile/CompilationRule.java | 124 +-- .../testing/compile/CompilationSubject.java | 447 +++++++++++ .../compile/CompilationSubjectFactory.java | 29 + .../com/google/testing/compile/Compiler.java | 142 ++++ .../compile/InMemoryJavaFileManager.java | 15 +- .../compile/JavaFileObjectSubject.java | 160 ++++ .../compile/JavaFileObjectSubjectFactory.java | 30 + .../testing/compile/JavaFileObjects.java | 10 - .../testing/compile/JavaSourcesSubject.java | 501 +++--------- .../com/google/testing/compile/MoreTrees.java | 42 +- .../com/google/testing/compile/Parser.java | 178 +++++ .../google/testing/compile/package-info.java | 98 ++- .../compile/CompilationSubjectTest.java | 741 ++++++++++++++++++ .../testing/compile/CompilationTest.java | 45 ++ .../google/testing/compile/CompilerTest.java | 91 +++ .../testing/compile/DiagnosticMessage.java | 68 ++ .../testing/compile/ErrorProcessor.java | 57 ++ .../compile/FailingGeneratingProcessor.java | 57 ++ .../testing/compile/GeneratingProcessor.java | 75 ++ .../compile/JavaFileObjectSubjectTest.java | 152 ++++ .../testing/compile/JavaFileObjectsTest.java | 18 +- .../JavaSourcesSubjectFactoryTest.java | 319 ++------ .../google/testing/compile/NoOpProcessor.java | 53 ++ .../testing/compile/ThrowingProcessor.java | 42 + .../testing/compile/TreeContextTest.java | 6 +- .../testing/compile/TreeDifferenceTest.java | 8 +- .../compile/VerificationFailureStrategy.java} | 33 +- 30 files changed, 2947 insertions(+), 984 deletions(-) create mode 100644 src/main/java/com/google/testing/compile/CompilationSubject.java create mode 100644 src/main/java/com/google/testing/compile/CompilationSubjectFactory.java create mode 100644 src/main/java/com/google/testing/compile/Compiler.java create mode 100644 src/main/java/com/google/testing/compile/JavaFileObjectSubject.java create mode 100644 src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java create mode 100644 src/main/java/com/google/testing/compile/Parser.java create mode 100644 src/test/java/com/google/testing/compile/CompilationSubjectTest.java create mode 100644 src/test/java/com/google/testing/compile/CompilationTest.java create mode 100644 src/test/java/com/google/testing/compile/CompilerTest.java create mode 100644 src/test/java/com/google/testing/compile/DiagnosticMessage.java create mode 100644 src/test/java/com/google/testing/compile/ErrorProcessor.java create mode 100644 src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java create mode 100644 src/test/java/com/google/testing/compile/GeneratingProcessor.java create mode 100644 src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java create mode 100644 src/test/java/com/google/testing/compile/NoOpProcessor.java create mode 100644 src/test/java/com/google/testing/compile/ThrowingProcessor.java rename src/{main/java/com/google/testing/compile/Diagnostics.java => test/java/com/google/testing/compile/VerificationFailureStrategy.java} (50%) diff --git a/pom.xml b/pom.xml index 8358a17a..d5423c1d 100644 --- a/pom.xml +++ b/pom.xml @@ -15,8 +15,9 @@ Utilities for testing compilation. + 1.3 19.0 - 0.28 + 0.30 4.12 3.0.1 @@ -78,6 +79,11 @@ 2.0.8 provided + + com.google.auto.value + auto-value + ${auto-value.version} + diff --git a/src/main/java/com/google/testing/compile/Compilation.java b/src/main/java/com/google/testing/compile/Compilation.java index 07187a61..9f5ef599 100644 --- a/src/main/java/com/google/testing/compile/Compilation.java +++ b/src/main/java/com/google/testing/compile/Compilation.java @@ -15,192 +15,264 @@ */ package com.google.testing.compile; -import static com.google.common.base.Charsets.UTF_8; -import static javax.tools.JavaFileObject.Kind.SOURCE; +import static com.google.common.base.Preconditions.checkState; +import static com.google.testing.compile.JavaFileObjects.asByteSource; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; +import static javax.tools.Diagnostic.Kind.ERROR; +import static javax.tools.Diagnostic.Kind.MANDATORY_WARNING; +import static javax.tools.Diagnostic.Kind.NOTE; +import static javax.tools.Diagnostic.Kind.WARNING; +import static javax.tools.JavaFileObject.Kind.CLASS; -import com.google.common.base.Function; -import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multimaps; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; -import com.sun.source.util.Trees; -import com.sun.tools.javac.api.JavacTool; - +import com.google.common.collect.Sets; import java.io.IOException; -import java.util.List; -import java.util.Locale; - -import javax.annotation.processing.Processor; +import java.util.Optional; +import java.util.stream.Collector; import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaCompiler; -import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.Diagnostic.Kind; +import javax.tools.JavaFileManager.Location; import javax.tools.JavaFileObject; -import javax.tools.ToolProvider; +import javax.tools.StandardLocation; -/** - * Utilities for performing compilation with {@code javac}. - * - * @author Gregory Kick - */ -final class Compilation { - private Compilation() {} +/** The results of {@linkplain Compiler#compile compiling} source files. */ +public final class Compilation { + + private final Compiler compiler; + private final ImmutableList sourceFiles; + private final Status status; + private final ImmutableList> diagnostics; + private final ImmutableList generatedFiles; + + Compilation( + Compiler compiler, + Iterable sourceFiles, + boolean successful, + Iterable> diagnostics, + Iterable generatedFiles) { + this.compiler = compiler; + this.sourceFiles = ImmutableList.copyOf(sourceFiles); + this.status = successful ? Status.SUCCESS : Status.FAILURE; + this.diagnostics = ImmutableList.copyOf(diagnostics); + this.generatedFiles = ImmutableList.copyOf(generatedFiles); + } + + /** The compiler. */ + Compiler compiler() { + return compiler; + } + + /** The source files compiled. */ + ImmutableList sourceFiles() { + return sourceFiles; + } + + /** The status of the compilation. */ + Status status() { + return status; + } /** - * Compile {@code sources} using {@code processors}. + * All diagnostics reported during compilation. The order of the returned list is unspecified. * - * @throws RuntimeException if compilation fails. + * @see #errors() + * @see #warnings() + * @see #notes() */ - static Result compile(Iterable processors, - Iterable options, Iterable sources) { - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - DiagnosticCollector diagnosticCollector = - new DiagnosticCollector(); - InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager( - compiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8)); - CompilationTask task = compiler.getTask( - null, // explicitly use the default because old versions of javac log some output on stderr - fileManager, - diagnosticCollector, - ImmutableList.copyOf(options), - ImmutableSet.of(), - sources); - task.setProcessors(processors); - boolean successful = task.call(); - return new Result(successful, sortDiagnosticsByKind(diagnosticCollector.getDiagnostics()), - fileManager.getOutputFiles()); + public ImmutableList> diagnostics() { + return diagnostics; + } + + /** {@linkplain Diagnostic.Kind#ERROR Errors} reported during compilation. */ + public ImmutableList> errors() { + return diagnosticsOfKind(ERROR); } /** - * Parse {@code sources} into {@linkplain CompilationUnitTree compilation units}. This method - * does not compile the sources. + * {@linkplain Diagnostic.Kind#WARNING Warnings} (including {@linkplain + * Diagnostic.Kind#MANDATORY_WARNING mandatory warnings}) reported during compilation. */ - static ParseResult parse(Iterable sources) { - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - DiagnosticCollector diagnosticCollector = - new DiagnosticCollector(); - InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager( - compiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8)); - JavacTask task = ((JavacTool) compiler).getTask( - null, // explicitly use the default because old versions of javac log some output on stderr - fileManager, - diagnosticCollector, - ImmutableSet.of(), - ImmutableSet.of(), - sources); - try { - Iterable parsedCompilationUnits = task.parse(); - List> diagnostics = diagnosticCollector.getDiagnostics(); - for (Diagnostic diagnostic : diagnostics) { - if (Diagnostic.Kind.ERROR == diagnostic.getKind()) { - throw new IllegalStateException("error while parsing:\n" - + Diagnostics.toString(diagnostics)); - } - } - return new ParseResult(sortDiagnosticsByKind(diagnostics), parsedCompilationUnits, - Trees.instance(task)); - } catch (IOException e) { - throw new RuntimeException(e); - } + public ImmutableList> warnings() { + return diagnosticsOfKind(WARNING, MANDATORY_WARNING); + } + + /** {@linkplain Diagnostic.Kind#NOTE Notes} reported during compilation. */ + public ImmutableList> notes() { + return diagnosticsOfKind(NOTE); } - private static ImmutableListMultimap> - sortDiagnosticsByKind(Iterable> diagnostics) { - return Multimaps.index(diagnostics, - new Function, Diagnostic.Kind>() { - @Override public Diagnostic.Kind apply(Diagnostic input) { - return input.getKind(); - } - }); + ImmutableList> diagnosticsOfKind(Kind kind, Kind... more) { + ImmutableSet kinds = Sets.immutableEnumSet(kind, more); + return diagnostics() + .stream() + .filter(diagnostic -> kinds.contains(diagnostic.getKind())) + .collect(toImmutableList()); } /** - * The diagnostic, parse trees, and {@link Trees} instance for a parse task. + * Files generated during compilation. * - *

Note: It is possible for the {@link Trees} instance contained within a {@code ParseResult} - * to be invalidated by a call to {@link com.sun.tools.javac.api.JavacTaskImpl#cleanup()}. Though - * we do not currently expose the {@link JavacTask} used to create a {@code ParseResult} to - * {@code cleanup()} calls on its underlying implementation, this should be acknowledged as an - * implementation detail that could cause unexpected behavior when making calls to methods in - * {@link Trees}. + * @throws IllegalStateException for {@linkplain #status() failed compilations}, since the state + * of the generated files is undefined in that case */ - static final class ParseResult { - private final ImmutableListMultimap> - diagnostics; - private final ImmutableList compilationUnits; - private final Trees trees; - - ParseResult( - ImmutableListMultimap> diagnostics, - Iterable compilationUnits, Trees trees) { - this.trees = trees; - this.compilationUnits = ImmutableList.copyOf(compilationUnits); - this.diagnostics = diagnostics; - } + public ImmutableList generatedFiles() { + checkState( + status.equals(Status.SUCCESS), + "compilation failed, so generated files are unavailable. %s", + describeErrors()); + return generatedFiles; + } - ImmutableListMultimap> - diagnosticsByKind() { - return diagnostics; - } + /** + * Source files generated during compilation. + * + * @throws IllegalStateException for {@linkplain #status() failed compilations}, since the state + * of the generated files is undefined in that case + */ + public ImmutableList generatedSourceFiles() { + return generatedFiles() + .stream() + .filter(generatedFile -> generatedFile.getKind().equals(JavaFileObject.Kind.SOURCE)) + .collect(toImmutableList()); + } - Iterable compilationUnits() { - return compilationUnits; + /** + * Returns the file at {@code path} if one was generated. + * + *

For example: + * + *

+   * {@code Optional} fooClassFile =
+   *     compilation.generatedFile(CLASS_OUTPUT, "com/google/myapp/Foo.class");
+   * 
+ * + * @throws IllegalStateException for {@linkplain #status() failed compilations}, since the state + * of the generated files is undefined in that case + */ + public Optional generatedFile(Location location, String path) { + // We're relying on the implementation of location.getName() to be equivalent to the first + // part of the path. + String expectedFilename = String.format("%s/%s", location.getName(), path); + return generatedFiles() + .stream() + .filter(generated -> generated.toUri().getPath().endsWith(expectedFilename)) + .findFirst(); + } + + /** + * Returns the file with name {@code fileName} in package {@code packageName} if one was + * generated. + * + *

For example: + * + *

+   * {@code Optional} fooClassFile =
+   *     compilation.generatedFile(CLASS_OUTPUT, "com.google.myapp", "Foo.class");
+   * 
+ * + * @throws IllegalStateException for {@linkplain #status() failed compilations}, since the state + * of the generated files is undefined in that case + */ + public Optional generatedFile( + Location location, String packageName, String fileName) { + return generatedFile( + location, + packageName.isEmpty() ? fileName : packageName.replace('.', '/') + '/' + fileName); + } + + /** + * Returns the source file with name {@code qualifiedName} (no extension) if one was generated. + * + *

For example: + * + *

+   * {@code Optional} fooSourceFile =
+   *     compilation.generatedSourceFile("com.google.myapp.Foo");
+   * 
+ * + * @throws IllegalStateException for {@linkplain #status() failed compilations}, since the state + * of the generated files is undefined in that case + */ + public Optional generatedSourceFile(String qualifiedName) { + return generatedFile(StandardLocation.SOURCE_OUTPUT, qualifiedName); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder + .append("compilation of ") + .append(sourceFiles.stream().map(JavaFileObject::getName).collect(toList())); + if (!compiler.processors().isEmpty()) { + builder.append(" using annotation processors ").append(compiler.processors()); + } + if (!compiler.options().isEmpty()) { + builder.append(" passing options ").append(compiler.options()); } + return builder.toString(); + } - Trees trees() { - return trees; + /** Returns a description of the errors reported during compilation. */ + String describeErrors() { + ImmutableList> errors = errors(); + if (errors.isEmpty()) { + return "Compilation produced no errors.\n"; } + StringBuilder message = new StringBuilder("Compilation produced the following errors:\n"); + errors.stream().forEach(error -> message.append(error).append('\n')); + return message.toString(); } - /** The diagnostic and file output of a compilation. */ - static final class Result { - private final boolean successful; - private final ImmutableListMultimap> - diagnostics; - private final ImmutableListMultimap generatedFilesByKind; - - Result(boolean successful, - ImmutableListMultimap> diagnostics, - Iterable generatedFiles) { - this.successful = successful; - this.diagnostics = diagnostics; - this.generatedFilesByKind = Multimaps.index(generatedFiles, - new Function() { - @Override public JavaFileObject.Kind apply(JavaFileObject input) { - return input.getKind(); - } - }); - if (!successful && diagnostics.get(Diagnostic.Kind.ERROR).isEmpty()) { - throw new CompilationFailureException(); + /** Returns a description of the source file generated by this compilation. */ + String describeGeneratedSourceFiles() { + ImmutableList generatedSourceFiles = + generatedFiles + .stream() + .filter(generatedFile -> generatedFile.getKind().equals(JavaFileObject.Kind.SOURCE)) + .collect(toImmutableList()); + if (generatedSourceFiles.isEmpty()) { + return "No files were generated.\n"; + } else { + StringBuilder message = new StringBuilder("Generated Source Files\n======================\n"); + for (JavaFileObject generatedFile : generatedSourceFiles) { + message.append(describeGeneratedFile(generatedFile)); } + return message.toString(); } + } - boolean successful() { - return successful; + /** Returns a description of the contents of a given generated file. */ + private String describeGeneratedFile(JavaFileObject generatedFile) { + try { + StringBuilder entry = new StringBuilder("\n").append(generatedFile.getName()).append(":\n"); + if (generatedFile.getKind().equals(CLASS)) { + entry.append( + String.format( + " [generated class file (%d bytes)]", asByteSource(generatedFile).size())); + } else { + entry.append(generatedFile.getCharContent(true)); + } + return entry.append('\n').toString(); + } catch (IOException e) { + throw new IllegalStateException( + "Couldn't read from JavaFileObject when it was already in memory.", e); } + } - ImmutableListMultimap> - diagnosticsByKind() { - return diagnostics; - } + // TODO(dpb): Use Guava's toImmutableList() once that's available. MOE:strip_line + private static Collector> toImmutableList() { + return collectingAndThen(toList(), ImmutableList::copyOf); + } - ImmutableListMultimap generatedFilesByKind() { - return generatedFilesByKind; - } + /** The status of a compilation. */ + public enum Status { - ImmutableList generatedSources() { - return generatedFilesByKind.get(SOURCE); - } + /** Compilation finished without errors. */ + SUCCESS, - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("successful", successful) - .add("diagnostics", diagnostics) - .toString(); - } + /** Compilation finished with errors. */ + FAILURE, } } diff --git a/src/main/java/com/google/testing/compile/CompilationFailureException.java b/src/main/java/com/google/testing/compile/CompilationFailureException.java index 035bafbc..114a67ec 100644 --- a/src/main/java/com/google/testing/compile/CompilationFailureException.java +++ b/src/main/java/com/google/testing/compile/CompilationFailureException.java @@ -20,9 +20,11 @@ */ @SuppressWarnings("serial") public class CompilationFailureException extends RuntimeException { - CompilationFailureException() { - super("Compilation failed, but did not report any error diagnostics or throw any exceptions. " - + "This behavior has been observed in older versions of javac, which swallow exceptions " - + "and log them on System.err. Check there for more information."); + CompilationFailureException(Compilation compilation) { + super( + compilation + + " failed, but did not report any error diagnostics or throw any exceptions. " + + "This behavior has been observed in older versions of javac, which swallow " + + "exceptions and log them on System.err. Check there for more information."); } } diff --git a/src/main/java/com/google/testing/compile/CompilationRule.java b/src/main/java/com/google/testing/compile/CompilationRule.java index 0be82843..81df84f0 100644 --- a/src/main/java/com/google/testing/compile/CompilationRule.java +++ b/src/main/java/com/google/testing/compile/CompilationRule.java @@ -16,20 +16,11 @@ package com.google.testing.compile; import static com.google.common.base.Preconditions.checkState; +import static com.google.testing.compile.Compilation.Status.SUCCESS; +import static com.google.testing.compile.Compiler.javac; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.testing.compile.Compilation.Result; - -import org.junit.Rule; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.JUnit4; -import org.junit.runners.model.Statement; - import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; @@ -37,69 +28,43 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.JUnit4; +import org.junit.runners.model.Statement; /** * A {@link JUnit4} {@link Rule} that executes tests such that a instances of {@link Elements} and * {@link Types} are available during execution. * - *

To use this rule in a test, just add the following field:


- *   {@code @Rule} public CompilationRule compilationRule = new CompilationRule();
+ *

To use this rule in a test, just add the following field: + * + *

{@code @Rule} public CompilationRule compilationRule = new CompilationRule();
* * @author Gregory Kick */ public final class CompilationRule implements TestRule { + private static final JavaFileObject DUMMY = + JavaFileObjects.forSourceLines("Dummy", "final class Dummy {}"); + private Elements elements; private Types types; @Override public Statement apply(final Statement base, Description description) { return new Statement() { - @Override public void evaluate() throws Throwable { - final AtomicReference thrown = new AtomicReference(); - Result result = Compilation.compile(ImmutableList.of(new AbstractProcessor() { - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latest(); - } - - @Override - public Set getSupportedAnnotationTypes() { - return ImmutableSet.of("*"); - } - - @Override - public synchronized void init(ProcessingEnvironment processingEnv) { - super.init(processingEnv); - elements = processingEnv.getElementUtils(); - types = processingEnv.getTypeUtils(); - } - - @Override - public boolean process(Set annotations, - RoundEnvironment roundEnv) { - // just run the test on the last round after compilation is over - if (roundEnv.processingOver()) { - try { - base.evaluate(); - } catch (Throwable e) { - thrown.set(e); - } - } - return false; - } - }), - ImmutableSet.of(), - // just compile _something_ - ImmutableList.of(JavaFileObjects.forSourceLines("Dummy", "final class Dummy {}"))); - checkState(result.successful(), result); - Throwable t = thrown.get(); - if (t != null) { - throw t; - } + @Override + public void evaluate() throws Throwable { + EvaluatingProcessor evaluatingProcessor = new EvaluatingProcessor(base); + Compilation compilation = javac().withProcessors(evaluatingProcessor).compile(DUMMY); + checkState(compilation.status().equals(SUCCESS), compilation); + evaluatingProcessor.throwIfStatementThrew(); } }; } - + /** * Returns the {@link Elements} instance associated with the current execution of the rule. * @@ -119,4 +84,51 @@ public Types getTypes() { checkState(elements != null, "Not running within the rule"); return types; } + + final class EvaluatingProcessor extends AbstractProcessor { + + final Statement base; + Throwable thrown; + + EvaluatingProcessor(Statement base) { + this.base = base; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public Set getSupportedAnnotationTypes() { + return ImmutableSet.of("*"); + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + elements = processingEnv.getElementUtils(); + types = processingEnv.getTypeUtils(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + // just run the test on the last round after compilation is over + if (roundEnv.processingOver()) { + try { + base.evaluate(); + } catch (Throwable e) { + thrown = e; + } + } + return false; + } + + /** Throws what the base {@link Statement} threw, if anything. */ + void throwIfStatementThrew() throws Throwable { + if (thrown != null) { + throw thrown; + } + } + } } diff --git a/src/main/java/com/google/testing/compile/CompilationSubject.java b/src/main/java/com/google/testing/compile/CompilationSubject.java new file mode 100644 index 00000000..14182a61 --- /dev/null +++ b/src/main/java/com/google/testing/compile/CompilationSubject.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2013 Google, 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.google.testing.compile; + +import static com.google.common.collect.Iterables.size; +import static com.google.common.truth.Truth.assertAbout; +import static com.google.testing.compile.Compilation.Status.FAILURE; +import static com.google.testing.compile.Compilation.Status.SUCCESS; +import static com.google.testing.compile.JavaFileObjectSubject.javaFileObjects; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; +import static javax.tools.Diagnostic.Kind.ERROR; +import static javax.tools.Diagnostic.Kind.MANDATORY_WARNING; +import static javax.tools.Diagnostic.Kind.NOTE; +import static javax.tools.Diagnostic.Kind.WARNING; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collector; +import javax.tools.Diagnostic; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +/** A {@link Truth} subject for a {@link Compilation}. */ +public final class CompilationSubject extends Subject { + + private static final SubjectFactory FACTORY = + new CompilationSubjectFactory(); + + /** Returns a {@link SubjectFactory} for a {@link Compilation}. */ + public static SubjectFactory compilations() { + return FACTORY; + } + + /** Starts making assertions about a {@link Compilation}. */ + public static CompilationSubject assertThat(Compilation actual) { + return assertAbout(compilations()).that(actual); + } + + CompilationSubject(FailureStrategy failureStrategy, Compilation actual) { + super(failureStrategy, actual); + } + + /** Asserts that the compilation succeeded. */ + public void succeeded() { + if (actual().status().equals(FAILURE)) { + failureStrategy.fail(actual().describeErrors() + actual().describeGeneratedSourceFiles()); + } + } + + /** Asserts that the compilation succeeded without warnings. */ + public void succeededWithoutWarnings() { + succeeded(); + hadWarningCount(0); + } + + /** Asserts that the compilation failed. */ + public void failed() { + if (actual().status().equals(SUCCESS)) { + failureStrategy.fail( + "Compilation was expected to fail, but contained no errors.\n\n" + + actual().describeGeneratedSourceFiles()); + } + } + + /** Asserts that the compilation had exactly {@code expectedCount} errors. */ + public void hadErrorCount(int expectedCount) { + checkDiagnosticCount(expectedCount, ERROR); + } + + /** Asserts that there was at least one error containing {@code expectedSubstring}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadErrorContaining(String expectedSubstring) { + return hadDiagnosticContaining(expectedSubstring, ERROR); + } + + /** Asserts that there was at least one error containing a match for {@code expectedPattern}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadErrorContainingMatch(String expectedPattern) { + return hadDiagnosticContainingMatch(expectedPattern, ERROR); + } + + /** Asserts that there was at least one error containing a match for {@code expectedPattern}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadErrorContainingMatch(Pattern expectedPattern) { + return hadDiagnosticContainingMatch(expectedPattern, ERROR); + } + + /** Asserts that the compilation had exactly {@code expectedCount} warnings. */ + public void hadWarningCount(int expectedCount) { + checkDiagnosticCount(expectedCount, WARNING, MANDATORY_WARNING); + } + + /** Asserts that there was at least one warning containing {@code expectedSubstring}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadWarningContaining(String expectedSubstring) { + return hadDiagnosticContaining(expectedSubstring, WARNING, MANDATORY_WARNING); + } + + /** Asserts that there was at least one warning containing a match for {@code expectedPattern}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadWarningContainingMatch(String expectedPattern) { + return hadDiagnosticContainingMatch(expectedPattern, WARNING, MANDATORY_WARNING); + } + + /** Asserts that there was at least one warning containing a match for {@code expectedPattern}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadWarningContainingMatch(Pattern expectedPattern) { + return hadDiagnosticContainingMatch(expectedPattern, WARNING, MANDATORY_WARNING); + } + + /** Asserts that the compilation had exactly {@code expectedCount} notes. */ + public void hadNoteCount(int expectedCount) { + checkDiagnosticCount(expectedCount, NOTE); + } + + /** Asserts that there was at least one note containing {@code expectedSubstring}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadNoteContaining(String expectedSubstring) { + return hadDiagnosticContaining(expectedSubstring, NOTE); + } + + /** Asserts that there was at least one note containing a match for {@code expectedPattern}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadNoteContainingMatch(String expectedPattern) { + return hadDiagnosticContainingMatch(expectedPattern, NOTE); + } + + /** Asserts that there was at least one note containing a match for {@code expectedPattern}. */ + @CanIgnoreReturnValue + public DiagnosticInFile hadNoteContainingMatch(Pattern expectedPattern) { + return hadDiagnosticContainingMatch(expectedPattern, NOTE); + } + + private void checkDiagnosticCount( + int expectedCount, Diagnostic.Kind kind, Diagnostic.Kind... more) { + Iterable> diagnostics = + actual().diagnosticsOfKind(kind, more); + int actualCount = size(diagnostics); + if (actualCount != expectedCount) { + failureStrategy.fail( + messageListing( + diagnostics, + "Expected %d %s, but found the following %d %s:", + expectedCount, + kindToString(kind, true), + actualCount, + kindToString(kind, true))); + } + } + + private static String messageListing( + Iterable> diagnostics, String headingFormat, Object... formatArgs) { + StringBuilder listing = + new StringBuilder(String.format(headingFormat, formatArgs)).append('\n'); + for (Diagnostic diagnostic : diagnostics) { + listing.append(diagnostic.getMessage(null)).append('\n'); + } + return listing.toString(); + } + + /** + * Returns a string representation of a diagnostic kind. + * + * @param expectingSpecificCount {@code true} if being used after a count, as in "Expected 5 + * errors"; {@code false} if being used to describe one message, as in "Expected a warning + * containing…". + */ + private static String kindToString(Diagnostic.Kind kind, boolean expectingSpecificCount) { + switch (kind) { + case ERROR: + return expectingSpecificCount ? "errors" : "an error"; + + case MANDATORY_WARNING: + case WARNING: + return expectingSpecificCount ? "warnings" : "a warning"; + + case NOTE: + return expectingSpecificCount ? "notes" : "a note"; + + case OTHER: + return expectingSpecificCount ? "diagnostic messages" : "a diagnostic message"; + + default: + throw new AssertionError(kind); + } + } + + private DiagnosticInFile hadDiagnosticContaining( + String expectedSubstring, Diagnostic.Kind kind, Diagnostic.Kind... more) { + return hadDiagnosticContainingMatch( + String.format("containing \"%s\"", expectedSubstring), + Pattern.compile(Pattern.quote(expectedSubstring)), + kind, + more); + } + + private DiagnosticInFile hadDiagnosticContainingMatch( + String expectedPattern, Diagnostic.Kind kind, Diagnostic.Kind... more) { + return hadDiagnosticContainingMatch(Pattern.compile(expectedPattern), kind, more); + } + + private DiagnosticInFile hadDiagnosticContainingMatch( + Pattern expectedPattern, Diagnostic.Kind kind, Diagnostic.Kind... more) { + return hadDiagnosticContainingMatch( + String.format("containing match for /%s/", expectedPattern), expectedPattern, kind, more); + } + + private DiagnosticInFile hadDiagnosticContainingMatch( + String verb, final Pattern expectedPattern, Diagnostic.Kind kind, Diagnostic.Kind... more) { + ImmutableList> diagnosticsOfKind = + actual().diagnosticsOfKind(kind, more); + ImmutableList> diagnosticsWithMessage = + diagnosticsOfKind + .stream() + .filter(diagnostic -> expectedPattern.matcher(diagnostic.getMessage(null)).find()) + .collect(toImmutableList()); + if (diagnosticsWithMessage.isEmpty()) { + failureStrategy.fail( + messageListing( + diagnosticsOfKind, + "Expected %s %s, but only found:", + kindToString(kind, false), + verb)); + } + return new DiagnosticInFile(kind, diagnosticsWithMessage); + } + + /** + * Asserts that compilation generated a file named {@code fileName} in package {@code + * packageName}. + */ + @CanIgnoreReturnValue + public JavaFileObjectSubject generatedFile( + Location location, String packageName, String fileName) { + return checkGeneratedFile( + actual().generatedFile(location, packageName, fileName), + location, + "named \"%s\" in %s", + fileName, + packageName.isEmpty() + ? "the default package" + : String.format("package \"%s\"", packageName)); + } + + /** Asserts that compilation generated a file at {@code path}. */ + @CanIgnoreReturnValue + public JavaFileObjectSubject generatedFile(Location location, String path) { + return checkGeneratedFile(actual().generatedFile(location, path), location, path); + } + + /** Asserts that compilation generated a source file for a type with a given qualified name. */ + @CanIgnoreReturnValue + public JavaFileObjectSubject generatedSourceFile(String qualifiedName) { + return generatedFile( + StandardLocation.SOURCE_OUTPUT, qualifiedName.replaceAll("\\.", "/") + ".java"); + } + + private JavaFileObjectSubject checkGeneratedFile( + Optional generatedFile, Location location, String format, Object... args) { + if (!generatedFile.isPresent()) { + StringBuilder builder = new StringBuilder("generated the file "); + builder.append(args.length == 0 ? format : String.format(format, args)); + builder.append("; it generated:\n"); + for (JavaFileObject generated : actual().generatedFiles()) { + if (generated.toUri().getPath().contains(location.getName())) { + builder.append(" ").append(generated.toUri().getPath()).append('\n'); + } + } + fail(builder.toString()); + } + return check().about(javaFileObjects()).that(generatedFile.get()); + } + + private static Collector> toImmutableList() { + return collectingAndThen(toList(), ImmutableList::copyOf); + } + + private static class DiagnosticsAssertions { + private final ImmutableList> diagnostics; + + DiagnosticsAssertions(Iterable> diagnostics) { + this.diagnostics = ImmutableList.copyOf(diagnostics); + } + + ImmutableList> diagnosticsMatching( + Predicate> predicate) { + return diagnostics.stream().filter(predicate).collect(toImmutableList()); + } + + ImmutableSet mapDiagnostics( + Function, ? extends String> mapper) { + return diagnostics + .stream() + .map(mapper) + .collect(collectingAndThen(toList(), ImmutableSet::copyOf)); + } + } + + /** Assertions that a note, warning, or error was found in a given file. */ + public final class DiagnosticInFile extends DiagnosticsAssertions { + + private final Diagnostic.Kind kind; + + private DiagnosticInFile( + Diagnostic.Kind kind, + Iterable> diagnosticsWithMessage) { + super(diagnosticsWithMessage); + this.kind = kind; + } + + /** Asserts that the note, warning, or error was found in a given file. */ + @CanIgnoreReturnValue + public DiagnosticOnLine inFile(final JavaFileObject expectedFile) { + ImmutableList> diagnosticsInFile = + diagnosticsInFile(expectedFile); + if (diagnosticsInFile.isEmpty()) { + failureStrategy.fail( + String.format( + "Expected %s in %s, but only found them in %s", + kindToString(kind, false), expectedFile.getName(), sourceFilesWithDiagnostics())); + } + return new DiagnosticOnLine(kind, expectedFile, diagnosticsInFile); + } + + private ImmutableList> diagnosticsInFile( + JavaFileObject expectedFile) { + String expectedFilePath = expectedFile.toUri().getPath(); + return diagnosticsMatching( + diagnostic -> { + JavaFileObject source = diagnostic.getSource(); + return source != null && source.toUri().getPath().equals(expectedFilePath); + }); + } + + private ImmutableSet sourceFilesWithDiagnostics() { + return mapDiagnostics( + diagnostic -> + diagnostic.getSource() == null + ? "(no associated file)" + : diagnostic.getSource().getName()); + } + } + + /** Assertions that a note, warning, or error was found on a given line. */ + public final class DiagnosticOnLine extends DiagnosticsAssertions { + + private final Diagnostic.Kind kind; + private final JavaFileObject file; + + private DiagnosticOnLine( + Diagnostic.Kind kind, + JavaFileObject file, + ImmutableList> diagnostics) { + super(diagnostics); + this.kind = kind; + this.file = file; + } + + /** Asserts that the note, warning, or error was found on a given line. */ + @CanIgnoreReturnValue + public DiagnosticAtColumn onLine(final long expectedLine) { + ImmutableList> diagnosticsOnLine = + diagnosticsMatching(diagnostic -> diagnostic.getLineNumber() == expectedLine); + if (diagnosticsOnLine.isEmpty()) { + failureStrategy.fail( + String.format( + "Expected %s on line %d of %s, but only found them on line(s) %s", + kindToString(kind, false), expectedLine, file.getName(), linesWithDiagnostics())); + } + return new DiagnosticAtColumn(kind, file, expectedLine, diagnosticsOnLine); + } + + private ImmutableSet linesWithDiagnostics() { + return mapDiagnostics( + diagnostic -> + diagnostic.getLineNumber() == Diagnostic.NOPOS + ? "(no associated position)" + : Long.toString(diagnostic.getLineNumber())); + } + } + + /** Assertions that a note, warning, or error was found at a given column. */ + public final class DiagnosticAtColumn extends DiagnosticsAssertions { + + private final Diagnostic.Kind kind; + private final JavaFileObject file; + private final long line; + + private DiagnosticAtColumn( + Diagnostic.Kind kind, + JavaFileObject file, + long line, + ImmutableList> diagnostics) { + super(diagnostics); + this.kind = kind; + this.file = file; + this.line = line; + } + + /** Asserts that the note, warning, or error was found at a given column. */ + public void atColumn(final long expectedColumn) { + if (diagnosticsMatching(diagnostic -> diagnostic.getColumnNumber() == expectedColumn) + .isEmpty()) { + failureStrategy.fail( + String.format( + "Expected %s at %d:%d of %s, but only found them at column(s) %s", + kindToString(kind, false), + line, + expectedColumn, + file.getName(), + columnsWithDiagnostics())); + } + } + + private ImmutableSet columnsWithDiagnostics() { + return mapDiagnostics( + diagnostic -> + diagnostic.getColumnNumber() == Diagnostic.NOPOS + ? "(no associated position)" + : Long.toString(diagnostic.getColumnNumber())); + } + } +} diff --git a/src/main/java/com/google/testing/compile/CompilationSubjectFactory.java b/src/main/java/com/google/testing/compile/CompilationSubjectFactory.java new file mode 100644 index 00000000..396a0a37 --- /dev/null +++ b/src/main/java/com/google/testing/compile/CompilationSubjectFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; + +/** A {@link Truth} subject factory for a {@link Compilation}. */ +final class CompilationSubjectFactory extends SubjectFactory { + + @Override + public CompilationSubject getSubject(FailureStrategy fs, Compilation that) { + return new CompilationSubject(fs, that); + } +} diff --git a/src/main/java/com/google/testing/compile/Compiler.java b/src/main/java/com/google/testing/compile/Compiler.java new file mode 100644 index 00000000..152a356c --- /dev/null +++ b/src/main/java/com/google/testing/compile/Compiler.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import static com.google.common.base.Functions.toStringFunction; +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.tools.ToolProvider.getSystemJavaCompiler; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.testing.compile.Compilation.Status; +import java.util.Locale; +import javax.annotation.processing.Processor; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; + +/** An object that can {@link #compile} Java source files. */ +@AutoValue +public abstract class Compiler { + + /** Returns the {@code javac} compiler. */ + public static Compiler javac() { + return compiler(getSystemJavaCompiler()); + } + + /** Returns a {@link Compiler} that uses a given {@link JavaCompiler} instance. */ + public static Compiler compiler(JavaCompiler javaCompiler) { + return new AutoValue_Compiler(javaCompiler, ImmutableList.of(), ImmutableList.of()); + } + + abstract JavaCompiler javaCompiler(); + + /** The annotation processors applied during compilation. */ + public abstract ImmutableList processors(); + + /** The options passed to the compiler. */ + public abstract ImmutableList options(); + + /** + * Uses annotation processors during compilation. These replace any previously specified. + * + *

Note that most annotation processors cannot be reused for more than one compilation. + * + * @return a new instance with the same options and the given processors + */ + public final Compiler withProcessors(Processor... processors) { + return withProcessors(ImmutableList.copyOf(processors)); + } + + /** + * Uses annotation processors during compilation. These replace any previously specified. + * + *

Note that most annotation processors cannot be reused for more than one compilation. + * + * @return a new instance with the same options and the given processors + */ + public final Compiler withProcessors(Iterable processors) { + return copy(ImmutableList.copyOf(processors), options()); + } + + /** + * Passes command-line options to the compiler. These replace any previously specified. + * + * @return a new instance with the same processors and the given options + */ + public final Compiler withOptions(Object... options) { + return withOptions(ImmutableList.copyOf(options)); + } + + /** + * Passes command-line options to the compiler. These replace any previously specified. + * + * @return a new instance with the same processors and the given options + */ + public final Compiler withOptions(Iterable options) { + return copy(processors(), FluentIterable.from(options).transform(toStringFunction()).toList()); + } + + /** + * Compiles Java source files. + * + * @return the results of the compilation + */ + public final Compilation compile(JavaFileObject... files) { + return compile(ImmutableList.copyOf(files)); + } + + /** + * Compiles Java source files. + * + * @return the results of the compilation + */ + public final Compilation compile(Iterable files) { + DiagnosticCollector diagnosticCollector = new DiagnosticCollector<>(); + InMemoryJavaFileManager fileManager = + new InMemoryJavaFileManager( + javaCompiler().getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8)); + CompilationTask task = + javaCompiler() + .getTask( + null, // use the default because old versions of javac log some output on stderr + fileManager, + diagnosticCollector, + options(), + ImmutableSet.of(), + files); + task.setProcessors(processors()); + boolean succeeded = task.call(); + Compilation compilation = + new Compilation( + this, + files, + succeeded, + diagnosticCollector.getDiagnostics(), + fileManager.getOutputFiles()); + if (compilation.status().equals(Status.FAILURE) && compilation.errors().isEmpty()) { + throw new CompilationFailureException(compilation); + } + return compilation; + } + + private Compiler copy(ImmutableList processors, ImmutableList options) { + return new AutoValue_Compiler(javaCompiler(), processors, options); + } +} diff --git a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java index 09a341eb..9d6442bb 100644 --- a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java +++ b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java @@ -15,7 +15,6 @@ */ package com.google.testing.compile; -import com.google.common.base.CharMatcher; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; @@ -23,7 +22,6 @@ import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteSource; - import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -35,7 +33,6 @@ import java.net.URI; import java.nio.charset.Charset; import java.util.Map.Entry; - import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileManager; @@ -64,15 +61,17 @@ public JavaFileObject load(URI key) { } private static URI uriForFileObject(Location location, String packageName, String relativeName) { - return URI.create( - "mem:///" + location.getName() + '/' + CharMatcher.is('.').replaceFrom(packageName, '/') - + '/' + relativeName); + StringBuilder uri = new StringBuilder("mem:///").append(location.getName()).append('/'); + if (!packageName.isEmpty()) { + uri.append(packageName.replace('.', '/')).append('/'); + } + uri.append(relativeName); + return URI.create(uri.toString()); } private static URI uriForJavaFileObject(Location location, String className, Kind kind) { return URI.create( - "mem:///" + location.getName() + '/' + CharMatcher.is('.').replaceFrom(className, '/') - + kind.extension); + "mem:///" + location.getName() + '/' + className.replace('.', '/') + kind.extension); } @Override diff --git a/src/main/java/com/google/testing/compile/JavaFileObjectSubject.java b/src/main/java/com/google/testing/compile/JavaFileObjectSubject.java new file mode 100644 index 00000000..b4fc9fcc --- /dev/null +++ b/src/main/java/com/google/testing/compile/JavaFileObjectSubject.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.truth.Truth.assertAbout; +import static com.google.testing.compile.JavaFileObjects.asByteSource; +import static com.google.testing.compile.TreeDiffer.diffCompilationUnits; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteSource; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.testing.compile.Parser.ParseResult; +import com.sun.source.tree.CompilationUnitTree; +import java.io.IOException; +import java.nio.charset.Charset; +import javax.annotation.Nullable; +import javax.tools.JavaFileObject; + +/** Assertions about {@link JavaFileObject}s. */ +public final class JavaFileObjectSubject extends Subject { + + private static final SubjectFactory FACTORY = + new JavaFileObjectSubjectFactory(); + + /** Returns a {@link SubjectFactory} for {@link JavaFileObjectSubject}s. */ + public static SubjectFactory javaFileObjects() { + return FACTORY; + } + + /** Starts making assertions about a {@link JavaFileObject}. */ + public static JavaFileObjectSubject assertThat(JavaFileObject actual) { + return assertAbout(FACTORY).that(actual); + } + + JavaFileObjectSubject(FailureStrategy failureStrategy, JavaFileObject actual) { + super(failureStrategy, actual); + } + + @Override + protected String actualCustomStringRepresentation() { + return actual().toUri().getPath(); + } + + /** + * If {@code other} is a {@link JavaFileObject}, tests that their contents are equal. Otherwise + * uses {@link Object#equals(Object)}. + */ + @Override + public void isEqualTo(@Nullable Object other) { + if (!(other instanceof JavaFileObject)) { + super.isEqualTo(other); + } + + JavaFileObject otherFile = (JavaFileObject) other; + try { + if (!asByteSource(actual()).contentEquals(asByteSource(otherFile))) { + fail("is equal to", other); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Asserts that the actual file's contents are equal to {@code expected}. */ + public void hasContents(ByteSource expected) { + try { + if (!asByteSource(actual()).contentEquals(expected)) { + fail("has contents", expected); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a {@link StringSubject} that makes assertions about the contents of the actual file as + * a string. + */ + public StringSubject contentsAsString(Charset charset) { + try { + return check() + .that(JavaFileObjects.asByteSource(actual()).asCharSource(charset).read()) + .named("the contents of " + actualAsString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a {@link StringSubject} that makes assertions about the contents of the actual file as + * a UTF-8 string. + */ + public StringSubject contentsAsUtf8String() { + return contentsAsString(UTF_8); + } + + /** + * Asserts that the actual file is a source file with contents equivalent to {@code + * expectedSource}. + */ + public void hasSourceEquivalentTo(JavaFileObject expectedSource) { + ParseResult actualResult = Parser.parse(ImmutableList.of(actual())); + CompilationUnitTree actualTree = getOnlyElement(actualResult.compilationUnits()); + + ParseResult expectedResult = Parser.parse(ImmutableList.of(expectedSource)); + CompilationUnitTree expectedTree = getOnlyElement(expectedResult.compilationUnits()); + + TreeDifference treeDifference = diffCompilationUnits(expectedTree, actualTree); + + if (!treeDifference.isEmpty()) { + String diffReport = + treeDifference.getDiffReport( + new TreeContext(expectedTree, expectedResult.trees()), + new TreeContext(actualTree, actualResult.trees())); + try { + fail( + Joiner.on('\n') + .join( + String.format("is equivalent to <%s>.", expectedSource.toUri().getPath()), + "", + "Diffs:", + "======", + "", + diffReport, + "", + "Expected Source:", + "================", + "", + expectedSource.getCharContent(false), + "", + "Actual Source:", + "==============", + "", + actual().getCharContent(false))); + } catch (IOException e) { + throw new IllegalStateException( + "Couldn't read from JavaFileObject when it was already in memory.", e); + } + } + } +} diff --git a/src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java b/src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java new file mode 100644 index 00000000..6207b546 --- /dev/null +++ b/src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.SubjectFactory; +import javax.tools.JavaFileObject; + +/** A factory for {@link JavaFileObjectSubject}s. */ +final class JavaFileObjectSubjectFactory + extends SubjectFactory { + + @Override + public JavaFileObjectSubject getSubject(FailureStrategy fs, JavaFileObject that) { + return new JavaFileObjectSubject(fs, that); + } +} diff --git a/src/main/java/com/google/testing/compile/JavaFileObjects.java b/src/main/java/com/google/testing/compile/JavaFileObjects.java index a1e8b00f..81900c32 100644 --- a/src/main/java/com/google/testing/compile/JavaFileObjects.java +++ b/src/main/java/com/google/testing/compile/JavaFileObjects.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.ByteSource; import com.google.common.io.Resources; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -37,7 +36,6 @@ import java.net.URL; import java.nio.charset.Charset; import java.util.Arrays; - import javax.tools.ForwardingJavaFileObject; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; @@ -183,13 +181,10 @@ static ByteSource asByteSource(final JavaFileObject javaFileObject) { private static final class JarFileJavaFileObject extends ForwardingJavaFileObject { - final String name; - JarFileJavaFileObject(URL jarUrl) { // this is a cheap way to give SimpleJavaFileObject a uri that satisfies the contract // then we just override the methods that we want to behave differently for jars super(new ResourceSourceJavaFileObject(jarUrl, getPathUri(jarUrl))); - this.name = jarUrl.toString(); } static final Splitter JAR_URL_SPLITTER = Splitter.on('!'); @@ -203,11 +198,6 @@ static final URI getPathUri(URL jarUrl) { pathPart); return URI.create(pathPart); } - - @Override - public String getName() { - return name; - } } private static final class ResourceSourceJavaFileObject extends SimpleJavaFileObject { diff --git a/src/main/java/com/google/testing/compile/JavaSourcesSubject.java b/src/main/java/com/google/testing/compile/JavaSourcesSubject.java index 978b70c0..c63ac104 100644 --- a/src/main/java/com/google/testing/compile/JavaSourcesSubject.java +++ b/src/main/java/com/google/testing/compile/JavaSourcesSubject.java @@ -15,10 +15,10 @@ */ package com.google.testing.compile; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertAbout; +import static com.google.testing.compile.CompilationSubject.compilations; +import static com.google.testing.compile.Compiler.javac; import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources; -import static javax.tools.JavaFileObject.Kind.CLASS; import com.google.common.base.Function; import com.google.common.base.Joiner; @@ -35,20 +35,20 @@ import com.google.common.truth.FailureStrategy; import com.google.common.truth.Subject; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.testing.compile.Compilation.Result; +import com.google.testing.compile.CompilationSubject.DiagnosticAtColumn; +import com.google.testing.compile.CompilationSubject.DiagnosticInFile; +import com.google.testing.compile.CompilationSubject.DiagnosticOnLine; +import com.google.testing.compile.Parser.ParseResult; import com.sun.source.tree.CompilationUnitTree; - import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; - import javax.annotation.processing.Processor; import javax.tools.Diagnostic; import javax.tools.Diagnostic.Kind; -import javax.tools.FileObject; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; @@ -125,47 +125,9 @@ private CompilationClause(Iterable processors) { this.processors = ImmutableSet.copyOf(processors); } - /** Returns a {@code String} report describing the contents of a given generated file. */ - private String reportFileGenerated(JavaFileObject generatedFile) { - try { - StringBuilder entry = - new StringBuilder().append(String.format("\n%s:\n", generatedFile.toUri().getPath())); - if (generatedFile.getKind().equals(CLASS)) { - entry.append(String.format("[generated class file (%s bytes)]", - JavaFileObjects.asByteSource(generatedFile).size())); - } else { - entry.append(generatedFile.getCharContent(true)); - } - return entry.append("\n").toString(); - } catch (IOException e) { - throw new IllegalStateException("Couldn't read from JavaFileObject when it was " - + "already in memory.", e); - } - } - - /** - * Returns a {@code String} report describing what files were generated in the given - * {@link Compilation.Result} - */ - private String reportFilesGenerated(Compilation.Result result) { - FluentIterable generatedFiles = - FluentIterable.from(result.generatedSources()); - StringBuilder message = new StringBuilder("\n\n"); - if (generatedFiles.isEmpty()) { - return message.append("(No files were generated.)\n").toString(); - } else { - message.append("Generated Files\n") - .append("===============\n"); - for (JavaFileObject generatedFile : generatedFiles) { - message.append(reportFileGenerated(generatedFile)); - } - return message.toString(); - } - } - @Override public void parsesAs(JavaFileObject first, JavaFileObject... rest) { - Compilation.ParseResult actualResult = Compilation.parse(getSubject()); + ParseResult actualResult = Parser.parse(actual()); ImmutableList> errors = actualResult.diagnosticsByKind().get(Kind.ERROR); if (!errors.isEmpty()) { @@ -176,7 +138,7 @@ public void parsesAs(JavaFileObject first, JavaFileObject... rest) { } failureStrategy.fail(message.toString()); } - final Compilation.ParseResult expectedResult = Compilation.parse(Lists.asList(first, rest)); + final ParseResult expectedResult = Parser.parse(Lists.asList(first, rest)); final FluentIterable actualTrees = FluentIterable.from( actualResult.compilationUnits()); final FluentIterable expectedTrees = FluentIterable.from( @@ -290,45 +252,29 @@ private void failWithCandidate(JavaFileObject expectedSource, @CanIgnoreReturnValue @Override public SuccessfulCompilationClause compilesWithoutError() { - return new SuccessfulCompilationBuilder(successfulCompilationResult()); + Compilation compilation = compilation(); + check().about(compilations()).that(compilation).succeeded(); + return new SuccessfulCompilationBuilder(compilation); } @CanIgnoreReturnValue @Override public CleanCompilationClause compilesWithoutWarnings() { - return new CleanCompilationBuilder(successfulCompilationResult()).withWarningCount(0); - } - - private Compilation.Result successfulCompilationResult() { - Compilation.Result result = - Compilation.compile(processors, options, getSubject()); - if (!result.successful()) { - ImmutableList> errors = - result.diagnosticsByKind().get(Kind.ERROR); - StringBuilder message = new StringBuilder("Compilation produced the following errors:\n"); - for (Diagnostic error : errors) { - message.append('\n'); - message.append(error); - } - message.append('\n'); - message.append(reportFilesGenerated(result)); - failureStrategy.fail(message.toString()); - } - return result; + Compilation compilation = compilation(); + check().about(compilations()).that(compilation).succeededWithoutWarnings(); + return new CleanCompilationBuilder(compilation); } @CanIgnoreReturnValue @Override public UnsuccessfulCompilationClause failsToCompile() { - Result result = Compilation.compile(processors, options, getSubject()); - if (result.successful()) { - String message = Joiner.on('\n').join( - "Compilation was expected to fail, but contained no errors.", - "", - reportFilesGenerated(result)); - failureStrategy.fail(message); - } - return new UnsuccessfulCompilationBuilder(result); + Compilation compilation = compilation(); + check().about(compilations()).that(compilation).failed(); + return new UnsuccessfulCompilationBuilder(compilation); + } + + private Compilation compilation() { + return javac().withProcessors(processors).withOptions(options).compile(actual()); } } @@ -340,244 +286,57 @@ private CompilationClause newCompilationClause(Iterable pro return new CompilationClause(processors); } - private static String messageListing(Iterable> diagnostics, - String headingFormat, Object... formatArgs) { - StringBuilder listing = new StringBuilder(String.format(headingFormat, formatArgs)) - .append('\n'); - for (Diagnostic diagnostic : diagnostics) { - listing.append(diagnostic.getMessage(null)).append('\n'); - } - return listing.toString(); - } - - /** - * Returns a string representation of a diagnostic kind. - * - * @param expectingSpecificCount {@code true} if being used after a count, as in "Expected 5 - * errors"; {@code false} if being used to describe one message, as in "Expected a warning - * containing…". - */ - private static String kindToString(Kind kind, boolean expectingSpecificCount) { - switch (kind) { - case ERROR: - return expectingSpecificCount ? "errors" : "an error"; - - case MANDATORY_WARNING: - case WARNING: - return expectingSpecificCount ? "warnings" : "a warning"; - - case NOTE: - return expectingSpecificCount ? "notes" : "a note"; - - case OTHER: - return expectingSpecificCount ? "diagnostic messages" : "a diagnostic message"; - - default: - throw new AssertionError(kind); - } - } - /** * Base implementation of {@link CompilationWithWarningsClause}. * * @param T the type parameter for {@link CompilationWithWarningsClause}. {@code this} must be an * instance of {@code T}; otherwise some calls will throw {@link ClassCastException}. */ - private abstract class CompilationWithWarningsBuilder - implements CompilationWithWarningsClause { - protected final Compilation.Result result; + abstract class CompilationWithWarningsBuilder implements CompilationWithWarningsClause { + protected final Compilation compilation; - protected CompilationWithWarningsBuilder(Compilation.Result result) { - this.result = result; + protected CompilationWithWarningsBuilder(Compilation compilation) { + this.compilation = compilation; } @CanIgnoreReturnValue @Override public T withNoteCount(int noteCount) { - return withDiagnosticCount(Kind.NOTE, noteCount); + check().about(compilations()).that(compilation).hadNoteCount(noteCount); + return thisObject(); } @CanIgnoreReturnValue @Override public FileClause withNoteContaining(String messageFragment) { - return withDiagnosticContaining(Kind.NOTE, messageFragment); + return new FileBuilder( + check().about(compilations()).that(compilation).hadNoteContaining(messageFragment)); } @CanIgnoreReturnValue @Override public T withWarningCount(int warningCount) { - return withDiagnosticCount(Kind.WARNING, warningCount); + check().about(compilations()).that(compilation).hadWarningCount(warningCount); + return thisObject(); } @CanIgnoreReturnValue @Override public FileClause withWarningContaining(String messageFragment) { - return withDiagnosticContaining(Kind.WARNING, messageFragment); + return new FileBuilder( + check().about(compilations()).that(compilation).hadWarningContaining(messageFragment)); } - /** - * Fails if the number of diagnostic messages of a given kind is not {@code expectedCount}. - */ @CanIgnoreReturnValue - protected T withDiagnosticCount(Kind kind, int expectedCount) { - List> diagnostics = result.diagnosticsByKind().get(kind); - if (diagnostics.size() != expectedCount) { - failureStrategy.fail( - messageListing( - diagnostics, - "Expected %d %s, but found the following %d %s:", - expectedCount, - kindToString(kind, true), - diagnostics.size(), - kindToString(kind, true))); - } + public T withErrorCount(int errorCount) { + check().about(compilations()).that(compilation).hadErrorCount(errorCount); return thisObject(); } - /** - * Fails if there is no diagnostic message of a given kind that contains - * {@code messageFragment}. - */ @CanIgnoreReturnValue - protected FileClause withDiagnosticContaining( - final Kind kind, final String messageFragment) { - FluentIterable> diagnostics = - FluentIterable.from(result.diagnosticsByKind().get(kind)); - final FluentIterable> diagnosticsWithMessage = - diagnostics.filter( - new Predicate>() { - @Override - public boolean apply(Diagnostic input) { - return input.getMessage(null).contains(messageFragment); - } - }); - if (diagnosticsWithMessage.isEmpty()) { - failureStrategy.fail( - messageListing( - diagnostics, - "Expected %s containing \"%s\", but only found:", - kindToString(kind, false), - messageFragment)); - } - return new FileClause() { - - @Override - public T and() { - return thisObject(); - } - - @CanIgnoreReturnValue - @Override - public LineClause in(final JavaFileObject file) { - final FluentIterable> diagnosticsInFile = - diagnosticsWithMessage.filter( - new Predicate>() { - @Override - public boolean apply(Diagnostic input) { - return ((input.getSource() != null) - && file.toUri().getPath().equals(input.getSource().toUri().getPath())); - } - }); - if (diagnosticsInFile.isEmpty()) { - failureStrategy.fail( - String.format( - "Expected %s in %s, but only found them in %s", - kindToString(kind, false), - file.getName(), - diagnosticsWithMessage - .transform( - new Function, String>() { - @Override - public String apply(Diagnostic input) { - return (input.getSource() != null) - ? input.getSource().getName() - : "(no associated file)"; - } - }) - .toSet())); - } - return new LineClause() { - @Override - public T and() { - return thisObject(); - } - - @CanIgnoreReturnValue - @Override - public ColumnClause onLine(final long lineNumber) { - final FluentIterable> diagnosticsOnLine = - diagnosticsWithMessage.filter( - new Predicate>() { - @Override - public boolean apply(Diagnostic input) { - return lineNumber == input.getLineNumber(); - } - }); - if (diagnosticsOnLine.isEmpty()) { - failureStrategy.fail( - String.format( - "Expected %s on line %d of %s, but only found them on line(s) %s", - kindToString(kind, false), - lineNumber, - file.getName(), - diagnosticsInFile - .transform( - new Function, String>() { - @Override - public String apply(Diagnostic input) { - long errLine = input.getLineNumber(); - return (errLine != Diagnostic.NOPOS) - ? errLine + "" - : "(no associated position)"; - } - }) - .toSet())); - } - return new ColumnClause() { - @Override - public T and() { - return thisObject(); - } - - @CanIgnoreReturnValue - @Override - public ChainingClause atColumn(final long columnNumber) { - FluentIterable> diagnosticsAtColumn = - diagnosticsOnLine.filter( - new Predicate>() { - @Override - public boolean apply(Diagnostic input) { - return columnNumber == input.getColumnNumber(); - } - }); - if (diagnosticsAtColumn.isEmpty()) { - failureStrategy.fail( - String.format( - "Expected %s at %d:%d of %s, but only found them at column(s) %s", - kindToString(kind, false), - lineNumber, - columnNumber, - file.getName(), - diagnosticsOnLine - .transform( - new Function, String>() { - @Override - public String apply(Diagnostic input) { - long errCol = input.getColumnNumber(); - return (errCol != Diagnostic.NOPOS) - ? errCol + "" - : "(no associated position)"; - } - }) - .toSet())); - } - return this; - } - }; - } - }; - } - }; + public FileClause withErrorContaining(String messageFragment) { + return new FileBuilder( + check().about(compilations()).that(compilation).hadErrorContaining(messageFragment)); } /** @@ -587,6 +346,49 @@ public String apply(Diagnostic input) { protected final T thisObject() { return (T) this; } + + private final class FileBuilder implements FileClause { + private final DiagnosticInFile diagnosticInFile; + + private FileBuilder(DiagnosticInFile diagnosticInFile) { + this.diagnosticInFile = diagnosticInFile; + } + + @Override + public T and() { + return thisObject(); + } + + @Override + public LineClause in(JavaFileObject file) { + final DiagnosticOnLine diagnosticOnLine = diagnosticInFile.inFile(file); + + return new LineClause() { + @Override + public T and() { + return thisObject(); + } + + @Override + public ColumnClause onLine(long lineNumber) { + final DiagnosticAtColumn diagnosticAtColumn = diagnosticOnLine.onLine(lineNumber); + + return new ColumnClause() { + @Override + public T and() { + return thisObject(); + } + + @Override + public ChainingClause atColumn(long columnNumber) { + diagnosticAtColumn.atColumn(columnNumber); + return this; + } + }; + } + }; + } + } } /** @@ -599,14 +401,14 @@ protected final T thisObject() { private abstract class GeneratedCompilationBuilder extends CompilationWithWarningsBuilder implements GeneratedPredicateClause, ChainingClause> { - protected GeneratedCompilationBuilder(Result result) { - super(result); + protected GeneratedCompilationBuilder(Compilation compilation) { + super(compilation); } @CanIgnoreReturnValue @Override public T generatesSources(JavaFileObject first, JavaFileObject... rest) { - new JavaSourcesSubject(failureStrategy, result.generatedSources()) + new JavaSourcesSubject(failureStrategy, compilation.generatedSourceFiles()) .parsesAs(first, rest); return thisObject(); } @@ -615,7 +417,7 @@ public T generatesSources(JavaFileObject first, JavaFileObject... rest) { @Override public T generatesFiles(JavaFileObject first, JavaFileObject... rest) { for (JavaFileObject expected : Lists.asList(first, rest)) { - if (!wasGenerated(result, expected)) { + if (!wasGenerated(expected)) { failureStrategy.fail("Did not find a generated file corresponding to " + expected.getName()); } @@ -623,12 +425,12 @@ public T generatesFiles(JavaFileObject first, JavaFileObject... rest) { return thisObject(); } - boolean wasGenerated(Compilation.Result result, JavaFileObject expected) { + boolean wasGenerated(JavaFileObject expected) { ByteSource expectedByteSource = JavaFileObjects.asByteSource(expected); - for (JavaFileObject generated : result.generatedFilesByKind().get(expected.getKind())) { + for (JavaFileObject generated : compilation.generatedFiles()) { try { - ByteSource generatedByteSource = JavaFileObjects.asByteSource(generated); - if (expectedByteSource.contentEquals(generatedByteSource)) { + if (generated.getKind().equals(expected.getKind()) + && expectedByteSource.contentEquals(JavaFileObjects.asByteSource(generated))) { return true; } } catch (IOException e) { @@ -642,28 +444,29 @@ boolean wasGenerated(Compilation.Result result, JavaFileObject expected) { @Override public SuccessfulFileClause generatesFileNamed( JavaFileManager.Location location, String packageName, String relativeName) { - // TODO(gak): Validate that these inputs aren't null, location is an output location, and - // packageName is a valid package name. - // We're relying on the implementation of location.getName() to be equivalent to the path of - // the location. - String expectedFilename = new StringBuilder(location.getName()).append('/') - .append(packageName.replace('.', '/')).append('/').append(relativeName).toString(); - - for (JavaFileObject generated : result.generatedFilesByKind().values()) { - if (generated.toUri().getPath().endsWith(expectedFilename)) { - return new SuccessfulFileBuilder( - this, generated.toUri().getPath(), JavaFileObjects.asByteSource(generated)); + final JavaFileObjectSubject javaFileObjectSubject = + check() + .about(compilations()) + .that(compilation) + .generatedFile(location, packageName, relativeName); + return new SuccessfulFileClause() { + @Override + public GeneratedPredicateClause and() { + return GeneratedCompilationBuilder.this; } - } - StringBuilder encounteredFiles = new StringBuilder(); - for (JavaFileObject generated : result.generatedFilesByKind().values()) { - if (generated.toUri().getPath().contains(location.getName())) { - encounteredFiles.append(" ").append(generated.toUri().getPath()).append('\n'); + + @Override + public SuccessfulFileClause withContents(ByteSource expectedByteSource) { + javaFileObjectSubject.hasContents(expectedByteSource); + return this; } - } - failureStrategy.fail("Did not find a generated file corresponding to " + relativeName - + " in package " + packageName + "; Found: " + encounteredFiles.toString()); - return new SuccessfulFileBuilder(this, null, null); + + @Override + public SuccessfulFileClause withStringContents(Charset charset, String expectedString) { + javaFileObjectSubject.contentsAsString(charset).isEqualTo(expectedString); + return this; + } + }; } @Override @@ -672,25 +475,18 @@ public GeneratedPredicateClause and() { } } + final class CompilationBuilder extends GeneratedCompilationBuilder { + CompilationBuilder(Compilation compilation) { + super(compilation); + } + } + private final class UnsuccessfulCompilationBuilder extends CompilationWithWarningsBuilder implements UnsuccessfulCompilationClause { - UnsuccessfulCompilationBuilder(Compilation.Result result) { - super(result); - checkArgument(!result.successful()); - } - - @CanIgnoreReturnValue - @Override - public UnsuccessfulCompilationClause withErrorCount(int errorCount) { - return withDiagnosticCount(Kind.ERROR, errorCount); - } - - @CanIgnoreReturnValue - @Override - public FileClause withErrorContaining(String messageFragment) { - return withDiagnosticContaining(Kind.ERROR, messageFragment); + UnsuccessfulCompilationBuilder(Compilation compilation) { + super(compilation); } } @@ -698,9 +494,8 @@ private final class SuccessfulCompilationBuilder extends GeneratedCompilationBuilder implements SuccessfulCompilationClause { - SuccessfulCompilationBuilder(Compilation.Result result) { - super(result); - checkArgument(result.successful()); + SuccessfulCompilationBuilder(Compilation compilation) { + super(compilation); } } @@ -708,60 +503,8 @@ private final class CleanCompilationBuilder extends GeneratedCompilationBuilder implements CleanCompilationClause { - CleanCompilationBuilder(Compilation.Result result) { - super(result); - checkArgument(result.successful()); - } - } - - private final class SuccessfulFileBuilder implements SuccessfulFileClause { - private final GeneratedPredicateClause chainedClause; - private final String generatedFilePath; - private final ByteSource generatedByteSource; - - SuccessfulFileBuilder( - GeneratedPredicateClause chainedClause, - String generatedFilePath, - ByteSource generatedByteSource) { - this.chainedClause = chainedClause; - this.generatedFilePath = generatedFilePath; - this.generatedByteSource = generatedByteSource; - } - - @Override - public GeneratedPredicateClause and() { - return chainedClause; - } - - @CanIgnoreReturnValue - @Override - public SuccessfulFileClause withContents(ByteSource expectedByteSource) { - try { - if (!expectedByteSource.contentEquals(generatedByteSource)) { - failureStrategy.fail("The contents in " + generatedFilePath - + " did not match the expected contents"); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return this; - } - - @CanIgnoreReturnValue - @Override - public SuccessfulFileClause withStringContents(Charset charset, String expectedString) { - try { - String generatedString = generatedByteSource.asCharSource(charset).read(); - if (!generatedString.equals(expectedString)) { - failureStrategy.failComparing( - "The contents in " + generatedFilePath + " did not match the expected string", - expectedString, - generatedString); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return this; + CleanCompilationBuilder(Compilation compilation) { + super(compilation); } } diff --git a/src/main/java/com/google/testing/compile/MoreTrees.java b/src/main/java/com/google/testing/compile/MoreTrees.java index b83456bb..7ffec537 100644 --- a/src/main/java/com/google/testing/compile/MoreTrees.java +++ b/src/main/java/com/google/testing/compile/MoreTrees.java @@ -15,34 +15,11 @@ */ package com.google.testing.compile; -import static com.sun.source.tree.Tree.Kind.ANNOTATION_TYPE; -import static com.sun.source.tree.Tree.Kind.BOOLEAN_LITERAL; -import static com.sun.source.tree.Tree.Kind.BREAK; -import static com.sun.source.tree.Tree.Kind.CHAR_LITERAL; -import static com.sun.source.tree.Tree.Kind.CLASS; -import static com.sun.source.tree.Tree.Kind.CONTINUE; -import static com.sun.source.tree.Tree.Kind.DOUBLE_LITERAL; -import static com.sun.source.tree.Tree.Kind.ENUM; -import static com.sun.source.tree.Tree.Kind.FLOAT_LITERAL; -import static com.sun.source.tree.Tree.Kind.IDENTIFIER; -import static com.sun.source.tree.Tree.Kind.INTERFACE; -import static com.sun.source.tree.Tree.Kind.INT_LITERAL; -import static com.sun.source.tree.Tree.Kind.LABELED_STATEMENT; -import static com.sun.source.tree.Tree.Kind.LONG_LITERAL; -import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT; -import static com.sun.source.tree.Tree.Kind.METHOD; -import static com.sun.source.tree.Tree.Kind.NULL_LITERAL; -import static com.sun.source.tree.Tree.Kind.STRING_LITERAL; -import static com.sun.source.tree.Tree.Kind.TYPE_PARAMETER; -import static com.sun.source.tree.Tree.Kind.VARIABLE; - import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; - +import com.google.testing.compile.Parser.ParseResult; import com.sun.source.tree.BreakTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; @@ -57,9 +34,7 @@ import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; - import java.util.Arrays; - import javax.annotation.Nullable; /** @@ -76,19 +51,20 @@ static CompilationUnitTree parseLinesToTree(String... source) { /** Parses the source given into a {@link CompilationUnitTree}. */ static CompilationUnitTree parseLinesToTree(Iterable source) { - Iterable parseResults = Compilation.parse(ImmutableList.of( - JavaFileObjects.forSourceLines("", source))).compilationUnits(); + Iterable parseResults = + Parser.parse(ImmutableList.of(JavaFileObjects.forSourceLines("", source))) + .compilationUnits(); return Iterables.getOnlyElement(parseResults); } - /** Parses the source given and produces a {@link Compilation.ParseResult}. */ - static Compilation.ParseResult parseLines(String... source) { + /** Parses the source given and produces a {@link ParseResult}. */ + static ParseResult parseLines(String... source) { return parseLines(Arrays.asList(source)); } - /** Parses the source given and produces a {@link Compilation.ParseResult}. */ - static Compilation.ParseResult parseLines(Iterable source) { - return Compilation.parse(ImmutableList.of(JavaFileObjects.forSourceLines("", source))); + /** Parses the source given and produces a {@link ParseResult}. */ + static ParseResult parseLines(Iterable source) { + return Parser.parse(ImmutableList.of(JavaFileObjects.forSourceLines("", source))); } /** diff --git a/src/main/java/com/google/testing/compile/Parser.java b/src/main/java/com/google/testing/compile/Parser.java new file mode 100644 index 00000000..849a9d2f --- /dev/null +++ b/src/main/java/com/google/testing/compile/Parser.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static java.lang.Boolean.TRUE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.function.Predicate.isEqual; +import static javax.tools.Diagnostic.Kind.ERROR; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimaps; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ErroneousTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreeScanner; +import com.sun.source.util.Trees; +import com.sun.tools.javac.api.JavacTool; +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.ToolProvider; + +/** Methods to parse Java source files. */ +public final class Parser { + + /** + * Parses {@code sources} into {@linkplain CompilationUnitTree compilation units}. This method + * does not compile the sources. + */ + static ParseResult parse(Iterable sources) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + DiagnosticCollector diagnosticCollector = new DiagnosticCollector<>(); + InMemoryJavaFileManager fileManager = + new InMemoryJavaFileManager( + compiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8)); + JavacTask task = + ((JavacTool) compiler) + .getTask( + null, // explicitly use the default because old javac logs some output on stderr + fileManager, + diagnosticCollector, + ImmutableSet.of(), + ImmutableSet.of(), + sources); + try { + Iterable parsedCompilationUnits = task.parse(); + List> diagnostics = diagnosticCollector.getDiagnostics(); + if (foundParseErrors(parsedCompilationUnits, diagnostics)) { + throw new IllegalStateException( + "error while parsing:\n" + Joiner.on('\n').join(diagnostics)); + } + return new ParseResult( + sortDiagnosticsByKind(diagnostics), parsedCompilationUnits, Trees.instance(task)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns {@code true} if errors were found while parsing source files. + * + *

Normally, the parser reports error diagnostics, but in some cases there are no diagnostics; + * instead the parse tree contains {@linkplain ErroneousTree "erroneous"} nodes. + */ + private static boolean foundParseErrors( + Iterable parsedCompilationUnits, + List> diagnostics) { + return diagnostics.stream().map(Diagnostic::getKind).anyMatch(isEqual(ERROR)) + || Iterables.any(parsedCompilationUnits, Parser::hasErrorNode); + } + + /** + * Returns {@code true} if the tree contains at least one {@linkplain ErroneousTree "erroneous"} + * node. + */ + private static boolean hasErrorNode(Tree tree) { + return isTrue(HAS_ERRONEOUS_NODE.scan(tree, false)); + } + + private static final TreeScanner HAS_ERRONEOUS_NODE = + new TreeScanner() { + @Override + public Boolean visitErroneous(ErroneousTree node, Boolean p) { + return true; + } + + @Override + public Boolean scan(Iterable nodes, Boolean p) { + for (Tree node : firstNonNull(nodes, ImmutableList.of())) { + if (isTrue(scan(node, p))) { + return true; + } + } + return p; + } + + @Override + public Boolean scan(Tree tree, Boolean p) { + return isTrue(p) ? p : super.scan(tree, p); + } + + @Override + public Boolean reduce(Boolean r1, Boolean r2) { + return isTrue(r1) || isTrue(r2); + } + }; + + private static boolean isTrue(Boolean p) { + return TRUE.equals(p); + } + + private static ImmutableListMultimap> + sortDiagnosticsByKind(Iterable> diagnostics) { + return Multimaps.index(diagnostics, input -> input.getKind()); + } + + /** + * The diagnostic, parse trees, and {@link Trees} instance for a parse task. + * + *

Note: It is possible for the {@link Trees} instance contained within a {@code ParseResult} + * to be invalidated by a call to {@link com.sun.tools.javac.api.JavacTaskImpl#cleanup()}. Though + * we do not currently expose the {@link JavacTask} used to create a {@code ParseResult} to {@code + * cleanup()} calls on its underlying implementation, this should be acknowledged as an + * implementation detail that could cause unexpected behavior when making calls to methods in + * {@link Trees}. + */ + static final class ParseResult { + private final ImmutableListMultimap> + diagnostics; + private final ImmutableList compilationUnits; + private final Trees trees; + + ParseResult( + ImmutableListMultimap> diagnostics, + Iterable compilationUnits, + Trees trees) { + this.trees = trees; + this.compilationUnits = ImmutableList.copyOf(compilationUnits); + this.diagnostics = diagnostics; + } + + ImmutableListMultimap> + diagnosticsByKind() { + return diagnostics; + } + + Iterable compilationUnits() { + return compilationUnits; + } + + Trees trees() { + return trees; + } + } +} diff --git a/src/main/java/com/google/testing/compile/package-info.java b/src/main/java/com/google/testing/compile/package-info.java index 159a4961..a750743c 100644 --- a/src/main/java/com/google/testing/compile/package-info.java +++ b/src/main/java/com/google/testing/compile/package-info.java @@ -15,57 +15,73 @@ */ /** - * This package contains two {@link com.google.common.truth.Truth} subjects - * ({@link JavaSourceSubjectFactory#javaSource} and {@link JavaSourcesSubjectFactory#javaSources}) - * that facilitate testing {@code javac} compilation. Particularly, this enables quick, small tests - * of {@linkplain javax.annotation.processing.Processor annotation processors} without forking - * {@code javac} or creating separate integration test projects. + * This package contains classes that let you compile Java source code in tests and make assertions + * about the results. This lets you easily test {@linkplain javax.annotation.processing.Processor + * annotation processors} without forking {@code javac} or creating separate integration test + * projects. * - *

The simplest invocation looks like this:

   {@code
+ * 
    + *
  • {@link Compiler} lets you choose command-line options, annotation processors, and source + * files to compile. + *
  • {@link Compilation} represents the immutable result of compiling source files: diagnostics + * and generated files. + *
  • {@link CompilationSubject} lets you make assertions about {@link Compilation} objects. + *
  • {@link JavaFileObjectSubject} lets you make assertions about {@link + * javax.tools.JavaFileObject} objects. + *
* - * assertAbout(javaSource()) - * .that(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}")) - * .compilesWithoutError(); - * }
+ *

A simple example that tests that compiling a source file succeeded is: * - *

The above code snippet tests that the provided source compiles without error. There is not - * much utility in testing compilation for simple sources, but the API also allows for the addition - * of {@linkplain javax.annotation.processing.Processor annotation processors}. Here is the same - * example with a processor:

   {@code
+ * 
+ * Compilation compilation =
+ *     javac().compile(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}");
+ * assertThat(compilation).succeeded();
+ * 
* - * assertAbout(javaSource()) - * .that(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}")) - * .processedWith(new MyAnnotationProcessor()) - * .compilesWithoutError(); - * }
+ *

A similar example that tests that compiling a source file with an annotation processor + * succeeded without errors or warnings (including compiling any source files generated by the + * annotation processor) is: * - *

This snippet tests that the given source and all sources generated by the processor - * compile without error. Any exception thrown by the annotation processor will be (wrapped by the - * compiler and) thrown by the tester. + *

+ * Compilation compilation =
+ *     javac()
+ *         .withProcessors(new MyAnnotationProcessor())
+ *         .compile(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}");
+ * assertThat(compilation).succeededWithoutWarnings();
+ * 
* - *

Further tests can be applied to compilation results as well. For example, the following - * snippet tests that a file (a class path resource) processed with an annotation processor - * generates a source file equivalent to a golden file:

   {@code
+ * 

You can make assertions about the files generated during the compilation as well. For example, + * the following snippet tests that compiling a source file with an annotation processor generates a + * source file equivalent to a golden file: * - * assertAbout(javaSource()) - * .that(JavaFileObjects.forResource("HelloWorld.java")) - * .processedWith(new MyAnnotationProcessor()) - * .compilesWithoutError() - * .and().generatesSources(JavaFileObjects.forResource("GeneratedHelloWorld.java")); - * }

+ *
+ * Compilation compilation =
+ *     javac()
+ *         .withProcessors(new MyAnnotationProcessor())
+ *         .compile(JavaFileObjects.forResource("HelloWorld.java"));
+ * assertThat(compilation).succeeded();
+ * assertThat(compilation)
+ *     .generatedSourceFile("GeneratedHelloWorld")
+ *     .hasSourceEquivalentTo(JavaFileObjects.forResource("GeneratedHelloWorld.java"));
+ * 
* - *

Finally, negative tests are possible as well. The following tests that a processor adds an - * error to a source file:

   {@code
+ * 

You can also test that errors or other diagnostics were reported. The following tests that + * compiling a source file with an annotation processor reported an error: * - * JavaFileObject fileObject = JavaFileObjects.forResource("HelloWorld.java"); - * assertAbout(javaSource()) - * .that(fileObject) - * .processedWith(new NoHelloWorld()) - * .failsToCompile() - * .withErrorContaining("No types named HelloWorld!").in(fileObject).onLine(23).atColumn(5); - * }

+ *
+ * JavaFileObject helloWorld = JavaFileObjects.forResource("HelloWorld.java");
+ * Compilation compilation =
+ *     javac()
+ *         .withProcessors(new NoHelloWorld())
+ *         .compile(helloWorld);
+ * assertThat(compilation).failed();
+ * assertThat(compilation)
+ *     .hadErrorContaining("No types named HelloWorld!")
+ *     .inFile(helloWorld)
+ *     .onLine(23)
+ *     .atColumn(5);
+ * 
*/ - @CheckReturnValue package com.google.testing.compile; diff --git a/src/test/java/com/google/testing/compile/CompilationSubjectTest.java b/src/test/java/com/google/testing/compile/CompilationSubjectTest.java new file mode 100644 index 00000000..33d28f0e --- /dev/null +++ b/src/test/java/com/google/testing/compile/CompilationSubjectTest.java @@ -0,0 +1,741 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.CompilationSubject.compilations; +import static com.google.testing.compile.Compiler.javac; +import static com.google.testing.compile.VerificationFailureStrategy.VERIFY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.tools.StandardLocation.CLASS_OUTPUT; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteSource; +import com.google.testing.compile.VerificationFailureStrategy.VerificationException; +import java.util.regex.Pattern; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** Tests {@link CompilationSubject}. */ +@RunWith(Enclosed.class) +public class CompilationSubjectTest { + private static final JavaFileObject HELLO_WORLD = + JavaFileObjects.forSourceLines( + "test.HelloWorld", + "package test;", + "", + "import " + DiagnosticMessage.class.getCanonicalName() + ";", + "", + "@DiagnosticMessage", + "public class HelloWorld {", + " @DiagnosticMessage Object foo;", + " void weird() {", + " foo.toString();", + " }", + "}"); + + private static final JavaFileObject HELLO_WORLD_BROKEN = + JavaFileObjects.forSourceLines( + "test.HelloWorld", + "package test;", + "", + "import " + DiagnosticMessage.class.getCanonicalName() + ";", + "", + "@DiagnosticMessage", + "public class HelloWorld {", + " @DiagnosticMessage Object foo;", + " Bar noSuchClass;", + "}"); + + private static final JavaFileObject HELLO_WORLD_RESOURCE = + JavaFileObjects.forResource("HelloWorld.java"); + + private static final JavaFileObject HELLO_WORLD_BROKEN_RESOURCE = + JavaFileObjects.forResource("HelloWorld-broken.java"); + + private static final JavaFileObject HELLO_WORLD_DIFFERENT_RESOURCE = + JavaFileObjects.forResource("HelloWorld-different.java"); + + @RunWith(JUnit4.class) + public static class StatusTest { + @Test + public void succeeded() { + assertThat(javac().compile(HELLO_WORLD)).succeeded(); + assertThat(javac().compile(HELLO_WORLD_RESOURCE)).succeeded(); + } + + @Test + public void succeeded_failureReportsGeneratedFiles() { + try { + verifyThat(compilerWithGeneratorAndError().compile(HELLO_WORLD_RESOURCE)).succeeded(); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()).contains("Compilation produced the following errors:\n"); + assertThat(expected.getMessage()).contains(FailingGeneratingProcessor.GENERATED_CLASS_NAME); + assertThat(expected.getMessage()).contains(FailingGeneratingProcessor.GENERATED_SOURCE); + } + } + + @Test + public void succeeded_failureReportsNoGeneratedFiles() { + try { + verifyThat(javac().compile(HELLO_WORLD_BROKEN_RESOURCE)).succeeded(); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith("Compilation produced the following errors:\n"); + assertThat(expected.getMessage()).contains("No files were generated."); + } + } + + @Test + public void succeeded_exceptionCreatedOrPassedThrough() { + RuntimeException e = new RuntimeException(); + try { + verifyThat(throwingCompiler(e).compile(HELLO_WORLD_RESOURCE)).succeeded(); + fail(); + } catch (CompilationFailureException expected) { + // some old javacs don't pass through exceptions, so we create one + } catch (RuntimeException expected) { + // newer jdks throw a runtime exception whose cause is the original exception + assertThat(expected.getCause()).isEqualTo(e); + } + } + + @Test + public void succeededWithoutWarnings() { + assertThat(javac().compile(HELLO_WORLD)).succeededWithoutWarnings(); + } + + @Test + public void succeededWithoutWarnings_failsWithWarnings() { + try { + verifyThat(compilerWithWarning().compile(HELLO_WORLD)).succeededWithoutWarnings(); + fail(); + } catch (VerificationException e) { + assertThat(e.getMessage()) + .contains("Expected 0 warnings, but found the following 2 warnings:\n"); + } + } + + @Test + public void failedToCompile() { + assertThat(javac().compile(HELLO_WORLD_BROKEN_RESOURCE)).failed(); + } + + @Test + public void failedToCompile_compilationSucceeded() { + try { + verifyThat(javac().compile(HELLO_WORLD_RESOURCE)).failed(); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith("Compilation was expected to fail, but contained no errors"); + assertThat(expected.getMessage()).contains("No files were generated."); + } + } + } + + /** + * Tests for {@link CompilationSubject}'s assertions about warnings and notes, for both successful + * and unsuccessful compilations. + */ + @RunWith(Parameterized.class) + public static final class WarningAndNoteTest { + private final JavaFileObject sourceFile; + + @Parameters + public static ImmutableList parameters() { + return ImmutableList.copyOf( + new Object[][] { + {HELLO_WORLD}, {HELLO_WORLD_BROKEN}, + }); + } + + public WarningAndNoteTest(JavaFileObject sourceFile) { + this.sourceFile = sourceFile; + } + + @Test + public void hadWarningContaining() { + assertThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(sourceFile) + .onLine(6) + .atColumn(8); + assertThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(sourceFile) + .onLine(7) + .atColumn(29); + } + + @Test + public void hadWarningContainingMatch() { + assertThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch("this is a? message") + .inFile(sourceFile) + .onLine(6) + .atColumn(8); + assertThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch("(this|here) is a message") + .inFile(sourceFile) + .onLine(7) + .atColumn(29); + } + + @Test + public void hadWarningContainingMatch_pattern() { + assertThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch(Pattern.compile("this is a? message")) + .inFile(sourceFile) + .onLine(6) + .atColumn(8); + assertThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch(Pattern.compile("(this|here) is a message")) + .inFile(sourceFile) + .onLine(7) + .atColumn(29); + } + + @Test + public void hadWarningContaining_noSuchWarning() { + try { + verifyThat(compilerWithWarning().compile(sourceFile)).hadWarningContaining("what is it?"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith("Expected a warning containing \"what is it?\", but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("this is a message\n"); + } + } + + @Test + public void hadWarningContainingMatch_noSuchWarning() { + try { + verifyThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch("(what|where) is it?"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith( + "Expected a warning containing match for /(what|where) is it?/, but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("this is a message\n"); + } + } + + @Test + public void hadWarningContainingMatch_pattern_noSuchWarning() { + try { + verifyThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch(Pattern.compile("(what|where) is it?")); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith( + "Expected a warning containing match for /(what|where) is it?/, but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("this is a message\n"); + } + } + + @Test + public void hadWarningContainingInFile_wrongFile() { + try { + verifyThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(HELLO_WORLD_DIFFERENT_RESOURCE); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning in %s", HELLO_WORLD_DIFFERENT_RESOURCE.getName())); + assertThat(expected.getMessage()).contains(sourceFile.getName()); + } + } + + @Test + public void hadWarningContainingInFileOnLine_wrongLine() { + try { + verifyThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(sourceFile) + .onLine(1); + fail(); + } catch (VerificationException expected) { + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains(String.format("Expected a warning on line 1 of %s", sourceFile.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); + } + } + + @Test + public void hadWarningContainingInFileOnLineAtColumn_wrongColumn() { + try { + verifyThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(sourceFile) + .onLine(6) + .atColumn(1); + fail(); + } catch (VerificationException expected) { + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains(String.format("Expected a warning at 6:1 of %s", sourceFile.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); + } + } + + @Test + public void hadWarningCount() { + assertThat(compilerWithWarning().compile(sourceFile)).hadWarningCount(2); + } + + @Test + public void hadWarningCount_wrongCount() { + try { + verifyThat(compilerWithWarning().compile(sourceFile)).hadWarningCount(42); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains("Expected 42 warnings, but found the following 2 warnings:\n"); + } + } + + @Test + public void hadNoteContaining() { + assertThat(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(sourceFile) + .onLine(6) + .atColumn(8); + assertThat(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(sourceFile) + .onLine(7) + .atColumn(29); + } + + @Test + public void hadNoteContainingMatch() { + assertThat(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch("this is a? message") + .inFile(sourceFile) + .onLine(6) + .atColumn(8); + assertThat(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch("(this|here) is a message") + .inFile(sourceFile) + .onLine(7) + .atColumn(29); + } + + @Test + public void hadNoteContainingMatch_pattern() { + assertThat(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch(Pattern.compile("this is a? message")) + .inFile(sourceFile) + .onLine(6) + .atColumn(8); + assertThat(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch(Pattern.compile("(this|here) is a message")) + .inFile(sourceFile) + .onLine(7) + .atColumn(29); + } + + @Test + public void hadNoteContaining_noSuchNote() { + try { + verifyThat(compilerWithNote().compile(sourceFile)).hadNoteContaining("what is it?"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith("Expected a note containing \"what is it?\", but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("this is a message\n"); + } + } + + @Test + public void hadNoteContainingMatch_noSuchNote() { + try { + verifyThat(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch("(what|where) is it?"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith( + "Expected a note containing match for /(what|where) is it?/, but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("this is a message\n"); + } + } + + @Test + public void hadNoteContainingMatch_pattern_noSuchNote() { + try { + verifyThat(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch(Pattern.compile("(what|where) is it?")); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith( + "Expected a note containing match for /(what|where) is it?/, but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("this is a message\n"); + } + } + + @Test + public void hadNoteContainingInFile_wrongFile() { + try { + verifyThat(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(HELLO_WORLD_DIFFERENT_RESOURCE); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains( + String.format("Expected a note in %s", HELLO_WORLD_DIFFERENT_RESOURCE.getName())); + assertThat(expected.getMessage()).contains(sourceFile.getName()); + } + } + + @Test + public void hadNoteContainingInFileOnLine_wrongLine() { + try { + verifyThat(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(sourceFile) + .onLine(1); + fail(); + } catch (VerificationException expected) { + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains(String.format("Expected a note on line 1 of %s", sourceFile.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); + } + } + + @Test + public void hadNoteContainingInFileOnLineAtColumn_wrongColumn() { + try { + verifyThat(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(sourceFile) + .onLine(6) + .atColumn(1); + fail(); + } catch (VerificationException expected) { + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains(String.format("Expected a note at 6:1 of %s", sourceFile.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); + } + } + + @Test + public void hadNoteCount() { + assertThat(compilerWithNote().compile(sourceFile)).hadNoteCount(2); + } + + @Test + public void hadNoteCount_wrongCount() { + try { + verifyThat(compilerWithNote().compile(sourceFile)).hadNoteCount(42); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains("Expected 42 notes, but found the following 2 notes:\n"); + } + } + } + + /** Tests for {@link CompilationSubject}'s assertions about errors. */ + @RunWith(JUnit4.class) + public static final class ErrorTest { + @Test + public void hadErrorContaining() { + assertThat(javac().compile(HELLO_WORLD_BROKEN_RESOURCE)) + .hadErrorContaining("not a statement") + .inFile(HELLO_WORLD_BROKEN_RESOURCE) + .onLine(23) + .atColumn(5); + assertThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("expected error!") + .inFile(HELLO_WORLD_RESOURCE) + .onLine(18) + .atColumn(8); + } + + @Test + public void hadErrorContainingMatch() { + assertThat(compilerWithError().compile(HELLO_WORLD_BROKEN_RESOURCE)) + .hadErrorContainingMatch("not+ +a? statement") + .inFile(HELLO_WORLD_BROKEN_RESOURCE) + .onLine(23) + .atColumn(5); + assertThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContainingMatch("(wanted|expected) error!") + .inFile(HELLO_WORLD_RESOURCE) + .onLine(18) + .atColumn(8); + } + + @Test + public void hadErrorContainingMatch_pattern() { + assertThat(compilerWithError().compile(HELLO_WORLD_BROKEN_RESOURCE)) + .hadErrorContainingMatch("not+ +a? statement") + .inFile(HELLO_WORLD_BROKEN_RESOURCE) + .onLine(23) + .atColumn(5); + assertThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContainingMatch("(wanted|expected) error!") + .inFile(HELLO_WORLD_RESOURCE) + .onLine(18) + .atColumn(8); + } + + @Test + public void hadErrorContaining_noSuchError() { + try { + verifyThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("some error"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith("Expected an error containing \"some error\", but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("expected error!\n"); + } + } + + @Test + public void hadErrorContainingMatch_noSuchError() { + try { + verifyThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContainingMatch("(what|where) is it?"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith( + "Expected an error containing match for /(what|where) is it?/, but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("expected error!\n"); + } + } + + @Test + public void hadErrorContainingMatch_pattern_noSuchError() { + try { + verifyThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContainingMatch(Pattern.compile("(what|where) is it?")); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .startsWith( + "Expected an error containing match for /(what|where) is it?/, but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected.getMessage()).endsWith("expected error!\n"); + } + } + + @Test + public void hadErrorContainingInFile_wrongFile() { + try { + verifyThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("expected error!") + .inFile(HELLO_WORLD_DIFFERENT_RESOURCE); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains( + String.format("Expected an error in %s", HELLO_WORLD_DIFFERENT_RESOURCE.getName())); + assertThat(expected.getMessage()).contains(HELLO_WORLD_RESOURCE.getName()); + // "(no associated file)"))); + } + } + + @Test + public void hadErrorContainingInFileOnLine_wrongLine() { + try { + verifyThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("expected error!") + .inFile(HELLO_WORLD_RESOURCE) + .onLine(1); + fail(); + } catch (VerificationException expected) { + int actualErrorLine = 18; + assertThat(expected.getMessage()) + .contains( + String.format("Expected an error on line 1 of %s", HELLO_WORLD_RESOURCE.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); + } + } + + @Test + public void hadErrorContainingInFileOnLineAtColumn_wrongColumn() { + try { + verifyThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("expected error!") + .inFile(HELLO_WORLD_RESOURCE) + .onLine(18) + .atColumn(1); + fail(); + } catch (VerificationException expected) { + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + String.format("Expected an error at 18:1 of %s", HELLO_WORLD_RESOURCE.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorCol); + } + } + + @Test + public void hadErrorCount() { + assertThat(compilerWithError().compile(HELLO_WORLD_BROKEN_RESOURCE)).hadErrorCount(4); + } + + @Test + public void hadErrorCount_wrongCount() { + try { + verifyThat(compilerWithError().compile(HELLO_WORLD_RESOURCE)).hadErrorCount(42); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains("Expected 42 errors, but found the following 2 errors:\n"); + } + } + } + + @RunWith(JUnit4.class) + public static class GeneratedFilesTest { + @Test + public void generatedSourceFile() { + assertThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedSourceFile(GeneratingProcessor.GENERATED_CLASS_NAME) + .hasSourceEquivalentTo( + JavaFileObjects.forSourceString( + GeneratingProcessor.GENERATED_CLASS_NAME, GeneratingProcessor.GENERATED_SOURCE)); + } + + @Test + public void generatedSourceFile_fail() { + try { + verifyThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedSourceFile("ThisIsNotTheRightFile"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()).contains("generated the file ThisIsNotTheRightFile.java"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); + } + } + + @Test + public void generatedFilePath() { + assertThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "com/google/testing/compile/Foo") + .hasContents(ByteSource.wrap("Bar".getBytes(UTF_8))); + } + + @Test + public void generatedFilePath_fail() { + try { + verifyThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "com/google/testing/compile/Bogus.class"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains("generated the file com/google/testing/compile/Bogus.class"); + } + } + + @Test + public void generatedFilePackageFile() { + assertThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "com.google.testing.compile", "Foo") + .hasContents(ByteSource.wrap("Bar".getBytes(UTF_8))); + } + + @Test + public void generatedFilePackageFile_fail() { + try { + verifyThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "com.google.testing.compile", "Bogus.class"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains( + "generated the file named \"Bogus.class\" " + + "in package \"com.google.testing.compile\""); + } + } + + @Test + public void generatedFileDefaultPackageFile_fail() { + try { + verifyThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "", "File.java"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .contains("generated the file named \"File.java\" in the default package"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); + } + } + } + + private static Compiler compilerWithError() { + return javac().withProcessors(new ErrorProcessor()); + } + + private static Compiler compilerWithWarning() { + return javac().withProcessors(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)); + } + + private static Compiler compilerWithNote() { + return javac().withProcessors(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)); + } + + private static Compiler compilerWithGenerator() { + return javac().withProcessors(new GeneratingProcessor()); + } + + private static Compiler compilerWithGeneratorAndError() { + return javac().withProcessors(new FailingGeneratingProcessor()); + } + + private static Compiler throwingCompiler(RuntimeException e) { + return javac().withProcessors(new ThrowingProcessor(e)); + } + + private static CompilationSubject verifyThat(Compilation compilation) { + return VERIFY.about(compilations()).that(compilation); + } +} diff --git a/src/test/java/com/google/testing/compile/CompilationTest.java b/src/test/java/com/google/testing/compile/CompilationTest.java new file mode 100644 index 00000000..8ff53efc --- /dev/null +++ b/src/test/java/com/google/testing/compile/CompilationTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CompilationTest { + @Test + public void generatedFiles_unsuccessfulCompilationThrows() { + Compilation compilation = + javac() + .compile( + JavaFileObjects.forSourceLines( + "test.Bad", "package test;", "", "this doesn't compile!")); + assertThat(compilation).failed(); + try { + ImmutableList unused = compilation.generatedFiles(); + fail(); + } catch (IllegalStateException expected) { + } + } +} diff --git a/src/test/java/com/google/testing/compile/CompilerTest.java b/src/test/java/com/google/testing/compile/CompilerTest.java new file mode 100644 index 00000000..381ce182 --- /dev/null +++ b/src/test/java/com/google/testing/compile/CompilerTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import javax.annotation.processing.Processor; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link Compiler}. */ +@RunWith(JUnit4.class) +public final class CompilerTest { + + private static final JavaFileObject HELLO_WORLD = JavaFileObjects.forResource("HelloWorld.java"); + + @Test + public void options() { + NoOpProcessor processor = new NoOpProcessor(); + Object[] options1 = {"-Agone=nowhere"}; + JavaFileObject[] files = {HELLO_WORLD}; + Compilation unused = + javac() + .withOptions(options1) + .withOptions(ImmutableList.of("-Ab=2", "-Ac=3")) + .withProcessors(processor) + .compile(files); + assertThat(processor.options) + .containsExactly( + "b", "2", + "c", "3") + .inOrder(); + } + + @Test + public void multipleProcesors() { + NoOpProcessor noopProcessor1 = new NoOpProcessor(); + NoOpProcessor noopProcessor2 = new NoOpProcessor(); + NoOpProcessor noopProcessor3 = new NoOpProcessor(); + assertThat(noopProcessor1.invoked).isFalse(); + assertThat(noopProcessor2.invoked).isFalse(); + assertThat(noopProcessor3.invoked).isFalse(); + Processor[] processors = {noopProcessor1, noopProcessor3}; + JavaFileObject[] files = {HELLO_WORLD}; + Compilation unused = + javac() + .withProcessors(processors) + .withProcessors(noopProcessor1, noopProcessor2) + .compile(files); + assertThat(noopProcessor1.invoked).isTrue(); + assertThat(noopProcessor2.invoked).isTrue(); + assertThat(noopProcessor3.invoked).isFalse(); + } + + @Test + public void multipleProcesors_asIterable() { + NoOpProcessor noopProcessor1 = new NoOpProcessor(); + NoOpProcessor noopProcessor2 = new NoOpProcessor(); + NoOpProcessor noopProcessor3 = new NoOpProcessor(); + assertThat(noopProcessor1.invoked).isFalse(); + assertThat(noopProcessor2.invoked).isFalse(); + assertThat(noopProcessor3.invoked).isFalse(); + JavaFileObject[] files = {HELLO_WORLD}; + Compilation unused = + javac() + .withProcessors(Arrays.asList(noopProcessor1, noopProcessor3)) + .withProcessors(Arrays.asList(noopProcessor1, noopProcessor2)) + .compile(files); + assertThat(noopProcessor1.invoked).isTrue(); + assertThat(noopProcessor2.invoked).isTrue(); + assertThat(noopProcessor3.invoked).isFalse(); + } +} diff --git a/src/test/java/com/google/testing/compile/DiagnosticMessage.java b/src/test/java/com/google/testing/compile/DiagnosticMessage.java new file mode 100644 index 00000000..4bce55ed --- /dev/null +++ b/src/test/java/com/google/testing/compile/DiagnosticMessage.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2013 Google, 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.google.testing.compile; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Retention; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.Diagnostic.Kind; + +/** + * Annotated elements will have a diagnostic message whose {@linkplain Kind kind} is determined by a + * parameter on {@link DiagnosticMessageProcessor}. + */ +@Retention(SOURCE) +public @interface DiagnosticMessage { + /** + * Adds diagnostic messages of a specified {@linkplain Kind kind} to elements annotated with + * {@link DiagnosticMessage}. + */ + class Processor extends AbstractProcessor { + + private final Diagnostic.Kind kind; + + Processor(Diagnostic.Kind kind) { + this.kind = kind; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public Set getSupportedAnnotationTypes() { + return ImmutableSet.of(DiagnosticMessage.class.getCanonicalName()); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (Element element : roundEnv.getElementsAnnotatedWith(DiagnosticMessage.class)) { + processingEnv.getMessager().printMessage(kind, "this is a message", element); + } + return true; + } + } +} diff --git a/src/test/java/com/google/testing/compile/ErrorProcessor.java b/src/test/java/com/google/testing/compile/ErrorProcessor.java new file mode 100644 index 00000000..a31016e9 --- /dev/null +++ b/src/test/java/com/google/testing/compile/ErrorProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2013 Google, 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.google.testing.compile; + +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic.Kind; + +final class ErrorProcessor extends AbstractProcessor { + Messager messager; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.messager = processingEnv.getMessager(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (Element element : roundEnv.getRootElements()) { + messager.printMessage(Kind.ERROR, "expected error!", element); + messager.printMessage(Kind.ERROR, "another expected error!"); + } + return false; + } + + @Override + public Set getSupportedAnnotationTypes() { + return ImmutableSet.of("*"); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } +} diff --git a/src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java b/src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java new file mode 100644 index 00000000..ffec2771 --- /dev/null +++ b/src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2013 Google, 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.google.testing.compile; + +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic.Kind; + +final class FailingGeneratingProcessor extends AbstractProcessor { + static final String GENERATED_CLASS_NAME = GeneratingProcessor.GENERATED_CLASS_NAME; + static final String GENERATED_SOURCE = GeneratingProcessor.GENERATED_SOURCE; + static final String ERROR_MESSAGE = "expected error!"; + final GeneratingProcessor delegate = new GeneratingProcessor(); + Messager messager; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + delegate.init(processingEnv); + this.messager = processingEnv.getMessager(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + delegate.process(annotations, roundEnv); + messager.printMessage(Kind.ERROR, ERROR_MESSAGE); + return false; + } + + @Override + public Set getSupportedAnnotationTypes() { + return delegate.getSupportedAnnotationTypes(); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return delegate.getSupportedSourceVersion(); + } +} diff --git a/src/test/java/com/google/testing/compile/GeneratingProcessor.java b/src/test/java/com/google/testing/compile/GeneratingProcessor.java new file mode 100644 index 00000000..a565d999 --- /dev/null +++ b/src/test/java/com/google/testing/compile/GeneratingProcessor.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 Google, 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.google.testing.compile; + +import static javax.tools.StandardLocation.CLASS_OUTPUT; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.io.Writer; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; + +final class GeneratingProcessor extends AbstractProcessor { + static final String GENERATED_CLASS_NAME = "Blah"; + static final String GENERATED_SOURCE = "final class Blah {\n String blah = \"blah\";\n}"; + + static final String GENERATED_RESOURCE_NAME = "Foo"; + static final String GENERATED_RESOURCE = "Bar"; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + Filer filer = processingEnv.getFiler(); + try (Writer writer = filer.createSourceFile(GENERATED_CLASS_NAME).openWriter()) { + writer.write(GENERATED_SOURCE); + } catch (IOException e) { + throw new RuntimeException(e); + } + + try (Writer writer = + filer + .createResource( + CLASS_OUTPUT, getClass().getPackage().getName(), GENERATED_RESOURCE_NAME) + .openWriter()) { + writer.write(GENERATED_RESOURCE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @CanIgnoreReturnValue + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + return false; + } + + @Override + public Set getSupportedAnnotationTypes() { + return ImmutableSet.of("*"); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } +} diff --git a/src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java b/src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java new file mode 100644 index 00000000..fab0c97c --- /dev/null +++ b/src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.testing.compile.JavaFileObjectSubject.assertThat; +import static com.google.testing.compile.JavaFileObjectSubject.javaFileObjects; +import static com.google.testing.compile.VerificationFailureStrategy.VERIFY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; + +import com.google.testing.compile.VerificationFailureStrategy.VerificationException; +import java.io.IOException; +import java.util.regex.Pattern; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class JavaFileObjectSubjectTest { + + private static final JavaFileObject CLASS = + JavaFileObjects.forSourceLines( + "test.TestClass", // + "package test;", + "", + "public class TestClass {}"); + + private static final JavaFileObject DIFFERENT_NAME = + JavaFileObjects.forSourceLines( + "test.TestClass2", // + "package test;", + "", + "public class TestClass2 {}"); + + private static final JavaFileObject CLASS_WITH_FIELD = + JavaFileObjects.forSourceLines( + "test.TestClass", // + "package test;", + "", + "public class TestClass {", + " Object field;", + "}"); + + private static final JavaFileObject UNKNOWN_TYPES = + JavaFileObjects.forSourceLines( + "test.TestClass", + "package test;", + "", + "public class TestClass {", + " Bar badMethod(Baz baz) { return baz.what(); }", + "}"); + + @Test + public void hasContents() { + assertThat(CLASS_WITH_FIELD).hasContents(JavaFileObjects.asByteSource(CLASS_WITH_FIELD)); + } + + @Test + public void hasContents_failure() { + try { + verifyThat(CLASS_WITH_FIELD).hasContents(JavaFileObjects.asByteSource(DIFFERENT_NAME)); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()).contains(CLASS_WITH_FIELD.getName()); + } + } + + @Test + public void contentsAsString() { + assertThat(CLASS_WITH_FIELD).contentsAsString(UTF_8).containsMatch("Object +field;"); + } + + @Test + public void contentsAsString_fail() { + try { + verifyThat(CLASS).contentsAsString(UTF_8).containsMatch("bad+"); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()) + .containsMatch("the contents of .*" + Pattern.quote(CLASS.getName())); + assertThat(expected.getMessage()).contains("bad+"); + } + } + + @Test + public void hasSourceEquivalentTo() { + assertThat(CLASS_WITH_FIELD).hasSourceEquivalentTo(CLASS_WITH_FIELD); + } + + @Test + public void hasSourceEquivalentTo_unresolvedReferences() { + assertThat(UNKNOWN_TYPES).hasSourceEquivalentTo(UNKNOWN_TYPES); + } + + @Test + public void hasSourceEquivalentTo_failOnDifferences() throws IOException { + try { + verifyThat(CLASS).hasSourceEquivalentTo(DIFFERENT_NAME); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()).contains("is equivalent to"); + assertThat(expected.getMessage()).contains(CLASS.getName()); + assertThat(expected.getMessage()).contains(CLASS.getCharContent(false)); + } + } + + @Test + public void hasSourceEquivalentTo_failOnExtraInExpected() throws IOException { + try { + verifyThat(CLASS).hasSourceEquivalentTo(CLASS_WITH_FIELD); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()).contains("is equivalent to"); + assertThat(expected.getMessage()).contains("unmatched nodes in the expected tree"); + assertThat(expected.getMessage()).contains(CLASS.getName()); + assertThat(expected.getMessage()).contains(CLASS.getCharContent(false)); + } + } + + @Test + public void hasSourceEquivalentTo_failOnExtraInActual() throws IOException { + try { + verifyThat(CLASS_WITH_FIELD).hasSourceEquivalentTo(CLASS); + fail(); + } catch (VerificationException expected) { + assertThat(expected.getMessage()).contains("is equivalent to"); + assertThat(expected.getMessage()).contains("unmatched nodes in the actual tree"); + assertThat(expected.getMessage()).contains(CLASS_WITH_FIELD.getName()); + assertThat(expected.getMessage()).contains(CLASS_WITH_FIELD.getCharContent(false)); + } + } + + private static JavaFileObjectSubject verifyThat(JavaFileObject file) { + return VERIFY.about(javaFileObjects()).that(file); + } +} diff --git a/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java b/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java index 81aa63ae..eaf38e81 100644 --- a/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java +++ b/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java @@ -19,18 +19,12 @@ import static javax.tools.JavaFileObject.Kind.CLASS; import static org.junit.Assert.fail; -import com.google.common.io.Resources; - - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; - import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; /** * Tests {@link JavaFileObjects}. @@ -39,12 +33,12 @@ */ @RunWith(JUnit4.class) public class JavaFileObjectsTest { - @Test public void forResource_inJarFile() throws URISyntaxException, IOException { + @Test + public void forResource_inJarFile() { JavaFileObject resourceInJar = JavaFileObjects.forResource("java/lang/Object.class"); assertThat(resourceInJar.getKind()).isEqualTo(CLASS); assertThat(resourceInJar.toUri()).isEqualTo(URI.create("/java/lang/Object.class")); - assertThat(resourceInJar.getName()) - .isEqualTo(Resources.getResource("java/lang/Object.class").toString()); + assertThat(resourceInJar.getName()).isEqualTo("/java/lang/Object.class"); assertThat(resourceInJar.isNameCompatible("Object", CLASS)).isTrue(); } diff --git a/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java b/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java index f6c7f5cb..bb2e373b 100644 --- a/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java +++ b/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java @@ -15,41 +15,24 @@ */ package com.google.testing.compile; -import static com.google.common.base.Charsets.UTF_8; import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; +import static com.google.testing.compile.VerificationFailureStrategy.VERIFY; +import static java.nio.charset.StandardCharsets.UTF_8; import static javax.tools.StandardLocation.CLASS_OUTPUT; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteSource; import com.google.common.io.Resources; -import com.google.common.truth.FailureStrategy; -import com.google.common.truth.TestVerb; -import com.google.errorprone.annotations.CanIgnoreReturnValue; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.io.IOException; -import java.io.Writer; +import com.google.testing.compile.VerificationFailureStrategy.VerificationException; import java.util.Arrays; -import java.util.Map; -import java.util.Set; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.Messager; -import javax.annotation.processing.ProcessingEnvironment; -import javax.annotation.processing.RoundEnvironment; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; -import javax.tools.Diagnostic.Kind; import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; /** * Tests {@link JavaSourcesSubjectFactory} (and {@link JavaSourceSubjectFactory}). @@ -58,14 +41,6 @@ */ @RunWith(JUnit4.class) public class JavaSourcesSubjectFactoryTest { - /** We need a {@link TestVerb} that throws anything except {@link AssertionError}. */ - private static final TestVerb VERIFY = new TestVerb(new FailureStrategy() { - @Override - public void fail(String message) { - throw new VerificationException(message); - } - }); - private static final JavaFileObject HELLO_WORLD = JavaFileObjects.forSourceLines( "test.HelloWorld", @@ -117,7 +92,7 @@ public void compilesWithoutWarnings() { public void compilesWithoutError_warnings() { assertAbout(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .compilesWithoutError() .withWarningContaining("this is a message") .in(HELLO_WORLD) @@ -136,7 +111,7 @@ public void compilesWithoutWarnings_failsWithWarnings() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .compilesWithoutWarnings(); fail(); } catch (VerificationException expected) { @@ -151,7 +126,7 @@ public void compilesWithoutError_noWarning() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .compilesWithoutError() .withWarningContaining("what is it?"); fail(); @@ -170,7 +145,7 @@ public void compilesWithoutError_warningNotInFile() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .compilesWithoutError() .withWarningContaining("this is a message") .in(otherSource); @@ -188,7 +163,7 @@ public void compilesWithoutError_warningNotOnLine() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .compilesWithoutError() .withWarningContaining("this is a message") .in(HELLO_WORLD) @@ -208,7 +183,7 @@ public void compilesWithoutError_warningNotAtColumn() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .compilesWithoutError() .withWarningContaining("this is a message") .in(HELLO_WORLD) @@ -229,7 +204,7 @@ public void compilesWithoutError_wrongWarningCount() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .compilesWithoutError() .withWarningCount(42); fail(); @@ -243,7 +218,7 @@ public void compilesWithoutError_wrongWarningCount() { public void compilesWithoutError_notes() { assertAbout(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .compilesWithoutError() .withNoteContaining("this is a message") .in(HELLO_WORLD) @@ -264,7 +239,7 @@ public void compilesWithoutError_noNote() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .compilesWithoutError() .withNoteContaining("what is it?"); fail(); @@ -283,7 +258,7 @@ public void compilesWithoutError_noteNotInFile() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .compilesWithoutError() .withNoteContaining("this is a message") .in(otherSource); @@ -301,7 +276,7 @@ public void compilesWithoutError_noteNotOnLine() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .compilesWithoutError() .withNoteContaining("this is a message") .in(HELLO_WORLD) @@ -321,7 +296,7 @@ public void compilesWithoutError_noteNotAtColumn() { VERIFY .about(javaSource()) .that(HELLO_WORLD) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .compilesWithoutError() .withNoteContaining("this is a message") .in(HELLO_WORLD) @@ -343,7 +318,7 @@ public void compilesWithoutError_wrongNoteCount() { VERIFY .about(javaSource()) .that(fileObject) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .compilesWithoutError() .withNoteCount(42); fail(); @@ -383,22 +358,12 @@ public void compilesWithoutError_throws() { @Test public void compilesWithoutError_exceptionCreatedOrPassedThrough() { - final RuntimeException e = new RuntimeException(); + RuntimeException e = new RuntimeException(); try { - VERIFY.about(javaSource()) + VERIFY + .about(javaSource()) .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new AbstractProcessor() { - @Override - public Set getSupportedAnnotationTypes() { - return ImmutableSet.of("*"); - } - - @Override - public boolean process(Set annotations, - RoundEnvironment roundEnv) { - throw e; - } - }) + .processedWith(new ThrowingProcessor(e)) .compilesWithoutError(); fail(); } catch (CompilationFailureException expected) { @@ -412,22 +377,25 @@ public boolean process(Set annotations, @Test public void parsesAs() { assertAbout(javaSource()) - .that(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java"))) - .parsesAs(JavaFileObjects.forSourceLines("test.HelloWorld", - "package test;", - "", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(\"Hello World!\");", - " }", - "}")); + .that(JavaFileObjects.forResource("HelloWorld.java")) + .parsesAs( + JavaFileObjects.forSourceLines( + "test.HelloWorld", + "package test;", + "", + "public class HelloWorld {", + " public static void main(String[] args) {", + " System.out.println(\"Hello World!\");", + " }", + "}")); } @Test public void parsesAs_expectedFileFailsToParse() { try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java"))) + VERIFY + .about(javaSource()) + .that(JavaFileObjects.forResource("HelloWorld.java")) .parsesAs(JavaFileObjects.forResource("HelloWorld-broken.java")); fail(); } catch (IllegalStateException expected) { @@ -438,9 +406,10 @@ public void parsesAs_expectedFileFailsToParse() { @Test public void parsesAs_actualFileFailsToParse() { try { - VERIFY.about(javaSource()) + VERIFY + .about(javaSource()) .that(JavaFileObjects.forResource("HelloWorld-broken.java")) - .parsesAs(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java"))); + .parsesAs(JavaFileObjects.forResource("HelloWorld.java")); fail(); } catch (IllegalStateException expected) { assertThat(expected.getMessage()).startsWith("error while parsing:"); @@ -450,7 +419,8 @@ public void parsesAs_actualFileFailsToParse() { @Test public void failsToCompile_throws() { try { - VERIFY.about(javaSource()) + VERIFY + .about(javaSource()) .that(JavaFileObjects.forResource("HelloWorld.java")) .failsToCompile(); fail(); @@ -554,7 +524,7 @@ public void failsToCompile_noWarning() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .failsToCompile() .withWarningContaining("what is it?"); fail(); @@ -573,7 +543,7 @@ public void failsToCompile_warningNotInFile() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .failsToCompile() .withWarningContaining("this is a message") .in(otherSource); @@ -591,7 +561,7 @@ public void failsToCompile_warningNotOnLine() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .failsToCompile() .withWarningContaining("this is a message") .in(HELLO_WORLD_BROKEN) @@ -612,7 +582,7 @@ public void failsToCompile_warningNotAtColumn() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .failsToCompile() .withWarningContaining("this is a message") .in(HELLO_WORLD_BROKEN) @@ -633,7 +603,7 @@ public void failsToCompile_wrongWarningCount() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.WARNING)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) .failsToCompile() .withWarningCount(42); fail(); @@ -649,7 +619,7 @@ public void failsToCompile_noNote() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .failsToCompile() .withNoteContaining("what is it?"); fail(); @@ -668,7 +638,7 @@ public void failsToCompile_noteNotInFile() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .failsToCompile() .withNoteContaining("this is a message") .in(otherSource); @@ -686,7 +656,7 @@ public void failsToCompile_noteNotOnLine() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .failsToCompile() .withNoteContaining("this is a message") .in(HELLO_WORLD_BROKEN) @@ -706,7 +676,7 @@ public void failsToCompile_noteNotAtColumn() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .failsToCompile() .withNoteContaining("this is a message") .in(HELLO_WORLD_BROKEN) @@ -727,7 +697,7 @@ public void failsToCompile_wrongNoteCount() { VERIFY .about(javaSource()) .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessageProcessor(Diagnostic.Kind.NOTE)) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) .failsToCompile() .withNoteCount(42); fail(); @@ -871,8 +841,7 @@ public void generatesFileNamed_failOnFileExistence() { .withContents(ByteSource.wrap("Bar".getBytes(UTF_8))); fail(); } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains("Did not find a generated file corresponding to Bogus"); + assertThat(expected.getMessage()).contains("generated the file named \"Bogus\""); assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_RESOURCE_NAME); } } @@ -890,7 +859,7 @@ public void generatesFileNamed_failOnFileContents() { fail(); } catch (VerificationException expected) { assertThat(expected.getMessage()).contains("Foo"); - assertThat(expected.getMessage()).contains(" did not match the expected contents"); + assertThat(expected.getMessage()).contains(" has contents "); } } @@ -947,184 +916,4 @@ public void invokesMultipleProcesors_asIterable() { assertThat(noopProcessor1.invoked).isTrue(); assertThat(noopProcessor2.invoked).isTrue(); } - - - /** - * Annotated elements will have a diagnostic message whose {@linkplain Kind kind} is determined by - * a parameter on {@link DiagnosticMessageProcessor}. - */ - public @interface DiagnosticMessage {} - - /** - * Adds diagnostic messages of a specified {@linkplain Kind kind} to elements annotated with - * {@link DiagnosticMessage}. - */ - private static final class DiagnosticMessageProcessor extends AbstractProcessor { - - private final Diagnostic.Kind kind; - - DiagnosticMessageProcessor(Diagnostic.Kind kind) { - this.kind = kind; - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latest(); - } - - @Override - public Set getSupportedAnnotationTypes() { - return ImmutableSet.of(DiagnosticMessage.class.getCanonicalName()); - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - for (Element element : roundEnv.getElementsAnnotatedWith(DiagnosticMessage.class)) { - processingEnv.getMessager().printMessage(kind, "this is a message", element); - } - return true; - } - } - - - private static final class GeneratingProcessor extends AbstractProcessor { - static final String GENERATED_CLASS_NAME = "Blah"; - static final String GENERATED_SOURCE = "final class Blah {\n String blah = \"blah\";\n}"; - - static final String GENERATED_RESOURCE_NAME = "Foo"; - static final String GENERATED_RESOURCE = "Bar"; - - @Override - public synchronized void init(ProcessingEnvironment processingEnv) { - try { - JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(GENERATED_CLASS_NAME); - Writer writer = sourceFile.openWriter(); - writer.write(GENERATED_SOURCE); - writer.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - - try { - Writer writer = processingEnv.getFiler().createResource(CLASS_OUTPUT, - JavaSourcesSubjectFactoryTest.class.getPackage().getName(), GENERATED_RESOURCE_NAME) - .openWriter(); - writer.write(GENERATED_RESOURCE); - writer.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @CanIgnoreReturnValue - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - return false; - } - - @Override - public Set getSupportedAnnotationTypes() { - return ImmutableSet.of("*"); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latestSupported(); - } - } - - private static final class FailingGeneratingProcessor extends AbstractProcessor { - static final String GENERATED_CLASS_NAME = GeneratingProcessor.GENERATED_CLASS_NAME; - static final String GENERATED_SOURCE = GeneratingProcessor.GENERATED_SOURCE; - static final String ERROR_MESSAGE = "expected error!"; - final GeneratingProcessor delegate = new GeneratingProcessor(); - Messager messager; - - @Override - public synchronized void init(ProcessingEnvironment processingEnv) { - delegate.init(processingEnv); - this.messager = processingEnv.getMessager(); - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - delegate.process(annotations, roundEnv); - messager.printMessage(Kind.ERROR, ERROR_MESSAGE); - return false; - } - - @Override - public Set getSupportedAnnotationTypes() { - return delegate.getSupportedAnnotationTypes(); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return delegate.getSupportedSourceVersion(); - } - } - - private static final class NoOpProcessor extends AbstractProcessor { - boolean invoked = false; - Map options; - - @Override - public synchronized void init(ProcessingEnvironment processingEnv) { - super.init(processingEnv); - options = processingEnv.getOptions(); - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - invoked = true; - return false; - } - - @Override - public Set getSupportedAnnotationTypes() { - return ImmutableSet.of("*"); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latestSupported(); - } - } - - private static final class VerificationException extends RuntimeException { - private static final long serialVersionUID = 1L; - - VerificationException(String message) { - super(message); - } - } - - private static final class ErrorProcessor extends AbstractProcessor { - Messager messager; - - @Override - public synchronized void init(ProcessingEnvironment processingEnv) { - super.init(processingEnv); - this.messager = processingEnv.getMessager(); - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - for (Element element : roundEnv.getRootElements()) { - messager.printMessage(Kind.ERROR, "expected error!", element); - messager.printMessage(Kind.ERROR, "another expected error!"); - } - return false; - } - - @Override - public Set getSupportedAnnotationTypes() { - return ImmutableSet.of("*"); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latestSupported(); - } - } } diff --git a/src/test/java/com/google/testing/compile/NoOpProcessor.java b/src/test/java/com/google/testing/compile/NoOpProcessor.java new file mode 100644 index 00000000..7ffe9fe0 --- /dev/null +++ b/src/test/java/com/google/testing/compile/NoOpProcessor.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 Google, 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.google.testing.compile; + +import com.google.common.collect.ImmutableSet; +import java.util.Map; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; + +final class NoOpProcessor extends AbstractProcessor { + boolean invoked = false; + Map options; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + options = processingEnv.getOptions(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + invoked = true; + return false; + } + + @Override + public Set getSupportedAnnotationTypes() { + return ImmutableSet.of("*"); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } +} diff --git a/src/test/java/com/google/testing/compile/ThrowingProcessor.java b/src/test/java/com/google/testing/compile/ThrowingProcessor.java new file mode 100644 index 00000000..8154c6c2 --- /dev/null +++ b/src/test/java/com/google/testing/compile/ThrowingProcessor.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 Google, 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.google.testing.compile; + +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.TypeElement; + +final class ThrowingProcessor extends AbstractProcessor { + + private final RuntimeException e; + + ThrowingProcessor(RuntimeException e) { + this.e = e; + } + + @Override + public Set getSupportedAnnotationTypes() { + return ImmutableSet.of("*"); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + throw e; + } +} diff --git a/src/test/java/com/google/testing/compile/TreeContextTest.java b/src/test/java/com/google/testing/compile/TreeContextTest.java index 538be942..c44eb546 100644 --- a/src/test/java/com/google/testing/compile/TreeContextTest.java +++ b/src/test/java/com/google/testing/compile/TreeContextTest.java @@ -18,12 +18,11 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; - +import com.google.testing.compile.Parser.ParseResult; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -56,8 +55,7 @@ public class TreeContextTest { " }", "}"); - private static final Compilation.ParseResult PARSE_RESULTS = - MoreTrees.parseLines(baseTreeSource); + private static final ParseResult PARSE_RESULTS = MoreTrees.parseLines(baseTreeSource); private static final CompilationUnitTree COMPILATION_UNIT = PARSE_RESULTS.compilationUnits().iterator().next(); private static final Trees TREES = PARSE_RESULTS.trees(); diff --git a/src/test/java/com/google/testing/compile/TreeDifferenceTest.java b/src/test/java/com/google/testing/compile/TreeDifferenceTest.java index ac5c2c62..c9d66c61 100644 --- a/src/test/java/com/google/testing/compile/TreeDifferenceTest.java +++ b/src/test/java/com/google/testing/compile/TreeDifferenceTest.java @@ -18,12 +18,11 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.Iterables; - +import com.google.testing.compile.Parser.ParseResult; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; - import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -33,8 +32,9 @@ */ @RunWith(JUnit4.class) public class TreeDifferenceTest { - private static final Compilation.ParseResult PARSE_RESULTS = - MoreTrees.parseLines("package test;", + private static final ParseResult PARSE_RESULTS = + MoreTrees.parseLines( + "package test;", "", "final class TestClass {", " public String toString() {", diff --git a/src/main/java/com/google/testing/compile/Diagnostics.java b/src/test/java/com/google/testing/compile/VerificationFailureStrategy.java similarity index 50% rename from src/main/java/com/google/testing/compile/Diagnostics.java rename to src/test/java/com/google/testing/compile/VerificationFailureStrategy.java index 5fc2c923..8e0e6804 100644 --- a/src/main/java/com/google/testing/compile/Diagnostics.java +++ b/src/test/java/com/google/testing/compile/VerificationFailureStrategy.java @@ -13,27 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.testing.compile; -import javax.tools.Diagnostic; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.TestVerb; -/** - * Utilities for working with {@link Diagnostic} instances and collections thereof. - * - * @author Gregory Kick - */ -final class Diagnostics { - private Diagnostics() {} +final class VerificationFailureStrategy extends FailureStrategy { + static final class VerificationException extends RuntimeException { + private static final long serialVersionUID = 1L; - /** - * Returns {@code diagnostics} as a {@link String} similar to the error output printed by - * {@code javac}. - */ - static String toString(Iterable> diagnostics) { - StringBuilder builder = new StringBuilder(); - for (Diagnostic diagnostic : diagnostics) { - builder.append(diagnostic.toString()).append('\n'); + VerificationException(String message) { + super(message); } - return builder.toString(); + } + + /** A {@link TestVerb} that throws something other than {@link AssertionError}. */ + static final TestVerb VERIFY = new TestVerb(new VerificationFailureStrategy()); + + @Override + public void fail(String message) { + throw new VerificationFailureStrategy.VerificationException(message); } } From 4ff194c3d2508de883dc78e53601e5b543c90ad2 Mon Sep 17 00:00:00 2001 From: cgruber Date: Wed, 26 Oct 2016 12:53:19 -0700 Subject: [PATCH 3/3] Update the travis-ci config to use JDK8, since we now require it for compile-testing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=137307930 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 05f13bc5..0995cc00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,7 @@ install: mvn -B -U install clean --fail-never --quiet -DskipTests=true -Dinvoker script: mvn -B verify jdk: - - openjdk7 - - oraclejdk7 + - oraclejdk8 notifications: email: false