diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1a6f7b71 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + groups: + dependencies: + applies-to: version-updates + patterns: + - "*" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + applies-to: version-updates + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6b7e2ae7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + name: "JDK ${{ matrix.java }}" + strategy: + matrix: + java: [ 8, 11, 17, 21 ] + runs-on: ubuntu-latest + steps: + # Cancel any previous runs for the same branch that are still running. + - name: 'Cancel previous runs' + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa + with: + access_token: ${{ github.token }} + - name: 'Check out repository' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: 'Set up JDK ${{ matrix.java }}' + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 + with: + java-version: ${{ matrix.java }} + distribution: 'zulu' + cache: 'maven' + - name: 'Install' + shell: bash + run: mvn -B -U install clean --fail-never --quiet -DskipTests=true -Dinvoker.skip=true + - name: 'Test' + shell: bash + run: mvn -B verify diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0995cc00..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: java -install: mvn -B -U install clean --fail-never --quiet -DskipTests=true -Dinvoker.skip=true -script: mvn -B verify - -jdk: - - oraclejdk8 - -notifications: - email: false - -branches: - only: - - master - - /^release.*$/ diff --git a/README.md b/README.md index 2fb5cec8..a7fa2b74 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,11 @@ Compile Testing =============== -A library for testing javac compilation with or without annotation processors. See the [javadoc][package-info] for usage examples. +[![Build Status][ci-shield]][ci-link] +[![Maven Release][maven-shield]][maven-link] +[![Javadoc][javadoc-shield]][javadoc-link] -Latest Release --------------- - -The latest release is version `0.8`. Include it as a [Maven](http://maven.apache.org/) dependency with the following snippet: - -``` - - com.google.testing.compile - compile-testing - 0.8 - test - -``` +A library for testing javac compilation with or without annotation processors. See the [javadoc][javadoc-link] for usage examples. License ------- @@ -34,4 +24,9 @@ License See the License for the specific language governing permissions and limitations under the License. -[package-info]: https://github.com/google/compile-testing/blob/master/src/main/java/com/google/testing/compile/package-info.java +[ci-shield]: https://github.com/google/compile-testing/actions/workflows/ci.yml/badge.svg?branch=main +[ci-link]: https://github.com/google/compile-testing/actions +[maven-shield]: https://img.shields.io/maven-central/v/com.google.testing.compile/compile-testing.png +[maven-link]: https://search.maven.org/artifact/com.google.testing.compile/compile-testing +[javadoc-shield]: https://javadoc.io/badge/com.google.testing.compile/compile-testing.svg?color=blue +[javadoc-link]: https://javadoc.io/doc/com.google.testing.compile/compile-testing diff --git a/pom.xml b/pom.xml index 90a328d9..efa72c06 100644 --- a/pom.xml +++ b/pom.xml @@ -9,25 +9,24 @@ com.google.testing.compile compile-testing - 1.0-SNAPSHOT + HEAD-SNAPSHOT Compile Testing Utilities for testing compilation. - 1.3 - 21.0 - 0.35 - 4.12 - 3.0.1 + 1.4.4 http://github.com/google/compile-testing + GitHub http://github.com/google/compile-testing/issues + 2013 + The Apache Software License, Version 2.0 @@ -35,20 +34,23 @@ repo + - 3.1.1 + 3.5.0 + scm:git:http://github.com/google/compile-testing scm:git:git@github.com:google/compile-testing.git http://github.com/google/compile-testing HEAD + junit junit - ${junit.version} + 4.13.2 com.google.truth @@ -59,42 +61,50 @@ com.google.truth.extensions truth-java8-extension ${truth.version} + test com.google.guava guava - ${guava.version} - - - com.sun - tools - ${java.version} - system - ${toolsjar} - - - com.google.code.findbugs - jsr305 - ${jsr305.version} - true + 33.4.8-jre com.google.errorprone error_prone_annotations - 2.0.8 + 2.38.0 provided com.google.auto.value auto-value - ${auto-value.version} + 1.11.0 + + + com.google.auto + auto-common + 1.2.2 + + + org.jspecify + jspecify + 1.0.0 + + + + . + + LICENSE.txt + + META-INF + + maven-compiler-plugin - 3.1 + 3.14.0 1.8 1.8 @@ -105,53 +115,92 @@ maven-release-plugin - 2.5.1 + 3.1.1 - release + sonatype-oss-release deploy maven-jar-plugin - 2.5 + 3.4.2 + + + maven-javadoc-plugin + 3.11.2 + + + --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + + + + + maven-site-plugin + 3.21.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + ${test.jvm.flags} + + - default-profile + tools-jar - true + false ${java.home}/../lib/tools.jar - - ${java.home}/../lib/tools.jar - + + + com.sun + tools + ${java.version} + system + ${java.home}/../lib/tools.jar + + - mac-profile + classes-jar false ${java.home}/../Classes/classes.jar - - ${java.home}/../Classes/classes.jar - + + + com.sun + tools + ${java.version} + system + + + ${java.home}/../Classes/classes.jar + + - release + sonatype-oss-release false + org.apache.maven.plugins maven-gpg-plugin - 1.4 + 3.2.7 sign-artifacts @@ -161,8 +210,9 @@ + org.apache.maven.plugins maven-source-plugin - 2.1.2 + 3.3.1 attach-sources @@ -171,8 +221,9 @@ + org.apache.maven.plugins maven-javadoc-plugin - 2.8 + 3.11.2 attach-docs @@ -183,5 +234,21 @@ + + add-exports + + [9,) + + + + --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + + + diff --git a/src/main/java/com/google/testing/compile/Breadcrumbs.java b/src/main/java/com/google/testing/compile/Breadcrumbs.java index a65b18b9..b5adef93 100644 --- a/src/main/java/com/google/testing/compile/Breadcrumbs.java +++ b/src/main/java/com/google/testing/compile/Breadcrumbs.java @@ -19,7 +19,6 @@ import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.google.common.collect.Lists; - import com.sun.source.tree.BlockTree; import com.sun.source.tree.BreakTree; import com.sun.source.tree.ClassTree; @@ -37,7 +36,6 @@ import com.sun.source.tree.VariableTree; import com.sun.source.util.SimpleTreeVisitor; import com.sun.source.util.TreePath; - import java.util.List; /** @@ -54,9 +52,6 @@ private Breadcrumbs() {} * Returns a string describing the {@link TreePath} given. */ static String describeTreePath(TreePath path) { - // TODO(spratt) The number of extra strings this creates by building a list of strings, - // and then joining it is at least a little wasteful. Consider modifying the BreadcrumbVisitor - // to take a StringBuilder and traverse the whole path. return Joiner.on("->").join(getBreadcrumbList(path)); } diff --git a/src/main/java/com/google/testing/compile/Compilation.java b/src/main/java/com/google/testing/compile/Compilation.java index 1893b0cc..57342b41 100644 --- a/src/main/java/com/google/testing/compile/Compilation.java +++ b/src/main/java/com/google/testing/compile/Compilation.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.testing.compile.JavaFileObjects.asByteSource; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toList; import static javax.tools.Diagnostic.Kind.ERROR; @@ -155,9 +156,8 @@ 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)) + return generatedFiles().stream() + .filter(generated -> requireNonNull(generated.toUri().getPath()).endsWith(expectedFilename)) .findFirst(); } @@ -220,16 +220,12 @@ public String toString() { /** Returns a description of the why the compilation failed. */ String describeFailureDiagnostics() { - 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')); - // If we compiled with -Werror we should output the warnings too - if (compiler.options().contains("-Werror")) { - warnings().stream().forEach(warning -> message.append(warning).append('\n')); + ImmutableList> diagnostics = diagnostics(); + if (diagnostics.isEmpty()) { + return "Compilation produced no diagnostics.\n"; } + StringBuilder message = new StringBuilder("Compilation produced the following diagnostics:\n"); + diagnostics.forEach(diagnostic -> message.append(diagnostic).append('\n')); return message.toString(); } @@ -269,7 +265,6 @@ private String describeGeneratedFile(JavaFileObject generatedFile) { } } - // TODO(dpb): Use Guava's toImmutableList() once that's available. MOE:strip_line private static Collector> toImmutableList() { return collectingAndThen(toList(), ImmutableList::copyOf); } diff --git a/src/main/java/com/google/testing/compile/CompilationRule.java b/src/main/java/com/google/testing/compile/CompilationRule.java index 81df84f0..d54e05ba 100644 --- a/src/main/java/com/google/testing/compile/CompilationRule.java +++ b/src/main/java/com/google/testing/compile/CompilationRule.java @@ -81,7 +81,7 @@ public Elements getElements() { * @throws IllegalStateException if this method is invoked outside the execution of the rule. */ public Types getTypes() { - checkState(elements != null, "Not running within the rule"); + checkState(types != null, "Not running within the rule"); return types; } diff --git a/src/main/java/com/google/testing/compile/CompilationSubject.java b/src/main/java/com/google/testing/compile/CompilationSubject.java index 7222e8a1..22b093ba 100644 --- a/src/main/java/com/google/testing/compile/CompilationSubject.java +++ b/src/main/java/com/google/testing/compile/CompilationSubject.java @@ -16,14 +16,17 @@ package com.google.testing.compile; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Predicates.notNull; import static com.google.common.collect.Iterables.size; -import static com.google.common.collect.Streams.mapWithIndex; +import static com.google.common.truth.Fact.fact; +import static com.google.common.truth.Fact.simpleFact; 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.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -35,9 +38,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.common.truth.FailureStrategy; +import com.google.common.collect.Streams; +import com.google.common.truth.Fact; +import com.google.common.truth.FailureMetadata; 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.io.IOException; @@ -53,15 +57,16 @@ import javax.tools.JavaFileManager.Location; import javax.tools.JavaFileObject; import javax.tools.StandardLocation; +import org.jspecify.annotations.Nullable; /** A {@link Truth} subject for a {@link Compilation}. */ -public final class CompilationSubject extends Subject { +public final class CompilationSubject extends Subject { - private static final SubjectFactory FACTORY = + private static final Subject.Factory FACTORY = new CompilationSubjectFactory(); - /** Returns a {@link SubjectFactory} for a {@link Compilation}. */ - public static SubjectFactory compilations() { + /** Returns a {@link Subject.Factory} for a {@link Compilation}. */ + public static Subject.Factory compilations() { return FACTORY; } @@ -70,15 +75,19 @@ public static CompilationSubject assertThat(Compilation actual) { return assertAbout(compilations()).that(actual); } - CompilationSubject(FailureStrategy failureStrategy, Compilation actual) { - super(failureStrategy, actual); + private final @Nullable Compilation actual; + + CompilationSubject(FailureMetadata failureMetadata, @Nullable Compilation actual) { + super(failureMetadata, actual); + this.actual = actual; } /** Asserts that the compilation succeeded. */ public void succeeded() { - if (actual().status().equals(FAILURE)) { - failureStrategy.fail( - actual().describeFailureDiagnostics() + actual().describeGeneratedSourceFiles()); + Compilation actual = actualNotNull(); + if (actual.status().equals(FAILURE)) { + failWithoutActual( + simpleFact(actual.describeFailureDiagnostics() + actual.describeGeneratedSourceFiles())); } } @@ -90,10 +99,11 @@ public void succeededWithoutWarnings() { /** 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()); + if (actualNotNull().status().equals(SUCCESS)) { + failWithoutActual( + simpleFact( + "Compilation was expected to fail, but contained no errors.\n\n" + + actualNotNull().describeGeneratedSourceFiles())); } } @@ -169,17 +179,18 @@ public DiagnosticInFile hadNoteContainingMatch(Pattern expectedPattern) { private void checkDiagnosticCount( int expectedCount, Diagnostic.Kind kind, Diagnostic.Kind... more) { Iterable> diagnostics = - actual().diagnosticsOfKind(kind, more); + actualNotNull().diagnosticsOfKind(kind, more); int actualCount = size(diagnostics); if (actualCount != expectedCount) { - failureStrategy.fail( - messageListing( - diagnostics, - "Expected %d %s, but found the following %d %s:", - expectedCount, - kindPlural(kind), - actualCount, - kindPlural(kind))); + failWithoutActual( + simpleFact( + messageListing( + diagnostics, + "Expected %d %s, but found the following %d %s:", + expectedCount, + kindPlural(kind), + actualCount, + kindPlural(kind)))); } } @@ -277,15 +288,17 @@ private ImmutableList> findMatchingDiagnost Diagnostic.Kind kind, Diagnostic.Kind... more) { ImmutableList> diagnosticsOfKind = - actual().diagnosticsOfKind(kind, more); + actualNotNull().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, but only found:", expectedDiagnostic)); + failWithoutActual( + simpleFact( + messageListing( + diagnosticsOfKind, "Expected %s, but only found:", expectedDiagnostic))); } return diagnosticsWithMessage; } @@ -297,20 +310,14 @@ private ImmutableList> findMatchingDiagnost @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)); + String path = packageName.isEmpty() ? fileName : packageName.replace('.', '/') + '/' + fileName; + return generatedFile(location, path); } /** 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); + return checkGeneratedFile(actualNotNull().generatedFile(location, path), location, path); } /** Asserts that compilation generated a source file for a type with a given qualified name. */ @@ -320,20 +327,32 @@ public JavaFileObjectSubject generatedSourceFile(String qualifiedName) { StandardLocation.SOURCE_OUTPUT, qualifiedName.replaceAll("\\.", "/") + ".java"); } + private static final JavaFileObject ALREADY_FAILED = + JavaFileObjects.forSourceLines( + "compile.Failure", "package compile;", "", "final class Failure {}"); + private JavaFileObjectSubject checkGeneratedFile( - Optional generatedFile, Location location, String format, Object... args) { + Optional generatedFile, Location location, String path) { 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'); + // TODO(b/132162475): Use Facts if it becomes public API. + ImmutableList.Builder facts = ImmutableList.builder(); + facts.add(fact("in location", location.getName())); + facts.add(simpleFact("it generated:")); + for (JavaFileObject generated : actualNotNull().generatedFiles()) { + if (requireNonNull(generated.toUri().getPath()).contains(location.getName())) { + facts.add(simpleFact(" " + generated.toUri().getPath())); } } - fail(builder.toString()); + failWithoutActual( + fact("expected to generate file", "/" + path), facts.build().toArray(new Fact[0])); + return ignoreCheck().about(javaFileObjects()).that(ALREADY_FAILED); } - return check().about(javaFileObjects()).that(generatedFile.get()); + return check("generatedFile(/%s)", path).about(javaFileObjects()).that(generatedFile.get()); + } + + private Compilation actualNotNull() { + isNotNull(); + return checkNotNull(actual); } private static Collector> toImmutableList() { @@ -371,11 +390,12 @@ Stream mapDiagnostics(Function> findDiagnosticsInFil filterDiagnostics( diagnostic -> { JavaFileObject source = diagnostic.getSource(); - return source != null && source.toUri().getPath().equals(expectedFilePath); + return source != null + && requireNonNull(source.toUri().getPath()).equals(expectedFilePath); }); if (diagnosticsInFile.isEmpty()) { failExpectingMatchingDiagnostic( @@ -445,7 +466,7 @@ ImmutableList linesInFile() { } return lines; } - + /** * Returns a {@link Collector} that lists the file lines numbered by the input stream (1-based). */ @@ -455,9 +476,12 @@ ImmutableList linesInFile() { /** Lists the line at a line number (1-based). */ String listLine(long lineNumber) { - return lineNumber == Diagnostic.NOPOS - ? "(no associated line)" - : String.format("%4d: %s", lineNumber, linesInFile().get((int) (lineNumber - 1))); + if (lineNumber == Diagnostic.NOPOS) { + return "(no associated line)"; + } + checkArgument(lineNumber > 0 && lineNumber <= linesInFile().size(), + "Invalid line number %s; number of lines is only %s", lineNumber, linesInFile().size()); + return String.format("%4d: %s", lineNumber, linesInFile().get((int) (lineNumber - 1))); } } @@ -496,8 +520,9 @@ public void onLineContaining(String expectedLineSubstring) { * expectedLineSubstring} */ private long findLineContainingSubstring(String expectedLineSubstring) { + // The explicit type arguments below are needed by our nullness checker. ImmutableSet matchingLines = - mapWithIndex( + Streams.mapWithIndex( linesInFile.linesInFile().stream(), (line, index) -> line.contains(expectedLineSubstring) ? index : null) .filter(notNull()) diff --git a/src/main/java/com/google/testing/compile/CompilationSubjectFactory.java b/src/main/java/com/google/testing/compile/CompilationSubjectFactory.java index 396a0a37..b2ff5ff4 100644 --- a/src/main/java/com/google/testing/compile/CompilationSubjectFactory.java +++ b/src/main/java/com/google/testing/compile/CompilationSubjectFactory.java @@ -15,15 +15,17 @@ */ package com.google.testing.compile; -import com.google.common.truth.FailureStrategy; -import com.google.common.truth.SubjectFactory; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; import com.google.common.truth.Truth; +import org.jspecify.annotations.Nullable; /** A {@link Truth} subject factory for a {@link Compilation}. */ -final class CompilationSubjectFactory extends SubjectFactory { +final class CompilationSubjectFactory implements Subject.Factory { @Override - public CompilationSubject getSubject(FailureStrategy fs, Compilation that) { - return new CompilationSubject(fs, that); + public CompilationSubject createSubject( + FailureMetadata failureMetadata, @Nullable Compilation that) { + return new CompilationSubject(failureMetadata, that); } } diff --git a/src/main/java/com/google/testing/compile/Compiler.java b/src/main/java/com/google/testing/compile/Compiler.java index 02d11f61..827f2912 100644 --- a/src/main/java/com/google/testing/compile/Compiler.java +++ b/src/main/java/com/google/testing/compile/Compiler.java @@ -16,30 +16,41 @@ package com.google.testing.compile; import static com.google.common.base.Functions.toStringFunction; +import static com.google.common.collect.ImmutableList.toImmutableList; import static java.nio.charset.StandardCharsets.UTF_8; import static javax.tools.ToolProvider.getSystemJavaCompiler; import com.google.auto.value.AutoValue; -import com.google.common.base.Joiner; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.base.StandardSystemProperty; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.testing.compile.Compilation.Status; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URL; import java.net.URLClassLoader; -import java.util.ArrayList; import java.util.LinkedHashSet; -import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Set; import javax.annotation.processing.Processor; import javax.tools.DiagnosticCollector; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import org.jspecify.annotations.Nullable; /** An object that can {@link #compile} Java source files. */ @AutoValue +// clashes with java.lang.Compiler (which is deprecated for removal in 9) +@SuppressWarnings("JavaLangClash") public abstract class Compiler { /** Returns the {@code javac} compiler. */ @@ -49,7 +60,8 @@ public static Compiler javac() { /** 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()); + return new AutoValue_Compiler( + javaCompiler, ImmutableList.of(), ImmutableList.of(), Optional.empty(), Optional.empty()); } abstract JavaCompiler javaCompiler(); @@ -60,6 +72,14 @@ public static Compiler compiler(JavaCompiler javaCompiler) { /** The options passed to the compiler. */ public abstract ImmutableList options(); + /** The compilation class path. If not present, the system class path is used. */ + public abstract Optional> classPath(); + + /** + * The annotation processor path. If not present, the system annotation processor path is used. + */ + public abstract Optional> annotationProcessorPath(); + /** * Uses annotation processors during compilation. These replace any previously specified. * @@ -79,7 +99,8 @@ public final Compiler withProcessors(Processor... processors) { * @return a new instance with the same options and the given processors */ public final Compiler withProcessors(Iterable processors) { - return copy(ImmutableList.copyOf(processors), options()); + return copy( + ImmutableList.copyOf(processors), options(), classPath(), annotationProcessorPath()); } /** @@ -96,22 +117,52 @@ public final Compiler withOptions(Object... options) { * * @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()); + public final Compiler withOptions(Iterable options) { + return copy( + processors(), + FluentIterable.from(options).transform(toStringFunction()).toList(), + classPath(), + annotationProcessorPath()); } /** - * Uses the classpath from the passed on classloader (and its parents) for the compilation - * instead of the system classpath. + * Uses the classpath from the passed on classloader (and its parents) for the compilation instead + * of the system classpath. * * @throws IllegalArgumentException if the given classloader had classpaths which we could not * determine or use for compilation. + * @deprecated prefer {@link #withClasspath(Iterable)}. This method only supports {@link + * URLClassLoader} and the default system classloader, and {@link File}s are usually a more + * natural way to expression compilation classpaths than class loaders. */ + @Deprecated public final Compiler withClasspathFrom(ClassLoader classloader) { - String classpath = getClasspathFromClassloader(classloader); - ImmutableList options = - ImmutableList.builder().add("-classpath").add(classpath).addAll(options()).build(); - return copy(processors(), options); + return copy( + processors(), + options(), + Optional.of(getClasspathFromClassloader(classloader)), + annotationProcessorPath()); + } + + /** Uses the given classpath for the compilation instead of the system classpath. */ + public final Compiler withClasspath(Iterable classPath) { + return copy( + processors(), + options(), + Optional.of(ImmutableList.copyOf(classPath)), + annotationProcessorPath()); + } + + /** + * Uses the given annotation processor path for the compilation instead of the system annotation + * processor path. + */ + public final Compiler withAnnotationProcessorPath(Iterable annotationProcessorPath) { + return copy( + processors(), + options(), + classPath(), + Optional.of(ImmutableList.copyOf(annotationProcessorPath))); } /** @@ -130,31 +181,57 @@ public final Compilation compile(JavaFileObject... files) { */ 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); + try (StandardJavaFileManager standardFileManager = standardFileManager(diagnosticCollector); + InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(standardFileManager)) { + fileManager.addSourceFiles(files); + classPath().ifPresent(path -> setLocation(fileManager, StandardLocation.CLASS_PATH, path)); + annotationProcessorPath() + .ifPresent( + path -> setLocation(fileManager, StandardLocation.ANNOTATION_PROCESSOR_PATH, path)); + + 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; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private StandardJavaFileManager standardFileManager( + DiagnosticCollector diagnosticCollector) { + return javaCompiler().getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8); + } + + @VisibleForTesting + static final @Nullable ClassLoader platformClassLoader = getPlatformClassLoader(); + + private static @Nullable ClassLoader getPlatformClassLoader() { + try { + // JDK >= 9 + return (ClassLoader) ClassLoader.class.getMethod("getPlatformClassLoader").invoke(null); + } catch (ReflectiveOperationException e) { + // Java <= 8 + return null; } - return compilation; } /** @@ -163,41 +240,63 @@ public final Compilation compile(Iterable files) { * @throws IllegalArgumentException if the given classloader had classpaths which we could not * determine or use for compilation. */ - private static String getClasspathFromClassloader(ClassLoader currentClassloader) { + private static ImmutableList getClasspathFromClassloader(ClassLoader classloader) { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); - // Add all URLClassloaders in the hirearchy till the system classloader. - List classloaders = new ArrayList<>(); - while(true) { - if (currentClassloader instanceof URLClassLoader) { - // We only know how to extract classpaths from URLClassloaders. - classloaders.add((URLClassLoader) currentClassloader); - } else { - throw new IllegalArgumentException("Classpath for compilation could not be extracted " - + "since given classloader is not an instance of URLClassloader"); - } + // Concatenate search paths from all classloaders in the hierarchy 'till the system classloader. + Set classpaths = new LinkedHashSet<>(); + for (ClassLoader currentClassloader = classloader; + ; + currentClassloader = currentClassloader.getParent()) { if (currentClassloader == systemClassLoader) { + Iterables.addAll( + classpaths, + Splitter.on(StandardSystemProperty.PATH_SEPARATOR.value()) + .split(StandardSystemProperty.JAVA_CLASS_PATH.value())); break; } - currentClassloader = currentClassloader.getParent(); - } - - Set classpaths = new LinkedHashSet<>(); - for (URLClassLoader classLoader : classloaders) { - for (URL url : classLoader.getURLs()) { - if (url.getProtocol().equals("file")) { - classpaths.add(url.getPath()); - } else { - throw new IllegalArgumentException("Given classloader consists of classpaths which are " - + "unsupported for compilation."); + if (currentClassloader == platformClassLoader) { + break; + } + if (currentClassloader instanceof URLClassLoader) { + // We only know how to extract classpaths from URLClassloaders. + for (URL url : ((URLClassLoader) currentClassloader).getURLs()) { + if (url.getProtocol().equals("file")) { + classpaths.add(url.getPath()); + } else { + throw new IllegalArgumentException( + "Given classloader consists of classpaths which are " + + "unsupported for compilation."); + } } + } else { + throw new IllegalArgumentException( + String.format( + "Classpath for compilation could not be extracted " + + "since %s is not an instance of URLClassloader", + currentClassloader)); } } - return Joiner.on(':').join(classpaths); + return classpaths.stream().map(File::new).collect(toImmutableList()); + } + + private static void setLocation( + InMemoryJavaFileManager fileManager, StandardLocation location, ImmutableList path) { + try { + fileManager.setLocation(location, path); + } catch (IOException e) { + // impossible by specification + throw new UncheckedIOException(e); + } } - private Compiler copy(ImmutableList processors, ImmutableList options) { - return new AutoValue_Compiler(javaCompiler(), processors, options); + private Compiler copy( + ImmutableList processors, + ImmutableList options, + Optional> classPath, + Optional> annotationProcessorPath) { + return new AutoValue_Compiler( + javaCompiler(), processors, options, classPath, annotationProcessorPath); } } diff --git a/src/main/java/com/google/testing/compile/ForwardingStandardJavaFileManager.java b/src/main/java/com/google/testing/compile/ForwardingStandardJavaFileManager.java new file mode 100644 index 00000000..9b612b9f --- /dev/null +++ b/src/main/java/com/google/testing/compile/ForwardingStandardJavaFileManager.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 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.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.Collection; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; + +/** + * Forwards calls to a given {@link StandardJavaFileManager}. Subclasses of this class might + * override some of these methods and might also provide additional fields and methods. + */ +public class ForwardingStandardJavaFileManager + extends ForwardingJavaFileManager implements StandardJavaFileManager { + + /** + * Creates a new instance of ForwardingStandardJavaFileManager. + * + * @param fileManager delegate to this file manager + */ + protected ForwardingStandardJavaFileManager(StandardJavaFileManager fileManager) { + super(fileManager); + } + + @Override + public Iterable getJavaFileObjectsFromFiles( + Iterable files) { + return fileManager.getJavaFileObjectsFromFiles(files); + } + + @Override + public Iterable getJavaFileObjects(File... files) { + return fileManager.getJavaFileObjects(files); + } + + @Override + public Iterable getJavaFileObjects(String... names) { + return fileManager.getJavaFileObjects(names); + } + + @Override + public Iterable getJavaFileObjectsFromStrings(Iterable names) { + return fileManager.getJavaFileObjectsFromStrings(names); + } + + @Override + public void setLocation(Location location, Iterable path) throws IOException { + fileManager.setLocation(location, path); + } + + @Override + public Iterable getLocation(Location location) { + return fileManager.getLocation(location); + } + + // @Override for JDK 9 only + public void setLocationFromPaths(Location location, Collection searchpath) + throws IOException { + Method setLocationFromPaths; + try { + setLocationFromPaths = + fileManager + .getClass() + .getMethod("setLocationFromPaths", Location.class, Collection.class); + } catch (ReflectiveOperationException e) { + // JDK < 9 + return; + } + try { + setLocationFromPaths.invoke(fileManager, location, searchpath); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java index 9d6442bb..b2641b3a 100644 --- a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java +++ b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java @@ -15,8 +15,10 @@ */ package com.google.testing.compile; +import static com.google.common.collect.MoreCollectors.toOptional; +import static java.util.Objects.requireNonNull; + import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -32,14 +34,16 @@ import java.io.Writer; import java.net.URI; import java.nio.charset.Charset; -import java.util.Map.Entry; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import javax.tools.FileObject; -import javax.tools.ForwardingJavaFileManager; -import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; +import org.jspecify.annotations.Nullable; /** * A file manager implementation that stores all output in memory. @@ -47,16 +51,20 @@ * @author Gregory Kick */ // TODO(gak): under java 1.7 this could all be done with a PathFileManager -final class InMemoryJavaFileManager extends ForwardingJavaFileManager { - private final LoadingCache inMemoryFileObjects = - CacheBuilder.newBuilder().build(new CacheLoader() { - @Override - public JavaFileObject load(URI key) { - return new InMemoryJavaFileObject(key); - } - }); +final class InMemoryJavaFileManager extends ForwardingStandardJavaFileManager { + private final LoadingCache inMemoryOutputs = + CacheBuilder.newBuilder() + .build( + new CacheLoader() { + @Override + public JavaFileObject load(URI key) { + return new InMemoryJavaFileObject(key); + } + }); + + private final Map inMemoryInputs = new HashMap<>(); - InMemoryJavaFileManager(JavaFileManager fileManager) { + InMemoryJavaFileManager(StandardJavaFileManager fileManager) { super(fileManager); } @@ -84,44 +92,68 @@ public boolean isSameFile(FileObject a, FileObject b) { } @Override - public FileObject getFileForInput(Location location, String packageName, - String relativeName) throws IOException { + public @Nullable FileObject getFileForInput( + Location location, String packageName, String relativeName) throws IOException { if (location.isOutputLocation()) { - return inMemoryFileObjects.getIfPresent( - uriForFileObject(location, packageName, relativeName)); - } else { - return super.getFileForInput(location, packageName, relativeName); + return inMemoryOutputs.getIfPresent(uriForFileObject(location, packageName, relativeName)); } + Optional inMemoryInput = findInMemoryInput(packageName, relativeName); + if (inMemoryInput.isPresent()) { + return inMemoryInput.get(); + } + return super.getFileForInput(location, packageName, relativeName); } @Override - public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) - throws IOException { + public @Nullable JavaFileObject getJavaFileForInput( + Location location, String className, Kind kind) throws IOException { if (location.isOutputLocation()) { - return inMemoryFileObjects.getIfPresent(uriForJavaFileObject(location, className, kind)); - } else { - return super.getJavaFileForInput(location, className, kind); + return inMemoryOutputs.getIfPresent(uriForJavaFileObject(location, className, kind)); + } + Optional inMemoryInput = findInMemoryInput(className); + if (inMemoryInput.isPresent()) { + return inMemoryInput.get(); } + return super.getJavaFileForInput(location, className, kind); + } + + private Optional findInMemoryInput(String className) { + int lastDot = className.lastIndexOf('.'); + return findInMemoryInput( + lastDot == -1 ? "" : className.substring(0, lastDot - 1), + className.substring(lastDot + 1) + ".java"); + } + + private Optional findInMemoryInput(String packageName, String relativeName) { + // Assume each input file's URI ends with the package/relative name. It might have other parts + // to the left. + String suffix = + packageName.isEmpty() ? relativeName : packageName.replace('.', '/') + "/" + relativeName; + return inMemoryInputs.entrySet().stream() + .filter(entry -> requireNonNull(entry.getKey().getPath()).endsWith(suffix)) + .map(Map.Entry::getValue) + .collect(toOptional()); // Might have problems if more than one input file matches. } @Override public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException { URI uri = uriForFileObject(location, packageName, relativeName); - return inMemoryFileObjects.getUnchecked(uri); + return inMemoryOutputs.getUnchecked(uri); } @Override public JavaFileObject getJavaFileForOutput(Location location, String className, final Kind kind, FileObject sibling) throws IOException { URI uri = uriForJavaFileObject(location, className, kind); - return inMemoryFileObjects.getUnchecked(uri); + return inMemoryOutputs.getUnchecked(uri); } ImmutableList getGeneratedSources() { ImmutableList.Builder result = ImmutableList.builder(); - for (Entry entry : inMemoryFileObjects.asMap().entrySet()) { - if (entry.getKey().getPath().startsWith("/" + StandardLocation.SOURCE_OUTPUT.name()) + for (Map.Entry entry : inMemoryOutputs.asMap().entrySet()) { + if (requireNonNull(entry.getKey().getPath()) + .startsWith("/" + StandardLocation.SOURCE_OUTPUT.name()) && (entry.getValue().getKind() == Kind.SOURCE)) { result.add(entry.getValue()); } @@ -130,13 +162,20 @@ ImmutableList getGeneratedSources() { } ImmutableList getOutputFiles() { - return ImmutableList.copyOf(inMemoryFileObjects.asMap().values()); + return ImmutableList.copyOf(inMemoryOutputs.asMap().values()); + } + + /** Adds files that should be available in the source path. */ + void addSourceFiles(Iterable files) { + for (JavaFileObject file : files) { + inMemoryInputs.put(file.toUri(), file); + } } private static final class InMemoryJavaFileObject extends SimpleJavaFileObject implements JavaFileObject { private long lastModified = 0L; - private Optional data = Optional.absent(); + private Optional data = Optional.empty(); InMemoryJavaFileObject(URI uri) { super(uri, JavaFileObjects.deduceKind(uri)); @@ -202,7 +241,7 @@ public long getLastModified() { @Override public boolean delete() { - this.data = Optional.absent(); + this.data = Optional.empty(); this.lastModified = 0L; return true; } diff --git a/src/main/java/com/google/testing/compile/JavaFileObjectSubject.java b/src/main/java/com/google/testing/compile/JavaFileObjectSubject.java index b4fc9fcc..80df4894 100644 --- a/src/main/java/com/google/testing/compile/JavaFileObjectSubject.java +++ b/src/main/java/com/google/testing/compile/JavaFileObjectSubject.java @@ -15,49 +15,55 @@ */ package com.google.testing.compile; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.truth.Fact.fact; 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 com.google.testing.compile.TreeDiffer.matchCompilationUnits; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; -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.FailureMetadata; 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 java.util.function.BiFunction; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** Assertions about {@link JavaFileObject}s. */ -public final class JavaFileObjectSubject extends Subject { +public final class JavaFileObjectSubject extends Subject { - private static final SubjectFactory FACTORY = + private static final Subject.Factory FACTORY = new JavaFileObjectSubjectFactory(); - /** Returns a {@link SubjectFactory} for {@link JavaFileObjectSubject}s. */ - public static SubjectFactory javaFileObjects() { + /** Returns a {@link Subject.Factory} for {@link JavaFileObjectSubject}s. */ + public static Subject.Factory javaFileObjects() { return FACTORY; } /** Starts making assertions about a {@link JavaFileObject}. */ - public static JavaFileObjectSubject assertThat(JavaFileObject actual) { + public static JavaFileObjectSubject assertThat(@Nullable JavaFileObject actual) { return assertAbout(FACTORY).that(actual); } - JavaFileObjectSubject(FailureStrategy failureStrategy, JavaFileObject actual) { - super(failureStrategy, actual); + private final @Nullable JavaFileObject actual; + + JavaFileObjectSubject(FailureMetadata failureMetadata, @Nullable JavaFileObject actual) { + super(failureMetadata, actual); + this.actual = actual; } @Override protected String actualCustomStringRepresentation() { - return actual().toUri().getPath(); + return requireNonNull(actualNotNull().toUri().getPath()); } /** @@ -68,12 +74,13 @@ protected String actualCustomStringRepresentation() { public void isEqualTo(@Nullable Object other) { if (!(other instanceof JavaFileObject)) { super.isEqualTo(other); + return; } JavaFileObject otherFile = (JavaFileObject) other; try { - if (!asByteSource(actual()).contentEquals(asByteSource(otherFile))) { - fail("is equal to", other); + if (!asByteSource(actualNotNull()).contentEquals(asByteSource(otherFile))) { + failWithActual("expected to be equal to", other); } } catch (IOException e) { throw new RuntimeException(e); @@ -83,8 +90,8 @@ public void isEqualTo(@Nullable Object other) { /** 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); + if (!asByteSource(actualNotNull()).contentEquals(expected)) { + failWithActual("expected to have contents", expected); } } catch (IOException e) { throw new RuntimeException(e); @@ -97,9 +104,8 @@ public void hasContents(ByteSource expected) { */ public StringSubject contentsAsString(Charset charset) { try { - return check() - .that(JavaFileObjects.asByteSource(actual()).asCharSource(charset).read()) - .named("the contents of " + actualAsString()); + return check("contents()") + .that(JavaFileObjects.asByteSource(actualNotNull()).asCharSource(charset).read()); } catch (IOException e) { throw new RuntimeException(e); } @@ -114,17 +120,65 @@ public StringSubject contentsAsUtf8String() { } /** - * Asserts that the actual file is a source file with contents equivalent to {@code + * Asserts that the actual file is a source file that has an equivalent AST to that of {@code * expectedSource}. */ public void hasSourceEquivalentTo(JavaFileObject expectedSource) { - ParseResult actualResult = Parser.parse(ImmutableList.of(actual())); + performTreeDifference( + expectedSource, + "expected to be equivalent to", + "expected", + (expectedResult, actualResult) -> + diffCompilationUnits( + getOnlyElement(expectedResult.compilationUnits()), + getOnlyElement(actualResult.compilationUnits()))); + } + + /** + * Asserts that the every node in the AST of {@code expectedPattern} + * exists in the actual file's AST, in the same order. + * + *

