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