From 63892891db9dcdd8513be54006ff56966e492c32 Mon Sep 17 00:00:00 2001 From: Igor Yakovlev Date: Tue, 20 Aug 2019 12:09:40 +0300 Subject: [PATCH] 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" --- idea/resources/META-INF/plugin-common.xml | 6 + .../CompilationStatusTracker.kt | 72 ++++++ .../FileSystemChangesTracker.kt | 58 +++++ .../MoveRefactoringAction.kt | 209 ++++++++++++++++ .../MoveRefactoringActionDialog.kt | 116 +++++++++ .../MoveRefactoringUtils.kt | 67 +++++ .../refactoringTesting/RefactoringCase.kt | 61 +++++ ...oveKotlinDeclarationsHandlerTestActions.kt | 232 ++++++++++++++++++ .../cases/MoveRefactoringCase.kt | 61 +++++ .../refactoringTesting/cases/RandomUtils.kt | 127 ++++++++++ 10 files changed, 1009 insertions(+) create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/CompilationStatusTracker.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/FileSystemChangesTracker.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringAction.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringActionDialog.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/MoveRefactoringUtils.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/RefactoringCase.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveRefactoringCase.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/RandomUtils.kt 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