diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml
index e8cc702e971..ebc9b17bf9b 100644
--- a/idea/resources/META-INF/plugin-common.xml
+++ b/idea/resources/META-INF/plugin-common.xml
@@ -112,6 +112,12 @@
+
+
+
+
Boolean): Boolean {
+
+ compilationStatusListener.reset()
+
+ ApplicationManager.getApplication().invokeAndWait {
+ runBuildAction()
+ }
+
+ while (!cancelledChecker() && !compilationStatusListener.compilationFinished) {
+ Thread.yield()
+ }
+
+ return !compilationStatusListener.hasCompilerError
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/FileSystemChangesTracker.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/FileSystemChangesTracker.kt
new file mode 100644
index 00000000000..56806b994ab
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/FileSystemChangesTracker.kt
@@ -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()
+
+ @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 = trackerListener.createdFilesMutable
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringAction.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringAction.kt
new file mode 100644
index 00000000000..47a814a6462
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringAction.kt
@@ -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()
+ }
+ }
+}
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringActionDialog.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringActionDialog.kt
new file mode 100644
index 00000000000..075df31d290
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringActionDialog.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringUtils.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringUtils.kt
new file mode 100644
index 00000000000..48baca51dbc
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringUtils.kt
@@ -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 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()
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/RefactoringCase.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/RefactoringCase.kt
new file mode 100644
index 00000000000..733d8e42409
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/RefactoringCase.kt
@@ -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()
+ .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
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt
new file mode 100644
index 00000000000..ac566f84cca
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt
@@ -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()
+ 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,
+ originalClass: KtClassOrObject,
+ targetClass: KtClassOrObject,
+ moveCallback: MoveCallback?
+ ) = throw NotImplementedError()
+
+ override fun invokeMoveKotlinTopLevelDeclarationsRefactoring(
+ project: Project,
+ elementsToMove: Set,
+ targetPackageName: String,
+ targetDirectory: PsiDirectory?,
+ targetFile: KtFile?,
+ moveToPackage: Boolean,
+ searchInComments: Boolean,
+ searchForTextOccurrences: Boolean,
+ deleteEmptySourceFiles: Boolean,
+ moveCallback: MoveCallback?
+ ) {
+ val selectedElementsToMove = mutableSetOf()
+ 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,
+ 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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveRefactoringCase.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveRefactoringCase.kt
new file mode 100644
index 00000000000..87a671587b8
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveRefactoringCase.kt
@@ -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)
+ }
+}
diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/RandomUtils.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/RandomUtils.kt
new file mode 100644
index 00000000000..ff8a65bff96
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/RandomUtils.kt
@@ -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 List.randomElement(): T = this[Random.nextInt(0, this.size)]
+
+internal fun List.randomElementOrNull(): T? = if (isNotEmpty()) randomElement() else null
+
+internal fun List.toRandomSequence(): Sequence = generateSequence { randomElementOrNull() }
+
+internal fun List.randomElements(count: Int): List =
+ mutableListOf().also { list -> repeat(count) { list.add(randomElement()) } }
+
+internal fun List.randomElements(): List = randomElements(Random.nextInt(0, size))
+
+internal fun getRandomFileClassElements(project: Project, ktFiles: List): List {
+ val randomSourceFile = ktFiles.randomElement()
+ return PsiTreeUtil.collectElementsOfType(randomSourceFile.toPsiFile(project), KtClass::class.java).toList()
+}
+
+internal fun randomBoolean() = Random.nextBoolean()
+
+internal fun Project.files(): List {
+ val scope = KotlinSourceFilterScope.projectSources(ProjectScope.getContentScope(this), this)
+ return FileTypeIndex.getFiles(KotlinFileType.INSTANCE, scope).toList()
+}
+
+internal inline fun 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 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
+}
\ No newline at end of file