Methods, constructors, fields, and types that are in the pattern must have the exact same + * modifiers and annotations as the actual AST. Ordering of AST nodes is also important (i.e. a + * type with identical members in a different order will fail the assertion). Types must match the + * entire type declaration: type parameters, {@code extends}/{@code implements} clauses, etc. + * Methods must also match the throws clause as well. + * + *

The body of a method or constructor, or field initializer in the actual AST must match the + * pattern in entirety if the member is present in the pattern. + * + *

Said in another way (from a graph-theoretic perspective): the pattern AST must be a subgraph + * of the actual AST. If a method, constructor, or field is in the pattern, that entire subtree, + * including modifiers and annotations, must be equal to the corresponding subtree in the actual + * AST (no proper subgraphs). + */ + public void containsElementsIn(JavaFileObject expectedPattern) { + performTreeDifference( + expectedPattern, + "expected to contain elements in", + "expected pattern", + (expectedResult, actualResult) -> + matchCompilationUnits( + getOnlyElement(expectedResult.compilationUnits()), + actualResult.trees(), + getOnlyElement(actualResult.compilationUnits()), + expectedResult.trees())); + } + + private void performTreeDifference( + JavaFileObject expected, + String failureVerb, + String expectedTitle, + BiFunction differencingFunction) { + ParseResult actualResult = Parser.parse(ImmutableList.of(actualNotNull()), "*actual* source"); CompilationUnitTree actualTree = getOnlyElement(actualResult.compilationUnits()); - ParseResult expectedResult = Parser.parse(ImmutableList.of(expectedSource)); + ParseResult expectedResult = Parser.parse(ImmutableList.of(expected), "*expected* source"); CompilationUnitTree expectedTree = getOnlyElement(expectedResult.compilationUnits()); - TreeDifference treeDifference = diffCompilationUnits(expectedTree, actualTree); + TreeDifference treeDifference = differencingFunction.apply(expectedResult, actualResult); if (!treeDifference.isEmpty()) { String diffReport = @@ -132,29 +186,21 @@ public void hasSourceEquivalentTo(JavaFileObject expectedSource) { 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))); + failWithoutActual( + fact("for file", actualNotNull().toUri().getPath()), + fact(failureVerb, expected.toUri().getPath()), + fact("diff", diffReport), + fact(expectedTitle, expected.getCharContent(false)), + fact("but was", actualNotNull().getCharContent(false))); } catch (IOException e) { throw new IllegalStateException( "Couldn't read from JavaFileObject when it was already in memory.", e); } } } + + private JavaFileObject actualNotNull() { + isNotNull(); + return checkNotNull(actual); + } } diff --git a/src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java b/src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java index 6207b546..174c29af 100644 --- a/src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java +++ b/src/main/java/com/google/testing/compile/JavaFileObjectSubjectFactory.java @@ -15,16 +15,18 @@ */ package com.google.testing.compile; -import com.google.common.truth.FailureStrategy; -import com.google.common.truth.SubjectFactory; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** A factory for {@link JavaFileObjectSubject}s. */ final class JavaFileObjectSubjectFactory - extends SubjectFactory { + implements Subject.Factory { @Override - public JavaFileObjectSubject getSubject(FailureStrategy fs, JavaFileObject that) { - return new JavaFileObjectSubject(fs, that); + public JavaFileObjectSubject createSubject( + FailureMetadata failureMetadata, @Nullable JavaFileObject that) { + return new JavaFileObjectSubject(failureMetadata, that); } } diff --git a/src/main/java/com/google/testing/compile/JavaFileObjects.java b/src/main/java/com/google/testing/compile/JavaFileObjects.java index 81900c32..174bd1e5 100644 --- a/src/main/java/com/google/testing/compile/JavaFileObjects.java +++ b/src/main/java/com/google/testing/compile/JavaFileObjects.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; import static javax.tools.JavaFileObject.Kind.SOURCE; import com.google.common.base.CharMatcher; @@ -162,7 +163,7 @@ public static JavaFileObject forResource(String resourceName) { } static Kind deduceKind(URI uri) { - String path = uri.getPath(); + String path = requireNonNull(uri.getPath()); for (Kind kind : Kind.values()) { if (path.endsWith(kind.extension)) { return kind; diff --git a/src/main/java/com/google/testing/compile/JavaSourceSubjectFactory.java b/src/main/java/com/google/testing/compile/JavaSourceSubjectFactory.java index 80e8041c..ba477913 100644 --- a/src/main/java/com/google/testing/compile/JavaSourceSubjectFactory.java +++ b/src/main/java/com/google/testing/compile/JavaSourceSubjectFactory.java @@ -15,19 +15,19 @@ */ package com.google.testing.compile; -import com.google.common.truth.FailureStrategy; -import com.google.common.truth.SubjectFactory; - +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** - * A Truth {@link SubjectFactory} similar to + * A Truth {@link Subject.Factory} similar to * {@link JavaSourcesSubjectFactory}, but for working with single source files. * * @author Gregory Kick */ public final class JavaSourceSubjectFactory - extends SubjectFactory { + implements Subject.Factory { public static JavaSourceSubjectFactory javaSource() { return new JavaSourceSubjectFactory(); } @@ -35,8 +35,8 @@ public static JavaSourceSubjectFactory javaSource() { private JavaSourceSubjectFactory() {} @Override - public JavaSourcesSubject.SingleSourceAdapter getSubject(FailureStrategy failureStrategy, - JavaFileObject subject) { - return new JavaSourcesSubject.SingleSourceAdapter(failureStrategy, subject); + public JavaSourcesSubject.SingleSourceAdapter createSubject( + FailureMetadata failureMetadata, @Nullable JavaFileObject subject) { + return new JavaSourcesSubject.SingleSourceAdapter(failureMetadata, subject); } } diff --git a/src/main/java/com/google/testing/compile/JavaSourcesSubject.java b/src/main/java/com/google/testing/compile/JavaSourcesSubject.java index f9759e70..1687c2e8 100644 --- a/src/main/java/com/google/testing/compile/JavaSourcesSubject.java +++ b/src/main/java/com/google/testing/compile/JavaSourcesSubject.java @@ -15,16 +15,18 @@ */ package com.google.testing.compile; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.truth.Fact.simpleFact; 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 com.google.testing.compile.TypeEnumerator.getTopLevelTypes; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; -import com.google.common.base.Function; +import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; -import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -32,7 +34,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.ByteSource; -import com.google.common.truth.FailureStrategy; +import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.testing.compile.CompilationSubject.DiagnosticAtColumn; @@ -40,34 +42,39 @@ import com.google.testing.compile.CompilationSubject.DiagnosticOnLine; import com.google.testing.compile.Parser.ParseResult; import com.sun.source.tree.CompilationUnitTree; +import java.io.File; 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.Nullable; +import java.util.Optional; +import java.util.stream.Collector; import javax.annotation.processing.Processor; import javax.tools.Diagnostic; import javax.tools.Diagnostic.Kind; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** * A Truth {@link Subject} that evaluates the result - * of a {@code javac} compilation. See {@link com.google.testing.compile} for usage examples + * of a {@code javac} compilation. See {@link com.google.testing.compile} for usage examples * * @author Gregory Kick */ @SuppressWarnings("restriction") // Sun APIs usage intended -public final class JavaSourcesSubject - extends Subject> +public final class JavaSourcesSubject extends Subject implements CompileTester, ProcessedCompileTesterFactory { - private final List options = new ArrayList(Arrays.asList("-Xlint")); + private final @Nullable Iterable actual; + private final List options = new ArrayList<>(Arrays.asList("-Xlint")); @Nullable private ClassLoader classLoader; + @Nullable private ImmutableList classPath; - JavaSourcesSubject(FailureStrategy failureStrategy, Iterable subject) { - super(failureStrategy, subject); + JavaSourcesSubject( + FailureMetadata failureMetadata, @Nullable Iterable actual) { + super(failureMetadata, actual); + this.actual = actual; } @Override @@ -82,12 +89,24 @@ public JavaSourcesSubject withCompilerOptions(String... options) { return this; } + /** + * @deprecated prefer {@link #withClasspath(Iterable)}. This method only supports {@link + * java.net.URLClassLoader} and the default system classloader, and {@link File}s are usually + * a more natural way to expression compilation classpaths than class loaders. + */ + @Deprecated @Override public JavaSourcesSubject withClasspathFrom(ClassLoader classLoader) { this.classLoader = classLoader; return this; } + @Override + public JavaSourcesSubject withClasspath(Iterable classPath) { + this.classPath = ImmutableList.copyOf(classPath); + return this; + } + @Override public CompileTester processedWith(Processor first, Processor... rest) { return processedWith(Lists.asList(first, rest)); @@ -126,7 +145,7 @@ private final class CompilationClause implements CompileTester { private final ImmutableSet processors; private CompilationClause() { - this(ImmutableSet.of()); + this(ImmutableSet.of()); } private CompilationClause(Iterable processors) { @@ -135,11 +154,13 @@ private CompilationClause(Iterable processors) { @Override public void parsesAs(JavaFileObject first, JavaFileObject... rest) { - if (Iterables.isEmpty(actual())) { - failureStrategy.fail( - "Compilation generated no additional source files, though some were expected."); + if (Iterables.isEmpty(actualNotNull())) { + failWithoutActual( + simpleFact( + "Compilation generated no additional source files, though some were expected.")); + return; } - ParseResult actualResult = Parser.parse(actual()); + ParseResult actualResult = Parser.parse(actualNotNull(), "*actual* source"); ImmutableList> errors = actualResult.diagnosticsByKind().get(Kind.ERROR); if (!errors.isEmpty()) { @@ -148,116 +169,116 @@ public void parsesAs(JavaFileObject first, JavaFileObject... rest) { message.append('\n'); message.append(error); } - failureStrategy.fail(message.toString()); + failWithoutActual(simpleFact(message.toString())); + return; } - final ParseResult expectedResult = Parser.parse(Lists.asList(first, rest)); - final FluentIterable actualTrees = FluentIterable.from( - actualResult.compilationUnits()); - final FluentIterable expectedTrees = FluentIterable.from( - expectedResult.compilationUnits()); - - Function> getTypesFunction = - new Function>() { - @Override public ImmutableSet apply(CompilationUnitTree compilationUnit) { - return TypeEnumerator.getTopLevelTypes(compilationUnit); - } - }; - - final ImmutableMap> expectedTreeTypes = - Maps.toMap(expectedTrees, getTypesFunction); - final ImmutableMap> actualTreeTypes = - Maps.toMap(actualTrees, getTypesFunction); - final ImmutableMap> - matchedTrees = Maps.toMap(expectedTrees, - new Function>() { - @Override public Optional apply( - final CompilationUnitTree expectedTree) { - return Iterables.tryFind(actualTrees, - new Predicate() { - @Override public boolean apply(CompilationUnitTree actualTree) { - return expectedTreeTypes.get(expectedTree).equals( - actualTreeTypes.get(actualTree)); + ParseResult expectedResult = Parser.parse(Lists.asList(first, rest), "*expected* source"); + ImmutableList actualTrees = + actualResult.compilationUnits().stream() + .map(TypedCompilationUnit::create) + .collect(toImmutableList()); + ImmutableList expectedTrees = + expectedResult.compilationUnits().stream() + .map(TypedCompilationUnit::create) + .collect(toImmutableList()); + + ImmutableMap> matchedTrees = + Maps.toMap( + expectedTrees, + expectedTree -> + actualTrees.stream() + .filter(actualTree -> expectedTree.types().equals(actualTree.types())) + .findFirst()); + + matchedTrees.forEach( + (expectedTree, maybeActualTree) -> { + if (!maybeActualTree.isPresent()) { + failNoCandidates(expectedTree.types(), expectedTree.tree(), actualTrees); + return; + } + TypedCompilationUnit actualTree = maybeActualTree.get(); + TreeDifference treeDifference = + TreeDiffer.diffCompilationUnits(expectedTree.tree(), actualTree.tree()); + if (!treeDifference.isEmpty()) { + String diffReport = + treeDifference.getDiffReport( + new TreeContext(expectedTree.tree(), expectedResult.trees()), + new TreeContext(actualTree.tree(), actualResult.trees())); + failWithCandidate( + expectedTree.tree().getSourceFile(), + actualTree.tree().getSourceFile(), + diffReport); } }); - } - }); - - for (Map.Entry> - matchedTreePair : matchedTrees.entrySet()) { - final CompilationUnitTree expectedTree = matchedTreePair.getKey(); - if (!matchedTreePair.getValue().isPresent()) { - failNoCandidates(expectedTreeTypes.get(expectedTree), expectedTree, - actualTreeTypes, actualTrees); - } else { - CompilationUnitTree actualTree = matchedTreePair.getValue().get(); - TreeDifference treeDifference = TreeDiffer.diffCompilationUnits(expectedTree, actualTree); - if (!treeDifference.isEmpty()) { - String diffReport = treeDifference.getDiffReport( - new TreeContext(expectedTree, expectedResult.trees()), - new TreeContext(actualTree, actualResult.trees())); - failWithCandidate(expectedTree.getSourceFile(), actualTree.getSourceFile(), diffReport); - } - } - } } /** Called when the {@code generatesSources()} verb fails with no diff candidates. */ - private void failNoCandidates(ImmutableSet expectedTypes, + private void failNoCandidates( + ImmutableSet expectedTypes, CompilationUnitTree expectedTree, - final ImmutableMap> actualTypes, - FluentIterable actualTrees) { - String generatedTypesReport = Joiner.on('\n').join( - actualTrees.transform(new Function() { - @Override public String apply(CompilationUnitTree generated) { - return String.format("- %s in <%s>", - actualTypes.get(generated), - generated.getSourceFile().toUri().getPath()); - } - }) - .toList()); - failureStrategy.fail(Joiner.on('\n').join( - "", - "An expected source declared one or more top-level types that were not present.", - "", - String.format("Expected top-level types: <%s>", expectedTypes), - String.format("Declared by expected file: <%s>", - expectedTree.getSourceFile().toUri().getPath()), - "", - "The top-level types that were present are as follows: ", - "", - generatedTypesReport, - "")); + ImmutableList actualTrees) { + String generatedTypesReport = + Joiner.on('\n') + .join( + actualTrees.stream() + .map( + generated -> + String.format( + "- %s in <%s>", + generated.types(), + generated.tree().getSourceFile().toUri().getPath())) + .collect(toList())); + failWithoutActual( + simpleFact( + Joiner.on('\n') + .join( + "", + "An expected source declared one or more top-level types that were not " + + "present.", + "", + String.format("Expected top-level types: <%s>", expectedTypes), + String.format( + "Declared by expected file: <%s>", + expectedTree.getSourceFile().toUri().getPath()), + "", + "The top-level types that were present are as follows: ", + "", + generatedTypesReport, + ""))); } /** Called when the {@code generatesSources()} verb fails with a diff candidate. */ - private void failWithCandidate(JavaFileObject expectedSource, - JavaFileObject actualSource, String diffReport) { + private void failWithCandidate( + JavaFileObject expectedSource, JavaFileObject actualSource, String diffReport) { try { - failureStrategy.fail(Joiner.on('\n').join( - "", - "Source declared the same top-level types of an expected source, but", - "didn't match exactly.", - "", - String.format("Expected file: <%s>", expectedSource.toUri().getPath()), - String.format("Actual file: <%s>", actualSource.toUri().getPath()), - "", - "Diffs:", - "======", - "", - diffReport, - "", - "Expected Source: ", - "================", - "", - expectedSource.getCharContent(false).toString(), - "", - "Actual Source:", - "=================", - "", - actualSource.getCharContent(false).toString())); + failWithoutActual( + simpleFact( + Joiner.on('\n') + .join( + "", + "Source declared the same top-level types of an expected source, but", + "didn't match exactly.", + "", + String.format("Expected file: <%s>", expectedSource.toUri().getPath()), + String.format("Actual file: <%s>", actualSource.toUri().getPath()), + "", + "Diffs:", + "======", + "", + diffReport, + "", + "Expected Source: ", + "================", + "", + expectedSource.getCharContent(false).toString(), + "", + "Actual Source:", + "=================", + "", + actualSource.getCharContent(false).toString()))); } catch (IOException e) { - throw new IllegalStateException("Couldn't read from JavaFileObject when it was already " - + "in memory.", e); + throw new IllegalStateException( + "Couldn't read from JavaFileObject when it was already " + "in memory.", e); } } @@ -265,7 +286,7 @@ private void failWithCandidate(JavaFileObject expectedSource, @Override public SuccessfulCompilationClause compilesWithoutError() { Compilation compilation = compilation(); - check().about(compilations()).that(compilation).succeeded(); + check("compilation()").about(compilations()).that(compilation).succeeded(); return new SuccessfulCompilationBuilder(compilation); } @@ -273,7 +294,7 @@ public SuccessfulCompilationClause compilesWithoutError() { @Override public CleanCompilationClause compilesWithoutWarnings() { Compilation compilation = compilation(); - check().about(compilations()).that(compilation).succeededWithoutWarnings(); + check("compilation()").about(compilations()).that(compilation).succeededWithoutWarnings(); return new CleanCompilationBuilder(compilation); } @@ -281,7 +302,7 @@ public CleanCompilationClause compilesWithoutWarnings() { @Override public UnsuccessfulCompilationClause failsToCompile() { Compilation compilation = compilation(); - check().about(compilations()).that(compilation).failed(); + check("compilation()").about(compilations()).that(compilation).failed(); return new UnsuccessfulCompilationBuilder(compilation); } @@ -290,7 +311,21 @@ private Compilation compilation() { if (classLoader != null) { compiler = compiler.withClasspathFrom(classLoader); } - return compiler.compile(actual()); + if (classPath != null) { + compiler = compiler.withClasspath(classPath); + } + return compiler.compile(actualNotNull()); + } + } + + @AutoValue + abstract static class TypedCompilationUnit { + abstract CompilationUnitTree tree(); + + abstract ImmutableSet types(); + + static TypedCompilationUnit create(CompilationUnitTree tree) { + return new AutoValue_JavaSourcesSubject_TypedCompilationUnit(tree, getTopLevelTypes(tree)); } } @@ -305,8 +340,8 @@ private CompilationClause newCompilationClause(Iterable pro /** * 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}. + * @param the type parameter for {@link CompilationWithWarningsClause}. {@code this} must be + * an instance of {@code T}; otherwise some calls will throw {@link ClassCastException}. */ abstract class CompilationWithWarningsBuilder implements CompilationWithWarningsClause { protected final Compilation compilation; @@ -318,7 +353,7 @@ protected CompilationWithWarningsBuilder(Compilation compilation) { @CanIgnoreReturnValue @Override public T withNoteCount(int noteCount) { - check().about(compilations()).that(compilation).hadNoteCount(noteCount); + check("compilation()").about(compilations()).that(compilation).hadNoteCount(noteCount); return thisObject(); } @@ -326,13 +361,16 @@ public T withNoteCount(int noteCount) { @Override public FileClause withNoteContaining(String messageFragment) { return new FileBuilder( - check().about(compilations()).that(compilation).hadNoteContaining(messageFragment)); + check("compilation()") + .about(compilations()) + .that(compilation) + .hadNoteContaining(messageFragment)); } @CanIgnoreReturnValue @Override public T withWarningCount(int warningCount) { - check().about(compilations()).that(compilation).hadWarningCount(warningCount); + check("compilation()").about(compilations()).that(compilation).hadWarningCount(warningCount); return thisObject(); } @@ -340,24 +378,28 @@ public T withWarningCount(int warningCount) { @Override public FileClause withWarningContaining(String messageFragment) { return new FileBuilder( - check().about(compilations()).that(compilation).hadWarningContaining(messageFragment)); + check("compilation()") + .about(compilations()) + .that(compilation) + .hadWarningContaining(messageFragment)); } @CanIgnoreReturnValue public T withErrorCount(int errorCount) { - check().about(compilations()).that(compilation).hadErrorCount(errorCount); + check("compilation()").about(compilations()).that(compilation).hadErrorCount(errorCount); return thisObject(); } @CanIgnoreReturnValue public FileClause withErrorContaining(String messageFragment) { return new FileBuilder( - check().about(compilations()).that(compilation).hadErrorContaining(messageFragment)); + check("compilation()") + .about(compilations()) + .that(compilation) + .hadErrorContaining(messageFragment)); } - /** - * Returns this object, cast to {@code T}. - */ + /** Returns this object, cast to {@code T}. */ @SuppressWarnings("unchecked") protected final T thisObject() { return (T) this; @@ -408,10 +450,10 @@ public ChainingClause atColumn(long columnNumber) { } /** - * Base implementation of {@link GeneratedPredicateClause GeneratedPredicateClause} and - * {@link ChainingClause ChainingClause>}. + * Base implementation of {@link GeneratedPredicateClause GeneratedPredicateClause} and {@link + * ChainingClause ChainingClause>}. * - * @param T the type parameter to {@link GeneratedPredicateClause}. {@code this} must be an + * @param the type parameter to {@link GeneratedPredicateClause}. {@code this} must be an * instance of {@code T}. */ private abstract class GeneratedCompilationBuilder extends CompilationWithWarningsBuilder @@ -424,7 +466,9 @@ protected GeneratedCompilationBuilder(Compilation compilation) { @CanIgnoreReturnValue @Override public T generatesSources(JavaFileObject first, JavaFileObject... rest) { - new JavaSourcesSubject(failureStrategy, compilation.generatedSourceFiles()) + check("generatedSourceFiles()") + .about(javaSources()) + .that(compilation.generatedSourceFiles()) .parsesAs(first, rest); return thisObject(); } @@ -434,8 +478,8 @@ public T generatesSources(JavaFileObject first, JavaFileObject... rest) { public T generatesFiles(JavaFileObject first, JavaFileObject... rest) { for (JavaFileObject expected : Lists.asList(first, rest)) { if (!wasGenerated(expected)) { - failureStrategy.fail("Did not find a generated file corresponding to " - + expected.getName()); + failWithoutActual( + simpleFact("Did not find a generated file corresponding to " + expected.getName())); } } return thisObject(); @@ -461,7 +505,7 @@ boolean wasGenerated(JavaFileObject expected) { public SuccessfulFileClause generatesFileNamed( JavaFileManager.Location location, String packageName, String relativeName) { final JavaFileObjectSubject javaFileObjectSubject = - check() + check("compilation()") .about(compilations()) .that(compilation) .generatedFile(location, packageName, relativeName); @@ -531,21 +575,39 @@ public static JavaSourcesSubject assertThat(JavaFileObject javaFileObject) { public static JavaSourcesSubject assertThat( JavaFileObject javaFileObject, JavaFileObject... javaFileObjects) { return assertAbout(javaSources()) - .that(ImmutableList.builder() - .add(javaFileObject) - .add(javaFileObjects) - .build()); + .that( + ImmutableList.builder() + .add(javaFileObject) + .add(javaFileObjects) + .build()); } - public static final class SingleSourceAdapter - extends Subject + private Iterable actualNotNull() { + isNotNull(); + return checkNotNull(actual); + } + + private static Collector> toImmutableList() { + return collectingAndThen(toList(), ImmutableList::copyOf); + } + + public static final class SingleSourceAdapter extends Subject implements CompileTester, ProcessedCompileTesterFactory { private final JavaSourcesSubject delegate; - SingleSourceAdapter(FailureStrategy failureStrategy, JavaFileObject subject) { - super(failureStrategy, subject); + SingleSourceAdapter(FailureMetadata failureMetadata, @Nullable JavaFileObject subject) { + super(failureMetadata, subject); + /* + * TODO(b/131918061): It would make more sense to eliminate SingleSourceAdapter entirely. + * Users can already use assertThat(JavaFileObject, JavaFileObject...) above for a single + * file. Anyone who needs a Subject.Factory could fall back to + * `about(javaSources()).that(ImmutableSet.of(source))`. + * + * We could take that on, or we could wait for JavaSourcesSubject to go away entirely in favor + * of CompilationSubject. + */ this.delegate = - new JavaSourcesSubject(failureStrategy, ImmutableList.of(subject)); + check("delegate()").about(javaSources()).that(ImmutableList.of(checkNotNull(subject))); } @Override @@ -558,11 +620,22 @@ public JavaSourcesSubject withCompilerOptions(String... options) { return delegate.withCompilerOptions(options); } + /** + * @deprecated prefer {@link #withClasspath(Iterable)}. This method only supports {@link + * java.net.URLClassLoader} and the default system classloader, and {@link File}s are + * usually a more natural way to expression compilation classpaths than class loaders. + */ + @Deprecated @Override public JavaSourcesSubject withClasspathFrom(ClassLoader classLoader) { return delegate.withClasspathFrom(classLoader); } + @Override + public JavaSourcesSubject withClasspath(Iterable classPath) { + return delegate.withClasspath(classPath); + } + @Override public CompileTester processedWith(Processor first, Processor... rest) { return delegate.newCompilationClause(Lists.asList(first, rest)); diff --git a/src/main/java/com/google/testing/compile/JavaSourcesSubjectFactory.java b/src/main/java/com/google/testing/compile/JavaSourcesSubjectFactory.java index 645f242a..517e21e1 100644 --- a/src/main/java/com/google/testing/compile/JavaSourcesSubjectFactory.java +++ b/src/main/java/com/google/testing/compile/JavaSourcesSubjectFactory.java @@ -15,19 +15,19 @@ */ package com.google.testing.compile; -import com.google.common.truth.FailureStrategy; -import com.google.common.truth.SubjectFactory; - +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** - * A Truth {@link SubjectFactory} for creating + * A Truth {@link Subject.Factory} for creating * {@link JavaSourcesSubject} instances. * * @author Gregory Kick */ public final class JavaSourcesSubjectFactory - extends SubjectFactory> { + implements Subject.Factory> { public static JavaSourcesSubjectFactory javaSources() { return new JavaSourcesSubjectFactory(); } @@ -35,8 +35,8 @@ public static JavaSourcesSubjectFactory javaSources() { private JavaSourcesSubjectFactory() {} @Override - public JavaSourcesSubject getSubject(FailureStrategy failureStrategy, - Iterable subject) { - return new JavaSourcesSubject(failureStrategy, subject); + public JavaSourcesSubject createSubject( + FailureMetadata failureMetadata, @Nullable Iterable subject) { + return new JavaSourcesSubject(failureMetadata, subject); } } diff --git a/src/main/java/com/google/testing/compile/MoreTrees.java b/src/main/java/com/google/testing/compile/MoreTrees.java index 7ffec537..9c4fd1d6 100644 --- a/src/main/java/com/google/testing/compile/MoreTrees.java +++ b/src/main/java/com/google/testing/compile/MoreTrees.java @@ -15,7 +15,6 @@ */ package com.google.testing.compile; -import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -35,7 +34,8 @@ import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; import java.util.Arrays; -import javax.annotation.Nullable; +import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * A class containing methods which are useful for gaining access to {@code Tree} instances from @@ -52,7 +52,7 @@ static CompilationUnitTree parseLinesToTree(String... source) { /** Parses the source given into a {@link CompilationUnitTree}. */ static CompilationUnitTree parseLinesToTree(Iterable source) { Iterable parseResults = - Parser.parse(ImmutableList.of(JavaFileObjects.forSourceLines("", source))) + Parser.parse(ImmutableList.of(JavaFileObjects.forSourceLines("", source)), "source") .compilationUnits(); return Iterables.getOnlyElement(parseResults); } @@ -64,13 +64,13 @@ static ParseResult parseLines(String... source) { /** Parses the source given and produces a {@link ParseResult}. */ static ParseResult parseLines(Iterable source) { - return Parser.parse(ImmutableList.of(JavaFileObjects.forSourceLines("", source))); + return Parser.parse(ImmutableList.of(JavaFileObjects.forSourceLines("", source)), "source"); } /** * Finds the first instance of the given {@link Tree.Kind} that is a subtree of the root provided. * - * @throw IllegalArgumentException if no such subtree exists. + * @throws IllegalArgumentException if no such subtree exists. */ static Tree findSubtree(CompilationUnitTree root, Tree.Kind treeKind) { return findSubtree(root, treeKind, null); @@ -82,10 +82,10 @@ static Tree findSubtree(CompilationUnitTree root, Tree.Kind treeKind) { * *

See the doc on {@link #findSubtreePath} for details on the identifier param. * - * @throw IllegalArgumentException if no such subtree exists. + * @throws IllegalArgumentException if no such subtree exists. */ - static Tree findSubtree(CompilationUnitTree root, Tree.Kind treeKind, - @Nullable String identifier) { + static Tree findSubtree( + CompilationUnitTree root, Tree.Kind treeKind, @Nullable String identifier) { return findSubtreePath(root, treeKind, identifier).getLeaf(); } @@ -93,7 +93,7 @@ static Tree findSubtree(CompilationUnitTree root, Tree.Kind treeKind, * Finds a path to the first instance of the given {@link Tree.Kind} that is a subtree of the root * provided. * - * @throw IllegalArgumentException if no such subtree exists. + * @throws IllegalArgumentException if no such subtree exists. */ static TreePath findSubtreePath(CompilationUnitTree root, Tree.Kind treeKind) { return findSubtreePath(root, treeKind, null); @@ -121,18 +121,15 @@ static TreePath findSubtreePath(CompilationUnitTree root, Tree.Kind treeKind) { */ static TreePath findSubtreePath(CompilationUnitTree root, Tree.Kind treeKind, @Nullable String identifier) { - SearchScanner subtreeFinder = new SearchScanner(treeKind, - (identifier == null) ? Optional.absent() : Optional.of(identifier)); + SearchScanner subtreeFinder = new SearchScanner(treeKind, Optional.ofNullable(identifier)); Optional res = subtreeFinder.scan(root, null); Preconditions.checkArgument(res.isPresent(), "Couldn't find any subtree matching the given " + "criteria. Root: %s, Class: %s, Identifier: %s", root, treeKind, identifier); return res.get(); } - /** - * A {@link TreePathScanner} to power the subtree searches in this class - */ - static final class SearchScanner extends TreePathScanner, Void> { + /** A {@link TreePathScanner} to power the subtree searches in this class */ + static final class SearchScanner extends TreePathScanner, @Nullable Void> { private final Optional identifier; private final Tree.Kind kindSought; @@ -152,8 +149,7 @@ private boolean isMatch(Tree node, Optional idValue) { } else if (!idValue.isPresent()) { idsMatch = false; } else { - idsMatch = (idValue.get() == null && identifier.get() == null) - || identifier.get().equals(idValue.get().toString()); + idsMatch = identifier.get().equals(idValue.get().toString()); } return kindSought.equals(node.getKind()) && idsMatch; } @@ -163,7 +159,7 @@ private boolean isMatch(Tree node, Optional idValue) { * and kind sought. */ private boolean isMatch(Tree node, Object idValue) { - return isMatch(node, Optional.fromNullable(idValue)); + return isMatch(node, Optional.ofNullable(idValue)); } /** Returns a TreePath that includes the current path plus the node provided */ @@ -172,48 +168,48 @@ private Optional currentPathPlus(Tree node) { } /** - * Returns the {@code Optional} value given, or {@code Optional.absent()} if the value given - * was {@code null}. + * Returns the {@code Optional} value given, or {@code Optional.empty()} if the value given was + * {@code null}. */ private Optional absentIfNull(Optional ret) { - return (ret != null) ? ret : Optional.absent(); + return (ret != null) ? ret : Optional.empty(); } @Override - public Optional scan(Tree node, Void v) { + public Optional scan(Tree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } - return isMatch(node, Optional.absent()) - ? currentPathPlus(node) : absentIfNull(super.scan(node, v)); + return isMatch(node, Optional.empty()) + ? currentPathPlus(node) + : absentIfNull(super.scan(node, v)); } @Override - public Optional scan(Iterable nodes, Void v) { - Optional ret = super.scan(nodes, v); - return (ret != null) ? ret : Optional.absent(); + public Optional scan(Iterable nodes, @Nullable Void v) { + return absentIfNull(super.scan(nodes, v)); } /** Returns the first present value. If both values are absent, then returns absent .*/ @Override public Optional reduce(Optional t1, Optional t2) { - return (t1.isPresent()) ? t1 : t2; + return t1.isPresent() ? t1 : t2; } @Override - public Optional visitBreak(@Nullable BreakTree node, Void v) { + public Optional visitBreak(@Nullable BreakTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } - return isMatch(node, node.getLabel()) ? currentPathPlus(node) : Optional.absent(); + return isMatch(node, node.getLabel()) ? currentPathPlus(node) : Optional.empty(); } @Override - public Optional visitClass(@Nullable ClassTree node, Void v) { + public Optional visitClass(@Nullable ClassTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getSimpleName())) { return currentPathPlus(node); } @@ -222,9 +218,9 @@ public Optional visitClass(@Nullable ClassTree node, Void v) { } @Override - public Optional visitContinue(@Nullable ContinueTree node, Void v) { + public Optional visitContinue(@Nullable ContinueTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getLabel())) { return currentPathPlus(node); } @@ -233,9 +229,9 @@ public Optional visitContinue(@Nullable ContinueTree node, Void v) { } @Override - public Optional visitIdentifier(@Nullable IdentifierTree node, Void v) { + public Optional visitIdentifier(@Nullable IdentifierTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getName())) { return currentPathPlus(node); } @@ -244,9 +240,10 @@ public Optional visitIdentifier(@Nullable IdentifierTree node, Void v) } @Override - public Optional visitLabeledStatement(@Nullable LabeledStatementTree node, Void v) { + public Optional visitLabeledStatement( + @Nullable LabeledStatementTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getLabel())) { return currentPathPlus(node); } @@ -255,9 +252,9 @@ public Optional visitLabeledStatement(@Nullable LabeledStatementTree n } @Override - public Optional visitLiteral(@Nullable LiteralTree node, Void v) { + public Optional visitLiteral(@Nullable LiteralTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getValue())) { return currentPathPlus(node); } @@ -266,9 +263,9 @@ public Optional visitLiteral(@Nullable LiteralTree node, Void v) { } @Override - public Optional visitMethod(@Nullable MethodTree node, Void v) { + public Optional visitMethod(@Nullable MethodTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getName())) { return currentPathPlus(node); } @@ -277,9 +274,9 @@ public Optional visitMethod(@Nullable MethodTree node, Void v) { } @Override - public Optional visitMemberSelect(@Nullable MemberSelectTree node, Void v) { + public Optional visitMemberSelect(@Nullable MemberSelectTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getIdentifier())) { return currentPathPlus(node); } @@ -288,9 +285,10 @@ public Optional visitMemberSelect(@Nullable MemberSelectTree node, Voi } @Override - public Optional visitTypeParameter(@Nullable TypeParameterTree node, Void v) { + public Optional visitTypeParameter( + @Nullable TypeParameterTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getName())) { return currentPathPlus(node); } @@ -299,9 +297,9 @@ public Optional visitTypeParameter(@Nullable TypeParameterTree node, V } @Override - public Optional visitVariable(@Nullable VariableTree node, Void v) { + public Optional visitVariable(@Nullable VariableTree node, @Nullable Void v) { if (node == null) { - return Optional.absent(); + return Optional.empty(); } else if (isMatch(node, node.getName())) { return currentPathPlus(node); } diff --git a/src/main/java/com/google/testing/compile/Parser.java b/src/main/java/com/google/testing/compile/Parser.java index 849a9d2f..c2b7314a 100644 --- a/src/main/java/com/google/testing/compile/Parser.java +++ b/src/main/java/com/google/testing/compile/Parser.java @@ -15,121 +15,78 @@ */ 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 com.sun.tools.javac.api.JavacTrees; +import com.sun.tools.javac.file.JavacFileManager; +import com.sun.tools.javac.parser.JavacParser; +import com.sun.tools.javac.parser.ParserFactory; +import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Log; import java.io.IOException; +import java.util.ArrayList; import java.util.List; -import java.util.Locale; import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; -import javax.tools.JavaCompiler; +import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; -import javax.tools.ToolProvider; /** Methods to parse Java source files. */ -public final class Parser { +final class Parser { /** * Parses {@code sources} into {@linkplain CompilationUnitTree compilation units}. This method * does not compile the sources. + * + * @param sourcesDescription describes the sources. Parsing exceptions will contain this string. + * @throws IllegalStateException if any parsing errors occur. */ - static ParseResult parse(Iterable sources) { - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + static ParseResult parse(Iterable sources, String sourcesDescription) { 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); + Context context = new Context(); + context.put(DiagnosticListener.class, diagnosticCollector); + Log log = Log.instance(context); + // The constructor registers the instance in the Context + JavacFileManager unused = new JavacFileManager(context, true, UTF_8); + ParserFactory parserFactory = ParserFactory.instance(context); try { - Iterable parsedCompilationUnits = task.parse(); + List parsedCompilationUnits = new ArrayList<>(); + for (JavaFileObject source : sources) { + log.useSource(source); + JavacParser parser = + parserFactory.newParser( + source.getCharContent(false), + /* keepDocComments= */ true, + /* keepEndPos= */ true, + /* keepLineMap= */ true); + JCCompilationUnit unit = parser.parseCompilationUnit(); + unit.sourcefile = source; + parsedCompilationUnits.add(unit); + } List> diagnostics = diagnosticCollector.getDiagnostics(); - if (foundParseErrors(parsedCompilationUnits, diagnostics)) { - throw new IllegalStateException( - "error while parsing:\n" + Joiner.on('\n').join(diagnostics)); + if (foundParseErrors(diagnostics)) { + String msgPrefix = String.format("Error while parsing %s:\n", sourcesDescription); + throw new IllegalStateException(msgPrefix + Joiner.on('\n').join(diagnostics)); } return new ParseResult( - sortDiagnosticsByKind(diagnostics), parsedCompilationUnits, Trees.instance(task)); + sortDiagnosticsByKind(diagnostics), parsedCompilationUnits, JavacTrees.instance(context)); } 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); + /** Returns {@code true} if errors were found while parsing source files. */ + private static boolean foundParseErrors(List> diagnostics) { + return diagnostics.stream().anyMatch(d -> d.getKind().equals(ERROR)); } private static ImmutableListMultimap> @@ -167,7 +124,7 @@ static final class ParseResult { return diagnostics; } - Iterable compilationUnits() { + ImmutableList compilationUnits() { return compilationUnits; } @@ -175,4 +132,6 @@ Trees trees() { return trees; } } + + private Parser() {} } diff --git a/src/main/java/com/google/testing/compile/ProcessedCompileTesterFactory.java b/src/main/java/com/google/testing/compile/ProcessedCompileTesterFactory.java index 50e034fa..30b7eeb3 100644 --- a/src/main/java/com/google/testing/compile/ProcessedCompileTesterFactory.java +++ b/src/main/java/com/google/testing/compile/ProcessedCompileTesterFactory.java @@ -15,7 +15,7 @@ */ package com.google.testing.compile; -import javax.annotation.CheckReturnValue; +import java.io.File; import javax.annotation.processing.Processor; /** @@ -30,28 +30,37 @@ public interface ProcessedCompileTesterFactory { * Adds options that will be passed to the compiler. {@code -Xlint} is the first option, by * default. */ - @CheckReturnValue ProcessedCompileTesterFactory withCompilerOptions(Iterable options); - + ProcessedCompileTesterFactory withCompilerOptions(Iterable options); + /** * Adds options that will be passed to the compiler. {@code -Xlint} is the first option, by * default. */ - @CheckReturnValue ProcessedCompileTesterFactory withCompilerOptions(String... options); + ProcessedCompileTesterFactory withCompilerOptions(String... options); /** * Attempts to extract the classpath from the classpath of the Classloader argument, including all * its parents up to (and including) the System Classloader. - *

- * If not specified, we will use the System classpath for compilation. + * + *

If not specified, we will use the System classpath for compilation. + * + * @deprecated prefer {@link #withClasspath(Iterable)}. This method only supports {@link + * java.net.URLClassLoader} and the default system classloader, and {@link File}s are usually + * a more natural way to expression compilation classpaths than class loaders. */ - @CheckReturnValue + @Deprecated ProcessedCompileTesterFactory withClasspathFrom(ClassLoader classloader); - - /** Adds {@linkplain Processor annotation processors} to the compilation being tested. */ - @CheckReturnValue + + /** + * Sets the compilation classpath. + * + *

If not specified, we will use the System classpath for compilation. + */ + ProcessedCompileTesterFactory withClasspath(Iterable classPath); + + /** Adds {@linkplain Processor annotation processors} to the compilation being tested. */ CompileTester processedWith(Processor first, Processor... rest); - /** Adds {@linkplain Processor annotation processors} to the compilation being tested. */ - @CheckReturnValue + /** Adds {@linkplain Processor annotation processors} to the compilation being tested. */ CompileTester processedWith(Iterable processors); } diff --git a/src/main/java/com/google/testing/compile/TreeContext.java b/src/main/java/com/google/testing/compile/TreeContext.java index 66dc5ef4..712c705f 100644 --- a/src/main/java/com/google/testing/compile/TreeContext.java +++ b/src/main/java/com/google/testing/compile/TreeContext.java @@ -58,17 +58,20 @@ Trees getTrees() { } /** - * Returns the {@code TreePath} to the given sub-{@code Tree} of this object's - * {@code CompilationUnitTree} + * Returns the {@code TreePath} to the given sub-{@code Tree} of this object's {@code + * CompilationUnitTree} * * @throws IllegalArgumentException if the node provided is not a sub-{@code Tree} of this - * object's {@code CompilationUnitTree}. + * object's {@code CompilationUnitTree}. */ TreePath getNodePath(Tree node) { TreePath treePath = trees.getPath(compilationUnit, node); - checkArgument(treePath != null, "The node provided was not a subtree of the " - + "CompilationUnitTree in this TreeContext. CompilationUnit: %s; Node:", - compilationUnit, node); + checkArgument( + treePath != null, + "The node provided was not a subtree of the " + + "CompilationUnitTree in this TreeContext. CompilationUnit: %s; Node: %s", + compilationUnit, + node); return treePath; } diff --git a/src/main/java/com/google/testing/compile/TreeDiffer.java b/src/main/java/com/google/testing/compile/TreeDiffer.java index e826c0d5..9df5585d 100644 --- a/src/main/java/com/google/testing/compile/TreeDiffer.java +++ b/src/main/java/com/google/testing/compile/TreeDiffer.java @@ -16,68 +16,45 @@ package com.google.testing.compile; import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.base.Objects; -import com.google.common.base.Optional; - -import com.sun.source.tree.AnnotationTree; -import com.sun.source.tree.ArrayAccessTree; -import com.sun.source.tree.ArrayTypeTree; -import com.sun.source.tree.AssertTree; -import com.sun.source.tree.AssignmentTree; -import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.BlockTree; -import com.sun.source.tree.BreakTree; -import com.sun.source.tree.CaseTree; -import com.sun.source.tree.CatchTree; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Iterables.isEmpty; +import static java.util.Objects.requireNonNull; + +import com.google.auto.common.MoreTypes; +import com.google.auto.value.AutoValue; +import com.google.common.base.CaseFormat; +import com.google.common.base.Equivalence; +import com.google.common.base.Joiner; +import com.google.common.base.VerifyException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.FormatMethod; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.tree.CompoundAssignmentTree; -import com.sun.source.tree.ConditionalExpressionTree; -import com.sun.source.tree.ContinueTree; -import com.sun.source.tree.DoWhileLoopTree; -import com.sun.source.tree.EmptyStatementTree; -import com.sun.source.tree.EnhancedForLoopTree; -import com.sun.source.tree.ErroneousTree; -import com.sun.source.tree.ExpressionStatementTree; -import com.sun.source.tree.ForLoopTree; import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.IfTree; import com.sun.source.tree.ImportTree; -import com.sun.source.tree.InstanceOfTree; -import com.sun.source.tree.LabeledStatementTree; -import com.sun.source.tree.LambdaExpressionTree; -import com.sun.source.tree.LiteralTree; -import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.LineMap; import com.sun.source.tree.MemberSelectTree; -import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; -import com.sun.source.tree.ModifiersTree; -import com.sun.source.tree.NewArrayTree; -import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.ParameterizedTypeTree; -import com.sun.source.tree.ParenthesizedTree; -import com.sun.source.tree.PrimitiveTypeTree; -import com.sun.source.tree.ReturnTree; -import com.sun.source.tree.SwitchTree; -import com.sun.source.tree.SynchronizedTree; -import com.sun.source.tree.ThrowTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import com.sun.source.tree.TryTree; -import com.sun.source.tree.TypeCastTree; -import com.sun.source.tree.TypeParameterTree; -import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.TreeVisitor; import com.sun.source.tree.VariableTree; -import com.sun.source.tree.WhileLoopTree; -import com.sun.source.tree.WildcardTree; import com.sun.source.util.SimpleTreeVisitor; import com.sun.source.util.TreePath; - +import com.sun.source.util.Trees; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.HashSet; import java.util.Iterator; - -import javax.annotation.Nullable; +import java.util.Objects; +import java.util.Set; import javax.lang.model.element.Name; +import javax.lang.model.type.TypeMirror; +import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** * A class for determining how two compilation {@code Tree}s differ from each other. @@ -98,57 +75,79 @@ final class TreeDiffer { private TreeDiffer() {} /** - * Returns a {@code TreeDifference} describing the difference between the two - * {@code CompilationUnitTree}s provided. + * Returns a {@code TreeDifference} describing the difference between the two {@code + * CompilationUnitTree}s provided. + */ + static TreeDifference diffCompilationUnits( + CompilationUnitTree expected, CompilationUnitTree actual) { + return createDiff(checkNotNull(expected), checkNotNull(actual), TreeFilter.KEEP_ALL); + } + + /** + * Returns a {@code TreeDifference} describing the difference between the actual {@code + * CompilationUnitTree} provided and the pattern. See {@link + * JavaFileObjectSubject#containsElementsIn(JavaFileObject)} for more details on how the pattern + * is used. */ - static final TreeDifference diffCompilationUnits(@Nullable CompilationUnitTree expected, - @Nullable CompilationUnitTree actual) { + static TreeDifference matchCompilationUnits( + CompilationUnitTree pattern, + Trees patternTrees, + CompilationUnitTree actual, + Trees actualTrees) { + checkNotNull(pattern); + checkNotNull(actual); + return createDiff( + pattern, actual, new MatchExpectedTreesFilter(pattern, patternTrees, actual, actualTrees)); + } + + private static TreeDifference createDiff( + @Nullable CompilationUnitTree expected, + @Nullable CompilationUnitTree actual, + TreeFilter treeFilter) { TreeDifference.Builder diffBuilder = new TreeDifference.Builder(); - DiffVisitor diffVisitor = new DiffVisitor(diffBuilder); + DiffVisitor diffVisitor = new DiffVisitor(diffBuilder, treeFilter); diffVisitor.scan(expected, actual); return diffBuilder.build(); } /** - * Returns a {@link TreeDifference} describing the difference between the two - * sub-{@code Tree}s. The trees diffed are the leaves of the {@link TreePath}s - * provided. + * Returns a {@link TreeDifference} describing the difference between the two sub-{@code Tree}s. + * The trees diffed are the leaves of the {@link TreePath}s provided. * *

Used for testing. */ - static final TreeDifference diffSubtrees(@Nullable TreePath pathToExpected, - @Nullable TreePath pathToActual) { + static TreeDifference diffSubtrees(TreePath pathToExpected, TreePath pathToActual) { TreeDifference.Builder diffBuilder = new TreeDifference.Builder(); - DiffVisitor diffVisitor = new DiffVisitor(diffBuilder, - pathToExpected, pathToActual); + DiffVisitor diffVisitor = + new DiffVisitor(diffBuilder, TreeFilter.KEEP_ALL, pathToExpected, pathToActual); diffVisitor.scan(pathToExpected.getLeaf(), pathToActual.getLeaf()); return diffBuilder.build(); } /** * A {@code SimpleTreeVisitor} that traverses a {@link Tree} and an argument {@link Tree}, - * verifying equality along the way. Appends each diff it finds to a - * {@link TreeDifference.Builder}. + * verifying equality along the way. Appends each diff it finds to a {@link + * TreeDifference.Builder}. */ - static final class DiffVisitor extends SimpleTreeVisitor { - private TreePath expectedPath; - private TreePath actualPath; + static final class DiffVisitor extends SimpleTreeVisitor<@Nullable Void, Tree> { + private @Nullable TreePath expectedPath; + private @Nullable TreePath actualPath; private final TreeDifference.Builder diffBuilder; + private final TreeFilter filter; - public DiffVisitor(TreeDifference.Builder diffBuilder) { - this.diffBuilder = diffBuilder; - expectedPath = null; - actualPath = null; + DiffVisitor(TreeDifference.Builder diffBuilder, TreeFilter filter) { + this(diffBuilder, filter, null, null); } - /** - * Constructs a DiffVisitor whose {@code TreePath}s are initialized with the paths - * provided. - */ - public DiffVisitor(TreeDifference.Builder diffBuilder, - TreePath pathToExpected, TreePath pathToActual) { + /** Constructs a DiffVisitor whose {@code TreePath}s are initialized with the paths provided. */ + private DiffVisitor( + TreeDifference.Builder diffBuilder, + TreeFilter filter, + @Nullable TreePath pathToExpected, + @Nullable TreePath pathToActual) { this.diffBuilder = diffBuilder; + this.filter = filter; expectedPath = pathToExpected; actualPath = pathToActual; } @@ -164,13 +163,15 @@ public void addTypeMismatch(Tree expected, Tree actual) { } /** - * Adds a {@code TwoWayDiff} if the predicate given evaluates to false. The {@code TwoWayDiff} - * is parameterized by the {@code Tree}s and message format provided. + * Adds a {@code TwoWayDiff} that is parameterized by the {@code Tree}s and message format + * provided. */ - private void checkForDiff(boolean p, String message, Object... formatArgs) { - if (!p) { - diffBuilder.addDifferingNodes(expectedPath, actualPath, String.format(message, formatArgs)); - } + @FormatMethod + private void reportDiff(String message, Object... formatArgs) { + diffBuilder.addDifferingNodes( + requireNonNull(expectedPath), + requireNonNull(actualPath), + String.format(message, formatArgs)); } private TreePath actualPathPlus(Tree actual) { @@ -184,13 +185,13 @@ private TreePath expectedPathPlus(Tree expected) { } /** - * Pushes the {@code expected} and {@code actual} {@link Tree}s onto their respective - * {@link TreePath}s and recurses with {@code expected.accept(this, actual)}, popping the - * stack when the call completes. + * Pushes the {@code expected} and {@code actual} {@link Tree}s onto their respective {@link + * TreePath}s and recurses with {@code expected.accept(this, actual)}, popping the stack when + * the call completes. * *

This should be the ONLY place where either {@link TreePath} is mutated. */ - private Void pushPathAndAccept(Tree expected, Tree actual) { + private @Nullable Void pushPathAndAccept(Tree expected, Tree actual) { TreePath prevExpectedPath = expectedPath; TreePath prevActualPath = actualPath; expectedPath = expectedPathPlus(expected); @@ -209,7 +210,7 @@ private boolean namesEqual(@Nullable Name expected, @Nullable Name actual) { : (actual != null && expected.contentEquals(actual)); } - public Void scan(@Nullable Tree expected, @Nullable Tree actual) { + private void scan(@Nullable Tree expected, @Nullable Tree actual) { if (expected == null && actual != null) { diffBuilder.addExtraActualNode(actualPathPlus(actual)); } else if (expected != null && actual == null) { @@ -217,11 +218,10 @@ public Void scan(@Nullable Tree expected, @Nullable Tree actual) { } else if (actual != null && expected != null) { pushPathAndAccept(expected, actual); } - return null; } - private Void parallelScan(Iterable expecteds, - Iterable actuals) { + private void parallelScan( + Iterable expecteds, Iterable actuals) { if (expecteds != null && actuals != null) { Iterator expectedsIterator = expecteds.iterator(); Iterator actualsIterator = actuals.iterator(); @@ -233,719 +233,342 @@ private Void parallelScan(Iterable expecteds, } else if (expectedsIterator.hasNext() && !actualsIterator.hasNext()) { diffBuilder.addExtraExpectedNode(expectedPathPlus(expectedsIterator.next())); } - } else if (expecteds == null && !isEmptyOrNull(actuals)) { + } else if (expecteds == null && actuals != null && !isEmpty(actuals)) { diffBuilder.addExtraActualNode(actualPathPlus(actuals.iterator().next())); - } else if (actuals == null && !isEmptyOrNull(expecteds)) { + } else if (actuals == null && expecteds != null && !isEmpty(expecteds)) { diffBuilder.addExtraExpectedNode(expectedPathPlus(expecteds.iterator().next())); } - return null; - } - - private boolean isEmptyOrNull(Iterable iterable) { - return iterable == null || !iterable.iterator().hasNext(); - } - - @Override - public Void visitAnnotation(AnnotationTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getAnnotationType(), other.get().getAnnotationType()); - parallelScan(expected.getArguments(), other.get().getArguments()); - return null; - } - - @Override - public Void visitMethodInvocation(MethodInvocationTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - parallelScan(expected.getTypeArguments(), other.get().getTypeArguments()); - scan(expected.getMethodSelect(), other.get().getMethodSelect()); - parallelScan(expected.getArguments(), other.get().getArguments()); - return null; - } - - @Override - public Void visitLambdaExpression(LambdaExpressionTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - parallelScan(expected.getParameters(), other.get().getParameters()); - scan(expected.getBody(), other.get().getBody()); - return null; - } - - @Override - public Void visitMemberReference(MemberReferenceTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getQualifierExpression(), other.get().getQualifierExpression()); - parallelScan(expected.getTypeArguments(), other.get().getTypeArguments()); - checkForDiff(expected.getName().contentEquals(other.get().getName()), - "Expected identifier to be <%s> but was <%s>.", - expected.getName(), other.get().getName()); - return null; - } - - @Override - public Void visitAssert(AssertTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getCondition(), other.get().getCondition()); - scan(expected.getDetail(), other.get().getDetail()); - return null; - } - - @Override - public Void visitAssignment(AssignmentTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getVariable(), other.get().getVariable()); - scan(expected.getExpression(), other.get().getExpression()); - return null; - } - - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getVariable(), other.get().getVariable()); - scan(expected.getExpression(), other.get().getExpression()); - return null; - } - - @Override - public Void visitBinary(BinaryTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getLeftOperand(), other.get().getLeftOperand()); - scan(expected.getRightOperand(), other.get().getRightOperand()); - return null; - } - - @Override - public Void visitBlock(BlockTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.isStatic() == other.get().isStatic(), - "Expected block to be <%s> but was <%s>.", expected.isStatic() ? "static" : "non-static", - other.get().isStatic() ? "static" : "non-static"); - - parallelScan(expected.getStatements(), other.get().getStatements()); - return null; - } - - @Override - public Void visitBreak(BreakTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(namesEqual(expected.getLabel(), other.get().getLabel()), - "Expected label on break statement to be <%s> but was <%s>.", - expected.getLabel(), other.get().getLabel()); - return null; - } - - @Override - public Void visitCase(CaseTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - parallelScan(expected.getStatements(), other.get().getStatements()); - return null; - } - - @Override - public Void visitCatch(CatchTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getParameter(), other.get().getParameter()); - scan(expected.getBlock(), other.get().getBlock()); - return null; - } - - @Override - public Void visitClass(ClassTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.getSimpleName().contentEquals(other.get().getSimpleName()), - "Expected name of type to be <%s> but was <%s>.", - expected.getSimpleName(), other.get().getSimpleName()); - - scan(expected.getModifiers(), other.get().getModifiers()); - parallelScan(expected.getTypeParameters(), other.get().getTypeParameters()); - scan(expected.getExtendsClause(), other.get().getExtendsClause()); - parallelScan(expected.getImplementsClause(), other.get().getImplementsClause()); - parallelScan(expected.getMembers(), other.get().getMembers()); - return null; - } - - @Override - public Void visitConditionalExpression(ConditionalExpressionTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getCondition(), other.get().getCondition()); - scan(expected.getTrueExpression(), other.get().getTrueExpression()); - scan(expected.getFalseExpression(), other.get().getFalseExpression()); - return null; - } - - @Override - public Void visitContinue(ContinueTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(namesEqual(expected.getLabel(), other.get().getLabel()), - "Expected label on continue statement to be <%s> but was <%s>.", - expected.getLabel(), other.get().getLabel()); - return null; - } - - @Override - public Void visitDoWhileLoop(DoWhileLoopTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getCondition(), other.get().getCondition()); - scan(expected.getStatement(), other.get().getStatement()); - return null; - } - - @Override - public Void visitErroneous(ErroneousTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - parallelScan(expected.getErrorTrees(), other.get().getErrorTrees()); - return null; - } - - @Override - public Void visitExpressionStatement(ExpressionStatementTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - return null; - } - - @Override - public Void visitEnhancedForLoop(EnhancedForLoopTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getVariable(), other.get().getVariable()); - scan(expected.getExpression(), other.get().getExpression()); - scan(expected.getStatement(), other.get().getStatement()); - return null; - } - - @Override - public Void visitForLoop(ForLoopTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - parallelScan(expected.getInitializer(), other.get().getInitializer()); - scan(expected.getCondition(), other.get().getCondition()); - parallelScan(expected.getUpdate(), other.get().getUpdate()); - scan(expected.getStatement(), other.get().getStatement()); - return null; - } - - @Override - public Void visitIdentifier(IdentifierTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.getName().contentEquals(other.get().getName()), - "Expected identifier to be <%s> but was <%s>.", - expected.getName(), other.get().getName()); - return null; - } - - @Override - public Void visitIf(IfTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getCondition(), other.get().getCondition()); - scan(expected.getThenStatement(), other.get().getThenStatement()); - scan(expected.getElseStatement(), other.get().getElseStatement()); - return null; - } - - @Override - public Void visitImport(ImportTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.isStatic() == other.get().isStatic(), - "Expected import to be <%s> but was <%s>.", - expected.isStatic() ? "static" : "non-static", - other.get().isStatic() ? "static" : "non-static"); - - scan(expected.getQualifiedIdentifier(), other.get().getQualifiedIdentifier()); - return null; - } - - @Override - public Void visitArrayAccess(ArrayAccessTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - scan(expected.getIndex(), other.get().getIndex()); - return null; - } - - @Override - public Void visitLabeledStatement(LabeledStatementTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.getLabel().contentEquals(other.get().getLabel()), - "Expected statement label to be <%s> but was <%s>.", - expected.getLabel(), other.get().getLabel()); - - scan(expected.getStatement(), other.get().getStatement()); - return null; - } - - @Override - public Void visitLiteral(LiteralTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(Objects.equal(expected.getValue(), other.get().getValue()), - "Expected literal value to be <%s> but was <%s>.", - expected.getValue(), other.get().getValue()); - return null; - } - - @Override - public Void visitMethod(MethodTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.getName().contentEquals(other.get().getName()), - "Expected method name to be <%s> but was <%s>.", - expected.getName(), other.get().getName()); - - scan(expected.getModifiers(), other.get().getModifiers()); - scan(expected.getReturnType(), other.get().getReturnType()); - parallelScan(expected.getTypeParameters(), other.get().getTypeParameters()); - parallelScan(expected.getParameters(), other.get().getParameters()); - parallelScan(expected.getThrows(), other.get().getThrows()); - scan(expected.getBody(), other.get().getBody()); - scan(expected.getDefaultValue(), other.get().getDefaultValue()); - return null; - } - - @Override - public Void visitModifiers(ModifiersTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.getFlags().equals(other.get().getFlags()), - "Expected modifier set to be <%s> but was <%s>.", - expected.getFlags(), other.get().getFlags()); - - parallelScan(expected.getAnnotations(), other.get().getAnnotations()); - return null; - } - - @Override - public Void visitNewArray(NewArrayTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getType(), other.get().getType()); - parallelScan(expected.getDimensions(), other.get().getDimensions()); - parallelScan(expected.getInitializers(), other.get().getInitializers()); - return null; - } - - @Override - public Void visitNewClass(NewClassTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getEnclosingExpression(), other.get().getEnclosingExpression()); - parallelScan(expected.getTypeArguments(), other.get().getTypeArguments()); - scan(expected.getIdentifier(), other.get().getIdentifier()); - parallelScan(expected.getArguments(), other.get().getArguments()); - scan(expected.getClassBody(), other.get().getClassBody()); - return null; - } - - @Override - public Void visitParenthesized(ParenthesizedTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - return null; } + /** + * {@inheritDoc} + * + *

The exact set of {@code visitFoo} methods depends on the compiler version. For example, if + * the compiler is for a version of the language that has the {@code yield} statement, then + * there will be a {@code visitYield(YieldTree)}. But if it's for an earlier version, then not + * only will there not be that method, there will also not be a {@code YieldTree} type at all. + * That means it is impossible for this class to have a complete set of visit methods and also + * compile on earlier versions. + * + *

Instead, we override {@link SimpleTreeVisitor#defaultAction} and inspect the visited tree + * with reflection. We can use {@link Tree.Kind#getInterface()} to get the specific interface, + * such as {@code YieldTree}, and within that interface we just look for {@code getFoo()} + * methods. The {@code actual} tree must have the same {@link Tree.Kind} and then we can compare + * the results of calling the corresponding {@code getFoo()} methods on both trees. The + * comparison depends on the return type of the method: + * + *

    + *
  • For a method returning {@link Tree} or a subtype, we call {@link #scan(Tree, Tree)}, + * which will visit the subtrees recursively. + *
  • For a method returning a type that is assignable to {@code Iterable}, + * we call {@link #parallelScan(Iterable, Iterable)}. + *
  • For a method returning {@link Name}, we compare with {@link Name#contentEquals}. + *
  • Otherwise we just compare with {@link Objects#equals(Object, Object)}. + *
  • Methods returning certain types are ignored: {@link LineMap}, because we don't care if + * the line numbers don't match between the two trees; {@link JavaFileObject}, because the + * value for two distinct trees will never compare equal. + *
+ * + *

This technique depends on the specific way the tree interfaces are defined. In practice it + * works well. Besides solving the {@code YieldTree} issue, it also ensures we don't overlook + * properties of any given tree type, include properties that may be added in later versions. + * For example, in versions that have sealed interfaces, the {@code permits} clause is + * represented by a method {@code ClassTree.getPermitsClause()}. Earlier versions obviously + * don't have that method. + */ @Override - public Void visitReturn(ReturnTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - return null; - } - - @Override - public Void visitMemberSelect(MemberSelectTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.getIdentifier().contentEquals(other.get().getIdentifier()), - "Expected member identifier to be <%s> but was <%s>.", - expected.getIdentifier(), other.get().getIdentifier()); - - scan(expected.getExpression(), other.get().getExpression()); - return null; - } - - @Override - public Void visitEmptyStatement(EmptyStatementTree expected, Tree actual) { - if (!checkTypeAndCast(expected, actual).isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - return null; - } - - @Override - public Void visitSwitch(SwitchTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - parallelScan(expected.getCases(), other.get().getCases()); - return null; - } - - @Override - public Void visitSynchronized(SynchronizedTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - scan(expected.getBlock(), other.get().getBlock()); - return null; - } - - @Override - public Void visitThrow(ThrowTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getExpression(), other.get().getExpression()); - return null; - } - - @Override - public Void visitCompilationUnit(CompilationUnitTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - parallelScan(expected.getPackageAnnotations(), other.get().getPackageAnnotations()); - scan(expected.getPackageName(), other.get().getPackageName()); - parallelScan(expected.getImports(), other.get().getImports()); - parallelScan(expected.getTypeDecls(), other.get().getTypeDecls()); - return null; - } - - @Override - public Void visitTry(TryTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getBlock(), other.get().getBlock()); - parallelScan(expected.getCatches(), other.get().getCatches()); - scan(expected.getFinallyBlock(), other.get().getFinallyBlock()); - return null; - } - - @Override - public Void visitParameterizedType(ParameterizedTypeTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getType(), other.get().getType()); - parallelScan(expected.getTypeArguments(), other.get().getTypeArguments()); - return null; - } - - @Override - public Void visitArrayType(ArrayTypeTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getType(), other.get().getType()); - return null; - } - - @Override - public Void visitTypeCast(TypeCastTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getType(), other.get().getType()); - scan(expected.getExpression(), other.get().getExpression()); - return null; - } - - @Override - public Void visitPrimitiveType(PrimitiveTypeTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - checkForDiff(expected.getPrimitiveTypeKind() == other.get().getPrimitiveTypeKind(), - "Expected primitive type kind to be <%s> but was <%s>.", - expected.getPrimitiveTypeKind(), other.get().getPrimitiveTypeKind()); - return null; - } - - @Override - public Void visitTypeParameter(TypeParameterTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { + public @Nullable Void defaultAction(Tree expected, Tree actual) { + if (expected.getKind() != actual.getKind()) { addTypeMismatch(expected, actual); return null; } - - checkForDiff(expected.getName().contentEquals(other.get().getName()), - "Expected type parameter name to be <%s> but was <%s>.", - expected.getName(), other.get().getName()); - - parallelScan(expected.getBounds(), other.get().getBounds()); - return null; - } - - @Override - public Void visitInstanceOf(InstanceOfTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; + Class treeInterface = expected.getKind().asInterface(); + for (Method method : treeInterface.getMethods()) { + if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) { + Object expectedValue; + Object actualValue; + try { + expectedValue = method.invoke(expected); + actualValue = method.invoke(actual); + } catch (ReflectiveOperationException e) { + throw new VerifyException(e); + } + defaultCompare(method, expected.getKind(), expectedValue, actualValue); + } } - - scan(expected.getExpression(), other.get().getExpression()); - scan(expected.getType(), other.get().getType()); return null; } - @Override - public Void visitUnary(UnaryTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; + private void defaultCompare(Method method, Tree.Kind kind, Object expected, Object actual) { + Type type = method.getGenericReturnType(); + if (isIterableOfTree(type)) { + @SuppressWarnings("unchecked") + Iterable expectedList = (Iterable) expected; + @SuppressWarnings("unchecked") + Iterable actualList = (Iterable) actual; + actualList = filterActual(method, kind, expectedList, actualList); + parallelScan(expectedList, actualList); + } else if (type instanceof Class && Tree.class.isAssignableFrom((Class) type)) { + scan((Tree) expected, (Tree) actual); + } else if (expected instanceof LineMap && actual instanceof LineMap) { + return; // we don't require lines to match exactly + } else if (expected instanceof JavaFileObject && actual instanceof JavaFileObject) { + return; // these will never be equal unless the inputs are identical + } else { + boolean eq = + (expected instanceof Name) + ? namesEqual((Name) expected, (Name) actual) + : Objects.equals(expected, actual); + if (!eq) { + // If MemberSelectTree.getIdentifier() doesn't match, we will say + // "Expected member-select identifier to be but was ." + String treeKind = + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, kind.name()); + String property = + CaseFormat.UPPER_CAMEL + .to(CaseFormat.LOWER_UNDERSCORE, method.getName().substring("get".length())) + .replace('_', ' '); + reportDiff( + "Expected %s %s to be <%s> but was <%s>.", + treeKind, + property, + expected, + actual); + } } - - scan(expected.getExpression(), other.get().getExpression()); - return null; } - @Override - public Void visitVariable(VariableTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; + /** + * Applies {@link #filter} to the list of subtrees from the actual tree. If it is a + * {@code CompilationUnitTree} then we filter its imports. If it is a {@code ClassTree} then we + * filter its members. + */ + private Iterable filterActual( + Method method, + Tree.Kind kind, + Iterable expected, + Iterable actual) { + switch (kind) { + case COMPILATION_UNIT: + if (method.getName().equals("getImports")) { + @SuppressWarnings("unchecked") + Iterable expectedImports = (Iterable) expected; + @SuppressWarnings("unchecked") + Iterable actualImports = (Iterable) actual; + return filter.filterImports( + ImmutableList.copyOf(expectedImports), ImmutableList.copyOf(actualImports)); + } + break; + case CLASS: + if (method.getName().equals("getMembers")) { + return filter.filterActualMembers( + ImmutableList.copyOf(expected), ImmutableList.copyOf(actual)); + } + break; + default: + } + return actual; + } + + private static boolean isIterableOfTree(Type type) { + if (!(type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (!Iterable.class.isAssignableFrom((Class) parameterizedType.getRawType()) + || parameterizedType.getActualTypeArguments().length != 1) { + return false; + } + Type argType = parameterizedType.getActualTypeArguments()[0]; + if (argType instanceof Class) { + return Tree.class.isAssignableFrom((Class) argType); + } else if (argType instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) argType; + return wildcardType.getUpperBounds().length == 1 + && wildcardType.getUpperBounds()[0] instanceof Class + && Tree.class.isAssignableFrom((Class) wildcardType.getUpperBounds()[0]); + } else { + return false; } - - checkForDiff(expected.getName().contentEquals(other.get().getName()), - "Expected variable name to be <%s> but was <%s>.", - expected.getName(), other.get().getName()); - - scan(expected.getModifiers(), other.get().getModifiers()); - scan(expected.getType(), other.get().getType()); - scan(expected.getInitializer(), other.get().getInitializer()); - return null; } @Override - public Void visitWhileLoop(WhileLoopTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getCondition(), other.get().getCondition()); - scan(expected.getStatement(), other.get().getStatement()); - return null; + public @Nullable Void visitOther(Tree expected, Tree actual) { + throw new UnsupportedOperationException("cannot compare unknown trees"); } + } - @Override - public Void visitWildcard(WildcardTree expected, Tree actual) { - Optional other = checkTypeAndCast(expected, actual); - if (!other.isPresent()) { - addTypeMismatch(expected, actual); - return null; - } - - scan(expected.getBound(), other.get().getBound()); - return null; - } + /** Strategy for determining which {link Tree}s should be diffed in {@link DiffVisitor}. */ + private interface TreeFilter { + + /** Returns the subset of {@code actualMembers} that should be diffed by {@link DiffVisitor}. */ + ImmutableList filterActualMembers( + ImmutableList expectedMembers, ImmutableList actualMembers); + + /** Returns the subset of {@code actualImports} that should be diffed by {@link DiffVisitor}. */ + ImmutableList filterImports( + ImmutableList expectedImports, ImmutableList actualImports); + + /** A {@link TreeFilter} that doesn't filter any subtrees out of the actual source AST. */ + TreeFilter KEEP_ALL = + new TreeFilter() { + @Override + public ImmutableList filterActualMembers( + ImmutableList expectedMembers, ImmutableList actualMembers) { + return actualMembers; + } + + @Override + public ImmutableList filterImports( + ImmutableList expectedImports, ImmutableList actualImports) { + return actualImports; + } + }; + } - @Override - public Void visitOther(Tree expected, Tree actual) { - throw new UnsupportedOperationException("cannot compare unknown trees"); + /** + * A {@link TreeFilter} that ignores all {@link Tree}s that don't have a matching {@link Tree} in + * a pattern. For more information on what trees are filtered, see {@link + * JavaFileObjectSubject#containsElementsIn(JavaFileObject)}. + */ + private static class MatchExpectedTreesFilter implements TreeFilter { + private final CompilationUnitTree pattern; + private final Trees patternTrees; + private final CompilationUnitTree actual; + private final Trees actualTrees; + + MatchExpectedTreesFilter( + CompilationUnitTree pattern, + Trees patternTrees, + CompilationUnitTree actual, + Trees actualTrees) { + this.pattern = pattern; + this.patternTrees = patternTrees; + this.actual = actual; + this.actualTrees = actualTrees; + } + + @Override + public ImmutableList filterActualMembers( + ImmutableList patternMembers, ImmutableList actualMembers) { + Set patternVariableNames = new HashSet<>(); + Set patternNestedTypeNames = new HashSet<>(); + Set patternMethods = new HashSet<>(); + for (Tree patternTree : patternMembers) { + patternTree.accept( + new SimpleTreeVisitor<@Nullable Void, @Nullable Void>() { + @Override + public @Nullable Void visitVariable(VariableTree variable, @Nullable Void p) { + patternVariableNames.add(variable.getName().toString()); + return null; + } + + @Override + public @Nullable Void visitMethod(MethodTree method, @Nullable Void p) { + patternMethods.add(MethodSignature.create(pattern, method, patternTrees)); + return null; + } + + @Override + public @Nullable Void visitClass(ClassTree clazz, @Nullable Void p) { + patternNestedTypeNames.add(clazz.getSimpleName().toString()); + return null; + } + }, + null); + } + + ImmutableList.Builder filteredActualTrees = ImmutableList.builder(); + for (Tree actualTree : actualMembers) { + actualTree.accept( + new SimpleTreeVisitor<@Nullable Void, @Nullable Void>() { + @Override + public @Nullable Void visitVariable(VariableTree variable, @Nullable Void p) { + if (patternVariableNames.contains(variable.getName().toString())) { + filteredActualTrees.add(actualTree); + } + return null; + } + + @Override + public @Nullable Void visitMethod(MethodTree method, @Nullable Void p) { + if (patternMethods.contains(MethodSignature.create(actual, method, actualTrees))) { + filteredActualTrees.add(method); + } + return null; + } + + @Override + public @Nullable Void visitClass(ClassTree clazz, @Nullable Void p) { + if (patternNestedTypeNames.contains(clazz.getSimpleName().toString())) { + filteredActualTrees.add(clazz); + } + return null; + } + + @Override + protected @Nullable Void defaultAction(Tree tree, @Nullable Void p) { + filteredActualTrees.add(tree); + return null; + } + }, + null); + } + return filteredActualTrees.build(); + } + + @Override + public ImmutableList filterImports( + ImmutableList patternImports, ImmutableList actualImports) { + ImmutableSet patternImportsAsStrings = + patternImports.stream().map(this::fullyQualifiedImport).collect(toImmutableSet()); + return actualImports + .stream() + .filter(importTree -> patternImportsAsStrings.contains(fullyQualifiedImport(importTree))) + .collect(toImmutableList()); + } + + private String fullyQualifiedImport(ImportTree importTree) { + ImmutableList.Builder names = ImmutableList.builder(); + importTree.getQualifiedIdentifier().accept(IMPORT_NAMES_ACCUMULATOR, names); + return Joiner.on('.').join(names.build().reverse()); } + } - private Optional checkTypeAndCast(T expected, Tree actual) { - Kind expectedKind = checkNotNull(expected).getKind(); - Kind treeKind = checkNotNull(actual).getKind(); - if (expectedKind == treeKind) { - @SuppressWarnings("unchecked") // checked by Kind - T treeAsExpectedType = (T) actual; - return Optional.of(treeAsExpectedType); - } else { - return Optional.absent(); - } + private static final TreeVisitor<@Nullable Void, ImmutableList.Builder> + IMPORT_NAMES_ACCUMULATOR = + new SimpleTreeVisitor<@Nullable Void, ImmutableList.Builder>() { + @Override + public @Nullable Void visitMemberSelect( + MemberSelectTree memberSelectTree, ImmutableList.Builder names) { + names.add(memberSelectTree.getIdentifier()); + return memberSelectTree.getExpression().accept(this, names); + } + + @Override + public @Nullable Void visitIdentifier( + IdentifierTree identifierTree, ImmutableList.Builder names) { + names.add(identifierTree.getName()); + return null; + } + }; + + @AutoValue + abstract static class MethodSignature { + abstract String name(); + abstract ImmutableList> parameterTypes(); + + static MethodSignature create( + CompilationUnitTree compilationUnitTree, MethodTree tree, Trees trees) { + ImmutableList.Builder> parameterTypes = + ImmutableList.builder(); + for (VariableTree parameter : tree.getParameters()) { + parameterTypes.add( + MoreTypes.equivalence() + .wrap(trees.getTypeMirror(trees.getPath(compilationUnitTree, parameter)))); + } + return new AutoValue_TreeDiffer_MethodSignature( + tree.getName().toString(), parameterTypes.build()); } } } diff --git a/src/main/java/com/google/testing/compile/TreeDifference.java b/src/main/java/com/google/testing/compile/TreeDifference.java index aa6d7667..f62481a1 100644 --- a/src/main/java/com/google/testing/compile/TreeDifference.java +++ b/src/main/java/com/google/testing/compile/TreeDifference.java @@ -20,11 +20,9 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; - import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; - -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * A data structure describing the set of syntactic differences between two {@link Tree}s. diff --git a/src/main/java/com/google/testing/compile/TypeEnumerator.java b/src/main/java/com/google/testing/compile/TypeEnumerator.java index 6ec4e95f..ac65357d 100644 --- a/src/main/java/com/google/testing/compile/TypeEnumerator.java +++ b/src/main/java/com/google/testing/compile/TypeEnumerator.java @@ -21,7 +21,6 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; - import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionStatementTree; @@ -29,12 +28,11 @@ import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreeScanner; - import java.util.Set; +import org.jspecify.annotations.Nullable; /** - * Provides information about the set of types that are declared by a - * {@code CompilationUnitTree}. + * Provides information about the set of types that are declared by a {@code CompilationUnitTree}. * * @author Stephen Pratt */ @@ -50,13 +48,11 @@ static ImmutableSet getTopLevelTypes(CompilationUnitTree t) { return ImmutableSet.copyOf(nameVisitor.scan(t, null)); } - /** - * A {@link TreeScanner} for determining type declarations - */ + /** A {@link TreeScanner} for determining type declarations */ @SuppressWarnings("restriction") // Sun APIs usage intended - static final class TypeScanner extends TreeScanner, Void> { + static final class TypeScanner extends TreeScanner, @Nullable Void> { @Override - public Set scan(Tree node, Void v) { + public Set scan(Tree node, @Nullable Void v) { return firstNonNull(super.scan(node, v), ImmutableSet.of()); } @@ -66,22 +62,23 @@ public Set reduce(Set r1, Set r2) { } @Override - public Set visitClass(ClassTree reference, Void v) { + public Set visitClass(ClassTree reference, @Nullable Void v) { return ImmutableSet.of(reference.getSimpleName().toString()); } @Override - public Set visitExpressionStatement(ExpressionStatementTree reference, Void v) { + public Set visitExpressionStatement( + ExpressionStatementTree reference, @Nullable Void v) { return scan(reference.getExpression(), v); } @Override - public Set visitIdentifier(IdentifierTree reference, Void v) { + public Set visitIdentifier(IdentifierTree reference, @Nullable Void v) { return ImmutableSet.of(reference.getName().toString()); } @Override - public Set visitMemberSelect(MemberSelectTree reference, Void v) { + public Set visitMemberSelect(MemberSelectTree reference, @Nullable Void v) { Set expressionSet = scan(reference.getExpression(), v); if (expressionSet.size() != 1) { throw new AssertionError("Internal error in NameFinder. Expected to find exactly one " @@ -92,7 +89,7 @@ public Set visitMemberSelect(MemberSelectTree reference, Void v) { } @Override - public Set visitCompilationUnit(CompilationUnitTree reference, Void v) { + public Set visitCompilationUnit(CompilationUnitTree reference, @Nullable Void v) { Set packageSet = reference.getPackageName() == null ? ImmutableSet.of("") : scan(reference.getPackageName(), v); if (packageSet.size() != 1) { @@ -100,7 +97,7 @@ public Set visitCompilationUnit(CompilationUnitTree reference, Void v) { "package identifier. Found " + packageSet); } final String packageName = packageSet.isEmpty() ? "" : packageSet.iterator().next(); - Set typeDeclSet = scan(reference.getTypeDecls(), v); + Set typeDeclSet = firstNonNull(scan(reference.getTypeDecls(), v), ImmutableSet.of()); return FluentIterable.from(typeDeclSet) .transform(new Function() { @Override public String apply(String typeName) { 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 a750743c..24015b5a 100644 --- a/src/main/java/com/google/testing/compile/package-info.java +++ b/src/main/java/com/google/testing/compile/package-info.java @@ -21,20 +21,20 @@ * projects. * *

    - *
  • {@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. + *
  • {@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. *
* *

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

  * Compilation compilation =
- *     javac().compile(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}");
+ *     javac().compile(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}"));
  * assertThat(compilation).succeeded();
  * 
* @@ -46,7 +46,7 @@ * Compilation compilation = * javac() * .withProcessors(new MyAnnotationProcessor()) - * .compile(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}"); + * .compile(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}")); * assertThat(compilation).succeededWithoutWarnings(); * * @@ -83,6 +83,8 @@ * */ @CheckReturnValue +@NullMarked package com.google.testing.compile; -import javax.annotation.CheckReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/com/google/testing/compile/AnnotationFileProcessor.java b/src/test/java/com/google/testing/compile/AnnotationFileProcessor.java new file mode 100644 index 00000000..55f94774 --- /dev/null +++ b/src/test/java/com/google/testing/compile/AnnotationFileProcessor.java @@ -0,0 +1,41 @@ +package com.google.testing.compile; + +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.io.UncheckedIOException; +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; +import javax.tools.StandardLocation; + +final class AnnotationFileProcessor extends AbstractProcessor { + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + Filer filer = processingEnv.getFiler(); + try { + filer.getResource(StandardLocation.ANNOTATION_PROCESSOR_PATH, "", "tmp.txt"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @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/CompilationSubjectTest.java b/src/test/java/com/google/testing/compile/CompilationSubjectTest.java index eb8fae97..63ab78ed 100644 --- a/src/test/java/com/google/testing/compile/CompilationSubjectTest.java +++ b/src/test/java/com/google/testing/compile/CompilationSubjectTest.java @@ -15,24 +15,29 @@ */ package com.google.testing.compile; +import static com.google.common.truth.ExpectFailure.assertThat; 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.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.joining; import static javax.tools.StandardLocation.CLASS_OUTPUT; +import static org.junit.Assert.assertThrows; 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 com.google.common.truth.ExpectFailure; +import com.google.common.truth.Truth; +import java.io.BufferedReader; +import java.io.IOException; import java.util.regex.Pattern; import java.util.stream.Stream; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; +import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; @@ -72,16 +77,18 @@ public class CompilationSubjectTest { "}"); private static final JavaFileObject HELLO_WORLD_RESOURCE = - JavaFileObjects.forResource("HelloWorld.java"); + JavaFileObjects.forResource("test/HelloWorld.java"); private static final JavaFileObject HELLO_WORLD_BROKEN_RESOURCE = - JavaFileObjects.forResource("HelloWorld-broken.java"); + JavaFileObjects.forResource("test/HelloWorld-broken.java"); private static final JavaFileObject HELLO_WORLD_DIFFERENT_RESOURCE = - JavaFileObjects.forResource("HelloWorld-different.java"); + JavaFileObjects.forResource("test/HelloWorld-different.java"); @RunWith(JUnit4.class) public static class StatusTest { + @Rule public final ExpectFailure expectFailure = new ExpectFailure(); + @Test public void succeeded() { assertThat(javac().compile(HELLO_WORLD)).succeeded(); @@ -90,33 +97,38 @@ public void 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); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithGeneratorAndError().compile(HELLO_WORLD_RESOURCE)) + .succeeded(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains( + "Compilation produced the following diagnostics:\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."); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(javac().compile(HELLO_WORLD_BROKEN_RESOURCE)) + .succeeded(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).startsWith( + "Compilation produced the following diagnostics:\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(); + Truth.assertAbout(compilations()) + .that(throwingCompiler(e).compile(HELLO_WORLD_RESOURCE)) + .succeeded(); fail(); } catch (CompilationFailureException expected) { // some old javacs don't pass through exceptions, so we create one @@ -126,6 +138,22 @@ public void succeeded_exceptionCreatedOrPassedThrough() { } } + @Test + public void succeeded_failureReportsWarnings() { + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(HELLO_WORLD_BROKEN)) + .succeeded(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .startsWith("Compilation produced the following diagnostics:\n"); + assertThat(expected.getMessage()).contains("No files were generated."); + // "this is a message" is output by compilerWithWarning() since the source has + // @DiagnosticMessage + assertThat(expected.getMessage()).contains("warning: this is a message"); + } + @Test public void succeededWithoutWarnings() { assertThat(javac().compile(HELLO_WORLD)).succeededWithoutWarnings(); @@ -133,13 +161,14 @@ public void 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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(HELLO_WORLD)) + .succeededWithoutWarnings(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 0 warnings, but found the following 2 warnings:\n"); } @Test @@ -149,14 +178,15 @@ public void failedToCompile() { @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."); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(javac().compile(HELLO_WORLD_RESOURCE)) + .failed(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .startsWith("Compilation was expected to fail, but contained no errors"); + assertThat(expected.getMessage()).contains("No files were generated."); } } @@ -166,6 +196,7 @@ public void failedToCompile_compilationSucceeded() { */ @RunWith(Parameterized.class) public static final class WarningAndNoteTest { + @Rule public final ExpectFailure expectFailure = new ExpectFailure(); private final JavaFileObject sourceFile; @Parameters @@ -239,83 +270,100 @@ public void hadWarningContainingMatch_pattern() { @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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("what is it?"); + AssertionError expected = expectFailure.getFailure(); + 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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch("(what|where) is it?"); + AssertionError expected = expectFailure.getFailure(); + 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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch(Pattern.compile("(what|where) is it?")); + AssertionError expected = expectFailure.getFailure(); + 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 containing \"this is a message\" in %s", - HELLO_WORLD_DIFFERENT_RESOURCE.getName())); - assertThat(expected.getMessage()).contains(sourceFile.getName()); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(HELLO_WORLD_DIFFERENT_RESOURCE); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning containing \"this is a message\" 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( - lines( - format( - "Expected a warning containing \"this is a message\" in %s on line:", - sourceFile.getName()), - " 1: ")); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(sourceFile) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains( + lines( + format( + "Expected a warning containing \"this is a message\" in %s on line:", + sourceFile.getName()), + " 1: ")); + assertThat(expected.getMessage()).contains("" + actualErrorLine); + } + + @Test + public void hadWarningContainingInFileOnLine_lineTooBig() throws IOException { + long lineCount = new BufferedReader(sourceFile.openReader(false)).lines().count(); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + assertThat(compilerWithWarning().compile(sourceFile)) + .hadWarningContainingMatch("this is a+ message") + .inFile(sourceFile) + .onLine(100)); + assertThat(exception) + .hasMessageThat() + .isEqualTo("Invalid line number 100; number of lines is only " + lineCount); } /* TODO(dpb): Negative cases for onLineContaining for @@ -323,30 +371,31 @@ public void hadWarningContainingInFileOnLine_wrongLine() { * (containing(String), containingMatch(String), containingMatch(Pattern)). */ @Test public void hadNoteContainingInFileOnLineContaining_wrongLine() { - try { - verifyThat(compilerWithNote().compile(sourceFile)) - .hadNoteContaining("this is a message") - .inFile(sourceFile) - .onLineContaining("package"); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .isEqualTo( - lines( - format( - "Expected a note containing \"this is a message\" in %s on line:", - sourceFile.getName()), - " 1: package test;", - "but found it on line(s):", - " 6: public class HelloWorld {", - " 7: @DiagnosticMessage Object foo;")); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(sourceFile) + .onLineContaining("package"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .isEqualTo( + lines( + format( + "Expected a note containing \"this is a message\" in %s on line:", + sourceFile.getName()), + " 1: package test;", + "but found it on line(s):", + " 6: public class HelloWorld {", + " 7: @DiagnosticMessage Object foo;")); } @Test public void hadWarningContainingMatchInFileOnLineContaining_noMatches() { try { - verifyThat(compilerWithWarning().compile(sourceFile)) + Truth.assertAbout(compilations()) + .that(compilerWithWarning().compile(sourceFile)) .hadWarningContainingMatch("this is a+ message") .inFile(sourceFile) .onLineContaining("not found!"); @@ -360,7 +409,8 @@ public void hadWarningContainingMatchInFileOnLineContaining_noMatches() { @Test public void hadWarningContainingInFileOnLineContaining_moreThanOneMatch() { try { - verifyThat(compilerWithWarning().compile(sourceFile)) + Truth.assertAbout(compilations()) + .that(compilerWithWarning().compile(sourceFile)) .hadWarningContainingMatch(Pattern.compile("this is ab? message")) .inFile(sourceFile) .onLineContaining("@DiagnosticMessage"); @@ -379,23 +429,23 @@ public void hadWarningContainingInFileOnLineContaining_moreThanOneMatch() { @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( - format( - "Expected a warning containing \"this is a message\" in %s " - + "at column 1 of line 6", - sourceFile.getName())); - assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(sourceFile)) + .hadWarningContaining("this is a message") + .inFile(sourceFile) + .onLine(6) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + format( + "Expected a warning containing \"this is a message\" in %s " + + "at column 1 of line 6", + sourceFile.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); } @Test @@ -405,13 +455,14 @@ public void hadWarningCount() { @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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithWarning().compile(sourceFile)) + .hadWarningCount(42); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 42 warnings, but found the following 2 warnings:\n"); } @Test @@ -458,103 +509,104 @@ public void hadNoteContainingMatch_pattern() { @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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("what is it?"); + AssertionError expected = expectFailure.getFailure(); + 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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch("(what|where) is it?"); + AssertionError expected = expectFailure.getFailure(); + 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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteContainingMatch(Pattern.compile("(what|where) is it?")); + AssertionError expected = expectFailure.getFailure(); + 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( - format( - "Expected a note containing \"this is a message\" in %s", - HELLO_WORLD_DIFFERENT_RESOURCE.getName())); - assertThat(expected.getMessage()).contains(sourceFile.getName()); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(HELLO_WORLD_DIFFERENT_RESOURCE); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + format( + "Expected a note containing \"this is a message\" 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( - lines( - format( - "Expected a note containing \"this is a message\" in %s on line:", - sourceFile.getName()), - " 1: ")); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(sourceFile) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains( + lines( + format( + "Expected a note containing \"this is a message\" in %s on line:", + sourceFile.getName()), + " 1: ")); + 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( - format( - "Expected a note containing \"this is a message\" in %s at column 1 of line 6", - sourceFile.getName())); - assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteContaining("this is a message") + .inFile(sourceFile) + .onLine(6) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + format( + "Expected a note containing \"this is a message\" in %s at column 1 of line 6", + sourceFile.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); } @Test @@ -564,19 +616,22 @@ public void hadNoteCount() { @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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithNote().compile(sourceFile)) + .hadNoteCount(42); + AssertionError expected = expectFailure.getFailure(); + 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 { + @Rule public final ExpectFailure expectFailure = new ExpectFailure(); + @Test public void hadErrorContaining() { assertThat(javac().compile(HELLO_WORLD_BROKEN_RESOURCE)) @@ -621,105 +676,105 @@ public void hadErrorContainingMatch_pattern() { @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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("some error"); + AssertionError expected = expectFailure.getFailure(); + 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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContainingMatch("(what|where) is it?"); + AssertionError expected = expectFailure.getFailure(); + 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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContainingMatch(Pattern.compile("(what|where) is it?")); + AssertionError expected = expectFailure.getFailure(); + 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( - format( - "Expected an error containing \"expected error!\" in %s", - HELLO_WORLD_DIFFERENT_RESOURCE.getName())); - assertThat(expected.getMessage()).contains(HELLO_WORLD_RESOURCE.getName()); - // "(no associated file)"))); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("expected error!") + .inFile(HELLO_WORLD_DIFFERENT_RESOURCE); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + format( + "Expected an error containing \"expected 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( - lines( - format( - "Expected an error containing \"expected error!\" in %s on line:", - HELLO_WORLD_RESOURCE.getName()), - " 1: ")); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("expected error!") + .inFile(HELLO_WORLD_RESOURCE) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 18; + assertThat(expected.getMessage()) + .contains( + lines( + format( + "Expected an error containing \"expected error!\" in %s on line:", + HELLO_WORLD_RESOURCE.getName()), + " 1: ")); + 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( - format( - "Expected an error containing \"expected error!\" in %s at column 1 of line 18", - HELLO_WORLD_RESOURCE.getName())); - assertThat(expected.getMessage()).contains("" + actualErrorCol); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorContaining("expected error!") + .inFile(HELLO_WORLD_RESOURCE) + .onLine(18) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + format( + "Expected an error containing \"expected error!\" in %s at column 1 of line 18", + HELLO_WORLD_RESOURCE.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorCol); } @Test @@ -729,18 +784,21 @@ public void hadErrorCount() { @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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithError().compile(HELLO_WORLD_RESOURCE)) + .hadErrorCount(42); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 42 errors, but found the following 2 errors:\n"); } } @RunWith(JUnit4.class) public static class GeneratedFilesTest { + @Rule public final ExpectFailure expectFailure = new ExpectFailure(); + @Test public void generatedSourceFile() { assertThat(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) @@ -750,16 +808,28 @@ public void generatedSourceFile() { GeneratingProcessor.GENERATED_CLASS_NAME, GeneratingProcessor.GENERATED_SOURCE)); } + @Test + public void generatedSourceFile_packageInfo() { + GeneratingProcessor generatingProcessor = new GeneratingProcessor("test"); + assertThat(javac().withProcessors(generatingProcessor).compile(HELLO_WORLD_RESOURCE)) + .generatedSourceFile("test.package-info") + .hasSourceEquivalentTo( + JavaFileObjects.forSourceString( + "test.package-info", generatingProcessor.generatedPackageInfoSource())); + } + @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); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedSourceFile("ThisIsNotTheRightFile"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected) + .factValue("expected to generate file") + .isEqualTo("/ThisIsNotTheRightFile.java"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); } @Test @@ -771,14 +841,15 @@ public void generatedFilePath() { @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"); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "com/google/testing/compile/Bogus.class"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected) + .factValue("expected to generate file") + .isEqualTo("/com/google/testing/compile/Bogus.class"); } @Test @@ -790,32 +861,30 @@ public void generatedFilePackageFile() { @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\""); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "com.google.testing.compile", "Bogus.class"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected) + .factValue("expected to generate file") + .isEqualTo("/com/google/testing/compile/Bogus.class"); } @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); - } + expectFailure + .whenTesting() + .about(compilations()) + .that(compilerWithGenerator().compile(HELLO_WORLD_RESOURCE)) + .generatedFile(CLASS_OUTPUT, "", "File.java"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected).factValue("expected to generate file").isEqualTo("/File.java"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); } } - + private static String lines(String... lines) { return Stream.of(lines).collect(joining("\n")); } @@ -843,8 +912,4 @@ private static Compiler compilerWithGeneratorAndError() { 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 index f5fecd2a..c8921c9f 100644 --- a/src/test/java/com/google/testing/compile/CompilationTest.java +++ b/src/test/java/com/google/testing/compile/CompilationTest.java @@ -17,7 +17,6 @@ package com.google.testing.compile; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import static javax.tools.StandardLocation.SOURCE_OUTPUT; @@ -67,6 +66,8 @@ public void compilerStatusFailure() { Compiler compiler = compilerWithGenerator(); Compilation compilation = compiler.compile(brokenSource); assertThat(compilation.status()).isEqualTo(Compilation.Status.FAILURE); + assertThat(compilation.errors()).hasSize(1); + assertThat(compilation.errors().get(0).getLineNumber()).isEqualTo(3); } @Test diff --git a/src/test/java/com/google/testing/compile/CompilerTest.java b/src/test/java/com/google/testing/compile/CompilerTest.java index 381ce182..272cf2db 100644 --- a/src/test/java/com/google/testing/compile/CompilerTest.java +++ b/src/test/java/com/google/testing/compile/CompilerTest.java @@ -16,13 +16,43 @@ 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.Compiler.javac; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeTrue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Arrays; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; +import javax.tools.FileObject; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -30,7 +60,10 @@ @RunWith(JUnit4.class) public final class CompilerTest { - private static final JavaFileObject HELLO_WORLD = JavaFileObjects.forResource("HelloWorld.java"); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private static final JavaFileObject HELLO_WORLD = + JavaFileObjects.forResource("test/HelloWorld.java"); @Test public void options() { @@ -88,4 +121,191 @@ public void multipleProcesors_asIterable() { assertThat(noopProcessor2.invoked).isTrue(); assertThat(noopProcessor3.invoked).isFalse(); } + + @Test + public void classPath_default() { + Compilation compilation = + javac() + .compile( + JavaFileObjects.forSourceLines( + "Test", + "import com.google.testing.compile.CompilerTest;", + "class Test {", + " CompilerTest t;", + "}")); + assertThat(compilation).succeeded(); + } + + @Test + public void classPath_empty() { + Compilation compilation = + javac() + .withClasspath(ImmutableList.of()) + .compile( + JavaFileObjects.forSourceLines( + "Test", + "import com.google.testing.compile.CompilerTest;", + "class Test {", + " CompilerTest t;", + "}")); + assertThat(compilation).hadErrorContaining("com.google.testing.compile does not exist"); + } + + /** Sets up a jar containing a single class 'Lib', for use in classpath tests. */ + private File compileTestLib() throws IOException { + File lib = temporaryFolder.newFolder("tmp"); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fileManager = + compiler.getStandardFileManager(/* diagnosticListener= */ null, Locale.getDefault(), UTF_8); + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, ImmutableList.of(lib)); + CompilationTask task = + compiler.getTask( + /* out= */ null, + fileManager, + /* diagnosticListener= */ null, + /* options= */ ImmutableList.of(), + /* classes= */ null, + ImmutableList.of(JavaFileObjects.forSourceLines("Lib", "class Lib {}"))); + assertThat(task.call()).isTrue(); + return lib; + } + + @Test + public void classPath_customFiles() throws Exception { + File lib = compileTestLib(); + // compile with only 'Lib' on the classpath + Compilation compilation = + javac() + .withClasspath(ImmutableList.of(lib)) + .withOptions("-verbose") + .compile( + JavaFileObjects.forSourceLines( + "Test", // + "class Test {", + " Lib lib;", + "}")); + assertThat(compilation).succeeded(); + } + + @Test + public void classPath_empty_urlClassLoader() { + Compilation compilation = + javac() + .withClasspathFrom(new URLClassLoader(new URL[0], Compiler.platformClassLoader)) + .compile( + JavaFileObjects.forSourceLines( + "Test", + "import com.google.testing.compile.CompilerTest;", + "class Test {", + " CompilerTest t;", + "}")); + assertThat(compilation).hadErrorContaining("com.google.testing.compile does not exist"); + } + + @Test + public void classPath_customFiles_urlClassLoader() throws Exception { + File lib = compileTestLib(); + Compilation compilation = + javac() + .withClasspathFrom(new URLClassLoader(new URL[] {lib.toURI().toURL()})) + .withOptions("-verbose") + .compile(JavaFileObjects.forSourceLines("Test", "class Test {", " Lib lib;", "}")); + assertThat(compilation).succeeded(); + } + + @Test + public void annotationProcessorPath_empty() { + AnnotationFileProcessor processor = new AnnotationFileProcessor(); + Compiler compiler = + javac().withProcessors(processor).withAnnotationProcessorPath(ImmutableList.of()); + RuntimeException expected = + assertThrows( + RuntimeException.class, + () -> compiler.compile(JavaFileObjects.forSourceLines("Test", "class Test {}"))); + assumeTrue( + isJdk9OrLater()); // with JDK 8, NullPointerException is thrown instead of the expected + // exception, and this bug is fixed after JDK 8 + assertThat(expected).hasCauseThat().hasCauseThat().hasMessageThat().contains("tmp.txt"); + } + + @Test + public void annotationProcessorPath_customFiles() throws Exception { + AnnotationFileProcessor processor = new AnnotationFileProcessor(); + File jar = compileTestJar(); + // compile with only 'tmp.txt' on the annotation processor path + Compilation compilation = + javac() + .withProcessors(processor) + .withAnnotationProcessorPath(ImmutableList.of(jar)) + .compile(JavaFileObjects.forSourceLines("Test", "class Test {}")); + assertThat(compilation).succeeded(); + } + + @Test // See https://github.com/google/compile-testing/issues/189 + public void readInputFile() throws IOException { + AtomicReference content = new AtomicReference<>(); + Compilation compilation = + javac() + .withProcessors( + new AbstractProcessor() { + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + Filer filer = processingEnv.getFiler(); + try { + FileObject helloWorld = + filer.getResource( + StandardLocation.SOURCE_PATH, "test", "HelloWorld.java"); + content.set(helloWorld.getCharContent(true).toString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public boolean process( + Set annotations, RoundEnvironment roundEnv) { + return false; + } + + @Override + public ImmutableSet getSupportedAnnotationTypes() { + return ImmutableSet.of("*"); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + }) + .compile(HELLO_WORLD); + assertThat(compilation).succeeded(); + assertThat(content.get()).isEqualTo(HELLO_WORLD.getCharContent(true).toString()); + } + + /** + * Sets up a jar containing a single file 'tmp.txt', for use in annotation processor path tests. + */ + private static File compileTestJar() throws IOException { + File file = File.createTempFile("tmp", ".jar"); + try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(file))) { + ZipEntry entry = new ZipEntry("tmp.txt"); + zipOutput.putNextEntry(entry); + zipOutput.closeEntry(); + } + return file; + } + + @Test + public void releaseFlag() { + assumeTrue(isJdk9OrLater()); + Compilation compilation = + javac() + .withOptions("--release", "8") + .compile(JavaFileObjects.forSourceString("HelloWorld", "final class HelloWorld {}")); + assertThat(compilation).succeeded(); + } + + static boolean isJdk9OrLater() { + return SourceVersion.latestSupported().compareTo(SourceVersion.RELEASE_8) > 0; + } } diff --git a/src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java b/src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java index ffec2771..540f171b 100644 --- a/src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java +++ b/src/test/java/com/google/testing/compile/FailingGeneratingProcessor.java @@ -52,6 +52,6 @@ public Set getSupportedAnnotationTypes() { @Override public SourceVersion getSupportedSourceVersion() { - return delegate.getSupportedSourceVersion(); + return SourceVersion.latestSupported(); } } diff --git a/src/test/java/com/google/testing/compile/GeneratingProcessor.java b/src/test/java/com/google/testing/compile/GeneratingProcessor.java index ff1c2e37..1e5a46b9 100644 --- a/src/test/java/com/google/testing/compile/GeneratingProcessor.java +++ b/src/test/java/com/google/testing/compile/GeneratingProcessor.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; +import java.io.UncheckedIOException; import java.io.Writer; import java.util.Set; import javax.annotation.processing.AbstractProcessor; @@ -29,6 +30,7 @@ import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; +import javax.tools.FileObject; final class GeneratingProcessor extends AbstractProcessor { static final String GENERATED_CLASS_NAME = "Blah"; @@ -50,31 +52,33 @@ final class GeneratingProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnv) { Filer filer = processingEnv.getFiler(); - try (Writer writer = filer.createSourceFile(generatedClassName()).openWriter()) { - writer.write(GENERATED_SOURCE); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + write(filer.createSourceFile(generatedClassName()), GENERATED_SOURCE); + write( + filer.createResource( + CLASS_OUTPUT, getClass().getPackage().getName(), GENERATED_RESOURCE_NAME), + GENERATED_RESOURCE); - try (Writer writer = - filer - .createResource( - CLASS_OUTPUT, getClass().getPackage().getName(), GENERATED_RESOURCE_NAME) - .openWriter()) { - writer.write(GENERATED_RESOURCE); + if (!packageName.isEmpty()) { + write(filer.createSourceFile(packageName + ".package-info"), generatedPackageInfoSource()); + } } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } } String packageName() { return packageName; } - + String generatedClassName() { return packageName.isEmpty() ? GENERATED_CLASS_NAME : packageName + "." + GENERATED_CLASS_NAME; } + String generatedPackageInfoSource() { + return "package " + packageName + ";\n"; + } + @CanIgnoreReturnValue @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { @@ -90,4 +94,10 @@ public Set getSupportedAnnotationTypes() { public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } + + private static void write(FileObject file, String contents) throws IOException { + try (Writer writer = file.openWriter()) { + writer.write(contents); + } + } } diff --git a/src/test/java/com/google/testing/compile/JarFileResourcesCompilationTest.java b/src/test/java/com/google/testing/compile/JarFileResourcesCompilationTest.java index 6b29fd05..62502b73 100644 --- a/src/test/java/com/google/testing/compile/JarFileResourcesCompilationTest.java +++ b/src/test/java/com/google/testing/compile/JarFileResourcesCompilationTest.java @@ -19,20 +19,18 @@ import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; import com.google.common.io.Resources; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; /** * An integration test to ensure that testing works when resources are in jar files. @@ -52,7 +50,7 @@ public void createJarFile() throws IOException { JarOutputStream out = new JarOutputStream(new FileOutputStream(jarFile)); JarEntry helloWorldEntry = new JarEntry("test/HelloWorld.java"); out.putNextEntry(helloWorldEntry); - out.write(Resources.toByteArray(Resources.getResource("HelloWorld.java"))); + out.write(Resources.toByteArray(Resources.getResource("test/HelloWorld.java"))); out.close(); } diff --git a/src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java b/src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java index fab0c97c..b3aaf218 100644 --- a/src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java +++ b/src/test/java/com/google/testing/compile/JavaFileObjectSubjectTest.java @@ -16,23 +16,24 @@ package com.google.testing.compile; +import static com.google.common.truth.ExpectFailure.assertThat; 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 static org.junit.Assert.assertThrows; -import com.google.testing.compile.VerificationFailureStrategy.VerificationException; +import com.google.common.truth.ExpectFailure; import java.io.IOException; -import java.util.regex.Pattern; import javax.tools.JavaFileObject; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class JavaFileObjectSubjectTest { + @Rule public final ExpectFailure expectFailure = new ExpectFailure(); private static final JavaFileObject CLASS = JavaFileObjects.forSourceLines( @@ -73,12 +74,13 @@ public void hasContents() { @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()); - } + expectFailure + .whenTesting() + .about(javaFileObjects()) + .that(CLASS_WITH_FIELD) + .hasContents(JavaFileObjects.asByteSource(DIFFERENT_NAME)); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains(CLASS_WITH_FIELD.getName()); } @Test @@ -88,14 +90,16 @@ public void contentsAsString() { @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+"); - } + expectFailure + .whenTesting() + .about(javaFileObjects()) + .that(CLASS) + .contentsAsString(UTF_8) + .containsMatch("bad+"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected).factValue("value of").isEqualTo("javaFileObject.contents()"); + assertThat(expected).factValue("javaFileObject was").startsWith(CLASS.getName()); + assertThat(expected).factValue("expected to contain a match for").isEqualTo("bad+"); } @Test @@ -110,43 +114,122 @@ public void hasSourceEquivalentTo_unresolvedReferences() { @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)); - } + expectFailure + .whenTesting() + .about(javaFileObjects()) + .that(CLASS) + .hasSourceEquivalentTo(DIFFERENT_NAME); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected).factKeys().contains("expected to be equivalent to"); + assertThat(expected.getMessage()).contains(CLASS.getName()); + assertThat(expected).factValue("but was").isEqualTo(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)); - } + expectFailure + .whenTesting() + .about(javaFileObjects()) + .that(CLASS) + .hasSourceEquivalentTo(CLASS_WITH_FIELD); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected).factKeys().contains("expected to be equivalent to"); + assertThat(expected.getMessage()).contains("unmatched nodes in the expected tree"); + assertThat(expected.getMessage()).contains(CLASS.getName()); + assertThat(expected).factValue("but was").isEqualTo(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)); - } + expectFailure + .whenTesting() + .about(javaFileObjects()) + .that(CLASS_WITH_FIELD) + .hasSourceEquivalentTo(CLASS); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected).factKeys().contains("expected to be equivalent to"); + assertThat(expected.getMessage()).contains("unmatched nodes in the actual tree"); + assertThat(expected.getMessage()).contains(CLASS_WITH_FIELD.getName()); + assertThat(expected).factValue("but was").isEqualTo(CLASS_WITH_FIELD.getCharContent(false)); } - private static JavaFileObjectSubject verifyThat(JavaFileObject file) { - return VERIFY.about(javaFileObjects()).that(file); + private static final JavaFileObject SAMPLE_ACTUAL_FILE_FOR_MATCHING = + JavaFileObjects.forSourceLines( + "test.SomeFile", + "package test;", + "", + "import pkg.AnAnnotation;", + "import static another.something.Special.CONSTANT;", + "", + "@AnAnnotation(with = @Some(values = {1,2,3}), and = \"a string\")", + "public class SomeFile {", + " private static final int CONSTANT_TIMES_2 = CONSTANT * 2;", + " private static final int CONSTANT_TIMES_3 = CONSTANT * 3;", + " private static final int CONSTANT_TIMES_4 = CONSTANT * 4;", + "", + " @Nullable private MaybeNull field;", + "", + " @Inject SomeFile() {", + " this.field = MaybeNull.constructorBody();", + " }", + "", + " protected int method(Parameter p, OtherParam o) {", + " return CONSTANT_TIMES_4 / p.hashCode() + o.hashCode();", + " }", + "", + " public static class InnerClass {", + " private static final int CONSTANT_TIMES_8 = CONSTANT_TIMES_4 * 2;", + "", + " @Nullable private MaybeNull innerClassField;", + "", + " @Inject", + " InnerClass() {", + " this.innerClassField = MaybeNull.constructorBody();", + " }", + "", + " protected int innerClassMethod(Parameter p, OtherParam o) {", + " return CONSTANT_TIMES_8 / p.hashCode() + o.hashCode();", + " }", + " }", + "}"); + + @Test + public void containsElementsIn_completeMatch() { + assertThat(SAMPLE_ACTUAL_FILE_FOR_MATCHING).containsElementsIn(SAMPLE_ACTUAL_FILE_FOR_MATCHING); + } + + private static final JavaFileObject SIMPLE_INVALID_FILE = + JavaFileObjects.forSourceLines( + "test.SomeClass", // + "package test;", + "", + "public syntax error class SomeClass {", + "}"); + private static final JavaFileObject SIMPLE_VALID_FILE = + JavaFileObjects.forSourceLines( + "test.SomeClass", // + "package test;", + "", + "public class SomeClass {", + "}"); + + @Test + public void containsElementsIn_badActual() { + IllegalStateException ex = + assertThrows( + IllegalStateException.class, + () -> assertThat(SIMPLE_INVALID_FILE).containsElementsIn(SIMPLE_VALID_FILE)); + + assertThat(ex).hasMessageThat().startsWith("Error while parsing *actual* source:\n"); + } + + @Test + public void containsElementsIn_badExpected() { + IllegalStateException ex = + assertThrows( + IllegalStateException.class, + () -> assertThat(SIMPLE_VALID_FILE).containsElementsIn(SIMPLE_INVALID_FILE)); + + assertThat(ex).hasMessageThat().startsWith("Error while parsing *expected* source:\n"); } } diff --git a/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java b/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java index eaf38e81..3546ea92 100644 --- a/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java +++ b/src/test/java/com/google/testing/compile/JavaFileObjectsTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.fail; import java.io.IOException; -import java.net.URI; import javax.tools.JavaFileObject; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,11 +34,14 @@ public class JavaFileObjectsTest { @Test public void forResource_inJarFile() { - JavaFileObject resourceInJar = JavaFileObjects.forResource("java/lang/Object.class"); + JavaFileObject resourceInJar = + JavaFileObjects.forResource("com/google/testing/compile/JavaFileObjectsTest.class"); assertThat(resourceInJar.getKind()).isEqualTo(CLASS); - assertThat(resourceInJar.toUri()).isEqualTo(URI.create("/java/lang/Object.class")); - assertThat(resourceInJar.getName()).isEqualTo("/java/lang/Object.class"); - assertThat(resourceInJar.isNameCompatible("Object", CLASS)).isTrue(); + assertThat(resourceInJar.toUri().getPath()) + .endsWith("/com/google/testing/compile/JavaFileObjectsTest.class"); + assertThat(resourceInJar.getName()) + .endsWith("/com/google/testing/compile/JavaFileObjectsTest.class"); + assertThat(resourceInJar.isNameCompatible("JavaFileObjectsTest", CLASS)).isTrue(); } @Test public void forSourceLines() throws IOException { diff --git a/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java b/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java index 0353ad44..267634f9 100644 --- a/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java +++ b/src/test/java/com/google/testing/compile/JavaSourcesSubjectFactoryTest.java @@ -15,21 +15,21 @@ */ package com.google.testing.compile; +import static com.google.common.truth.ExpectFailure.assertThat; 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.io.ByteSource; -import com.google.common.io.Resources; -import com.google.testing.compile.VerificationFailureStrategy.VerificationException; +import com.google.common.truth.ExpectFailure; import java.util.Arrays; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -41,6 +41,10 @@ */ @RunWith(JUnit4.class) public class JavaSourcesSubjectFactoryTest { + + private static final JavaFileObject HELLO_WORLD_RESOURCE = + JavaFileObjects.forResource("test/HelloWorld.java"); + private static final JavaFileObject HELLO_WORLD = JavaFileObjects.forSourceLines( "test.HelloWorld", @@ -66,20 +70,22 @@ public class JavaSourcesSubjectFactoryTest { " Bar noSuchClass;", "}"); + @Rule public final ExpectFailure expectFailure = new ExpectFailure(); + @Test public void compilesWithoutError() { + assertAbout(javaSource()).that(HELLO_WORLD_RESOURCE).compilesWithoutError(); assertAbout(javaSource()) - .that(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java"))) - .compilesWithoutError(); - assertAbout(javaSource()) - .that(JavaFileObjects.forSourceLines("test.HelloWorld", - "package test;", - "", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(\"Hello World!\");", - " }", - "}")) + .that( + JavaFileObjects.forSourceLines( + "test.HelloWorld", + "package test;", + "", + "public class HelloWorld {", + " public static void main(String[] args) {", + " System.out.println(\"Hello World!\");", + " }", + "}")) .compilesWithoutError(); } @@ -107,120 +113,108 @@ public void compilesWithoutError_warnings() { @Test public void compilesWithoutWarnings_failsWithWarnings() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .compilesWithoutWarnings(); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains("Expected 0 warnings, but found the following 2 warnings:\n"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .compilesWithoutWarnings(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 0 warnings, but found the following 2 warnings:\n"); } @Test public void compilesWithoutError_noWarning() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .compilesWithoutError() - .withWarningContaining("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"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .compilesWithoutError() + .withWarningContaining("what is it?"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("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).hasMessageThat().contains("this is a message\n"); } @Test public void compilesWithoutError_warningNotInFile() { - JavaFileObject otherSource = JavaFileObjects.forResource("HelloWorld.java"); - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .compilesWithoutError() - .withWarningContaining("this is a message") - .in(otherSource); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a warning containing \"this is a message\" in %s", - otherSource.getName())); - assertThat(expected.getMessage()).contains(HELLO_WORLD.getName()); - } + JavaFileObject otherSource = HELLO_WORLD_RESOURCE; + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .compilesWithoutError() + .withWarningContaining("this is a message") + .in(otherSource); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning containing \"this is a message\" in %s", + otherSource.getName())); + assertThat(expected.getMessage()).contains(HELLO_WORLD.getName()); } @Test public void compilesWithoutError_warningNotOnLine() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .compilesWithoutError() - .withWarningContaining("this is a message") - .in(HELLO_WORLD) - .onLine(1); - fail(); - } catch (VerificationException expected) { - int actualErrorLine = 6; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a warning containing \"this is a message\" in %s on line:\n 1: ", - HELLO_WORLD.getName())); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .compilesWithoutError() + .withWarningContaining("this is a message") + .in(HELLO_WORLD) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning containing \"this is a message\" in %s on line:\n 1: ", + HELLO_WORLD.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); } @Test public void compilesWithoutError_warningNotAtColumn() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .compilesWithoutError() - .withWarningContaining("this is a message") - .in(HELLO_WORLD) - .onLine(6) - .atColumn(1); - fail(); - } catch (VerificationException expected) { - int actualErrorCol = 8; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a warning containing \"this is a message\" in %s at column 1 of line 6", - HELLO_WORLD.getName())); - assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .compilesWithoutError() + .withWarningContaining("this is a message") + .in(HELLO_WORLD) + .onLine(6) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning containing \"this is a message\" in %s at column 1 of line 6", + HELLO_WORLD.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); } @Test public void compilesWithoutError_wrongWarningCount() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .compilesWithoutError() - .withWarningCount(42); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains("Expected 42 warnings, but found the following 2 warnings:\n"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .compilesWithoutError() + .withWarningCount(42); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 42 warnings, but found the following 2 warnings:\n"); } @Test @@ -244,142 +238,131 @@ public void compilesWithoutError_notes() { @Test public void compilesWithoutError_noNote() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .compilesWithoutError() - .withNoteContaining("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"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .compilesWithoutError() + .withNoteContaining("what is it?"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("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).hasMessageThat().contains("this is a message\n"); } @Test public void compilesWithoutError_noteNotInFile() { - JavaFileObject otherSource = JavaFileObjects.forResource("HelloWorld.java"); - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .compilesWithoutError() - .withNoteContaining("this is a message") - .in(otherSource); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a note containing \"this is a message\" in %s", otherSource.getName())); - assertThat(expected.getMessage()).contains(HELLO_WORLD.getName()); - } + JavaFileObject otherSource = HELLO_WORLD_RESOURCE; + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .compilesWithoutError() + .withNoteContaining("this is a message") + .in(otherSource); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a note containing \"this is a message\" in %s", otherSource.getName())); + assertThat(expected.getMessage()).contains(HELLO_WORLD.getName()); } @Test public void compilesWithoutError_noteNotOnLine() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .compilesWithoutError() - .withNoteContaining("this is a message") - .in(HELLO_WORLD) - .onLine(1); - fail(); - } catch (VerificationException expected) { - int actualErrorLine = 6; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a note containing \"this is a message\" in %s on line:\n 1: ", - HELLO_WORLD.getName())); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .compilesWithoutError() + .withNoteContaining("this is a message") + .in(HELLO_WORLD) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a note containing \"this is a message\" in %s on line:\n 1: ", + HELLO_WORLD.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); } @Test public void compilesWithoutError_noteNotAtColumn() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .compilesWithoutError() - .withNoteContaining("this is a message") - .in(HELLO_WORLD) - .onLine(6) - .atColumn(1); - fail(); - } catch (VerificationException expected) { - int actualErrorCol = 8; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a note containing \"this is a message\" in %s at column 1 of line 6", - HELLO_WORLD.getName())); - assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .compilesWithoutError() + .withNoteContaining("this is a message") + .in(HELLO_WORLD) + .onLine(6) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a note containing \"this is a message\" in %s at column 1 of line 6", + HELLO_WORLD.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); } @Test public void compilesWithoutError_wrongNoteCount() { JavaFileObject fileObject = HELLO_WORLD; - try { - VERIFY - .about(javaSource()) - .that(fileObject) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .compilesWithoutError() - .withNoteCount(42); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains("Expected 42 notes, but found the following 2 notes:\n"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(fileObject) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .compilesWithoutError() + .withNoteCount(42); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 42 notes, but found the following 2 notes:\n"); } @Test public void compilesWithoutError_failureReportsFiles() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java"))) - .processedWith(new FailingGeneratingProcessor()) - .compilesWithoutError(); - 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); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new FailingGeneratingProcessor()) + .compilesWithoutError(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains("Compilation produced the following diagnostics:\n"); + assertThat(expected.getMessage()).contains(FailingGeneratingProcessor.GENERATED_CLASS_NAME); + assertThat(expected.getMessage()).contains(FailingGeneratingProcessor.GENERATED_SOURCE); } @Test public void compilesWithoutError_throws() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld-broken.java")) - .compilesWithoutError(); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).startsWith("Compilation produced the following errors:\n"); - assertThat(expected.getMessage()).contains("No files were generated."); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(JavaFileObjects.forResource("test/HelloWorld-broken.java")) + .compilesWithoutError(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected) + .hasMessageThat() + .contains("Compilation produced the following" + " diagnostics:\n"); + assertThat(expected.getMessage()).contains("No files were generated."); } @Test public void compilesWithoutError_exceptionCreatedOrPassedThrough() { RuntimeException e = new RuntimeException(); try { - VERIFY - .about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + assertAbout(javaSource()) + .that(HELLO_WORLD_RESOURCE) .processedWith(new ThrowingProcessor(e)) .compilesWithoutError(); fail(); @@ -394,7 +377,7 @@ public void compilesWithoutError_exceptionCreatedOrPassedThrough() { @Test public void parsesAs() { assertAbout(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + .that(HELLO_WORLD_RESOURCE) .parsesAs( JavaFileObjects.forSourceLines( "test.HelloWorld", @@ -410,477 +393,471 @@ public void parsesAs() { @Test public void parsesAs_expectedFileFailsToParse() { try { - VERIFY - .about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .parsesAs(JavaFileObjects.forResource("HelloWorld-broken.java")); + assertAbout(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .parsesAs(JavaFileObjects.forResource("test/HelloWorld-broken.java")); fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).startsWith("error while parsing:"); + assertThat(expected.getMessage()).startsWith("Error while parsing *expected* source:\n"); } } @Test public void parsesAs_actualFileFailsToParse() { try { - VERIFY - .about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld-broken.java")) - .parsesAs(JavaFileObjects.forResource("HelloWorld.java")); + assertAbout(javaSource()) + .that(JavaFileObjects.forResource("test/HelloWorld-broken.java")) + .parsesAs(HELLO_WORLD_RESOURCE); fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).startsWith("error while parsing:"); + assertThat(expected.getMessage()).startsWith("Error while parsing *actual* source:\n"); } } @Test public void failsToCompile_throws() { - try { - VERIFY - .about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .failsToCompile(); - 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."); - } + expectFailure.whenTesting().about(javaSource()).that(HELLO_WORLD_RESOURCE).failsToCompile(); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Compilation was expected to fail, but contained no errors"); + assertThat(expected.getMessage()).contains("No files were generated."); } @Test public void failsToCompile_throwsNoMessage() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new ErrorProcessor()) - .failsToCompile().withErrorContaining("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"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new ErrorProcessor()) + .failsToCompile() + .withErrorContaining("some error"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected an error containing \"some error\", but only found:\n"); + // some versions of javac wedge the file and position in the middle + assertThat(expected).hasMessageThat().contains("expected error!\n"); } @Test public void failsToCompile_throwsNotInFile() { - JavaFileObject fileObject = JavaFileObjects.forResource("HelloWorld.java"); - JavaFileObject otherFileObject = JavaFileObjects.forResource("HelloWorld-different.java"); - try { - VERIFY.about(javaSource()) - .that(fileObject) - .processedWith(new ErrorProcessor()) - .failsToCompile().withErrorContaining("expected error!") - .in(otherFileObject); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected an error containing \"expected error!\" in %s", - otherFileObject.getName())); - assertThat(expected.getMessage()).contains(fileObject.getName()); - // "(no associated file)"))); - } + JavaFileObject fileObject = HELLO_WORLD_RESOURCE; + JavaFileObject otherFileObject = JavaFileObjects.forResource("test/HelloWorld-different.java"); + expectFailure + .whenTesting() + .about(javaSource()) + .that(fileObject) + .processedWith(new ErrorProcessor()) + .failsToCompile() + .withErrorContaining("expected error!") + .in(otherFileObject); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected an error containing \"expected error!\" in %s", + otherFileObject.getName())); + assertThat(expected.getMessage()).contains(fileObject.getName()); } @Test public void failsToCompile_throwsNotOnLine() { - JavaFileObject fileObject = JavaFileObjects.forResource("HelloWorld.java"); - try { - VERIFY.about(javaSource()) - .that(fileObject) - .processedWith(new ErrorProcessor()) - .failsToCompile().withErrorContaining("expected error!") - .in(fileObject).onLine(1); - fail(); - } catch (VerificationException expected) { - int actualErrorLine = 18; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected an error containing \"expected error!\" in %s on line:\n 1: ", - fileObject.getName())); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + JavaFileObject fileObject = HELLO_WORLD_RESOURCE; + expectFailure + .whenTesting() + .about(javaSource()) + .that(fileObject) + .processedWith(new ErrorProcessor()) + .failsToCompile() + .withErrorContaining("expected error!") + .in(fileObject) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 18; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected an error containing \"expected error!\" in %s on line:\n 1: ", + fileObject.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); } @Test public void failsToCompile_throwsNotAtColumn() { - JavaFileObject fileObject = JavaFileObjects.forResource("HelloWorld.java"); - try { - VERIFY.about(javaSource()) - .that(fileObject) - .processedWith(new ErrorProcessor()) - .failsToCompile().withErrorContaining("expected error!") - .in(fileObject).onLine(18).atColumn(1); - fail(); - } catch (VerificationException expected) { - int actualErrorCol = 8; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected an error containing \"expected error!\" in %s at column 1 of line 18", - fileObject.getName())); - assertThat(expected.getMessage()).contains("" + actualErrorCol); - } + JavaFileObject fileObject = HELLO_WORLD_RESOURCE; + expectFailure + .whenTesting() + .about(javaSource()) + .that(fileObject) + .processedWith(new ErrorProcessor()) + .failsToCompile() + .withErrorContaining("expected error!") + .in(fileObject) + .onLine(18) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected an error containing \"expected error!\" in %s at column 1 of line 18", + fileObject.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorCol); } @Test public void failsToCompile_wrongErrorCount() { - JavaFileObject fileObject = JavaFileObjects.forResource("HelloWorld.java"); - try { - VERIFY.about(javaSource()) - .that(fileObject) - .processedWith(new ErrorProcessor()) - .failsToCompile() - .withErrorCount(42); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains("Expected 42 errors, but found the following 2 errors:\n"); - } + JavaFileObject fileObject = HELLO_WORLD_RESOURCE; + expectFailure + .whenTesting() + .about(javaSource()) + .that(fileObject) + .processedWith(new ErrorProcessor()) + .failsToCompile() + .withErrorCount(42); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 42 errors, but found the following 2 errors:\n"); } @Test public void failsToCompile_noWarning() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .failsToCompile() - .withWarningContaining("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"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .failsToCompile() + .withWarningContaining("what is it?"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("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).hasMessageThat().contains("this is a message\n"); } @Test public void failsToCompile_warningNotInFile() { - JavaFileObject otherSource = JavaFileObjects.forResource("HelloWorld.java"); - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .failsToCompile() - .withWarningContaining("this is a message") - .in(otherSource); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a warning containing \"this is a message\" in %s", - otherSource.getName())); - assertThat(expected.getMessage()).contains(HELLO_WORLD_BROKEN.getName()); - } + JavaFileObject otherSource = HELLO_WORLD_RESOURCE; + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .failsToCompile() + .withWarningContaining("this is a message") + .in(otherSource); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning containing \"this is a message\" in %s", + otherSource.getName())); + assertThat(expected.getMessage()).contains(HELLO_WORLD_BROKEN.getName()); } @Test public void failsToCompile_warningNotOnLine() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .failsToCompile() - .withWarningContaining("this is a message") - .in(HELLO_WORLD_BROKEN) - .onLine(1); - fail(); - } catch (VerificationException expected) { - int actualErrorLine = 6; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a warning containing \"this is a message\" in %s on line:\n 1: ", - HELLO_WORLD_BROKEN.getName())); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .failsToCompile() + .withWarningContaining("this is a message") + .in(HELLO_WORLD_BROKEN) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning containing \"this is a message\" in %s on line:\n 1: ", + HELLO_WORLD_BROKEN.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); } @Test public void failsToCompile_warningNotAtColumn() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .failsToCompile() - .withWarningContaining("this is a message") - .in(HELLO_WORLD_BROKEN) - .onLine(6) - .atColumn(1); - fail(); - } catch (VerificationException expected) { - int actualErrorCol = 8; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a warning containing \"this is a message\" in %s at column 1 of line 6", - HELLO_WORLD_BROKEN.getName())); - assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .failsToCompile() + .withWarningContaining("this is a message") + .in(HELLO_WORLD_BROKEN) + .onLine(6) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a warning containing \"this is a message\" in %s at column 1 of line 6", + HELLO_WORLD_BROKEN.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); } @Test public void failsToCompile_wrongWarningCount() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) - .failsToCompile() - .withWarningCount(42); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains("Expected 42 warnings, but found the following 2 warnings:\n"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.WARNING)) + .failsToCompile() + .withWarningCount(42); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 42 warnings, but found the following 2 warnings:\n"); } @Test public void failsToCompile_noNote() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .failsToCompile() - .withNoteContaining("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"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .failsToCompile() + .withNoteContaining("what is it?"); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("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).hasMessageThat().contains("this is a message\n"); } @Test public void failsToCompile_noteNotInFile() { - JavaFileObject otherSource = JavaFileObjects.forResource("HelloWorld.java"); - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .failsToCompile() - .withNoteContaining("this is a message") - .in(otherSource); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a note containing \"this is a message\" in %s", otherSource.getName())); - assertThat(expected.getMessage()).contains(HELLO_WORLD_BROKEN.getName()); - } + JavaFileObject otherSource = HELLO_WORLD_RESOURCE; + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .failsToCompile() + .withNoteContaining("this is a message") + .in(otherSource); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a note containing \"this is a message\" in %s", otherSource.getName())); + assertThat(expected.getMessage()).contains(HELLO_WORLD_BROKEN.getName()); } @Test public void failsToCompile_noteNotOnLine() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .failsToCompile() - .withNoteContaining("this is a message") - .in(HELLO_WORLD_BROKEN) - .onLine(1); - fail(); - } catch (VerificationException expected) { - int actualErrorLine = 6; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a note containing \"this is a message\" in %s on line:\n 1: ", - HELLO_WORLD_BROKEN.getName())); - assertThat(expected.getMessage()).contains("" + actualErrorLine); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .failsToCompile() + .withNoteContaining("this is a message") + .in(HELLO_WORLD_BROKEN) + .onLine(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorLine = 6; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a note containing \"this is a message\" in %s on line:\n 1: ", + HELLO_WORLD_BROKEN.getName())); + assertThat(expected.getMessage()).contains("" + actualErrorLine); } @Test public void failsToCompile_noteNotAtColumn() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .failsToCompile() - .withNoteContaining("this is a message") - .in(HELLO_WORLD_BROKEN) - .onLine(6) - .atColumn(1); - fail(); - } catch (VerificationException expected) { - int actualErrorCol = 8; - assertThat(expected.getMessage()) - .contains( - String.format( - "Expected a note containing \"this is a message\" in %s at column 1 of line 6", - HELLO_WORLD_BROKEN.getName())); - assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .failsToCompile() + .withNoteContaining("this is a message") + .in(HELLO_WORLD_BROKEN) + .onLine(6) + .atColumn(1); + AssertionError expected = expectFailure.getFailure(); + int actualErrorCol = 8; + assertThat(expected.getMessage()) + .contains( + String.format( + "Expected a note containing \"this is a message\" in %s at column 1 of line 6", + HELLO_WORLD_BROKEN.getName())); + assertThat(expected.getMessage()).contains("[" + actualErrorCol + "]"); } @Test public void failsToCompile_wrongNoteCount() { - try { - VERIFY - .about(javaSource()) - .that(HELLO_WORLD_BROKEN) - .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) - .failsToCompile() - .withNoteCount(42); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()) - .contains("Expected 42 notes, but found the following 2 notes:\n"); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_BROKEN) + .processedWith(new DiagnosticMessage.Processor(Diagnostic.Kind.NOTE)) + .failsToCompile() + .withNoteCount(42); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Expected 42 notes, but found the following 2 notes:\n"); } @Test public void failsToCompile() { - JavaFileObject brokenFileObject = JavaFileObjects.forResource("HelloWorld-broken.java"); + JavaFileObject brokenFileObject = JavaFileObjects.forResource("test/HelloWorld-broken.java"); assertAbout(javaSource()) .that(brokenFileObject) .failsToCompile() - .withErrorContaining("not a statement").in(brokenFileObject).onLine(23).atColumn(5) + .withErrorContaining("not a statement") + .in(brokenFileObject) + .onLine(23) + .atColumn(5) .and() .withErrorCount(4); - JavaFileObject happyFileObject = JavaFileObjects.forResource("HelloWorld.java"); + JavaFileObject happyFileObject = HELLO_WORLD_RESOURCE; assertAbout(javaSource()) .that(happyFileObject) .processedWith(new ErrorProcessor()) .failsToCompile() - .withErrorContaining("expected error!").in(happyFileObject).onLine(18).atColumn(8); + .withErrorContaining("expected error!") + .in(happyFileObject) + .onLine(18) + .atColumn(8); } @Test public void generatesSources() { assertAbout(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + .that(HELLO_WORLD_RESOURCE) .processedWith(new GeneratingProcessor()) .compilesWithoutError() - .and().generatesSources(JavaFileObjects.forSourceString( - GeneratingProcessor.GENERATED_CLASS_NAME, - GeneratingProcessor.GENERATED_SOURCE)); + .and() + .generatesSources( + JavaFileObjects.forSourceString( + GeneratingProcessor.GENERATED_CLASS_NAME, GeneratingProcessor.GENERATED_SOURCE)); + } + + @Test + public void generatesSources_packageInfo() { + GeneratingProcessor generatingProcessor = new GeneratingProcessor("test.generated"); + assertAbout(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(generatingProcessor) + .compilesWithoutError() + .and() + .generatesSources( + JavaFileObjects.forSourceString( + "test.generated.package-info", generatingProcessor.generatedPackageInfoSource())); } @Test public void generatesSources_failOnUnexpected() { String failingExpectationSource = "abstract class Blah {}"; - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new GeneratingProcessor()) - .compilesWithoutError() - .and().generatesSources(JavaFileObjects.forSourceString( - GeneratingProcessor.GENERATED_CLASS_NAME, - failingExpectationSource)); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).contains("didn't match exactly"); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_SOURCE); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new GeneratingProcessor()) + .compilesWithoutError() + .and() + .generatesSources( + JavaFileObjects.forSourceString( + GeneratingProcessor.GENERATED_CLASS_NAME, failingExpectationSource)); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains("didn't match exactly"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_SOURCE); } @Test public void generatesSources_failOnExtraExpected() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new GeneratingProcessor()) - .compilesWithoutError() - .and().generatesSources(JavaFileObjects.forSourceLines( - GeneratingProcessor.GENERATED_CLASS_NAME, - "import java.util.List; // Extra import", - "final class Blah {", - " String blah = \"blah\";", - "}")); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).contains("didn't match exactly"); - assertThat(expected.getMessage()).contains("unmatched nodes in the expected tree"); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_SOURCE); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new GeneratingProcessor()) + .compilesWithoutError() + .and() + .generatesSources( + JavaFileObjects.forSourceLines( + GeneratingProcessor.GENERATED_CLASS_NAME, + "import java.util.List; // Extra import", + "final class Blah {", + " String blah = \"blah\";", + "}")); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains("didn't match exactly"); + assertThat(expected.getMessage()).contains("unmatched nodes in the expected tree"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_SOURCE); } @Test public void generatesSources_failOnExtraActual() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new GeneratingProcessor()) - .compilesWithoutError() - .and().generatesSources(JavaFileObjects.forSourceLines( - GeneratingProcessor.GENERATED_CLASS_NAME, - "final class Blah {", - " // missing field", - "}")); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).contains("didn't match exactly"); - assertThat(expected.getMessage()).contains("unmatched nodes in the actual tree"); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_SOURCE); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new GeneratingProcessor()) + .compilesWithoutError() + .and() + .generatesSources( + JavaFileObjects.forSourceLines( + GeneratingProcessor.GENERATED_CLASS_NAME, + "final class Blah {", + " // missing field", + "}")); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains("didn't match exactly"); + assertThat(expected.getMessage()).contains("unmatched nodes in the actual tree"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_SOURCE); } @Test public void generatesSources_failWithNoCandidates() { String failingExpectationName = "ThisIsNotTheRightFile"; String failingExpectationSource = "abstract class ThisIsNotTheRightFile {}"; - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new GeneratingProcessor()) - .compilesWithoutError() - .and().generatesSources(JavaFileObjects.forSourceString( - failingExpectationName, - failingExpectationSource)); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).contains("top-level types that were not present"); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); - assertThat(expected.getMessage()).contains(failingExpectationName); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new GeneratingProcessor()) + .compilesWithoutError() + .and() + .generatesSources( + JavaFileObjects.forSourceString(failingExpectationName, failingExpectationSource)); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains("top-level types that were not present"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_CLASS_NAME); + assertThat(expected.getMessage()).contains(failingExpectationName); } @Test public void generatesSources_failWithNoGeneratedSources() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new NonGeneratingProcessor()) - .compilesWithoutError() - .and().generatesSources(JavaFileObjects.forSourceString( - GeneratingProcessor.GENERATED_CLASS_NAME, - GeneratingProcessor.GENERATED_SOURCE)); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).contains( - "Compilation generated no additional source files, though some were expected."); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new NonGeneratingProcessor()) + .compilesWithoutError() + .and() + .generatesSources( + JavaFileObjects.forSourceString( + GeneratingProcessor.GENERATED_CLASS_NAME, GeneratingProcessor.GENERATED_SOURCE)); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()) + .contains("Compilation generated no additional source files, though some were expected."); } @Test public void generatesFileNamed() { assertAbout(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + .that(HELLO_WORLD_RESOURCE) .processedWith(new GeneratingProcessor()) .compilesWithoutError() .and() @@ -890,42 +867,42 @@ public void generatesFileNamed() { @Test public void generatesFileNamed_failOnFileExistence() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new GeneratingProcessor()) - .compilesWithoutError() - .and() - .generatesFileNamed(CLASS_OUTPUT, "com.google.testing.compile", "Bogus") - .withContents(ByteSource.wrap("Bar".getBytes(UTF_8))); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).contains("generated the file named \"Bogus\""); - assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_RESOURCE_NAME); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new GeneratingProcessor()) + .compilesWithoutError() + .and() + .generatesFileNamed(CLASS_OUTPUT, "com.google.testing.compile", "Bogus") + .withContents(ByteSource.wrap("Bar".getBytes(UTF_8))); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected) + .factValue("expected to generate file") + .isEqualTo("/com/google/testing/compile/Bogus"); + assertThat(expected.getMessage()).contains(GeneratingProcessor.GENERATED_RESOURCE_NAME); } @Test public void generatesFileNamed_failOnFileContents() { - try { - VERIFY.about(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) - .processedWith(new GeneratingProcessor()) - .compilesWithoutError() - .and() - .generatesFileNamed(CLASS_OUTPUT, "com.google.testing.compile", "Foo") - .withContents(ByteSource.wrap("Bogus".getBytes(UTF_8))); - fail(); - } catch (VerificationException expected) { - assertThat(expected.getMessage()).contains("Foo"); - assertThat(expected.getMessage()).contains(" has contents "); - } + expectFailure + .whenTesting() + .about(javaSource()) + .that(HELLO_WORLD_RESOURCE) + .processedWith(new GeneratingProcessor()) + .compilesWithoutError() + .and() + .generatesFileNamed(CLASS_OUTPUT, "com.google.testing.compile", "Foo") + .withContents(ByteSource.wrap("Bogus".getBytes(UTF_8))); + AssertionError expected = expectFailure.getFailure(); + assertThat(expected.getMessage()).contains("Foo"); + assertThat(expected.getMessage()).contains(" have contents"); } @Test public void withStringContents() { assertAbout(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + .that(HELLO_WORLD_RESOURCE) .processedWith(new GeneratingProcessor()) .compilesWithoutError() .and() @@ -937,7 +914,7 @@ public void withStringContents() { public void passesOptions() { NoOpProcessor processor = new NoOpProcessor(); assertAbout(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + .that(HELLO_WORLD_RESOURCE) .withCompilerOptions("-Aa=1") .withCompilerOptions(ImmutableList.of("-Ab=2", "-Ac=3")) .processedWith(processor) @@ -955,7 +932,7 @@ public void invokesMultipleProcesors() { assertThat(noopProcessor1.invoked).isFalse(); assertThat(noopProcessor2.invoked).isFalse(); assertAbout(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + .that(HELLO_WORLD_RESOURCE) .processedWith(noopProcessor1, noopProcessor2) .compilesWithoutError(); assertThat(noopProcessor1.invoked).isTrue(); @@ -969,7 +946,7 @@ public void invokesMultipleProcesors_asIterable() { assertThat(noopProcessor1.invoked).isFalse(); assertThat(noopProcessor2.invoked).isFalse(); assertAbout(javaSource()) - .that(JavaFileObjects.forResource("HelloWorld.java")) + .that(HELLO_WORLD_RESOURCE) .processedWith(Arrays.asList(noopProcessor1, noopProcessor2)) .compilesWithoutError(); assertThat(noopProcessor1.invoked).isTrue(); diff --git a/src/test/java/com/google/testing/compile/ParserTest.java b/src/test/java/com/google/testing/compile/ParserTest.java new file mode 100644 index 00000000..d4ae1eb1 --- /dev/null +++ b/src/test/java/com/google/testing/compile/ParserTest.java @@ -0,0 +1,27 @@ +package com.google.testing.compile; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +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 final class ParserTest { + + private static final JavaFileObject HELLO_WORLD_BROKEN = + JavaFileObjects.forSourceLines( + "test.HelloWorld", "package test;", "", "public class HelloWorld {", "}}"); + + @Test + public void failsToParse() { + IllegalStateException expected = + assertThrows( + IllegalStateException.class, + () -> Parser.parse(ImmutableList.of(HELLO_WORLD_BROKEN), "hello world")); + assertThat(expected).hasMessageThat().contains("HelloWorld.java:4: error"); + } +} diff --git a/src/test/java/com/google/testing/compile/ThrowingProcessor.java b/src/test/java/com/google/testing/compile/ThrowingProcessor.java index 8154c6c2..3fd108cd 100644 --- a/src/test/java/com/google/testing/compile/ThrowingProcessor.java +++ b/src/test/java/com/google/testing/compile/ThrowingProcessor.java @@ -20,6 +20,7 @@ import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; final class ThrowingProcessor extends AbstractProcessor { @@ -35,6 +36,11 @@ public Set getSupportedAnnotationTypes() { return ImmutableSet.of("*"); } + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { throw e; diff --git a/src/test/java/com/google/testing/compile/TreeDifferTest.java b/src/test/java/com/google/testing/compile/TreeDifferTest.java index 92653b2e..cd77317f 100644 --- a/src/test/java/com/google/testing/compile/TreeDifferTest.java +++ b/src/test/java/com/google/testing/compile/TreeDifferTest.java @@ -15,25 +15,25 @@ */ package com.google.testing.compile; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Iterables.getOnlyElement; 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 java.util.Objects; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** - * A test for {@link DetailedEqualityScanner} + * A test for {@link TreeDiffer}. */ @RunWith(JUnit4.class) public class TreeDifferTest { - @Rule public final ExpectedException expectedExn = ExpectedException.none(); private static final CompilationUnitTree EXPECTED_TREE = MoreTrees.parseLinesToTree("package test;", "import java.util.Set;", @@ -153,6 +153,65 @@ public class TreeDifferTest { " };", "}"); + private static final CompilationUnitTree TRY_WITH_RESOURCES_1 = + MoreTrees.parseLinesToTree("package test;", + "final class TestClass {", + " void f() {", + " try (Resource1 r = new Resource1()) {}", + " }", + "}"); + + private static final CompilationUnitTree TRY_WITH_RESOURCES_2 = + MoreTrees.parseLinesToTree("package test;", + "final class TestClass {", + " void f() {", + " try (Resource2 r = new Resource2()) {}", + " }", + "}"); + + private static final ImmutableList ANNOTATED_TYPE_SOURCE = + ImmutableList.of( + "package test;", + "", + "import java.lang.annotation.*;", + "import java.util.List;", + "", + "@Target(ElementType.TYPE_USE)", + "@interface Nullable {}", + "", + "interface NullableStringList extends List<@Nullable String> {}"); + + private static final CompilationUnitTree ANNOTATED_TYPE_1 = + MoreTrees.parseLinesToTree(ANNOTATED_TYPE_SOURCE); + + private static final CompilationUnitTree ANNOTATED_TYPE_2 = + MoreTrees.parseLinesToTree( + ANNOTATED_TYPE_SOURCE.stream() + .map(s -> s.replace("@Nullable ", "")) + .collect(toImmutableList())); + + private static final ImmutableList MULTICATCH_SOURCE = + ImmutableList.of( + "package test;", + "", + "class TestClass {", + " void f() {", + " try {", + " System.gc();", + " } catch (IllegalArgumentException | NullPointerException e) {", + " }", + " }", + "}"); + + private static final CompilationUnitTree MULTICATCH_1 = + MoreTrees.parseLinesToTree(MULTICATCH_SOURCE); + + private static final CompilationUnitTree MULTICATCH_2 = + MoreTrees.parseLinesToTree( + MULTICATCH_SOURCE.stream() + .map(s -> s.replace("IllegalArgumentException", "IllegalStateException")) + .collect(toImmutableList())); + @Test public void scan_differingCompilationUnits() { TreeDifference diff = TreeDiffer.diffCompilationUnits(EXPECTED_TREE, ACTUAL_TREE); @@ -164,15 +223,15 @@ public void scan_differingCompilationUnits() { ImmutableList differingNodesExpected = ImmutableList.of( new SimplifiedDiff(Tree.Kind.MEMBER_SELECT, - "Expected member identifier to be but was ."), + "Expected member-select identifier to be but was ."), new SimplifiedDiff(Tree.Kind.VARIABLE, "Expected variable name to be but was ."), new SimplifiedDiff(Tree.Kind.IDENTIFIER, - "Expected identifier to be but was ."), + "Expected identifier name to be but was ."), new SimplifiedDiff(Tree.Kind.IDENTIFIER, - "Expected identifier to be but was ."), + "Expected identifier name to be but was ."), new SimplifiedDiff(Tree.Kind.BREAK, - "Expected label on break statement to be but was .")); + "Expected break label to be but was .")); assertThat(diff.getExtraExpectedNodes().isEmpty()).isTrue(); assertThat(diff.getExtraActualNodes().size()).isEqualTo(extraNodesExpected.size()); @@ -181,15 +240,13 @@ public void scan_differingCompilationUnits() { for (TreeDifference.OneWayDiff extraNode : diff.getExtraActualNodes()) { extraNodesFound.add(SimplifiedDiff.create(extraNode)); } - assertThat(extraNodesExpected).containsExactlyElementsIn(extraNodesFound.build()).inOrder(); + assertThat(extraNodesFound.build()).containsExactlyElementsIn(extraNodesExpected).inOrder(); ImmutableList.Builder differingNodesFound = new ImmutableList.Builder(); for (TreeDifference.TwoWayDiff differingNode : diff.getDifferingNodes()) { differingNodesFound.add(SimplifiedDiff.create(differingNode)); } - assertThat(differingNodesExpected) - .containsExactlyElementsIn(differingNodesFound.build()) - .inOrder(); + assertThat(differingNodesFound.build()).containsExactlyElementsIn(differingNodesExpected); } @Test @@ -228,7 +285,7 @@ public void scan_testTwoNullIterableTrees() { assertThat(diff.isEmpty()).isFalse(); for (TreeDifference.TwoWayDiff differingNode : diff.getDifferingNodes()) { assertThat(differingNode.getDetails()) - .contains("Expected literal value to be <3> but was <4>"); + .contains("Expected int-literal value to be <3> but was <4>"); } } @@ -250,7 +307,7 @@ public void scan_testActualNullIterableTree() { public void scan_testLambdas() { TreeDifference diff = TreeDiffer.diffCompilationUnits(LAMBDA_1, LAMBDA_2); - assertThat(diff.isEmpty()).isTrue(); + assertThat(diff.getDiffReport()).isEmpty(); } @Test @@ -265,7 +322,7 @@ public void scan_testLambdasParensVsNone() { TreeDifference diff = TreeDiffer.diffCompilationUnits( LAMBDA_IMPLICIT_ARG_TYPE, LAMBDA_IMPLICIT_ARG_TYPE_NO_PARENS); - assertThat(diff.isEmpty()).isTrue(); + assertThat(diff.getDiffReport()).isEmpty(); } @Test @@ -282,6 +339,447 @@ public void scan_testLambdaVersusAnonymousClass() { assertThat(diff.isEmpty()).isFalse(); } + @Test + public void scan_testTryWithResources() { + TreeDifference diff = + TreeDiffer.diffCompilationUnits(TRY_WITH_RESOURCES_1, TRY_WITH_RESOURCES_1); + assertThat(diff.getDiffReport()).isEmpty(); + } + + @Test + public void scan_testTryWithResourcesDifferent() { + TreeDifference diff = + TreeDiffer.diffCompilationUnits(TRY_WITH_RESOURCES_1, TRY_WITH_RESOURCES_2); + assertThat(diff.isEmpty()).isFalse(); + } + + @Test + public void scan_testAnnotatedType() { + TreeDifference diff = + TreeDiffer.diffCompilationUnits(ANNOTATED_TYPE_1, ANNOTATED_TYPE_1); + assertThat(diff.getDiffReport()).isEmpty(); + } + + @Test + public void scan_testAnnotatedTypeDifferent() { + TreeDifference diff = + TreeDiffer.diffCompilationUnits(ANNOTATED_TYPE_1, ANNOTATED_TYPE_2); + assertThat(diff.isEmpty()).isFalse(); + } + + @Test + public void scan_testMulticatch() { + TreeDifference diff = + TreeDiffer.diffCompilationUnits(MULTICATCH_1, MULTICATCH_1); + assertThat(diff.getDiffReport()).isEmpty(); + } + + @Test + public void scan_testMulticatchDifferent() { + TreeDifference diff = + TreeDiffer.diffCompilationUnits(MULTICATCH_1, MULTICATCH_2); + assertThat(diff.isEmpty()).isFalse(); + } + + @Test + public void matchCompilationUnits() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "", + "import not.NotUsed;", + "import is.IsUsed;", + "", + "public class HasExtras { ", + " private NotUsed skipped;", + " private Object matched;", + " private IsUsed usedFromImport;", + " private Object skipped2;", + "", + " HasExtras() {}", + " HasExtras(int overloadedConstructor) {}", + "", + " public String skippedMethod() { return null; }", + " public String matchedMethod() { return null; }", + " public Object overloadedMethod(int skipWithDifferentSignature) { return null; }", + " public String overloadedMethod(int i, Double d) { return null; }", + " public String overloadedMethod(int i, Double d, IsUsed u) { return null; }", + "", + " class NestedClass {", + " int matchMe = 0;", + " double ignoreMe = 0;", + " }", + "", + " class IgnoredNestedClass {}", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "import is.IsUsed;", + "", + "public class HasExtras { ", + " private Object matched;", + " private IsUsed usedFromImport;", + "", + " HasExtras(int overloadedConstructor) {}", + "", + " public String matchedMethod() { return null; }", + " public String overloadedMethod(int i, Double d) { return null; }", + " public String overloadedMethod(int i, Double d, IsUsed u) { return null; }", + "", + " class NestedClass {", + " int matchMe = 0;", + " }", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDiffReport()).isEmpty(); + } + + @Test + public void matchCompilationUnits_unresolvedTypeInPattern() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "", + "import is.IsUsed;", + "", + "public class HasExtras { ", + " private IsUsed usedFromImport;", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "public class HasExtras { ", + " private IsUsed usedFromImport;", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDiffReport()).isEmpty(); + } + + @Test + public void matchCompilationUnits_sameSignature_differentReturnType() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "public class HasExtras { ", + " private Object method(int i, double d) { return null; };", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "public class HasExtras { ", + " private String method(int i, double d) { return null; };", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDifferingNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_sameSignature_differentParameterNames() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "public class HasExtras { ", + " private Object method(int i, double d) { return null; };", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "public class HasExtras { ", + " private Object method(int i2, double d2) { return null; };", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDifferingNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_sameSignature_differentParameters() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "public class HasExtras { ", + " private Object method(int i, Object o) { return null; };", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "public class HasExtras { ", + " private Object method(int i2, @Nullable Object o) { return null; };", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDifferingNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_sameSignature_differentModifiers() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "public class HasExtras { ", + " private Object method(int i, Object o) { return null; };", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "public class HasExtras { ", + " public Object method(int i2, @Nullable Object o) { return null; };", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDifferingNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_sameSignature_differentThrows() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "public class HasExtras { ", + " private void method() throws RuntimeException {}", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "public class HasExtras { ", + " private void method() throws Error {}", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDifferingNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_variablesWithDifferentTypes() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "public class HasExtras { ", + " private Object field;", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "public class HasExtras { ", + " private String field;", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDifferingNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_importsWithSameSimpleName() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "", + "import foo.Imported;", + "", + "public class HasExtras { ", + " private Imported field;", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "import bar.Imported;", + "", + "public class HasExtras { ", + " private Imported field;", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getExtraExpectedNodes()).isNotEmpty(); + assertThat(diff.getExtraActualNodes()).isEmpty(); + } + + @Test + public void matchCompilationUnits_wrongOrder() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "", + "class Foo {", + " private String method1() { return new String(); }", + " private String method2() { return new String(); }", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "class Foo {", + " private String method2() { return new String(); }", + " private String method1() { return new String(); }", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDifferingNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_missingParameter() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "", + "class Foo {", + " private String method1(String s) { return s; }", + " private String method2() { return new String(); }", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "class Foo {", + " private String method1() { return s; }", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getExtraExpectedNodes()).isNotEmpty(); + assertThat(diff.getExtraActualNodes()).isEmpty(); + } + + @Test + public void matchCompilationUnits_missingMethodBodyStatement() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "", + "class Foo {", + " private String method1(String s) { ", + " System.out.println(s);", + " return s;", + " }", + " private String method2() { return new String(); }", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "class Foo {", + " private String method1(String s) { ", + " return s;", + " }", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getExtraActualNodes()).isNotEmpty(); + } + + @Test + public void matchCompilationUnits_skipsImports() { + ParseResult actual = + MoreTrees.parseLines( + "package test;", + "", + "import bar.Bar;", + "", + "class Foo {", + " private Bar bar;", + "}"); + + ParseResult pattern = + MoreTrees.parseLines( + "package test;", + "", + "class Foo {", + " private Bar bar;", + "}"); + TreeDifference diff = + TreeDiffer.matchCompilationUnits( + getOnlyElement(pattern.compilationUnits()), + pattern.trees(), + getOnlyElement(actual.compilationUnits()), + actual.trees()); + + assertThat(diff.getDiffReport()).isEmpty(); + } + private TreePath asPath(CompilationUnitTree compilationUnit) { return MoreTrees.findSubtreePath(compilationUnit, Tree.Kind.COMPILATION_UNIT); } @@ -303,14 +801,6 @@ private static class SimplifiedDiff { this.details = details; } - Tree.Kind getKind() { - return kind; - } - - String getDetails() { - return details; - } - static SimplifiedDiff create(TreeDifference.OneWayDiff other) { return new SimplifiedDiff(other.getNodePath().getLeaf().getKind(), other.getDetails()); } diff --git a/src/test/java/com/google/testing/compile/VerificationFailureStrategy.java b/src/test/java/com/google/testing/compile/VerificationFailureStrategy.java deleted file mode 100644 index a64edc0d..00000000 --- a/src/test/java/com/google/testing/compile/VerificationFailureStrategy.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.truth.AbstractFailureStrategy; -import com.google.common.truth.StandardSubjectBuilder; - -/** @deprecated prefer {@link com.google.common.truth.ExpectFailure} for testing Truth failures. */ -@Deprecated -final class VerificationFailureStrategy extends AbstractFailureStrategy { - static final class VerificationException extends RuntimeException { - private static final long serialVersionUID = 1L; - - VerificationException(String message) { - super(message); - } - } - - /** - * A {@link StandardSubjectBuilder} that throws something other than {@link AssertionError}. - * - * @deprecated prefer {@link com.google.common.truth.ExpectFailure} for testing Truth failures. - */ - @Deprecated - static final StandardSubjectBuilder VERIFY = - StandardSubjectBuilder.forCustomFailureStrategy(new VerificationFailureStrategy()); - - @Override - public void fail(String message, Throwable unused) { - throw new VerificationFailureStrategy.VerificationException(message); - } -} diff --git a/src/test/resources/HelloWorld-broken.java b/src/test/resources/test/HelloWorld-broken.java similarity index 100% rename from src/test/resources/HelloWorld-broken.java rename to src/test/resources/test/HelloWorld-broken.java diff --git a/src/test/resources/HelloWorld-different.java b/src/test/resources/test/HelloWorld-different.java similarity index 100% rename from src/test/resources/HelloWorld-different.java rename to src/test/resources/test/HelloWorld-different.java diff --git a/src/test/resources/HelloWorld-v2.java b/src/test/resources/test/HelloWorld-v2.java similarity index 100% rename from src/test/resources/HelloWorld-v2.java rename to src/test/resources/test/HelloWorld-v2.java diff --git a/src/test/resources/HelloWorld.java b/src/test/resources/test/HelloWorld.java similarity index 100% rename from src/test/resources/HelloWorld.java rename to src/test/resources/test/HelloWorld.java