Unit Test Tooling: Implement "Create Test" action

#KT-6472 In Progress
This commit is contained in:
Alexey Sedunov
2015-10-02 20:38:14 +03:00
parent c697eef803
commit ad929a6577
14 changed files with 361 additions and 90 deletions
@@ -17,8 +17,9 @@
package org.jetbrains.kotlin.idea.util.application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
public fun runReadAction<T>(action: () -> T): T {
return ApplicationManager.getApplication().runReadAction<T>(action)
@@ -32,13 +33,20 @@ public fun Project.executeWriteCommand(name: String, command: () -> Unit) {
CommandProcessor.getInstance().executeCommand(this, { runWriteAction(command) }, name, null)
}
public fun Project.executeCommand(name: String, groupId: Any? = null, command: () -> Unit) {
CommandProcessor.getInstance().executeCommand(this, command, name, groupId)
public fun <T> Project.executeWriteCommand(name: String, groupId: Any? = null, command: () -> T): T {
return executeCommand<T>(name, groupId) { runWriteAction(command) }
}
public fun <T> Project.executeWriteCommand(name: String, groupId: Any? = null, command: () -> T): T {
public fun <T> Project.executeCommand(name: String, groupId: Any? = null, command: () -> T): T {
var result: T = null as T
CommandProcessor.getInstance().executeCommand(this, { result = runWriteAction(command) }, name, groupId)
CommandProcessor.getInstance().executeCommand(this, { result = command() }, name, groupId)
@Suppress("USELESS_CAST")
return result as T
}
public fun <T> Project.runWithAlternativeResolveEnabled(action: () -> T): T {
var result: T = null as T
DumbService.getInstance(this).withAlternativeResolveEnabled { result = action() }
@Suppress("USELESS_CAST")
return result as T
}
@@ -0,0 +1,11 @@
class Foo {
fun foo() {
}
}
class FooTest : TestCase {
fun testFoo() {
}
}
@@ -0,0 +1,5 @@
<spot>class Foo</spot> {
fun foo() {
}
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention generates a test case for the selected class. The generated class contains skeleton test functions for the chosen public functions.
</body>
</html>
+5
View File
@@ -1062,6 +1062,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.testIntegration.KotlinCreateTestIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.DeprecatedCallableAddReplaceWithInspection"
displayName="Add 'replaceWith' argument to 'deprecated' annotation"
groupName="Kotlin"
@@ -32,67 +32,104 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileVisitor
import com.intellij.psi.PsiJavaFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.core.refactoring.toPsiFile
import org.jetbrains.kotlin.idea.j2k.IdeaJavaToKotlinServices
import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.j2k.ConverterSettings
import org.jetbrains.kotlin.j2k.JavaToKotlinConverter
import org.jetbrains.kotlin.psi.JetFile
import java.io.File
import java.io.IOException
import java.util.ArrayList
public class JavaToKotlinAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
ApplicationManager.getApplication().saveAll()
companion object {
private fun uniqueKotlinFileName(javaFile: VirtualFile): String {
val ioFile = File(javaFile.getPath().replace('/', File.separatorChar))
var i = 0
while (true) {
val fileName = javaFile.getNameWithoutExtension() + (if (i > 0) i else "") + ".kt"
if (!ioFile.resolveSibling(fileName).exists()) return fileName
i++
}
}
private fun saveResults(javaFiles: List<PsiJavaFile>, convertedTexts: List<String>): List<VirtualFile> {
val result = ArrayList<VirtualFile>()
for ((psiFile, text) in javaFiles.zip(convertedTexts)) {
val virtualFile = psiFile.getVirtualFile()
val fileName = uniqueKotlinFileName(virtualFile)
try {
virtualFile.rename(this, fileName)
virtualFile.setBinaryContent(CharsetToolkit.getUtf8Bytes(text))
result.add(virtualFile)
}
catch (e: IOException) {
MessagesEx.error(psiFile.getProject(), e.getMessage()).showLater()
}
}
return result
}
fun convertFiles(javaFiles: List<PsiJavaFile>, project: Project, enableExternalCodeProcessing: Boolean = true): List<JetFile> {
ApplicationManager.getApplication().saveAll()
var converterResult: JavaToKotlinConverter.FilesResult? = null
fun convert() {
val converter = JavaToKotlinConverter(project, ConverterSettings.defaultSettings, IdeaJavaToKotlinServices)
converterResult = converter.filesToKotlin(javaFiles, J2kPostProcessor(formatCode = true), ProgressManager.getInstance().getProgressIndicator())
}
val title = "Convert Java to Kotlin"
if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
runReadAction(::convert)
},
title,
true,
project)) return emptyList()
var externalCodeUpdate: (() -> Unit)? = null
if (enableExternalCodeProcessing && converterResult!!.externalCodeProcessing != null) {
val question = "Some code in the rest of your project may require corrections after performing this conversion. Do you want to find such code and correct it too?"
if (Messages.showOkCancelDialog(project, question, title, Messages.getQuestionIcon()) == Messages.OK) {
ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
runReadAction {
externalCodeUpdate = converterResult!!.externalCodeProcessing!!.prepareWriteOperation(ProgressManager.getInstance().getProgressIndicator())
}
},
title,
true,
project)
}
}
return project.executeWriteCommand("Convert files from Java to Kotlin", null) {
CommandProcessor.getInstance().markCurrentCommandAsGlobal(project)
val newFiles = saveResults(javaFiles, converterResult!!.results)
externalCodeUpdate?.invoke()
newFiles.singleOrNull()?.let {
FileEditorManager.getInstance(project).openFile(it, true)
}
newFiles.map { it.toPsiFile(project) as JetFile }
}
}
}
override fun actionPerformed(e: AnActionEvent) {
val javaFiles = selectedJavaFiles(e).toList()
val project = CommonDataKeys.PROJECT.getData(e.getDataContext())!!
var converterResult: JavaToKotlinConverter.FilesResult? = null
fun convert() {
val converter = JavaToKotlinConverter(project, ConverterSettings.defaultSettings, IdeaJavaToKotlinServices)
converterResult = converter.filesToKotlin(javaFiles, J2kPostProcessor(formatCode = true), ProgressManager.getInstance().getProgressIndicator())
}
val title = "Convert Java to Kotlin"
if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
runReadAction(::convert)
},
title,
true,
project)) return
var externalCodeUpdate: (() -> Unit)? = null
if (converterResult!!.externalCodeProcessing != null) {
val question = "Some code in the rest of your project may require corrections after performing this conversion. Do you want to find such code and correct it too?"
if (Messages.showOkCancelDialog(project, question, title, Messages.getQuestionIcon()) == Messages.OK) {
ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
runReadAction {
externalCodeUpdate = converterResult!!.externalCodeProcessing!!.prepareWriteOperation(ProgressManager.getInstance().getProgressIndicator())
}
},
title,
true,
project)
}
}
project.executeWriteCommand("Convert files from Java to Kotlin") {
CommandProcessor.getInstance().markCurrentCommandAsGlobal(project)
val newFiles = saveResults(javaFiles, converterResult!!.results)
externalCodeUpdate?.invoke()
newFiles.singleOrNull()?.let {
FileEditorManager.getInstance(project).openFile(it, true)
}
}
convertFiles(javaFiles, project)
}
override fun update(e: AnActionEvent) {
@@ -126,32 +163,4 @@ public class JavaToKotlinAction : AnAction() {
}
return result
}
private fun saveResults(javaFiles: List<PsiJavaFile>, convertedTexts: List<String>): List<VirtualFile> {
val result = ArrayList<VirtualFile>()
for ((psiFile, text) in javaFiles.zip(convertedTexts)) {
val virtualFile = psiFile.getVirtualFile()
val fileName = uniqueKotlinFileName(virtualFile)
try {
virtualFile.rename(this, fileName)
virtualFile.setBinaryContent(CharsetToolkit.getUtf8Bytes(text))
result.add(virtualFile)
}
catch (e: IOException) {
MessagesEx.error(psiFile.getProject(), e.getMessage()).showLater()
}
}
return result
}
private fun uniqueKotlinFileName(javaFile: VirtualFile): String {
val ioFile = File(javaFile.getPath().replace('/', File.separatorChar))
var i = 0
while (true) {
val fileName = javaFile.getNameWithoutExtension() + (if (i > 0) i else "") + ".kt"
if (!ioFile.resolveSibling(fileName).exists()) return fileName
i++
}
}
}
@@ -21,14 +21,12 @@ import com.intellij.codeInsight.generation.actions.GenerateActionPopupTemplateIn
import com.intellij.codeInsight.hint.HintManager
import com.intellij.ide.fileTemplates.FileTemplateManager
import com.intellij.ide.fileTemplates.impl.AllFileTemplatesConfigurable
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.InputValidator
import com.intellij.openapi.ui.Messages
@@ -42,7 +40,6 @@ import com.intellij.testIntegration.TestFramework
import com.intellij.testIntegration.TestIntegrationUtils.MethodKind
import com.intellij.ui.components.JBList
import com.intellij.util.IncorrectOperationException
import com.intellij.util.SmartList
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
@@ -52,6 +49,7 @@ import org.jetbrains.kotlin.idea.core.overrideImplement.generateUnsupportedOrSup
import org.jetbrains.kotlin.idea.core.refactoring.j2k
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.setupEditorSelection
import org.jetbrains.kotlin.idea.quickfix.generateMember
import org.jetbrains.kotlin.idea.testIntegration.findSuitableFrameworks
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.lexer.JetTokens
import org.jetbrains.kotlin.psi.JetClassOrObject
@@ -69,13 +67,6 @@ abstract class KotlinGenerateTestSupportActionBase(
return elementAtCaret.parentsWithSelf.filterIsInstance<JetClassOrObject>().firstOrNull { !it.isLocal() }
}
private fun findSuitableFrameworks(klass: JetClassOrObject): List<TestFramework> {
val lightClass = klass.toLightClass() ?: return emptyList()
val frameworks = Extensions.getExtensions(TestFramework.EXTENSION_NAME).filter { it.language == JavaLanguage.INSTANCE }
return frameworks.firstOrNull { it.isTestClass(lightClass) }?.let { listOf(it) }
?: frameworks.filterTo(SmartList<TestFramework>()) { it.isPotentialTestClass(lightClass) }
}
private fun chooseAndPerform(editor: Editor, frameworks: List<TestFramework>, consumer: (TestFramework) -> Unit) {
frameworks.ifEmpty { return }
frameworks.singleOrNull()?.let { return consumer(it) }
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 org.jetbrains.kotlin.idea.testIntegration
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiPackage
import com.intellij.testIntegration.createTest.CreateTestDialog
class KotlinCreateTestDialog(project: Project,
title: String,
targetClass: PsiClass?,
targetPackage: PsiPackage?,
targetModule: Module) : CreateTestDialog(project, title, targetClass, targetPackage, targetModule) {
public var explicitClassName: String? = null
override fun getClassName() = explicitClassName ?: super.getClassName()
}
@@ -0,0 +1,169 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 org.jetbrains.kotlin.idea.testIntegration
import com.intellij.CommonBundle
import com.intellij.codeInsight.CodeInsightBundle
import com.intellij.codeInsight.FileModificationService
import com.intellij.codeInsight.navigation.NavigationUtil
import com.intellij.ide.util.PropertiesComponent
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScopesCore
import com.intellij.testIntegration.createTest.CreateTestAction
import com.intellij.testIntegration.createTest.TestGenerators
import org.jetbrains.kotlin.asJava.KotlinLightClass
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.idea.actions.JavaToKotlinAction
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.core.getPackage
import org.jetbrains.kotlin.idea.core.refactoring.j2k
import org.jetbrains.kotlin.idea.core.refactoring.toPsiDirectory
import org.jetbrains.kotlin.idea.intentions.JetSelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.util.application.executeCommand
import org.jetbrains.kotlin.idea.util.application.runWithAlternativeResolveEnabled
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.JetClass
import org.jetbrains.kotlin.psi.JetClassOrObject
import org.jetbrains.kotlin.psi.JetEnumEntry
import org.jetbrains.kotlin.psi.JetNamedFunction
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 : JetSelfTargetingRangeIntention<JetClassOrObject>(
JetClassOrObject::class.java,
CodeInsightBundle.message("intention.create.test")
) {
override fun applicabilityRange(element: JetClassOrObject): TextRange? {
if (element.isLocal()) return null
if (element is JetEnumEntry) return null
if (element is JetClass && (element.isAnnotation() || element.isInterface())) return null
if (ModuleUtilCore.findModuleForPsiElement(element) == null) return null
if (element.resolveToDescriptorIfAny() == null) return null
return TextRange(
element.startOffset,
element.getDelegationSpecifierList()?.startOffset ?: element.getBody()?.startOffset ?: element.endOffset
)
}
override fun applyTo(element: JetClassOrObject, editor: Editor) {
object : CreateTestAction() {
// Based on the com.intellij.testIntegration.createTest.JavaTestGenerator.createTestClass()
private fun findTestClass(targetDirectory: PsiDirectory, className: String): PsiClass? {
val psiPackage = targetDirectory.getPackage() ?: return null
val scope = GlobalSearchScopesCore.directoryScope(targetDirectory, false)
val klass = psiPackage.findClassByShortName(className, scope).firstOrNull() ?: return null
if (!FileModificationService.getInstance().preparePsiElementForWrite(klass)) return null
return klass
}
private fun getTempJavaClassName(project: Project, kotlinFile: VirtualFile): String {
val baseName = kotlinFile.nameWithoutExtension
val psiDir = kotlinFile.parent!!.toPsiDirectory(project)!!
return sequence(0) { it + 1 }
.map { "$baseName$it" }
.first { psiDir.findFile("$it.java") == null && findTestClass(psiDir, it) == null }
}
// Based on the com.intellij.testIntegration.createTest.CreateTestAction.CreateTestAction.invoke()
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
val srcModule = ModuleUtilCore.findModuleForPsiElement(element) ?: return
val propertiesComponent = PropertiesComponent.getInstance()
val testFolders = HashSet<VirtualFile>()
CreateTestAction.checkForTestRoots(srcModule, testFolders)
if (testFolders.isEmpty() && !propertiesComponent.getBoolean("create.test.in.the.same.root")) {
if (Messages.showOkCancelDialog(
project,
"Create test in the same source root?",
"No Test Roots Found",
Messages.getQuestionIcon()) != Messages.OK) return
propertiesComponent.setValue("create.test.in.the.same.root", true)
}
val srcClass = CreateTestAction.getContainingClass(element) ?: return
val srcDir = element.containingFile.containingDirectory
val srcPackage = JavaDirectoryService.getInstance().getPackage(srcDir)
val dialog = KotlinCreateTestDialog(project, text, srcClass, srcPackage, srcModule)
if (!dialog.showAndGet()) return
val existingClass = (findTestClass(dialog.targetDirectory, dialog.className) as? KotlinLightClass)?.getOrigin()
if (existingClass != null) {
// TODO: Override dialog method when it becomes protected
val answer = Messages.showYesNoDialog(
project,
"Kotlin class '${existingClass.name}' already exists. Do you want to update it?",
CommonBundle.getErrorTitle(),
"Rewrite",
"Cancel",
Messages.getErrorIcon()
)
if (answer == Messages.NO) return
}
val generatedClass = project.executeCommand(CodeInsightBundle.message("intention.create.test"), this) {
val generator = TestGenerators.INSTANCE.forLanguage(dialog.selectedTestFrameworkDescriptor.language)
project.runWithAlternativeResolveEnabled {
if (existingClass != null) {
dialog.explicitClassName = getTempJavaClassName(project, existingClass.containingFile.virtualFile)
}
generator.generateTest(project, dialog)
}
} as? PsiClass ?: return
val generatedFile = generatedClass.containingFile as? PsiJavaFile ?: return
if (generatedClass.language == JavaLanguage.INSTANCE) {
project.executeCommand("Convert class '${generatedClass.name}' to Kotlin", this) {
runWriteAction {
generatedClass.methods.forEach { it.throwsList.referenceElements.forEach { it.delete() } }
}
if (existingClass != null) {
runWriteAction {
val existingMethodNames = existingClass
.declarations
.filterIsInstance<JetNamedFunction>()
.mapTo(HashSet()) { it.name }
generatedClass
.methods
.filter { it.name !in existingMethodNames }
.forEach { it.j2k()?.let { existingClass.addDeclaration(it) } }
generatedClass.delete()
}
NavigationUtil.activateFileWithPsiElement(existingClass)
}
else {
JavaToKotlinAction.convertFiles(generatedFile.singletonList(), project, false).singleOrNull()
}
}
}
}
}.invoke(element.project, editor, element.toLightClass()!!)
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 org.jetbrains.kotlin.idea.testIntegration
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.extensions.Extensions
import com.intellij.testIntegration.TestFramework
import com.intellij.util.SmartList
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.psi.JetClassOrObject
public fun findSuitableFrameworks(klass: JetClassOrObject): List<TestFramework> {
val lightClass = klass.toLightClass() ?: return emptyList()
val frameworks = Extensions.getExtensions(TestFramework.EXTENSION_NAME).filter { it.language == JavaLanguage.INSTANCE }
return frameworks.firstOrNull { it.isTestClass(lightClass) }?.let { listOf(it) }
?: frameworks.filterTo(SmartList<TestFramework>()) { it.isPotentialTestClass(lightClass) }
}
@@ -1,5 +1,6 @@
// "Create class 'A'" "false"
// ACTION: Create interface 'A'
// ACTION: Create Test
// ERROR: Unresolved reference: A
package p
@@ -1,5 +1,6 @@
// "class org.jetbrains.kotlin.idea.quickfix.AddOpenModifierToClassDeclarationFix" "false"
// ERROR: This type is final, so it cannot be inherited from
// ACTION: Create Test
import testPackage.*
class foo : <caret>JavaClass() {}
@@ -1,4 +1,5 @@
// "Cast expression 'Foo<Number>()' to 'Foo<Int>'" "false"
// ACTION: Create Test
// ERROR: <html>Type mismatch.<table><tr><td>Required:</td><td>Foo&lt;kotlin.Int&gt;</td></tr><tr><td>Found:</td><td>Foo&lt;kotlin.Number&gt;</td></tr></table></html>
class Foo<T>
@@ -1,5 +1,6 @@
// "Remove 'val' from parameter" "false"
// ACTION: Make internal
// ACTION: Make private
// ACTION: Create Test
class C(<caret>val x: String) {
}