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

Commit 5eb87555 authored by David Saff's avatar David Saff
Browse files

Add linter to recommend Kosmos.runTest

Bug: 342622417
Test: Run locally
Flag: TEST_ONLY

Change-Id: I6556766fa2e06ca69b757695d8799e944f585a50
parent ce27eff9
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)
                """
            )
    }
}