/* * Copyright 2013 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.bugpatterns; import static com.google.errorprone.BugPattern.Category.JUNIT; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.matchers.JUnitMatchers.containsTestMethod; import static com.google.errorprone.matchers.JUnitMatchers.hasJUnitAnnotation; import static com.google.errorprone.matchers.JUnitMatchers.isJunit3TestCase; import static com.google.errorprone.matchers.Matchers.allOf; import static com.google.errorprone.matchers.Matchers.enclosingClass; import static com.google.errorprone.matchers.Matchers.hasModifier; import static com.google.errorprone.matchers.Matchers.methodHasParameters; import static com.google.errorprone.matchers.Matchers.methodReturns; import static com.google.errorprone.matchers.Matchers.not; import static com.google.errorprone.suppliers.Suppliers.VOID_TYPE; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static javax.lang.model.element.Modifier.ABSTRACT; import static javax.lang.model.element.Modifier.PUBLIC; import static javax.lang.model.element.Modifier.STATIC; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.fixes.SuggestedFixes; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.JUnitMatchers.JUnit4TestClassMatcher; import com.google.errorprone.matchers.Matcher; import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.util.Options; import java.util.List; import javax.lang.model.element.Modifier; /** @author eaftan@google.com (Eddie Aftandilian) */ @BugPattern( name = "JUnit4TestNotRun", summary = "This looks like a test method but is not run; please add @Test or @Ignore, or, if this is a " + "helper method, reduce its visibility.", explanation = "Unlike in JUnit 3, JUnit 4 tests will not be run unless annotated with @Test. " + "The test method that triggered this error looks like it was meant to be a test, but " + "was not so annotated, so it will not be run. If you intend for this test method not " + "to run, please add both an @Test and an @Ignore annotation to make it clear that you " + "are purposely disabling it. If this is a helper method and not a test, consider " + "reducing its visibility to non-public, if possible.", category = JUNIT, severity = ERROR ) public class JUnit4TestNotRun extends BugChecker implements MethodTreeMatcher { private static final String TEST_CLASS = "org.junit.Test"; private static final String IGNORE_CLASS = "org.junit.Ignore"; private static final String TEST_ANNOTATION = "@Test "; private static final String IGNORE_ANNOTATION = "@Ignore "; private static final JUnit4TestClassMatcher isJUnit4TestClass = new JUnit4TestClassMatcher(); /** * Looks for methods that are structured like tests but aren't run. Matches public, void, no-param * methods in JUnit4 test classes that aren't annotated with any JUnit4 annotations * */ private static final Matcher<MethodTree> POSSIBLE_TEST_METHOD = allOf( hasModifier(PUBLIC), methodReturns(VOID_TYPE), methodHasParameters(), not(hasJUnitAnnotation), enclosingClass(isJUnit4TestClass), not(enclosingClass(hasModifier(ABSTRACT)))); protected boolean useExpandedHeuristic(VisitorState state) { return Options.instance(state.context).getBoolean("expandedTestNotRunHeuristic"); } /** * Matches if: * * <ol> * <li>The method is public, void, and has no parameters, * <li>The method is not annotated with {@code @Test}, {@code @Before}, {@code @After}, * {@code @BeforeClass}, or {@code @AfterClass}, * <li>The enclosing class has an {@code @RunWith} annotation and does not extend TestCase. This * marks that the test is intended to run with JUnit 4, and * <li> Either: * <ol type="a"> * <li>The method body contains a method call with a name that contains "assert", * "verify", "check", "fail", or "expect". * </ol> * * </ol> */ @Override public Description matchMethod(MethodTree methodTree, VisitorState state) { if (!POSSIBLE_TEST_METHOD.matches(methodTree, state)) { return NO_MATCH; } // Method appears to be a JUnit 3 test case (name prefixed with "test"), probably a test. if (isJunit3TestCase.matches(methodTree, state)) { return describeFixes(methodTree, state); } // TODO(b/34062183): Remove check for flag once cleanup complete. if (useExpandedHeuristic(state)) { // Method is annotated, probably not a test. List<? extends AnnotationTree> annotations = methodTree.getModifiers().getAnnotations(); if (annotations != null && !annotations.isEmpty()) { return NO_MATCH; } // Method non-static and contains call(s) to testing method, probably a test, // unless it is called elsewhere in the class, in which case it is a helper method. if (not(hasModifier(STATIC)).matches(methodTree, state) && containsTestMethod(methodTree) && !calledElsewhere(methodTree, state)) { return describeFixes(methodTree, state); } } return NO_MATCH; } /** Whether the given method is called elsewhere in the enclosing class. */ private static boolean calledElsewhere(MethodTree methodTree, VisitorState state) { MethodSymbol methodSymbol = getSymbol(methodTree); if (methodSymbol == null) { return false; } return state .findEnclosing(ClassTree.class) .accept( new TreeScanner<Boolean, Void>() { @Override public Boolean visitMethodInvocation(MethodInvocationTree callTree, Void unused) { if (methodSymbol.equals(getSymbol(callTree.getMethodSelect()))) { return true; } return super.visitMethodInvocation(callTree, unused); } @Override public Boolean reduce(Boolean r1, Boolean r2) { r1 = (r1 == null) ? false : r1; r2 = (r2 == null) ? false : r2; return r1 || r2; } }, null); } /** * Returns a finding for the given method tree containing fixes: * * <ol> * <li>Add @Test, remove static modifier if present. * <li>Add @Test and @Ignore, remove static modifier if present. * <li>Change visibility to private (for local helper methods). * </ol> */ private Description describeFixes(MethodTree methodTree, VisitorState state) { SuggestedFix removeStatic = SuggestedFixes.removeModifiers(methodTree, state, Modifier.STATIC); SuggestedFix testFix = SuggestedFix.builder() .merge(removeStatic) .addImport(TEST_CLASS) .prefixWith(methodTree, TEST_ANNOTATION) .build(); SuggestedFix ignoreFix = SuggestedFix.builder() .merge(testFix) .addImport(IGNORE_CLASS) .prefixWith(methodTree, IGNORE_ANNOTATION) .build(); SuggestedFix visibilityFix = SuggestedFix.builder() .merge(SuggestedFixes.removeModifiers(methodTree, state, Modifier.PUBLIC)) .merge(SuggestedFixes.addModifiers(methodTree, state, Modifier.PRIVATE)) .build(); // Suggest @Ignore first if test method is named like a purposely disabled test. String methodName = methodTree.getName().toString(); if (methodName.startsWith("disabl") || methodName.startsWith("ignor")) { return buildDescription(methodTree) .addFix(ignoreFix) .addFix(testFix) .addFix(visibilityFix) .build(); } return buildDescription(methodTree) .addFix(testFix) .addFix(ignoreFix) .addFix(visibilityFix) .build(); } }