Add internal move refactoring testing action

This action doing further steps in infinite loop (breaks after user request):
1) Generate random move refactoring model
2) Run this model (if it is correct)
3) Recompile the project
4) If compilation was not succeeded write model parameters into output file
5) Delete all new files and directories and make "git reset"
This commit is contained in:
Igor Yakovlev
2019-08-20 12:09:40 +03:00
parent 3951fde081
commit 63892891db
10 changed files with 1009 additions and 0 deletions
@@ -112,6 +112,12 @@
<action id="DumbModeTremble" class="org.jetbrains.kotlin.idea.actions.internal.DumbModeTrembleAction"
text="Tremble Dumb Mode"/>
<group id="KotlinRefactoringTesting" text="Kotlin refactoring testing" popup="true">
<action id="TestMoveRefactiringAction"
class="org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.MoveRefactoringAction"
text="Test move refactoring on opened project"/>
</group>
<group id="KotlinInternalGroup" text="Kotlin" popup="true">
<group id="KotlinCompletionBenchmarkGroup" popup="true" text="Benchmark completion">
<action id="TopLevelCompletionBenchmarkAction"
@@ -0,0 +1,72 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.compiler.CompilationStatusListener
import com.intellij.openapi.compiler.CompileContext
import com.intellij.openapi.compiler.CompilerTopics.COMPILATION_STATUS
import com.intellij.openapi.project.Project
internal class CompilationStatusTracker(private val project: Project) {
private val compilationStatusListener = object : CompilationStatusListener {
init {
project.messageBus.connect(project).subscribe(COMPILATION_STATUS, this)
}
var hasCompilerError = false
private set
var compilationFinished = false
private set
fun reset() {
compilationFinished = false
hasCompilerError = false
}
override fun compilationFinished(aborted: Boolean, errors: Int, warnings: Int, compileContext: CompileContext) {
hasCompilerError = hasCompilerError || aborted || errors != 0
compilationFinished = true
}
}
private val runBuildAction by lazyPub {
val am = ActionManager.getInstance();
val action = am.getAction("CompileProject")
val event = AnActionEvent(
null,
DataManager.getInstance().dataContext,
ActionPlaces.UNKNOWN, Presentation(),
ActionManager.getInstance(), 0
)
return@lazyPub { action.actionPerformed(event) }
}
fun checkByBuild(cancelledChecker: () -> Boolean): Boolean {
compilationStatusListener.reset()
ApplicationManager.getApplication().invokeAndWait {
runBuildAction()
}
while (!cancelledChecker() && !compilationStatusListener.compilationFinished) {
Thread.yield()
}
return !compilationStatusListener.hasCompilerError
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.*
internal class FileSystemChangesTracker(project: Project) : Disposable {
override fun dispose() {
trackerListener.dispose()
}
private val trackerListener = object : VirtualFileListener, Disposable {
val createdFilesMutable = mutableSetOf<VirtualFile>()
@Volatile
private var disposed = false
init {
VirtualFileManager.getInstance().addVirtualFileListener(this)
Disposer.register(project, this)
}
@Synchronized
override fun dispose() {
if (!disposed) {
disposed = true
VirtualFileManager.getInstance().removeVirtualFileListener(this)
createdFilesMutable.clear()
}
}
override fun fileCreated(event: VirtualFileEvent) {
createdFilesMutable.add(event.file)
}
override fun fileCopied(event: VirtualFileCopyEvent) {
createdFilesMutable.add(event.file)
}
override fun fileMoved(event: VirtualFileMoveEvent) {
createdFilesMutable.add(event.file)
}
}
fun reset() {
trackerListener.createdFilesMutable.clear()
}
val createdFiles: Set<VirtualFile> = trackerListener.createdFilesMutable
}
@@ -0,0 +1,209 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.cases.MoveRefactoringCase
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import java.io.File
import java.io.IOException
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
class MoveRefactoringAction : AnAction() {
companion object {
const val WINDOW_TITLE: String = "Move refactoring testing"
const val RECENT_SELECTED_PATH = "org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.RECENT_SELECTED_PATH"
}
private val refactoring = MoveRefactoringCase()
private var iteration = 0
private var fails = 0
private var verifications = 0
private fun randomRefactoringAndCheck(
project: Project,
projectRoot: VirtualFile,
setIndicator: (String, Double) -> Unit,
actionRunner: CompilationStatusTracker,
fileTracker: FileSystemChangesTracker,
resultsFile: File,
refactoringCountBeforeCheck: Int,
cancelledChecker: () -> Boolean
) {
try {
setIndicator("Update indices...", 0.0)
DumbService.getInstance(project).waitForSmartMode()
setIndicator("Perform refactoring ...", 0.1)
fileTracker.reset()
var refactoringResult: RandomMoveRefactoringResult = RandomMoveRefactoringResult.Failed
edtExecute {
refactoringResult = refactoring.tryCreateAndRun(project, refactoringCountBeforeCheck)
setIndicator("Saving files...", 0.3)
FileDocumentManager.getInstance().saveAllDocuments()
VfsUtil.markDirtyAndRefresh(false, true, true, projectRoot)
}
when (val localRefactoringResult = refactoringResult) {
is RandomMoveRefactoringResult.Success -> {
verifications++
setIndicator("Compiling project...", 0.7)
if (!actionRunner.checkByBuild(cancelledChecker)) {
fails++
resultsFile.appendText("${localRefactoringResult.caseData}\n\n")
}
}
is RandomMoveRefactoringResult.ExceptionCaused -> {
fails++
resultsFile.appendText("${localRefactoringResult.caseData}\nWith exception\n${localRefactoringResult.message}\n\n")
}
is RandomMoveRefactoringResult.Failed -> {
}
}
} finally {
setIndicator("Reset files...", 0.9)
fileTracker.createdFiles.toList().map {
try {
edtExecute {
runWriteAction {
if (it.exists()) it.delete(null)
}
}
} catch (e: IOException) {
//pass
}
}
gitReset(project, projectRoot)
}
setIndicator("Done", 1.0)
}
private fun createFileIfNotExist(targetPath: String): File? {
val stamp = DateTimeFormatter
.ofPattern("yyyy-MM-dd-HH-mm-ss")
.withZone(ZoneOffset.UTC)
.format(Instant.now())
.toString()
val resultsFile = File(targetPath, "REFACTORING_TEST_RESULT-$stamp.txt")
return try {
resultsFile.apply { createNewFile() }
} catch (e: IOException) {
null
}
}
override fun actionPerformed(e: AnActionEvent) {
val project = e.project
val projectRoot = project?.guessProjectDir()
if (projectRoot === null) {
Messages.showErrorDialog(project, "Cannot get project root directory", WINDOW_TITLE)
return
}
val dialog = MoveRefactoringActionDialog(project, projectRoot.path)
dialog.show()
if (!dialog.isOK) return
val targetPath = dialog.selectedDirectoryName
val countOfMovesBeforeCheck = dialog.selectedCount
val resultsFile = createFileIfNotExist(targetPath)
if (resultsFile === null) {
Messages.showErrorDialog(project, "Cannot get or create results file", WINDOW_TITLE)
return
}
PropertiesComponent.getInstance().setValue(RECENT_SELECTED_PATH, targetPath)
ProgressManager.getInstance().run(object : Task.Modal(project, WINDOW_TITLE, /* canBeCancelled = */ true) {
override fun run(indicator: ProgressIndicator) {
try {
unsafeRefactoringAndCheck(
project = project,
indicator = indicator,
projectRoot = projectRoot,
resultsFile = resultsFile,
countOfMovesBeforeCheck = countOfMovesBeforeCheck
)
} catch (e: Exception) {
if (e !is ProcessCanceledException && e.cause !is ProcessCanceledException) {
throw e
}
}
}
})
}
private fun unsafeRefactoringAndCheck(
project: Project,
indicator: ProgressIndicator,
projectRoot: VirtualFile,
resultsFile: File,
countOfMovesBeforeCheck: Int
) {
val compilationStatusTracker = CompilationStatusTracker(project)
val fileSystemChangesTracker = FileSystemChangesTracker(project)
val cancelledChecker = { indicator.isCanceled }
val setIndicator = { text: String, fraction: Double ->
indicator.text2 = text
indicator.fraction = fraction
}
iteration = 0
fails = 0
verifications = 0
try {
while (!cancelledChecker()) {
iteration++
indicator.text = "$WINDOW_TITLE [Try $iteration with $fails fails and $verifications verifications]"
randomRefactoringAndCheck(
project = project,
projectRoot = projectRoot,
setIndicator = setIndicator,
actionRunner = compilationStatusTracker,
fileTracker = fileSystemChangesTracker,
resultsFile = resultsFile,
refactoringCountBeforeCheck = countOfMovesBeforeCheck,
cancelledChecker = cancelledChecker
)
}
} finally {
fileSystemChangesTracker.dispose()
indicator.stop()
}
}
}
@@ -0,0 +1,116 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.util.Disposer
import com.intellij.refactoring.copy.CopyFilesOrDirectoriesDialog
import com.intellij.ui.components.JBLabelDecorator
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import com.intellij.util.ui.UIUtil
import org.jetbrains.kotlin.idea.core.util.onTextChange
import java.io.File
import javax.swing.InputVerifier
import javax.swing.JComponent
class MoveRefactoringActionDialog(
private val project: Project, private val defaultDirectory: String
) : DialogWrapper(project, true) {
companion object {
const val WINDOW_TITLE = "Move refactoring test"
const val COUNT_LABEL_TEXT = "Maximum count of applied refactoring before validity check"
const val LOG_FILE_WILL_BE_PLACED_HERE = "Test result log file will be placed here"
const val RECENT_SELECTED_PATH = "org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.RECENT_SELECTED_PATH"
const val RECENT_SELECTED_RUN_COUNT = "org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.RECENT_SELECTED_RUN_COUNT"
}
private val nameLabel = JBLabelDecorator.createJBLabelDecorator().setBold(true)
private val tfTargetDirectory = TextFieldWithBrowseButton()
private val tfRefactoringRunCount = JBTextField()
init {
title = WINDOW_TITLE
init()
initializeData()
}
override fun createActions() = arrayOf(okAction, cancelAction)
override fun getPreferredFocusedComponent() = tfTargetDirectory.childComponent
override fun createCenterPanel(): JComponent? = null
override fun createNorthPanel(): JComponent {
val descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
tfTargetDirectory.addBrowseFolderListener(
WINDOW_TITLE,
LOG_FILE_WILL_BE_PLACED_HERE,
project,
descriptor
)
tfTargetDirectory.setTextFieldPreferredWidth(CopyFilesOrDirectoriesDialog.MAX_PATH_LENGTH)
tfTargetDirectory.textField.onTextChange { validateOKButton() }
Disposer.register(disposable, tfTargetDirectory)
tfRefactoringRunCount.inputVerifier = object : InputVerifier() {
override fun verify(input: JComponent?) = tfRefactoringRunCount.text.toIntOrNull()?.let { it > 0 } ?: false
}
tfRefactoringRunCount.onTextChange { validateOKButton() }
return FormBuilder.createFormBuilder()
.addComponent(nameLabel)
.addLabeledComponent(LOG_FILE_WILL_BE_PLACED_HERE, tfTargetDirectory, UIUtil.LARGE_VGAP)
.addLabeledComponent(COUNT_LABEL_TEXT, tfRefactoringRunCount)
.panel
}
private fun initializeData() {
tfTargetDirectory.childComponent.text = PropertiesComponent.getInstance().getValue(RECENT_SELECTED_PATH, defaultDirectory)
tfRefactoringRunCount.text =
PropertiesComponent.getInstance().getValue(RECENT_SELECTED_RUN_COUNT, "1")
.toIntOrNull()
?.let { if (it < 1) "1" else it.toString() }
validateOKButton()
}
private fun validateOKButton() {
val isCorrectCount = tfRefactoringRunCount.text.toIntOrNull()?.let { it > 0 } ?: false
if (!isCorrectCount) {
isOKActionEnabled = false
return
}
val isCorrectPath = tfTargetDirectory.childComponent.text
?.let { it.isNotEmpty() && File(it).let { path -> path.exists() && path.isDirectory } }
?: false
isOKActionEnabled = isCorrectPath
}
val selectedDirectoryName get() = tfTargetDirectory.childComponent.text!!
val selectedCount get() = tfRefactoringRunCount.text.toInt()
override fun doOKAction() {
PropertiesComponent.getInstance().setValue(RECENT_SELECTED_PATH, selectedDirectoryName)
PropertiesComponent.getInstance().setValue(RECENT_SELECTED_RUN_COUNT, tfRefactoringRunCount.text)
close(OK_EXIT_CODE, /* isOk = */ true)
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
import com.intellij.ide.plugins.PluginManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.gradle.getMethodOrNull
internal fun <T> lazyPub(initializer: () -> T) = lazy(LazyThreadSafetyMode.PUBLICATION, initializer)
internal fun gitReset(project: Project, projectRoot: VirtualFile) {
fun ClassLoader.loadClassOrThrow(name: String) =
loadClass(name) ?: error("$name not loaded")
fun Class<*>.loadMethodOrThrow(name: String, vararg arguments: Class<*>) =
getMethodOrNull(name, *arguments) ?: error("${this.name}::$name not loaded")
val loader = PluginManager.getPlugin(PluginId.getId("Git4Idea"))?.pluginClassLoader ?: error("Git plugin is not found")
val gitCls = loader.loadClassOrThrow("git4idea.commands.Git")
val gitLineHandlerCls = loader.loadClassOrThrow("git4idea.commands.GitLineHandler")
val gitCommandCls = loader.loadClassOrThrow("git4idea.commands.GitCommand")
val gitCommandResultCls = loader.loadClassOrThrow("git4idea.commands.GitCommandResult")
val gitLineHandlerCtor = gitLineHandlerCls.getConstructor(Project::class.java, VirtualFile::class.java, gitCommandCls) ?: error(
"git4idea.commands.GitLineHandler::ctor not loaded"
)
val runCommand = gitCls.loadMethodOrThrow("runCommand", gitLineHandlerCls)
val getExitCode = gitCommandResultCls.loadMethodOrThrow("getExitCode")
val gitLineHandlerAddParameters = gitLineHandlerCls.loadMethodOrThrow("addParameters", List::class.java)
val gitCommandReset = gitCommandCls.getField("RESET")?.get(null) ?: error("git4idea.commands.GitCommand.RESET not loaded")
val resetLineHandler = gitLineHandlerCtor.newInstance(project, projectRoot, gitCommandReset)
gitLineHandlerAddParameters.invoke(resetLineHandler, listOf("--hard", "HEAD"))
val gitService = ServiceManager.getService(gitCls)
val runCommandResult = runCommand.invoke(gitService, resetLineHandler)
val gitResetResultCode = getExitCode.invoke(runCommandResult) as Int
if (gitResetResultCode == 0) {
VfsUtil.markDirtyAndRefresh(false, true, false, projectRoot)
//GitRepositoryManager.getInstance(project).updateRepository(d.getGitRoot())
} else {
error("Git reset failed")
}
}
inline fun edtExecute(crossinline body: () -> Unit) {
ApplicationManager.getApplication().invokeAndWait {
body()
}
}
inline fun readAction(crossinline body: () -> Unit) {
ApplicationManager.getApplication().runReadAction {
body()
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
import com.intellij.openapi.project.Project
internal sealed class RandomMoveRefactoringResult {
internal class Success(val caseData: String) : RandomMoveRefactoringResult()
internal class ExceptionCaused(val caseData: String, val message: String) : RandomMoveRefactoringResult()
internal companion object Failed : RandomMoveRefactoringResult()
}
internal interface RefactoringCase {
fun tryCreateAndRun(project: Project): RandomMoveRefactoringResult
}
internal fun RefactoringCase.tryCreateAndRun(project: Project, refactoringCountBeforeCheck: Int): RandomMoveRefactoringResult {
require(refactoringCountBeforeCheck >= 1) { "refactoringCountBeforeCheck must be greater than zero" }
if (refactoringCountBeforeCheck == 1)
return tryCreateAndRun(project)
var maxTries = refactoringCountBeforeCheck * 10
var exception: RandomMoveRefactoringResult.ExceptionCaused? = null
val refactoringSequence = generateSequence {
if (maxTries-- < 0) return@generateSequence null
val refactoringResult = tryCreateAndRun(project)
if (refactoringResult is RandomMoveRefactoringResult.ExceptionCaused) {
exception = refactoringResult
return@generateSequence null
}
refactoringResult
}
val caseHistory = refactoringSequence
.filterIsInstance<RandomMoveRefactoringResult.Success>()
.take(refactoringCountBeforeCheck)
.map { it.caseData }
.foldIndexed("") { index, acc, argument -> "$acc\n\nRefactoring #${index + 1}:\n$argument" }
exception?.run {
val exceptionCaseData =
if (caseHistory.isNotEmpty()) "Refactorings sequence:\n$caseHistory\ncaused an exception on further refactoring\n$caseData"
else caseData
RandomMoveRefactoringResult.ExceptionCaused(exceptionCaseData, message)
}?.let { return it }
return if (caseHistory.isNotEmpty()) RandomMoveRefactoringResult.Success(caseHistory)
else RandomMoveRefactoringResult.Failed
}
@@ -0,0 +1,232 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.cases
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileSystemItem
import com.intellij.refactoring.move.MoveCallback
import com.intellij.refactoring.move.MoveHandler
import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsHandlerActions
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesModel
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesModel
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesToUpperLevelModel
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsModel
import org.jetbrains.kotlin.idea.util.application.executeCommand
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
internal data class TestDataKeeper(var caseData: String)
internal class FailedToRunCaseException : Exception()
internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeeper: TestDataKeeper) : MoveKotlinDeclarationsHandlerActions {
override fun showErrorHint(project: Project, editor: Editor?, message: String, title: String, helpId: String?) {
throw FailedToRunCaseException()
}
private fun MoveKotlinNestedClassesModel.testDataString(): String {
return "MoveKotlinNestedClassesModel:" +
"\nopenInEditorCheckBox = $openInEditorCheckBox\n" +
"selectedElementsToMove + ${selectedElementsToMove.joinToString { it.getKotlinFqName().toString() }}\n" +
"targetClass = ${targetClass?.getKotlinFqName() ?: "null"}"
}
private fun MoveKotlinNestedClassesToUpperLevelModel.testDataString(): String {
return "MoveKotlinNestedClassesToUpperLevelModel:\n" +
"innerClass = ${innerClass.name}\n" +
"target = ${target.getKotlinFqName()}\n" +
"parameter = ${parameter ?: "null"}\n" +
"className = $className\n" +
"passOuterClass = $passOuterClass\n" +
"searchInComments = $searchInComments\n" +
"isSearchInNonJavaFiles = $isSearchInNonJavaFiles\n" +
"packageName = $packageName\n"
}
private fun MoveKotlinTopLevelDeclarationsModel.testDataString(): String {
return "MoveKotlinTopLevelDeclarationsModel:\n" +
"elementsToMove = ${elementsToMove.joinToString { it.getKotlinFqName().toString() }}\n" +
"targetPackage = $targetPackage\n" +
"selectedPsiDirectory = ${selectedPsiDirectory ?: "null"}\n" +
"fileNameInPackage = $fileNameInPackage\n" +
"targetFilePath = $targetFilePath\n" +
"isMoveToPackage = $isMoveToPackage\n" +
"isSearchReferences = $isSearchReferences\n" +
"isSearchInComments = $isSearchInComments\n" +
"isSearchInNonJavaFiles = $isSearchInNonJavaFiles\n" +
"isDeleteEmptyFiles = $isDeleteEmptyFiles\n" +
"isUpdatePackageDirective = $isUpdatePackageDirective\n" +
"isFullFileMove = $isFullFileMove"
}
private fun KotlinAwareMoveFilesOrDirectoriesModel.testDataString(): String {
return "KotlinAwareMoveFilesOrDirectoriesModel:\n" +
"elementsToMove = ${elementsToMove.joinToString { if (it is PsiFileSystemItem) it.virtualFile.path else it.javaClass.name }}\n" +
"directoryName = $targetDirectoryName\n" +
"updatePackageDirective = $updatePackageDirective\n" +
"searchReferences = $searchReferences"
}
private fun doWithMoveKotlinNestedClassesModel(nestedClass: KtClassOrObject, targetContainer: PsiElement?) {
val containingClassOrObject = nestedClass.containingClassOrObject!!
val elementsToMove = containingClassOrObject.declarations.mapNotNull { declaration ->
if (declaration !is KtClassOrObject) return@mapNotNull null
if (declaration is KtClass && declaration.isInner()) return@mapNotNull null
if (declaration is KtObjectDeclaration && declaration.isCompanion()) return@mapNotNull null
declaration as KtClassOrObject
}
val selectedElements = mutableSetOf<KtClassOrObject>()
repeat(elementsToMove.size) {
selectedElements.add(elementsToMove.random())
}
val model = MoveKotlinNestedClassesModel(
project = nestedClass.project,
openInEditorCheckBox = false,
selectedElementsToMove = selectedElements.toList(),
originalClass = containingClassOrObject,
targetClass = targetContainer,
moveCallback = null
)
caseDataKeeper.caseData = model.testDataString()
model.computeModelResult(throwOnConflicts = true).run()
}
private fun doWithMoveKotlinNestedClassesToUpperLevelModel(nestedClass: KtClassOrObject, targetContainer: PsiElement?) {
val outerClass = nestedClass.containingClassOrObject ?: throw FailedToRunCaseException()
val newTarget = targetContainer
?: outerClass.containingClassOrObject
?: outerClass.containingFile.let { it.containingDirectory ?: it }
val model = MoveKotlinNestedClassesToUpperLevelModel(
project = nestedClass.project,
innerClass = nestedClass,
target = newTarget,
parameter = randomNullability { randomString() },
className = randomClassName(),
passOuterClass = randomBoolean(),
searchInComments = randomBoolean(),
isSearchInNonJavaFiles = randomBoolean(),
packageName = randomClassName(),
isOpenInEditor = false
)
caseDataKeeper.caseData = model.testDataString()
model.computeModelResult(throwOnConflicts = true).run()
}
override fun invokeMoveKotlinNestedClassesRefactoring(
project: Project,
elementsToMove: List<KtClassOrObject>,
originalClass: KtClassOrObject,
targetClass: KtClassOrObject,
moveCallback: MoveCallback?
) = throw NotImplementedError()
override fun invokeMoveKotlinTopLevelDeclarationsRefactoring(
project: Project,
elementsToMove: Set<KtNamedDeclaration>,
targetPackageName: String,
targetDirectory: PsiDirectory?,
targetFile: KtFile?,
moveToPackage: Boolean,
searchInComments: Boolean,
searchForTextOccurrences: Boolean,
deleteEmptySourceFiles: Boolean,
moveCallback: MoveCallback?
) {
val selectedElementsToMove = mutableSetOf<KtNamedDeclaration>()
repeat(elementsToMove.size) {
selectedElementsToMove.add(elementsToMove.random())
}
val targetFilePath = targetFile?.virtualFile?.path
?: selectedElementsToMove.firstOrNull()?.containingKtFile?.virtualFile?.path ?: throw NotImplementedError()
val mutatedPath = randomFilePathMutator(targetFilePath, ".kt")
val model = MoveKotlinTopLevelDeclarationsModel(
project = project,
elementsToMove = selectedElementsToMove.toList(),
targetPackage = randomFunctor(0.5F, { targetPackageName }, { randomClassName() }),
selectedPsiDirectory = randomNullability { targetDirectory },
fileNameInPackage = randomFileNameMutator("hello.kt", ".kt"),
targetFilePath = mutatedPath,
isMoveToPackage = randomBoolean(),
isSearchReferences = true,
isSearchInComments = randomBoolean(),
isSearchInNonJavaFiles = randomBoolean(),
isDeleteEmptyFiles = randomBoolean(),
isUpdatePackageDirective = randomBoolean(),
isFullFileMove = randomBoolean(),
moveCallback = null
)
caseDataKeeper.caseData = model.testDataString()
model.computeModelResult(throwOnConflicts = true).run()
}
override fun invokeKotlinSelectNestedClassChooser(nestedClass: KtClassOrObject, targetContainer: PsiElement?) {
when {
targetContainer != null && targetContainer !is KtClassOrObject || nestedClass is KtClass && nestedClass.isInner() -> {
doWithMoveKotlinNestedClassesToUpperLevelModel(nestedClass, targetContainer)
}
nestedClass is KtEnumEntry -> return
else -> {
if (randomBoolean()) {
doWithMoveKotlinNestedClassesToUpperLevelModel(
nestedClass = nestedClass,
targetContainer = targetContainer
)
} else {
doWithMoveKotlinNestedClassesModel(
nestedClass = nestedClass,
targetContainer = targetContainer
)
}
}
}
}
override fun invokeKotlinAwareMoveFilesOrDirectoriesRefactoring(
project: Project,
initialDirectory: PsiDirectory?,
elements: Array<out PsiElement>,
moveCallback: MoveCallback?
) {
val targetPath =
initialDirectory?.virtualFile?.path
?: elements.firstOrNull()?.containingFile?.virtualFile?.path
?: throw NotImplementedError()
val model = KotlinAwareMoveFilesOrDirectoriesModel(
project = project,
elementsToMove = elements.toList(),
targetDirectoryName = randomDirectoryPathMutator(targetPath),
updatePackageDirective = randomBoolean(),
searchReferences = randomBoolean(),
moveCallback = null
)
caseDataKeeper.caseData = model.testDataString()
project.executeCommand(MoveHandler.REFACTORING_NAME) {
model.computeModelResult().run()
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.cases;
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.project.Project
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.RefactoringCase
import org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.RandomMoveRefactoringResult
import org.jetbrains.kotlin.idea.core.util.toPsiFile
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsHandler
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.RefactoringConflictsFoundException
import org.jetbrains.kotlin.psi.KtClass
internal class MoveRefactoringCase : RefactoringCase {
override fun tryCreateAndRun(project: Project): RandomMoveRefactoringResult {
val projectFiles = project.files()
if (projectFiles.isEmpty()) RandomMoveRefactoringResult.Failed
fun getRandomKotlinClassOrNull(): KtClass? {
val classes = PsiTreeUtil.collectElementsOfType(projectFiles.random().toPsiFile(project), KtClass::class.java)
return if (classes.isNotEmpty()) return classes.random() else null
}
val targetClass: KtClass? = null
val sourceClass = getRandomKotlinClassOrNull() ?: return RandomMoveRefactoringResult.Failed
val sourceClassAsArray = arrayOf(sourceClass)
val testDataKeeper = TestDataKeeper("no data")
val handler = MoveKotlinDeclarationsHandler(MoveKotlinDeclarationsHandlerTestActions(testDataKeeper))
if (!handler.canMove(sourceClassAsArray, targetClass)) {
return RandomMoveRefactoringResult.Failed
}
try {
handler.doMove(project, sourceClassAsArray, targetClass, null)
} catch (e: Throwable) {
return when (e) {
is NotImplementedError,
is FailedToRunCaseException,
is RefactoringConflictsFoundException,
is ConfigurationException -> RandomMoveRefactoringResult.Failed
else -> RandomMoveRefactoringResult.ExceptionCaused(
testDataKeeper.caseData,
"${e.javaClass.typeName}: ${e.message ?: "No message"}"
)
}
}
return RandomMoveRefactoringResult.Success(testDataKeeper.caseData)
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.cases
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.FileTypeIndex
import com.intellij.psi.search.ProjectScope
import org.apache.commons.lang.RandomStringUtils
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.idea.core.util.toPsiFile
import org.jetbrains.kotlin.psi.KtClass
import java.io.File
import kotlin.random.Random
internal fun <T> List<T>.randomElement(): T = this[Random.nextInt(0, this.size)]
internal fun <T : Any> List<T>.randomElementOrNull(): T? = if (isNotEmpty()) randomElement() else null
internal fun <T : Any> List<T>.toRandomSequence(): Sequence<T> = generateSequence { randomElementOrNull() }
internal fun <T> List<T>.randomElements(count: Int): List<T> =
mutableListOf<T>().also { list -> repeat(count) { list.add(randomElement()) } }
internal fun <T> List<T>.randomElements(): List<T> = randomElements(Random.nextInt(0, size))
internal fun getRandomFileClassElements(project: Project, ktFiles: List<VirtualFile>): List<KtClass> {
val randomSourceFile = ktFiles.randomElement()
return PsiTreeUtil.collectElementsOfType(randomSourceFile.toPsiFile(project), KtClass::class.java).toList()
}
internal fun randomBoolean() = Random.nextBoolean()
internal fun Project.files(): List<VirtualFile> {
val scope = KotlinSourceFilterScope.projectSources(ProjectScope.getContentScope(this), this)
return FileTypeIndex.getFiles(KotlinFileType.INSTANCE, scope).toList()
}
internal inline fun <T> randomNullability(body: () -> T) = randomFunctor(0.8F, body, { null })
internal fun randomString() = RandomStringUtils.randomAlphanumeric(Random.nextInt(1, 10))
internal fun randomClassName(): String {
var count = Random.nextInt(1, 10)
val strings = generateSequence {
count--
if (count > 0) randomString() else null
}
return strings.joinToString(".")
}
inline fun randomAction(probability: Float, body: () -> Unit) {
if (Random.nextFloat() <= probability) {
body()
}
}
inline fun <T> randomFunctor(probability: Float, `then`: () -> T, otherwise: () -> T): T {
return if (Random.nextFloat() <= probability) {
then()
} else {
otherwise()
}
}
inline fun randomActionFiftyFifty(body: () -> Unit) {
if (Random.nextBoolean()) body()
}
private fun mutatePath(path: File): File {
randomAction(0.2F) {
return path
}
var file = path
repeat(Random.nextInt(2)) {
file = file.parentFile
}
repeat(Random.nextInt(2)) {
file = File(file, randomString())
}
return file
}
internal fun randomFileNameMutator(fileName: String, preferableExtension: String): String {
randomAction(0.2F) {
return fileName
}
val extension = randomFunctor(
0.8F,
then = { preferableExtension },
otherwise = { ".${randomString()}" }
)
return "${randomString()}$extension"
}
internal fun randomFilePathMutator(filePath: String, preferableExtension: String): String {
randomAction(0.2F) {
return filePath
}
val file = File(filePath)
val mutatedDirectory = mutatePath(file.parentFile)
return File(
mutatedDirectory,
randomFileNameMutator(file.name, preferableExtension)
).absolutePath
}
internal fun randomDirectoryPathMutator(baseDirectory: String): String {
return mutatePath(File(baseDirectory)).absolutePath
}