Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c2ac4e9f authored by David Saff's avatar David Saff Committed by Android (Google) Code Review
Browse files

Merge "Add linter to recommend Kosmos.runTest" into main

parents 0cbbf872 5eb87555
Loading
Loading
Loading
Loading
+81 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.
 */

package com.android.internal.systemui.lint

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.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 com.android.tools.lint.detector.api.getReceiver
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getContainingUFile

/**
 * Detects test function naming violations regarding use of the backtick-wrapped space-allowed
 * feature of Kotlin functions.
 */
class RunTestShouldUseKosmosDetector : Detector(), SourceCodeScanner {
    override fun getApplicableMethodNames() = listOf("runTest")

    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        if (method.getReceiver()?.qualifiedName == "kotlinx.coroutines.test.TestScope") {

            val imports =
                node.getContainingUFile()?.imports.orEmpty().mapNotNull {
                    it.importReference?.asSourceString()
                }
            if (imports.any { it == "com.android.systemui.kosmos.Kosmos" }) {
                context.report(
                    issue = ISSUE,
                    scope = node,
                    location = context.getLocation(node.methodIdentifier),
                    message =
                        "Prefer Kosmos.runTest to TestScope.runTest in sysui tests that use Kosmos.  go/kosmos-runtest",
                )
                super.visitMethodCall(context, node, method)
            }
        }
    }

    companion object {
        @JvmStatic
        val ISSUE =
            Issue.create(
                id = "RunTestShouldUseKosmos",
                briefDescription = "When you can, use Kosmos.runTest instead of TestScope.runTest.",
                explanation =
                    """
                    Kosmos.runTest helps to ensure that the test uses the same coroutine
                    dispatchers that are used in Kosmos fixtures, preventing subtle bugs.
                    See go/kosmos-runtest
                """,
                category = Category.TESTING,
                priority = 8,
                severity = Severity.WARNING,
                implementation =
                    Implementation(
                        RunTestShouldUseKosmosDetector::class.java,
                        Scope.JAVA_FILE_SCOPE,
                    ),
            )
    }
}
+214 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.
 *
 */

package com.android.internal.systemui.lint

import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintResult
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test

class RunTestShouldUseKosmosDetectorTest : SystemUILintDetectorTest() {
    override fun getDetector(): Detector = RunTestShouldUseKosmosDetector()

    override fun getIssues(): List<Issue> = listOf(RunTestShouldUseKosmosDetector.ISSUE)

    @Test
    fun wronglyTriesToUseScopeRunTest() {
        val runOnSource =
            runOnSource(
                """
                      package test.pkg.name

                      import com.android.systemui.kosmos.Kosmos
                      import kotlinx.coroutines.test.runTest
                      import kotlinx.coroutines.test.TestScope
                      import org.junit.Test

                      class MyTest {
                          val scope: TestScope
                          val kosmos: Kosmos

                          @Test
                          fun badTest() = scope.runTest {
                              // test code
                          }
                      }
                """
            )

        runOnSource
            .expectWarningCount(1)
            .expect(
                """
                src/test/pkg/name/MyTest.kt:13: Warning: Prefer Kosmos.runTest to TestScope.runTest in sysui tests that use Kosmos.  go/kosmos-runtest [RunTestShouldUseKosmos]
                    fun badTest() = scope.runTest {
                                          ~~~~~~~
                0 errors, 1 warnings
                """
            )
    }

    @Test
    fun testScopeRunTestIsOKifKosmosNotUsed() {
        runOnSource(
                """
                      package test.pkg.name

                      import kotlinx.coroutines.test.runTest
                      import kotlinx.coroutines.test.TestScope
                      import org.junit.Test

                      class MyTest {
                          val scope: TestScope

                          @Test
                          fun okTest() = scope.runTest {
                              // test code
                          }
                      }
                """
            )
            .expectWarningCount(0)
    }

    @Test
    fun otherTestScopeMethodsAreOK() {
        runOnSource(
                """
                       package test.pkg.name

                       import com.android.systemui.kosmos.Kosmos
                       import com.android.systemui.kosmos.runTest
                       import kotlinx.coroutines.test.TestScope
                       import org.junit.Test

                       class MyTest {
                           val scope: TestScope
                           val kosmos: Kosmos

                           @Test
                           fun okTest() = kosmos.runTest {
                               scope.cancel()
                               // test code
                           }
                       }
                   """
            )
            .expectWarningCount(0)
    }

    @Test
    fun correctlyUsesKosmosRunTest() {
        runOnSource(
                """
                       package test.pkg.name

                       import com.android.systemui.kosmos.Kosmos
                       import com.android.systemui.kosmos.runTest
                       import kotlinx.coroutines.test.TestScope
                       import org.junit.Test

                       class MyTest {
                           val scope: TestScope
                           val kosmos: Kosmos

                           @Test
                           fun okTest() = kosmos.runTest {
                               // test code
                           }
                       }
                   """
            )
            .expectWarningCount(0)
    }

    private fun runOnSource(source: String): TestLintResult {
        return lint()
            .files(
                TestFiles.kotlin(source).indented(),
                testAnnotationStub,
                runTestStub,
                testScopeStub,
                kosmosStub,
                kosmosRunTestStub,
            )
            .issues(RunTestShouldUseKosmosDetector.ISSUE)
            .run()
    }

    companion object {
        private val testAnnotationStub: TestFile =
            kotlin(
                """
                package org.junit

                import java.lang.annotation.ElementType
                import java.lang.annotation.Retention
                import java.lang.annotation.RetentionPolicy
                import java.lang.annotation.Target

                @Retention(RetentionPolicy.RUNTIME)
                @Target({ElementType.METHOD})
                annotation class Test
            """
            )

        private val runTestStub: TestFile =
            kotlin(
                """
                package kotlinx.coroutines.test

                fun TestScope.runTest(
                    timeout: Duration = DEFAULT_TIMEOUT.getOrThrow(),
                    testBody: suspend TestScope.() -> Unit
                ): Unit = {}
            """
            )

        private val testScopeStub: TestFile =
            kotlin(
                """
                package kotlinx.coroutines.test

                class TestScope

                public fun TestScope.cancel() {}
            """
            )

        private val kosmosStub: TestFile =
            kotlin(
                """
                package com.android.systemui.kosmos

                class Kosmos
            """
            )

        private val kosmosRunTestStub: TestFile =
            kotlin(
                """
                package com.android.systemui.kosmos

                fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit)
                """
            )
    }
}