[kotlin junit5] performance improvement IDEA-370050 IDEA-372531 IJ-CR-162315

GitOrigin-RevId: 4889a63228b0db3cfe35052b8a872d3ceeaa6497
This commit is contained in:
Aleksey Dobrynin
2025-05-27 16:43:32 +02:00
committed by intellij-monorepo-bot
parent a7b9a1e647
commit e525c8afd3
5 changed files with 247 additions and 38 deletions

View File

@@ -74,8 +74,8 @@ public abstract class MetaAnnotationUtil {
}
private static @NotNull @Unmodifiable Collection<PsiClass> findAnnotationClasses(@NotNull Module module,
@NotNull String qualifiedName,
boolean includeTests) {
@NotNull String qualifiedName,
boolean includeTests) {
PsiClass annotationClass = JavaPsiFacade.getInstance(module.getProject())
.findClass(qualifiedName, GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module));
if (annotationClass == null || !annotationClass.isAnnotationType()) {
@@ -198,7 +198,7 @@ public abstract class MetaAnnotationUtil {
}
private static @NotNull @Unmodifiable Collection<PsiClass> findAnnotationTypesWithChildren(Collection<PsiClass> annotationClasses,
GlobalSearchScope scope) {
GlobalSearchScope scope) {
if (scope == GlobalSearchScope.EMPTY_SCOPE) return annotationClasses;
Set<PsiClass> classes = CollectionFactory.createCustomHashingStrategySet(HASHING_STRATEGY);
@@ -335,23 +335,23 @@ public abstract class MetaAnnotationUtil {
stack.push(aClass);
while (!stack.isEmpty()) {
PsiClass currentClass = stack.pop();
PsiAnnotation directAnnotation = AnnotationUtil.findAnnotation(currentClass, true, annotation);
if (directAnnotation != null) {
return directAnnotation;
}
PsiClass currentClass = stack.pop();
List<PsiClass> resolvedAnnotations = getResolvedClassesInAnnotationsList(currentClass);
for (PsiClass resolvedAnnotation : resolvedAnnotations) {
if (visited.add(resolvedAnnotation)) {
stack.push(resolvedAnnotation);
}
PsiAnnotation directAnnotation = AnnotationUtil.findAnnotation(currentClass, true, annotation);
if (directAnnotation != null) {
return directAnnotation;
}
List<PsiClass> resolvedAnnotations = getResolvedClassesInAnnotationsList(currentClass);
for (PsiClass resolvedAnnotation : resolvedAnnotations) {
if (visited.add(resolvedAnnotation)) {
stack.push(resolvedAnnotation);
}
}
}
return null;
}
}
public static @NotNull Stream<PsiAnnotation> findMetaAnnotations(@NotNull PsiModifierListOwner listOwner,
@NotNull Collection<String> annotations) {

View File

@@ -1,10 +1,14 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.testIntegration.framework
import com.intellij.codeInsight.MetaAnnotationUtil
import com.intellij.java.library.JavaLibraryUtil
import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.Processor
import com.intellij.util.ThreeState
import com.intellij.util.ThreeState.*
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.idea.base.util.module
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
import org.jetbrains.kotlin.idea.stubindex.KotlinTopLevelTypeAliasFqNameIndex
@@ -80,28 +84,51 @@ abstract class AbstractKotlinPsiBasedTestFramework : KotlinPsiBasedTestFramework
|| isAnnotated(declaration, disabledTestAnnotation)) && isTestMethod(declaration)
}
protected fun checkNameMatch(file: KtFile, fqNames: Set<String>, shortName: String): Boolean {
if (shortName in fqNames || "${file.packageFqName}.$shortName" in fqNames) return true
protected fun checkNameMatch(file: KtFile, fqNames: Set<String>, shortName: String): Boolean =
fqNames.intersect(findFqNameCandidates(file, shortName)).isNotEmpty()
private fun findFqNameCandidates(file: KtFile, shortName: String): Set<String> {
val outer = shortName.substringBefore('.')
val inner = shortName.substringAfter('.', "")
fun append(innerPart: String?) = if (innerPart.isNullOrEmpty()) "" else ".$innerPart"
// direct import
for (importDirective in file.importDirectives) {
if (!importDirective.isValidImport) {
continue
}
if (!importDirective.isValidImport) continue
val importedFqName = importDirective.importedFqName?.asString() ?: continue
if (!importDirective.isAllUnder) {
if (importDirective.aliasName == shortName && importedFqName in fqNames) {
return true
} else if (importedFqName in fqNames && importedFqName.endsWith(".$shortName")) {
return true
}
} else if ("$importedFqName.$shortName" in fqNames) {
return true
when {
importDirective.aliasName == shortName -> return setOf(importedFqName)
importedFqName.endsWith(".$shortName") -> return setOf(importedFqName)
importDirective.aliasName == outer -> return setOf(importedFqName + append(inner))
importedFqName.endsWith(".$outer") -> return setOf(importedFqName + append(inner))
}
}
return false
val candidates = mutableSetOf<String>()
// in the current file
PsiTreeUtil.findChildrenOfType(file, KtClassOrObject::class.java)
.firstOrNull { it.name == outer }
?.fqName?.asString()
?.let { candidates.add(it + append(inner)) }
// same package
if (file.packageFqName.isRoot) {
candidates.add(shortName)
} else {
candidates.add("${file.packageFqName}.$shortName")
}
// demand imports
for (importDirective in file.importDirectives) {
if (!importDirective.isValidImport) continue
if (!importDirective.isAllUnder) continue
val importedFqName = importDirective.importedFqName?.asString() ?: continue
candidates.add("$importedFqName.$shortName")
}
return candidates
}
protected fun isAnnotated(element: KtAnnotated, fqName: String): Boolean {
@@ -128,6 +155,24 @@ abstract class AbstractKotlinPsiBasedTestFramework : KotlinPsiBasedTestFramework
}
}
for (annotationEntry in annotationEntries) {
val shortName = annotationEntry.shortName ?: continue
val fqName = annotationEntry.typeReference?.text ?: shortName.asString()
val clazz = PsiTreeUtil.findChildrenOfType(file, KtClassOrObject::class.java)
.firstOrNull { it.fqName?.asString()?.endsWith(".${fqName}") == true || it.fqName?.asString() == fqName }
?.toLightClass()
if (clazz != null && MetaAnnotationUtil.isMetaAnnotated(clazz, fqNames)) return annotationEntry
for (fq in findFqNameCandidates(file, fqName)) {
val classes = JavaFullClassNameIndex.getInstance().getClasses(fq, element.project, element.resolveScope)
for (cls in classes) {
if (MetaAnnotationUtil.isMetaAnnotated(cls, fqNames)) {
return annotationEntry
}
}
}
}
return null
}

View File

@@ -0,0 +1,10 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.codeInsight.codeVision
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
import org.jetbrains.kotlin.idea.run.KotlinJUnitLightTest
class K2KotlinJUnitLightTest : KotlinJUnitLightTest() {
override val pluginMode: KotlinPluginMode
get() = KotlinPluginMode.K2
}

View File

@@ -0,0 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.run
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
class K1KotlinJUnitLightTest : KotlinJUnitLightTest() {
override val pluginMode: KotlinPluginMode
get() = KotlinPluginMode.K1
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.run
import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection
@@ -29,14 +29,18 @@ import com.intellij.psi.PsiClassOwner
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.testFramework.TestActionEvent
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import com.intellij.util.ThreeState
import org.jetbrains.kotlin.asJava.toLightMethods
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
import org.jetbrains.kotlin.idea.junit.JunitKotlinTestFrameworkProvider
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCaseBase
import org.jetbrains.kotlin.psi.KtFunction
import org.junit.Assert
import org.junit.internal.runners.JUnit38ClassRunner
import org.junit.runner.RunWith
class KotlinJUnitLightTest : LightJavaCodeInsightFixtureTestCase() {
@RunWith(JUnit38ClassRunner::class)
abstract class KotlinJUnitLightTest : KotlinLightCodeInsightFixtureTestCaseBase() {
private val tempSettings: MutableSet<RunnerAndConfigurationSettings> = HashSet()
@Throws(Exception::class)
@@ -52,7 +56,7 @@ class KotlinJUnitLightTest : LightJavaCodeInsightFixtureTestCase() {
super.tearDown()
}
}
override fun setUp() {
super.setUp()
myFixture.addClass("package junit.framework; public class TestCase {}")
@@ -60,6 +64,7 @@ class KotlinJUnitLightTest : LightJavaCodeInsightFixtureTestCase() {
myFixture.addClass("package org.junit.platform.commons.annotation; public @interface Testable{}")
myFixture.addClass("package org.junit.jupiter.api; import org.junit.platform.commons.annotation.Testable; @Testable public @interface Test {}")
myFixture.addClass("package org.junit.jupiter.api; public @interface Nested {}")
myFixture.addClass("package org.junit.jupiter.api; public @interface BeforeEach {}")
}
fun testAvailableInsideAnonymous() {
@@ -179,7 +184,7 @@ class KotlinJUnitLightTest : LightJavaCodeInsightFixtureTestCase() {
val settings = RunnerAndConfigurationSettingsImpl(manager as RunManagerImpl, test)
manager.addConfiguration(settings)
tempSettings.add(settings)
val element = file.findElementAt(myFixture.caretOffset)!!
val location = PsiLocation(element)
@@ -208,7 +213,7 @@ class KotlinJUnitLightTest : LightJavaCodeInsightFixtureTestCase() {
tempSettings.add(settings)
}
}
private fun doTestClassWithMain(setupExisting: Runnable?) {
myFixture.configureByText(
"ATest.kt", """import org.junit.Test
@@ -245,7 +250,7 @@ fun main(args: Array<String>) {}
fun testStackTraceParserAcceptsJavaStacktrace() {
myFixture.configureByText("tests.kt",
"""class tests : junit.framework.TestCase() {
"""class tests : junit.framework.TestCase() {
fun testMe() {
doTest {
assertTrue(false)
@@ -289,6 +294,8 @@ fun main(args: Array<String>) {}
}
fun `test unused beforeAll`() {
if (pluginMode == KotlinPluginMode.K2) return
myFixture.addClass("package org.junit.jupiter.api; public @interface BeforeAll{}")
myFixture.addClass("package kotlin.jvm; public @interface JvmStatic{}")
myFixture.configureByText("Demo.kt", """
@@ -306,4 +313,142 @@ class DemoTest {
val ktFunction = PsiTreeUtil.getParentOfType(myFixture.elementAtCaret, KtFunction::class.java, false)
assertTrue(UnusedDeclarationInspection().isEntryPoint(ktFunction!!.toLightMethods()[0]))
}
fun testMethodWithTestAnnotation() {
val file = myFixture.configureByText(
"MyTest.kt", """
class MyTest {
@org.junit.jupiter.api.Test
@Retention(AnnotationRetention.RUNTIME)
annotation class TestAnnotation
@TestAnnotation
fun `dispatch <caret>thread`() {}
}
""".trimIndent()
)
val gutters = myFixture.findGuttersAtCaret()
Assert.assertTrue("Test method with meta-annotation should have a gutter icon", gutters.isNotEmpty())
val element = file.findElementAt(myFixture.caretOffset)!!
val location = PsiLocation(element)
val context = ConfigurationContext.createEmptyContextForLocation(location)
val contexts = context.configurationsFromContext
Assert.assertEquals(1, contexts?.size ?: 0)
val fromContext = contexts?.get(0)
Assert.assertTrue(fromContext?.configuration is JUnitConfiguration)
val configuration = fromContext?.configuration as JUnitConfiguration
Assert.assertEquals("MyTest.dispatch thread", configuration.name)
val testObject = configuration.persistentData.TEST_OBJECT
Assert.assertEquals(
"Method should be suggested to run, but $testObject was used instead",
JUnitConfiguration.TEST_METHOD,
testObject
)
Assert.assertNotNull(JunitKotlinTestFrameworkProvider.getInstance().getJavaTestEntity(element, checkMethod = true))
}
fun testMethodWithTestAnnotationAndBeforeEach() {
myFixture.addClass("""
package c;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@org.junit.jupiter.api.Test
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
""")
val file = myFixture.configureByText(
"MyTest.kt", """
import c.*
import org.junit.jupiter.api.BeforeEach
class MyTest {
@BeforeEach
fun cleanEDTQueue() {}
@TestAnnotation
fun `dispatch <caret>thread`() {}
}
""".trimIndent()
)
val gutters = myFixture.findGuttersAtCaret()
Assert.assertTrue("Test method with meta-annotation should have a gutter icon", gutters.isNotEmpty())
val element = file.findElementAt(myFixture.caretOffset)!!
val location = PsiLocation(element)
val context = ConfigurationContext.createEmptyContextForLocation(location)
val contexts = context.configurationsFromContext
Assert.assertEquals(1, contexts?.size ?: 0)
val fromContext = contexts?.get(0)
Assert.assertTrue(fromContext?.configuration is JUnitConfiguration)
val configuration = fromContext?.configuration as JUnitConfiguration
Assert.assertEquals("MyTest.dispatch thread", configuration.name)
val testObject = configuration.persistentData.TEST_OBJECT
Assert.assertEquals(
"Method should be suggested to run, but $testObject was used instead",
JUnitConfiguration.TEST_METHOD,
testObject
)
Assert.assertNotNull(JunitKotlinTestFrameworkProvider.getInstance().getJavaTestEntity(element, checkMethod = true))
}
fun testMethodWithInnerTestAnnotationAndBeforeEach() {
val file = myFixture.configureByText(
"MyTest.kt", """
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
class MyTest {
@BeforeEach
fun cleanEDTQueue() {}
@Inner.Additional.TestAnnotation
fun `dispatch <caret>thread`() {}
class Inner {
class Additional {
@Test
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME)
annotation class TestAnnotation
}
}
}
""".trimIndent()
)
val gutters = myFixture.findGuttersAtCaret()
Assert.assertTrue("Test method with meta-annotation should have a gutter icon", gutters.isNotEmpty())
val element = file.findElementAt(myFixture.caretOffset)!!
val location = PsiLocation(element)
val context = ConfigurationContext.createEmptyContextForLocation(location)
val contexts = context.configurationsFromContext
Assert.assertEquals(1, contexts?.size ?: 0)
val fromContext = contexts?.get(0)
Assert.assertTrue(fromContext?.configuration is JUnitConfiguration)
val configuration = fromContext?.configuration as JUnitConfiguration
Assert.assertEquals("MyTest.dispatch thread", configuration.name)
val testObject = configuration.persistentData.TEST_OBJECT
Assert.assertEquals(
"Method should be suggested to run, but $testObject was used instead",
JUnitConfiguration.TEST_METHOD,
testObject
)
Assert.assertNotNull(JunitKotlinTestFrameworkProvider.getInstance().getJavaTestEntity(element, checkMethod = true))
}
}