Unit Test Tooling: Support Go to Test/Create Test actions for top-level declarations
#KT-10133 Fixed
This commit is contained in:
@@ -20,10 +20,7 @@ import com.intellij.extapi.psi.PsiFileBase;
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.intellij.lang.FileASTNode;
|
||||
import com.intellij.openapi.fileTypes.FileType;
|
||||
import com.intellij.psi.FileViewProvider;
|
||||
import com.intellij.psi.PsiClass;
|
||||
import com.intellij.psi.PsiClassOwner;
|
||||
import com.intellij.psi.PsiElementVisitor;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.stubs.StubElement;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import kotlin.ArraysKt;
|
||||
@@ -42,7 +39,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class KtFile extends PsiFileBase implements KtDeclarationContainer, KtAnnotated, KtElement, PsiClassOwner {
|
||||
public class KtFile extends PsiFileBase implements KtDeclarationContainer, KtAnnotated, KtElement, PsiClassOwner, PsiNamedElement {
|
||||
|
||||
private final boolean isCompiled;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jetbrains.kotlin.asJava
|
||||
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import org.jetbrains.kotlin.load.java.JvmAbi
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
@@ -29,6 +30,12 @@ import java.util.*
|
||||
|
||||
public fun KtClassOrObject.toLightClass(): KtLightClass? = LightClassUtil.getPsiClass(this) as KtLightClass?
|
||||
|
||||
public fun KtFile.findFacadeClass(): KtLightClass? {
|
||||
return LightClassGenerationSupport.getInstance(project)
|
||||
.getFacadeClassesInPackage(packageFqName, this.useScope as? GlobalSearchScope ?: GlobalSearchScope.projectScope(project))
|
||||
.firstOrNull { it is KtLightClassForFacade && this in it.files } as? KtLightClass
|
||||
}
|
||||
|
||||
public fun KtDeclaration.toLightElements(): List<PsiNamedElement> =
|
||||
when (this) {
|
||||
is KtClassOrObject -> LightClassUtil.getPsiClass(this).singletonOrEmptyList()
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.intellij.psi.search.GlobalSearchScopesCore
|
||||
import com.intellij.testIntegration.createTest.CreateTestAction
|
||||
import com.intellij.testIntegration.createTest.TestGenerators
|
||||
import org.jetbrains.kotlin.asJava.KtLightClass
|
||||
import org.jetbrains.kotlin.asJava.findFacadeClass
|
||||
import org.jetbrains.kotlin.asJava.toLightClass
|
||||
import org.jetbrains.kotlin.idea.actions.JavaToKotlinAction
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
||||
@@ -44,30 +45,50 @@ import org.jetbrains.kotlin.idea.util.application.executeCommand
|
||||
import org.jetbrains.kotlin.idea.util.runWithAlternativeResolveEnabled
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.idea.util.runWhenSmart
|
||||
import org.jetbrains.kotlin.psi.KtClass
|
||||
import org.jetbrains.kotlin.psi.KtClassOrObject
|
||||
import org.jetbrains.kotlin.psi.KtEnumEntry
|
||||
import org.jetbrains.kotlin.psi.KtNamedFunction
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.endOffset
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.singletonList
|
||||
import java.util.*
|
||||
|
||||
class KotlinCreateTestIntention : SelfTargetingRangeIntention<KtClassOrObject>(KtClassOrObject::class.java, "Create test") {
|
||||
override fun applicabilityRange(element: KtClassOrObject): TextRange? {
|
||||
if (element.isLocal()) return null
|
||||
if (element is KtEnumEntry) return null
|
||||
if (element is KtClass && (element.isAnnotation() || element.isInterface())) return null
|
||||
class KotlinCreateTestIntention : SelfTargetingRangeIntention<KtNamedDeclaration>(KtNamedDeclaration::class.java, "Create test") {
|
||||
override fun applicabilityRange(element: KtNamedDeclaration): TextRange? {
|
||||
if (ModuleUtilCore.findModuleForPsiElement(element) == null) return null
|
||||
if (element.resolveToDescriptorIfAny() == null) return null
|
||||
if (element.nameIdentifier == null) return null
|
||||
|
||||
return TextRange(
|
||||
element.startOffset,
|
||||
element.getDelegationSpecifierList()?.startOffset ?: element.getBody()?.startOffset ?: element.endOffset
|
||||
)
|
||||
if (element is KtClassOrObject) {
|
||||
if (element.isLocal()) return null
|
||||
if (element is KtEnumEntry) return null
|
||||
if (element is KtClass && (element.isAnnotation() || element.isInterface())) return null
|
||||
|
||||
if (element.resolveToDescriptorIfAny() == null) return null
|
||||
|
||||
return TextRange(
|
||||
element.startOffset,
|
||||
element.getDelegationSpecifierList()?.startOffset ?: element.getBody()?.startOffset ?: element.endOffset
|
||||
)
|
||||
}
|
||||
|
||||
if (element.parent !is KtFile) return null
|
||||
|
||||
if (element is KtNamedFunction) {
|
||||
return TextRange((element.funKeyword ?: element.nameIdentifier!!).startOffset, element.nameIdentifier!!.endOffset)
|
||||
}
|
||||
|
||||
if (element is KtProperty) {
|
||||
if (element.getter == null && element.delegate == null) return null
|
||||
return TextRange(element.valOrVarKeyword.startOffset, element.nameIdentifier!!.endOffset)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun applyTo(element: KtClassOrObject, editor: Editor) {
|
||||
override fun applyTo(element: KtNamedDeclaration, editor: Editor) {
|
||||
val lightClass = when (element) {
|
||||
is KtClassOrObject -> element.toLightClass()
|
||||
else -> element.getContainingKtFile().findFacadeClass()
|
||||
} ?: return
|
||||
|
||||
object : CreateTestAction() {
|
||||
// Based on the com.intellij.testIntegration.createTest.JavaTestGenerator.createTestClass()
|
||||
private fun findTestClass(targetDirectory: PsiDirectory, className: String): PsiClass? {
|
||||
@@ -164,6 +185,6 @@ class KotlinCreateTestIntention : SelfTargetingRangeIntention<KtClassOrObject>(K
|
||||
}
|
||||
}
|
||||
}
|
||||
}.invoke(element.project, editor, element.toLightClass()!!)
|
||||
}.invoke(element.project, editor, lightClass)
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,10 @@ import com.intellij.testIntegration.JavaTestFinder
|
||||
import com.intellij.testIntegration.TestFinderHelper
|
||||
import com.intellij.util.CommonProcessors
|
||||
import com.intellij.util.containers.HashSet
|
||||
import org.jetbrains.kotlin.asJava.KtLightClassForExplicitDeclaration
|
||||
import org.jetbrains.kotlin.asJava.toLightClass
|
||||
import org.jetbrains.kotlin.asJava.*
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
||||
import org.jetbrains.kotlin.psi.KtClassOrObject
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
@@ -41,9 +41,11 @@ class KotlinTestFinder : JavaTestFinder() {
|
||||
override fun findSourceElement(from: PsiElement): PsiClass? {
|
||||
super.findSourceElement(from)?.let { return it }
|
||||
|
||||
val classOrObject = from.parentsWithSelf.filterIsInstance<KtClassOrObject>().firstOrNull { !it.isLocal() } ?: return null
|
||||
if (classOrObject.resolveToDescriptorIfAny() == null) return null
|
||||
return classOrObject.toLightClass()
|
||||
from.parentsWithSelf.filterIsInstance<KtClassOrObject>().firstOrNull { !it.isLocal() }?.let {
|
||||
return if (it.resolveToDescriptorIfAny() == null) null else it.toLightClass()
|
||||
}
|
||||
|
||||
return (from.containingFile as? KtFile)?.findFacadeClass()
|
||||
}
|
||||
|
||||
override fun isTest(element: PsiElement): Boolean {
|
||||
@@ -63,9 +65,13 @@ class KotlinTestFinder : JavaTestFinder() {
|
||||
for (candidateNameWithWeight in TestFinderHelper.collectPossibleClassNamesWithWeights(klass.name)) {
|
||||
for (eachClass in cache.getClassesByName(candidateNameWithWeight.first, scope)) {
|
||||
if (eachClass.isAnnotationType || frameworks.isTestClass(eachClass)) continue
|
||||
if (!eachClass.isPhysical && eachClass !is KtLightClassForExplicitDeclaration) continue
|
||||
|
||||
classesWithWeights.add(Pair.create(eachClass, candidateNameWithWeight.second))
|
||||
if (eachClass is KtLightClassForFacade) {
|
||||
eachClass.files.mapTo(classesWithWeights) { Pair.create(it, candidateNameWithWeight.second) }
|
||||
}
|
||||
else if (eachClass.isPhysical || eachClass is KtLightClassForExplicitDeclaration) {
|
||||
classesWithWeights.add(Pair.create(eachClass, candidateNameWithWeight.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@file:JvmName("FooUtils")
|
||||
@file:JvmMultifileClass
|
||||
import junit.framework.TestCase
|
||||
|
||||
fun bar() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@file:JvmName("FooUtils")
|
||||
@file:JvmMultifileClass
|
||||
import junit.framework.TestCase
|
||||
|
||||
class Foo
|
||||
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
|
||||
val x = 1
|
||||
|
||||
@JvmField val y = 2
|
||||
@@ -0,0 +1,9 @@
|
||||
// CONFIGURE_LIBRARY: JUnit@lib/junit-4.12.jar
|
||||
// REF: (<root>).Foo
|
||||
// REF: /src.fromJavaTestToKotlinFile.kt
|
||||
// REF: /src.fromJavaTestToKotlinFile.2.kt
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class <caret>FooUtilsTest extends TestCase {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class FooUtilsTest2 extends TestCase {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// CONFIGURE_LIBRARY: JUnit@lib/junit-4.12.jar
|
||||
// REF: (<root>).FooUtilsTest
|
||||
// REF: FooUtilsTest2
|
||||
@file:JvmName("FooUtils")
|
||||
import junit.framework.TestCase
|
||||
|
||||
class Foo
|
||||
|
||||
fun <caret>foo() {
|
||||
|
||||
}
|
||||
|
||||
val x = 1
|
||||
|
||||
@JvmField val y = 2
|
||||
|
||||
class FooUtilsTest : TestCase() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@file:JvmName("FooUtils")
|
||||
@file:JvmMultifileClass
|
||||
import junit.framework.TestCase
|
||||
|
||||
fun bar() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@file:JvmName("FooUtils")
|
||||
@file:JvmMultifileClass
|
||||
import junit.framework.TestCase
|
||||
|
||||
fun foo() {
|
||||
|
||||
}
|
||||
|
||||
val x = 1
|
||||
|
||||
@JvmField val y = 2
|
||||
@@ -0,0 +1,8 @@
|
||||
// CONFIGURE_LIBRARY: JUnit@lib/junit-4.12.jar
|
||||
// REF: /src.fromKotlinTestToKotlinFile.kt
|
||||
// REF: /src.fromKotlinTestToKotlinFile.2.kt
|
||||
import junit.framework.TestCase
|
||||
|
||||
class <caret>FooUtilsTest : TestCase()
|
||||
|
||||
class FooUtilsTest2 : TestCase()
|
||||
@@ -1,4 +1,5 @@
|
||||
// "Delete redundant extension property" "false"
|
||||
// ACTION: Create test
|
||||
import java.io.File
|
||||
|
||||
var File.<caret>name: String
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// "Delete redundant extension property" "false"
|
||||
// ACTION: Create test
|
||||
|
||||
var Thread.<caret>priority: Int
|
||||
get() = getPriority() + 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// "Delete redundant extension property" "false"
|
||||
// ACTION: Convert property to function
|
||||
// ACTION: Create test
|
||||
import java.io.File
|
||||
|
||||
public val File.<caret>parent: File?
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// "Delete redundant extension property" "false"
|
||||
// ACTION: Create test
|
||||
|
||||
var Thread.<caret>priority: Int
|
||||
get() = this.getPriority()
|
||||
|
||||
@@ -47,12 +47,24 @@ public class GotoTestOrCodeActionTestGenerated extends AbstractGotoTestOrCodeAct
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("fromJavaTestToKotlinFile.main.java")
|
||||
public void testFromJavaTestToKotlinFile() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/navigation/gotoTestOrCode/fromJavaTestToKotlinFile.main.java");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("fromKotlinClassToTest.main.kt")
|
||||
public void testFromKotlinClassToTest() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/navigation/gotoTestOrCode/fromKotlinClassToTest.main.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("fromKotlinFileToTest.main.kt")
|
||||
public void testFromKotlinFileToTest() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/navigation/gotoTestOrCode/fromKotlinFileToTest.main.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("fromKotlinTestToJavaClass.main.kt")
|
||||
public void testFromKotlinTestToJavaClass() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/navigation/gotoTestOrCode/fromKotlinTestToJavaClass.main.kt");
|
||||
@@ -64,4 +76,10 @@ public class GotoTestOrCodeActionTestGenerated extends AbstractGotoTestOrCodeAct
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/navigation/gotoTestOrCode/fromKotlinTestToKotlinClass.main.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("fromKotlinTestToKotlinFile.main.kt")
|
||||
public void testFromKotlinTestToKotlinFile() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/navigation/gotoTestOrCode/fromKotlinTestToKotlinFile.main.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.intellij.psi.PsiDocumentManager;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.testFramework.UsefulTestCase;
|
||||
import com.intellij.util.PathUtil;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import junit.framework.TestCase;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -51,6 +52,10 @@ public final class NavigationTestUtils {
|
||||
public static void assertGotoDataMatching(Editor editor, GotoTargetHandler.GotoData gotoData) {
|
||||
// Get expected references from the tested document
|
||||
List<String> expectedReferences = InTextDirectivesUtils.findListWithPrefixes(editor.getDocument().getText(), "// REF:");
|
||||
for (int i = 0; i < expectedReferences.size(); i++) {
|
||||
expectedReferences.set(i, PathUtil.toSystemDependentName(expectedReferences.get(i)).replace("//", "/"));
|
||||
}
|
||||
|
||||
Collections.sort(expectedReferences);
|
||||
|
||||
if (gotoData != null) {
|
||||
|
||||
Reference in New Issue
Block a user