/* * Copyright 2012 Google Inc. All Rights Reserved. * * 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.errorprone; import static java.util.Locale.ENGLISH; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.common.io.CharSource; import java.io.BufferedReader; import java.io.IOException; import java.io.LineNumberReader; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; /** * Utility class for tests which need to assert on the diagnostics produced during compilation. * * @author alexeagle@google.com (Alex Eagle) */ public class DiagnosticTestHelper { // When testing a single error-prone check, the name of the check. Used to validate diagnostics. // Null if not testing a single error-prone check. private final String checkName; private final Map<String, Predicate<? super String>> expectedErrorMsgs = new HashMap<>(); /** * Construct a {@link DiagnosticTestHelper} not associated with a specific check. */ public DiagnosticTestHelper() { this(null); } /** * Construct a {@link DiagnosticTestHelper} for a check with the given name. */ public DiagnosticTestHelper(String checkName) { this.checkName = checkName; } public final ClearableDiagnosticCollector<JavaFileObject> collector = new ClearableDiagnosticCollector<JavaFileObject>(); public static Matcher<Diagnostic<? extends JavaFileObject>> suggestsRemovalOfLine( URI fileURI, int line) { return allOf( diagnosticOnLine(fileURI, line), diagnosticMessage(containsString("remove this line"))); } public List<Diagnostic<? extends JavaFileObject>> getDiagnostics() { return collector.getDiagnostics(); } public void clearDiagnostics() { collector.clear(); } public String describe() { StringBuilder stringBuilder = new StringBuilder().append("Diagnostics:\n"); for (Diagnostic<? extends JavaFileObject> diagnostic : getDiagnostics()) { stringBuilder.append(" [") .append(diagnostic.getLineNumber()).append(":") .append(diagnostic.getColumnNumber()) .append("]\t"); stringBuilder.append(diagnostic.getMessage(Locale.getDefault()).replaceAll("\n", "\\\\n")); stringBuilder.append("\n"); } return stringBuilder.toString(); } public static Matcher<Diagnostic<? extends JavaFileObject>> diagnosticLineAndColumn( final long line, final long column) { return new TypeSafeDiagnosingMatcher<Diagnostic<? extends JavaFileObject>>() { @Override protected boolean matchesSafely(Diagnostic<? extends JavaFileObject> item, Description mismatchDescription) { if (item.getLineNumber() != line) { mismatchDescription.appendText("diagnostic not on line ") .appendValue(item.getLineNumber()); return false; } if (item.getColumnNumber() != column) { mismatchDescription.appendText("diagnostic not on column ") .appendValue(item.getColumnNumber()); return false; } return true; } @Override public void describeTo(Description description) { description .appendText("a diagnostic on line:column ") .appendValue(line) .appendText(":") .appendValue(column); } }; } public static Matcher<Diagnostic<? extends JavaFileObject>> diagnosticOnLine( final URI fileURI, final long line) { return new TypeSafeDiagnosingMatcher<Diagnostic<? extends JavaFileObject>>() { @Override public boolean matchesSafely(Diagnostic<? extends JavaFileObject> item, Description mismatchDescription) { if (item.getSource() == null) { mismatchDescription.appendText("diagnostic not attached to a file: ") .appendValue(item.getMessage(ENGLISH)); return false; } if (!item.getSource().toUri().equals(fileURI)) { mismatchDescription.appendText("diagnostic not in file ").appendValue(fileURI); return false; } if (item.getLineNumber() != line) { mismatchDescription.appendText("diagnostic not on line ") .appendValue(item.getLineNumber()); return false; } return true; } @Override public void describeTo(Description description) { description .appendText("a diagnostic on line ") .appendValue(line); } }; } public static Matcher<Diagnostic<? extends JavaFileObject>> diagnosticOnLine( final URI fileURI, final long line, final Predicate<? super String> matcher) { return new TypeSafeDiagnosingMatcher<Diagnostic<? extends JavaFileObject>>() { @Override public boolean matchesSafely(Diagnostic<? extends JavaFileObject> item, Description mismatchDescription) { if (item.getSource() == null) { mismatchDescription.appendText("diagnostic not attached to a file: ") .appendValue(item.getMessage(ENGLISH)); return false; } if (!item.getSource().toUri().equals(fileURI)) { mismatchDescription.appendText("diagnostic not in file ").appendValue(fileURI); return false; } if (item.getLineNumber() != line) { mismatchDescription.appendText("diagnostic not on line ") .appendValue(item.getLineNumber()); return false; } if (!matcher.apply(item.getMessage(Locale.getDefault()))) { mismatchDescription.appendText("diagnostic does not match ").appendValue(matcher); return false; } return true; } @Override public void describeTo(Description description) { description .appendText("a diagnostic on line ") .appendValue(line) .appendText(" that matches \n") .appendValue(matcher) .appendText("\n"); } }; } public static Matcher<Diagnostic<? extends JavaFileObject>> diagnosticMessage( final Matcher<String> matcher) { return new TypeSafeDiagnosingMatcher<Diagnostic<? extends JavaFileObject>>() { @Override public boolean matchesSafely(Diagnostic<? extends JavaFileObject> item, Description mismatchDescription) { if (!matcher.matches(item.getMessage(Locale.getDefault()))) { mismatchDescription.appendText("diagnostic message does not match ") .appendDescriptionOf(matcher); return false; } return true; } @Override public void describeTo(Description description) { description.appendText("a diagnostic with message ").appendDescriptionOf(matcher); } }; } /** * Comment that marks a bug on the next line in a test file. For example, * "// BUG: Diagnostic contains: foo.bar()", where "foo.bar()" is a string that should be in the * diagnostic for the line. Multiple expected strings may be separated by newlines, e.g. * // BUG: Diagnostic contains: foo.bar() * // bar.baz() * // baz.foo() */ private static final String BUG_MARKER_COMMENT_INLINE = "// BUG: Diagnostic contains:"; private static final String BUG_MARKER_COMMENT_LOOKUP = "// BUG: Diagnostic matches:"; private final Set<String> usedLookupKeys = new HashSet<>(); enum LookForCheckNameInDiagnostic { YES, NO; } /** * Expects an error message matching {@code matcher} at the line below a comment matching the key. * For example, given the source * <pre> * // BUG: Diagnostic matches: X * a = b + c; * </pre> * ... you can use * {@code expectErrorMessage("X", Predicates.containsPattern("Can't add b to c"));} * * <p>Error message keys that don't match any diagnostics will cause test to fail. */ public void expectErrorMessage(String key, Predicate<? super String> matcher) { expectedErrorMsgs.put(key, matcher); } /** * Asserts that the diagnostics contain a diagnostic on each line of the source file that * matches our bug marker pattern. Parses the bug marker pattern for the specific string to * look for in the diagnostic. * @param source File in which to find matching lines * * TODO(eaftan): Switch to use assertThat instead of assertTrue. */ public void assertHasDiagnosticOnAllMatchingLines( JavaFileObject source, LookForCheckNameInDiagnostic lookForCheckNameInDiagnostic) throws IOException { final List<Diagnostic<? extends JavaFileObject>> diagnostics = getDiagnostics(); final LineNumberReader reader = new LineNumberReader( CharSource.wrap(source.getCharContent(false)).openStream()); do { String line = reader.readLine(); if (line == null) { break; } List<Predicate<? super String>> predicates = null; if (line.contains(BUG_MARKER_COMMENT_INLINE)) { // Diagnostic must contain all patterns from the bug marker comment. List<String> patterns = extractPatterns(line, reader, BUG_MARKER_COMMENT_INLINE); predicates = new ArrayList<>(patterns.size()); for (String pattern : patterns) { predicates.add(new SimpleStringContains(pattern)); } } else if (line.contains(BUG_MARKER_COMMENT_LOOKUP)) { int markerLineNumber = reader.getLineNumber(); List<String> lookupKeys = extractPatterns(line, reader, BUG_MARKER_COMMENT_LOOKUP); predicates = new ArrayList<>(lookupKeys.size()); for (String lookupKey : lookupKeys) { assertTrue( "No expected error message with key [" + lookupKey + "] as expected from line [" + markerLineNumber + "] with diagnostic [" + line.trim() + "]", expectedErrorMsgs.containsKey(lookupKey)); predicates.add(expectedErrorMsgs.get(lookupKey)); usedLookupKeys.add(lookupKey); } } if (predicates != null) { int lineNumber = reader.getLineNumber(); for (Predicate<? super String> predicate : predicates) { Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> patternMatcher = hasItem(diagnosticOnLine(source.toUri(), lineNumber, predicate)); assertTrue( "Did not see an error on line " + lineNumber + " matching " + predicate + ". All errors:\n" + diagnostics, patternMatcher.matches(diagnostics)); } if (checkName != null && lookForCheckNameInDiagnostic == LookForCheckNameInDiagnostic.YES) { // Diagnostic must contain check name. Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> checkNameMatcher = hasItem(diagnosticOnLine( source.toUri(), lineNumber, new SimpleStringContains("[" + checkName + "]"))); assertTrue( "Did not see an error on line " + lineNumber + " containing [" + checkName + "]. All errors:\n" + diagnostics, checkNameMatcher.matches(diagnostics)); } } else { int lineNumber = reader.getLineNumber(); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticOnLine(source.toUri(), lineNumber)); if (matcher.matches(diagnostics)) { fail("Saw unexpected error on line " + lineNumber + ". All errors:\n" + diagnostics); } } } while (true); reader.close(); } /** Returns the lookup keys that weren't used. */ public Set<String> getUnusedLookupKeys() { return Sets.difference(expectedErrorMsgs.keySet(), usedLookupKeys); } /** * Extracts the patterns from a bug marker comment. * * @param line The first line of the bug marker comment * @param reader A reader for the test file * @param matchString The bug marker comment match string. * @return A list of patterns that the diagnostic is expected to contain * @throws IOException */ private static List<String> extractPatterns( String line, BufferedReader reader, String matchString) throws IOException { int bugMarkerIndex = line.indexOf(matchString); if (bugMarkerIndex < 0) { throw new IllegalArgumentException("Line must contain bug marker prefix"); } List<String> result = new ArrayList<String>(); String restOfLine = line.substring(bugMarkerIndex + matchString.length()).trim(); result.add(restOfLine); line = reader.readLine().trim(); while (line.startsWith("//")) { restOfLine = line.substring(2).trim(); result.add(restOfLine); line = reader.readLine().trim(); } return result; } private static class SimpleStringContains implements Predicate<String> { private final String pattern; SimpleStringContains(String pattern) { this.pattern = pattern; } @Override public boolean apply(String input) { return input.contains(pattern); } @Override public String toString() { return pattern; } } private static class ClearableDiagnosticCollector<S> implements DiagnosticListener<S> { private final List<Diagnostic<? extends S>> diagnostics = new ArrayList<>(); @Override public void report(Diagnostic<? extends S> diagnostic) { diagnostics.add(diagnostic); } public List<Diagnostic<? extends S>> getDiagnostics() { return ImmutableList.copyOf(diagnostics); } public void clear() { diagnostics.clear(); } } }