blob: d06866111c2a5f6d7f9ce0892df3a3519e5c4b58 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* 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.
*/
@file:Suppress("UnstableApiUsage")
package androidx.build.lint
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Incident
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import java.util.Collections
import java.util.EnumSet
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UClassLiteralExpression
import org.jetbrains.uast.UElement
/**
* Lint check to enforce that every device side test (tests in the androidTest dir) has correct
* size annotations, and a test runner that supports timeouts, so that we can correctly split up
* test runs and enforce timeouts.
*
* Also enforces that test runners that do not support timeouts and host side tests do not
* include test size annotations, as these are not used and can be misleading.
*/
class TestSizeAnnotationEnforcer : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>>? =
Collections.singletonList(UClass::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler {
return TestSizeAnnotationHandler(context)
}
class TestSizeAnnotationHandler(private val context: JavaContext) : UElementHandler() {
override fun visitClass(node: UClass) {
// Enforce no size annotations for host side tests
val testPath = context.file.absolutePath
if (ANDROID_TEST_DIRS.none { testPath.contains(it) }) {
enforceHasNoSizeAnnotations(node)
return
}
// Ignore any non-test classes and classes without an explicit runner specified.
// (missing a @RunWith() annotation)
val runWithAnnotation = node.findAnnotation(RUN_WITH) ?: return
val testRunner = runWithAnnotation.findAttributeValue(RUN_WITH_VALUE)
val testRunnerClassName = (testRunner as? UClassLiteralExpression)
?.type?.canonicalText ?: return
if (testRunnerClassName !in ALLOWED_TEST_RUNNERS) {
val incident = Incident(context)
.issue(UNSUPPORTED_TEST_RUNNER)
.location(context.getNameLocation(testRunner))
.message("Unsupported test runner." +
" Supported runners are: $ALLOWED_TEST_RUNNERS")
.scope(testRunner)
context.report(incident)
return
}
if (testRunnerClassName in TIMEOUT_ENFORCED_TEST_RUNNERS) {
enforceHasSizeAnnotations(node)
}
}
/**
* Enforces that either [node] has a valid size annotation and/or every method has a valid
* size annotation.
*/
private fun enforceHasSizeAnnotations(node: UClass) {
node.methods.filter {
it.hasAnnotation(TEST_ANNOTATION)
}.forEach { method ->
val combinedAnnotations = node.uAnnotations + method.uAnnotations
// Report an issue if neither the test method nor the surrounding class have a
// valid test size annotation
if (combinedAnnotations.none { it.qualifiedName in TEST_SIZE_ANNOTATIONS }) {
val incident = Incident(context)
.issue(MISSING_TEST_SIZE_ANNOTATION)
.location(context.getNameLocation(method))
.message("Missing test size annotation")
.scope(method)
context.report(incident)
}
}
}
/**
* Enforces that [node] has no size annotations either on the class, or any test methods.
*/
private fun enforceHasNoSizeAnnotations(node: UClass) {
// Report an issue if the class has a size annotation
node.uAnnotations
.find { it.qualifiedName in TEST_SIZE_ANNOTATIONS }
?.let { annotation ->
val incident = Incident(context)
.issue(UNEXPECTED_TEST_SIZE_ANNOTATION)
.location(context.getNameLocation(annotation))
.message("Unexpected test size annotation")
.scope(annotation)
context.report(incident)
}
node.methods.filter {
it.hasAnnotation(TEST_ANNOTATION)
}.forEach { method ->
// Report an issue if the method has a size annotation
method.uAnnotations
.find { it.qualifiedName in TEST_SIZE_ANNOTATIONS }
?.let { annotation ->
val incident = Incident(context)
.issue(UNEXPECTED_TEST_SIZE_ANNOTATION)
.location(context.getNameLocation(annotation))
.message("Unexpected test size annotation")
.scope(annotation)
context.report(incident)
}
}
}
}
companion object {
/**
* List of test runners that support timeouts. This is a subset of [ALLOWED_TEST_RUNNERS].
*/
private val TIMEOUT_ENFORCED_TEST_RUNNERS = listOf(
"androidx.test.ext.junit.runners.AndroidJUnit4"
)
/**
* Only AndroidJUnit4 enforces timeouts, so it should be used over JUnit4 / other such
* runners. Parameterized does not enforce timeouts, but there is no equivalent that
* does, so it is still fine to use.
*/
private val ALLOWED_TEST_RUNNERS = listOf(
"androidx.test.ext.junit.runners.AndroidJUnit4",
"org.junit.runners.Parameterized"
)
private const val RUN_WITH = "org.junit.runner.RunWith"
private const val RUN_WITH_VALUE = "value"
/**
* TODO: b/170214947
* Directories that contain device test source. Unfortunately we don't currently have a
* better way of figuring out what test we are analyzing, as [Scope.TEST_SOURCES]
* includes both host and device side tests.
*/
private val ANDROID_TEST_DIRS = listOf(
"androidTest",
"androidInstrumentedTest",
"androidDeviceTest",
"androidDeviceTestDebug",
"androidDeviceTestRelease"
)
private const val TEST_ANNOTATION = "org.junit.Test"
private val TEST_SIZE_ANNOTATIONS = listOf(
"androidx.test.filters.SmallTest",
"androidx.test.filters.MediumTest",
"androidx.test.filters.LargeTest"
)
val UNSUPPORTED_TEST_RUNNER = Issue.create(
"UnsupportedTestRunner",
"Unsupported test runner",
"Only AndroidJUnit4 supports setting a timeout for tests using the test size " +
"annotation, so this test runner should be used instead. There is no " +
"equivalent parameterized runner that also sets timeouts, so Parameterized is " +
"also allowed.",
Category.CORRECTNESS, 5, Severity.ERROR,
Implementation(
TestSizeAnnotationEnforcer::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
val MISSING_TEST_SIZE_ANNOTATION = Issue.create(
"MissingTestSizeAnnotation",
"Missing test size annotation",
"All tests require a valid test size annotation, on the class or per method." +
"\nYou must use at least one of: @SmallTest, @MediumTest or @LargeTest." +
"\nUse @SmallTest for tests that run in under 200ms, @MediumTest for tests " +
"that run in under 1000ms, and @LargeTest for tests that run for more " +
"than a second.",
Category.CORRECTNESS, 5, Severity.ERROR,
Implementation(
TestSizeAnnotationEnforcer::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
val UNEXPECTED_TEST_SIZE_ANNOTATION = Issue.create(
"UnexpectedTestSizeAnnotation",
"Unexpected test size annotation",
"Host side tests and device tests with runners that do not support timeouts " +
"should not have any test size annotations. Host side tests all run together," +
" and device tests with runners that do not support timeouts will be placed in" +
" the 'large' test bucket, since we cannot enforce that they will be fast enough" +
" to run in the 'small' / 'medium' buckets.",
Category.CORRECTNESS, 5, Severity.ERROR,
Implementation(
TestSizeAnnotationEnforcer::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
}
}