Unit Test Tooling: Support Go to Test/Create Test actions for top-level declarations

#KT-10133 Fixed
This commit is contained in:
Alexey Sedunov
2015-12-01 22:22:35 +03:00
parent ecac0f177b
commit 8fcda32287
18 changed files with 165 additions and 28 deletions
@@ -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) {