Add internal move refactoring testing action
This action doing further steps in infinite loop (breaks after user request): 1) Generate random move refactoring model 2) Run this model (if it is correct) 3) Recompile the project 4) If compilation was not succeeded write model parameters into output file 5) Delete all new files and directories and make "git reset"
This commit is contained in:
@@ -112,6 +112,12 @@
|
||||
<action id="DumbModeTremble" class="org.jetbrains.kotlin.idea.actions.internal.DumbModeTrembleAction"
|
||||
text="Tremble Dumb Mode"/>
|
||||
|
||||
<group id="KotlinRefactoringTesting" text="Kotlin refactoring testing" popup="true">
|
||||
<action id="TestMoveRefactiringAction"
|
||||
class="org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.MoveRefactoringAction"
|
||||
text="Test move refactoring on opened project"/>
|
||||
</group>
|
||||
|
||||
<group id="KotlinInternalGroup" text="Kotlin" popup="true">
|
||||
<group id="KotlinCompletionBenchmarkGroup" popup="true" text="Benchmark completion">
|
||||
<action id="TopLevelCompletionBenchmarkAction"
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
|
||||
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.Presentation
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.compiler.CompilationStatusListener
|
||||
import com.intellij.openapi.compiler.CompileContext
|
||||
import com.intellij.openapi.compiler.CompilerTopics.COMPILATION_STATUS
|
||||
import com.intellij.openapi.project.Project
|
||||
|
||||
internal class CompilationStatusTracker(private val project: Project) {
|
||||
|
||||
private val compilationStatusListener = object : CompilationStatusListener {
|
||||
|
||||
init {
|
||||
project.messageBus.connect(project).subscribe(COMPILATION_STATUS, this)
|
||||
}
|
||||
|
||||
var hasCompilerError = false
|
||||
private set
|
||||
|
||||
var compilationFinished = false
|
||||
private set
|
||||
|
||||
fun reset() {
|
||||
compilationFinished = false
|
||||
hasCompilerError = false
|
||||
}
|
||||
|
||||
override fun compilationFinished(aborted: Boolean, errors: Int, warnings: Int, compileContext: CompileContext) {
|
||||
hasCompilerError = hasCompilerError || aborted || errors != 0
|
||||
compilationFinished = true
|
||||
}
|
||||
}
|
||||
|
||||
private val runBuildAction by lazyPub {
|
||||
val am = ActionManager.getInstance();
|
||||
val action = am.getAction("CompileProject")
|
||||
|
||||
val event = AnActionEvent(
|
||||
null,
|
||||
DataManager.getInstance().dataContext,
|
||||
ActionPlaces.UNKNOWN, Presentation(),
|
||||
ActionManager.getInstance(), 0
|
||||
)
|
||||
|
||||
return@lazyPub { action.actionPerformed(event) }
|
||||
}
|
||||
|
||||
fun checkByBuild(cancelledChecker: () -> Boolean): Boolean {
|
||||
|
||||
compilationStatusListener.reset()
|
||||
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
runBuildAction()
|
||||
}
|
||||
|
||||
while (!cancelledChecker() && !compilationStatusListener.compilationFinished) {
|
||||
Thread.yield()
|
||||
}
|
||||
|
||||
return !compilationStatusListener.hasCompilerError
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.vfs.*
|
||||
|
||||
internal class FileSystemChangesTracker(project: Project) : Disposable {
|
||||
|
||||
override fun dispose() {
|
||||
trackerListener.dispose()
|
||||
}
|
||||
|
||||
private val trackerListener = object : VirtualFileListener, Disposable {
|
||||
|
||||
val createdFilesMutable = mutableSetOf<VirtualFile>()
|
||||
|
||||
@Volatile
|
||||
private var disposed = false
|
||||
|
||||
init {
|
||||
VirtualFileManager.getInstance().addVirtualFileListener(this)
|
||||
Disposer.register(project, this)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun dispose() {
|
||||
if (!disposed) {
|
||||
disposed = true
|
||||
VirtualFileManager.getInstance().removeVirtualFileListener(this)
|
||||
createdFilesMutable.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun fileCreated(event: VirtualFileEvent) {
|
||||
createdFilesMutable.add(event.file)
|
||||
}
|
||||
|
||||
override fun fileCopied(event: VirtualFileCopyEvent) {
|
||||
createdFilesMutable.add(event.file)
|
||||
}
|
||||
|
||||
override fun fileMoved(event: VirtualFileMoveEvent) {
|
||||
createdFilesMutable.add(event.file)
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
trackerListener.createdFilesMutable.clear()
|
||||
}
|
||||
|
||||
val createdFiles: Set<VirtualFile> = trackerListener.createdFilesMutable
|
||||
}
|
||||
+209
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -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)
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
|
||||
|
||||
import com.intellij.ide.plugins.PluginManager
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.ServiceManager
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import org.jetbrains.kotlin.gradle.getMethodOrNull
|
||||
|
||||
internal fun <T> lazyPub(initializer: () -> T) = lazy(LazyThreadSafetyMode.PUBLICATION, initializer)
|
||||
|
||||
internal fun gitReset(project: Project, projectRoot: VirtualFile) {
|
||||
|
||||
fun ClassLoader.loadClassOrThrow(name: String) =
|
||||
loadClass(name) ?: error("$name not loaded")
|
||||
|
||||
fun Class<*>.loadMethodOrThrow(name: String, vararg arguments: Class<*>) =
|
||||
getMethodOrNull(name, *arguments) ?: error("${this.name}::$name not loaded")
|
||||
|
||||
val loader = PluginManager.getPlugin(PluginId.getId("Git4Idea"))?.pluginClassLoader ?: error("Git plugin is not found")
|
||||
|
||||
val gitCls = loader.loadClassOrThrow("git4idea.commands.Git")
|
||||
val gitLineHandlerCls = loader.loadClassOrThrow("git4idea.commands.GitLineHandler")
|
||||
val gitCommandCls = loader.loadClassOrThrow("git4idea.commands.GitCommand")
|
||||
val gitCommandResultCls = loader.loadClassOrThrow("git4idea.commands.GitCommandResult")
|
||||
val gitLineHandlerCtor = gitLineHandlerCls.getConstructor(Project::class.java, VirtualFile::class.java, gitCommandCls) ?: error(
|
||||
"git4idea.commands.GitLineHandler::ctor not loaded"
|
||||
)
|
||||
val runCommand = gitCls.loadMethodOrThrow("runCommand", gitLineHandlerCls)
|
||||
val getExitCode = gitCommandResultCls.loadMethodOrThrow("getExitCode")
|
||||
val gitLineHandlerAddParameters = gitLineHandlerCls.loadMethodOrThrow("addParameters", List::class.java)
|
||||
|
||||
val gitCommandReset = gitCommandCls.getField("RESET")?.get(null) ?: error("git4idea.commands.GitCommand.RESET not loaded")
|
||||
|
||||
val resetLineHandler = gitLineHandlerCtor.newInstance(project, projectRoot, gitCommandReset)
|
||||
gitLineHandlerAddParameters.invoke(resetLineHandler, listOf("--hard", "HEAD"))
|
||||
|
||||
val gitService = ServiceManager.getService(gitCls)
|
||||
val runCommandResult = runCommand.invoke(gitService, resetLineHandler)
|
||||
|
||||
val gitResetResultCode = getExitCode.invoke(runCommandResult) as Int
|
||||
if (gitResetResultCode == 0) {
|
||||
VfsUtil.markDirtyAndRefresh(false, true, false, projectRoot)
|
||||
//GitRepositoryManager.getInstance(project).updateRepository(d.getGitRoot())
|
||||
} else {
|
||||
error("Git reset failed")
|
||||
}
|
||||
}
|
||||
|
||||
inline fun edtExecute(crossinline body: () -> Unit) {
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
body()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun readAction(crossinline body: () -> Unit) {
|
||||
ApplicationManager.getApplication().runReadAction {
|
||||
body()
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
|
||||
internal sealed class RandomMoveRefactoringResult {
|
||||
internal class Success(val caseData: String) : RandomMoveRefactoringResult()
|
||||
internal class ExceptionCaused(val caseData: String, val message: String) : RandomMoveRefactoringResult()
|
||||
internal companion object Failed : RandomMoveRefactoringResult()
|
||||
}
|
||||
|
||||
internal interface RefactoringCase {
|
||||
fun tryCreateAndRun(project: Project): RandomMoveRefactoringResult
|
||||
}
|
||||
|
||||
internal fun RefactoringCase.tryCreateAndRun(project: Project, refactoringCountBeforeCheck: Int): RandomMoveRefactoringResult {
|
||||
|
||||
require(refactoringCountBeforeCheck >= 1) { "refactoringCountBeforeCheck must be greater than zero" }
|
||||
|
||||
if (refactoringCountBeforeCheck == 1)
|
||||
return tryCreateAndRun(project)
|
||||
|
||||
var maxTries = refactoringCountBeforeCheck * 10
|
||||
|
||||
var exception: RandomMoveRefactoringResult.ExceptionCaused? = null
|
||||
val refactoringSequence = generateSequence {
|
||||
|
||||
if (maxTries-- < 0) return@generateSequence null
|
||||
|
||||
val refactoringResult = tryCreateAndRun(project)
|
||||
|
||||
if (refactoringResult is RandomMoveRefactoringResult.ExceptionCaused) {
|
||||
exception = refactoringResult
|
||||
return@generateSequence null
|
||||
}
|
||||
|
||||
refactoringResult
|
||||
}
|
||||
|
||||
val caseHistory = refactoringSequence
|
||||
.filterIsInstance<RandomMoveRefactoringResult.Success>()
|
||||
.take(refactoringCountBeforeCheck)
|
||||
.map { it.caseData }
|
||||
.foldIndexed("") { index, acc, argument -> "$acc\n\nRefactoring #${index + 1}:\n$argument" }
|
||||
|
||||
|
||||
exception?.run {
|
||||
val exceptionCaseData =
|
||||
if (caseHistory.isNotEmpty()) "Refactorings sequence:\n$caseHistory\ncaused an exception on further refactoring\n$caseData"
|
||||
else caseData
|
||||
|
||||
RandomMoveRefactoringResult.ExceptionCaused(exceptionCaseData, message)
|
||||
}?.let { return it }
|
||||
|
||||
return if (caseHistory.isNotEmpty()) RandomMoveRefactoringResult.Success(caseHistory)
|
||||
else RandomMoveRefactoringResult.Failed
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.cases
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiDirectory
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
import com.intellij.refactoring.move.MoveCallback
|
||||
import com.intellij.refactoring.move.MoveHandler
|
||||
import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName
|
||||
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsHandlerActions
|
||||
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesModel
|
||||
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesModel
|
||||
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesToUpperLevelModel
|
||||
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsModel
|
||||
import org.jetbrains.kotlin.idea.util.application.executeCommand
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||
|
||||
internal data class TestDataKeeper(var caseData: String)
|
||||
|
||||
internal class FailedToRunCaseException : Exception()
|
||||
|
||||
internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeeper: TestDataKeeper) : MoveKotlinDeclarationsHandlerActions {
|
||||
|
||||
override fun showErrorHint(project: Project, editor: Editor?, message: String, title: String, helpId: String?) {
|
||||
throw FailedToRunCaseException()
|
||||
}
|
||||
|
||||
private fun MoveKotlinNestedClassesModel.testDataString(): String {
|
||||
return "MoveKotlinNestedClassesModel:" +
|
||||
"\nopenInEditorCheckBox = $openInEditorCheckBox\n" +
|
||||
"selectedElementsToMove + ${selectedElementsToMove.joinToString { it.getKotlinFqName().toString() }}\n" +
|
||||
"targetClass = ${targetClass?.getKotlinFqName() ?: "null"}"
|
||||
}
|
||||
|
||||
private fun MoveKotlinNestedClassesToUpperLevelModel.testDataString(): String {
|
||||
return "MoveKotlinNestedClassesToUpperLevelModel:\n" +
|
||||
"innerClass = ${innerClass.name}\n" +
|
||||
"target = ${target.getKotlinFqName()}\n" +
|
||||
"parameter = ${parameter ?: "null"}\n" +
|
||||
"className = $className\n" +
|
||||
"passOuterClass = $passOuterClass\n" +
|
||||
"searchInComments = $searchInComments\n" +
|
||||
"isSearchInNonJavaFiles = $isSearchInNonJavaFiles\n" +
|
||||
"packageName = $packageName\n"
|
||||
}
|
||||
|
||||
private fun MoveKotlinTopLevelDeclarationsModel.testDataString(): String {
|
||||
return "MoveKotlinTopLevelDeclarationsModel:\n" +
|
||||
"elementsToMove = ${elementsToMove.joinToString { it.getKotlinFqName().toString() }}\n" +
|
||||
"targetPackage = $targetPackage\n" +
|
||||
"selectedPsiDirectory = ${selectedPsiDirectory ?: "null"}\n" +
|
||||
"fileNameInPackage = $fileNameInPackage\n" +
|
||||
"targetFilePath = $targetFilePath\n" +
|
||||
"isMoveToPackage = $isMoveToPackage\n" +
|
||||
"isSearchReferences = $isSearchReferences\n" +
|
||||
"isSearchInComments = $isSearchInComments\n" +
|
||||
"isSearchInNonJavaFiles = $isSearchInNonJavaFiles\n" +
|
||||
"isDeleteEmptyFiles = $isDeleteEmptyFiles\n" +
|
||||
"isUpdatePackageDirective = $isUpdatePackageDirective\n" +
|
||||
"isFullFileMove = $isFullFileMove"
|
||||
}
|
||||
|
||||
private fun KotlinAwareMoveFilesOrDirectoriesModel.testDataString(): String {
|
||||
return "KotlinAwareMoveFilesOrDirectoriesModel:\n" +
|
||||
"elementsToMove = ${elementsToMove.joinToString { if (it is PsiFileSystemItem) it.virtualFile.path else it.javaClass.name }}\n" +
|
||||
"directoryName = $targetDirectoryName\n" +
|
||||
"updatePackageDirective = $updatePackageDirective\n" +
|
||||
"searchReferences = $searchReferences"
|
||||
}
|
||||
|
||||
private fun doWithMoveKotlinNestedClassesModel(nestedClass: KtClassOrObject, targetContainer: PsiElement?) {
|
||||
|
||||
val containingClassOrObject = nestedClass.containingClassOrObject!!
|
||||
|
||||
val elementsToMove = containingClassOrObject.declarations.mapNotNull { declaration ->
|
||||
if (declaration !is KtClassOrObject) return@mapNotNull null
|
||||
if (declaration is KtClass && declaration.isInner()) return@mapNotNull null
|
||||
if (declaration is KtObjectDeclaration && declaration.isCompanion()) return@mapNotNull null
|
||||
declaration as KtClassOrObject
|
||||
}
|
||||
|
||||
val selectedElements = mutableSetOf<KtClassOrObject>()
|
||||
repeat(elementsToMove.size) {
|
||||
selectedElements.add(elementsToMove.random())
|
||||
}
|
||||
|
||||
val model = MoveKotlinNestedClassesModel(
|
||||
project = nestedClass.project,
|
||||
openInEditorCheckBox = false,
|
||||
selectedElementsToMove = selectedElements.toList(),
|
||||
originalClass = containingClassOrObject,
|
||||
targetClass = targetContainer,
|
||||
moveCallback = null
|
||||
)
|
||||
|
||||
caseDataKeeper.caseData = model.testDataString()
|
||||
|
||||
model.computeModelResult(throwOnConflicts = true).run()
|
||||
}
|
||||
|
||||
private fun doWithMoveKotlinNestedClassesToUpperLevelModel(nestedClass: KtClassOrObject, targetContainer: PsiElement?) {
|
||||
|
||||
val outerClass = nestedClass.containingClassOrObject ?: throw FailedToRunCaseException()
|
||||
val newTarget = targetContainer
|
||||
?: outerClass.containingClassOrObject
|
||||
?: outerClass.containingFile.let { it.containingDirectory ?: it }
|
||||
|
||||
val model = MoveKotlinNestedClassesToUpperLevelModel(
|
||||
project = nestedClass.project,
|
||||
innerClass = nestedClass,
|
||||
target = newTarget,
|
||||
parameter = randomNullability { randomString() },
|
||||
className = randomClassName(),
|
||||
passOuterClass = randomBoolean(),
|
||||
searchInComments = randomBoolean(),
|
||||
isSearchInNonJavaFiles = randomBoolean(),
|
||||
packageName = randomClassName(),
|
||||
isOpenInEditor = false
|
||||
)
|
||||
|
||||
caseDataKeeper.caseData = model.testDataString()
|
||||
|
||||
model.computeModelResult(throwOnConflicts = true).run()
|
||||
}
|
||||
|
||||
override fun invokeMoveKotlinNestedClassesRefactoring(
|
||||
project: Project,
|
||||
elementsToMove: List<KtClassOrObject>,
|
||||
originalClass: KtClassOrObject,
|
||||
targetClass: KtClassOrObject,
|
||||
moveCallback: MoveCallback?
|
||||
) = throw NotImplementedError()
|
||||
|
||||
override fun invokeMoveKotlinTopLevelDeclarationsRefactoring(
|
||||
project: Project,
|
||||
elementsToMove: Set<KtNamedDeclaration>,
|
||||
targetPackageName: String,
|
||||
targetDirectory: PsiDirectory?,
|
||||
targetFile: KtFile?,
|
||||
moveToPackage: Boolean,
|
||||
searchInComments: Boolean,
|
||||
searchForTextOccurrences: Boolean,
|
||||
deleteEmptySourceFiles: Boolean,
|
||||
moveCallback: MoveCallback?
|
||||
) {
|
||||
val selectedElementsToMove = mutableSetOf<KtNamedDeclaration>()
|
||||
repeat(elementsToMove.size) {
|
||||
selectedElementsToMove.add(elementsToMove.random())
|
||||
}
|
||||
val targetFilePath = targetFile?.virtualFile?.path
|
||||
?: selectedElementsToMove.firstOrNull()?.containingKtFile?.virtualFile?.path ?: throw NotImplementedError()
|
||||
|
||||
val mutatedPath = randomFilePathMutator(targetFilePath, ".kt")
|
||||
|
||||
val model = MoveKotlinTopLevelDeclarationsModel(
|
||||
project = project,
|
||||
elementsToMove = selectedElementsToMove.toList(),
|
||||
targetPackage = randomFunctor(0.5F, { targetPackageName }, { randomClassName() }),
|
||||
selectedPsiDirectory = randomNullability { targetDirectory },
|
||||
fileNameInPackage = randomFileNameMutator("hello.kt", ".kt"),
|
||||
targetFilePath = mutatedPath,
|
||||
isMoveToPackage = randomBoolean(),
|
||||
isSearchReferences = true,
|
||||
isSearchInComments = randomBoolean(),
|
||||
isSearchInNonJavaFiles = randomBoolean(),
|
||||
isDeleteEmptyFiles = randomBoolean(),
|
||||
isUpdatePackageDirective = randomBoolean(),
|
||||
isFullFileMove = randomBoolean(),
|
||||
moveCallback = null
|
||||
)
|
||||
|
||||
caseDataKeeper.caseData = model.testDataString()
|
||||
|
||||
model.computeModelResult(throwOnConflicts = true).run()
|
||||
}
|
||||
|
||||
override fun invokeKotlinSelectNestedClassChooser(nestedClass: KtClassOrObject, targetContainer: PsiElement?) {
|
||||
when {
|
||||
targetContainer != null && targetContainer !is KtClassOrObject || nestedClass is KtClass && nestedClass.isInner() -> {
|
||||
doWithMoveKotlinNestedClassesToUpperLevelModel(nestedClass, targetContainer)
|
||||
}
|
||||
nestedClass is KtEnumEntry -> return
|
||||
else -> {
|
||||
if (randomBoolean()) {
|
||||
doWithMoveKotlinNestedClassesToUpperLevelModel(
|
||||
nestedClass = nestedClass,
|
||||
targetContainer = targetContainer
|
||||
)
|
||||
} else {
|
||||
doWithMoveKotlinNestedClassesModel(
|
||||
nestedClass = nestedClass,
|
||||
targetContainer = targetContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invokeKotlinAwareMoveFilesOrDirectoriesRefactoring(
|
||||
project: Project,
|
||||
initialDirectory: PsiDirectory?,
|
||||
elements: Array<out PsiElement>,
|
||||
moveCallback: MoveCallback?
|
||||
) {
|
||||
val targetPath =
|
||||
initialDirectory?.virtualFile?.path
|
||||
?: elements.firstOrNull()?.containingFile?.virtualFile?.path
|
||||
?: throw NotImplementedError()
|
||||
|
||||
val model = KotlinAwareMoveFilesOrDirectoriesModel(
|
||||
project = project,
|
||||
elementsToMove = elements.toList(),
|
||||
targetDirectoryName = randomDirectoryPathMutator(targetPath),
|
||||
updatePackageDirective = randomBoolean(),
|
||||
searchReferences = randomBoolean(),
|
||||
moveCallback = null
|
||||
)
|
||||
|
||||
caseDataKeeper.caseData = model.testDataString()
|
||||
|
||||
project.executeCommand(MoveHandler.REFACTORING_NAME) {
|
||||
model.computeModelResult().run()
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -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)
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.actions.internal.refactoringTesting.cases
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.search.FileTypeIndex
|
||||
import com.intellij.psi.search.ProjectScope
|
||||
import org.apache.commons.lang.RandomStringUtils
|
||||
import org.jetbrains.kotlin.idea.KotlinFileType
|
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import org.jetbrains.kotlin.idea.core.util.toPsiFile
|
||||
import org.jetbrains.kotlin.psi.KtClass
|
||||
import java.io.File
|
||||
import kotlin.random.Random
|
||||
|
||||
internal fun <T> List<T>.randomElement(): T = this[Random.nextInt(0, this.size)]
|
||||
|
||||
internal fun <T : Any> List<T>.randomElementOrNull(): T? = if (isNotEmpty()) randomElement() else null
|
||||
|
||||
internal fun <T : Any> List<T>.toRandomSequence(): Sequence<T> = generateSequence { randomElementOrNull() }
|
||||
|
||||
internal fun <T> List<T>.randomElements(count: Int): List<T> =
|
||||
mutableListOf<T>().also { list -> repeat(count) { list.add(randomElement()) } }
|
||||
|
||||
internal fun <T> List<T>.randomElements(): List<T> = randomElements(Random.nextInt(0, size))
|
||||
|
||||
internal fun getRandomFileClassElements(project: Project, ktFiles: List<VirtualFile>): List<KtClass> {
|
||||
val randomSourceFile = ktFiles.randomElement()
|
||||
return PsiTreeUtil.collectElementsOfType(randomSourceFile.toPsiFile(project), KtClass::class.java).toList()
|
||||
}
|
||||
|
||||
internal fun randomBoolean() = Random.nextBoolean()
|
||||
|
||||
internal fun Project.files(): List<VirtualFile> {
|
||||
val scope = KotlinSourceFilterScope.projectSources(ProjectScope.getContentScope(this), this)
|
||||
return FileTypeIndex.getFiles(KotlinFileType.INSTANCE, scope).toList()
|
||||
}
|
||||
|
||||
internal inline fun <T> randomNullability(body: () -> T) = randomFunctor(0.8F, body, { null })
|
||||
|
||||
internal fun randomString() = RandomStringUtils.randomAlphanumeric(Random.nextInt(1, 10))
|
||||
|
||||
internal fun randomClassName(): String {
|
||||
var count = Random.nextInt(1, 10)
|
||||
val strings = generateSequence {
|
||||
count--
|
||||
if (count > 0) randomString() else null
|
||||
}
|
||||
return strings.joinToString(".")
|
||||
}
|
||||
|
||||
inline fun randomAction(probability: Float, body: () -> Unit) {
|
||||
if (Random.nextFloat() <= probability) {
|
||||
body()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> randomFunctor(probability: Float, `then`: () -> T, otherwise: () -> T): T {
|
||||
return if (Random.nextFloat() <= probability) {
|
||||
then()
|
||||
} else {
|
||||
otherwise()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun randomActionFiftyFifty(body: () -> Unit) {
|
||||
if (Random.nextBoolean()) body()
|
||||
}
|
||||
|
||||
|
||||
private fun mutatePath(path: File): File {
|
||||
|
||||
randomAction(0.2F) {
|
||||
return path
|
||||
}
|
||||
|
||||
var file = path
|
||||
|
||||
repeat(Random.nextInt(2)) {
|
||||
file = file.parentFile
|
||||
}
|
||||
|
||||
repeat(Random.nextInt(2)) {
|
||||
file = File(file, randomString())
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
internal fun randomFileNameMutator(fileName: String, preferableExtension: String): String {
|
||||
|
||||
randomAction(0.2F) {
|
||||
return fileName
|
||||
}
|
||||
|
||||
val extension = randomFunctor(
|
||||
0.8F,
|
||||
then = { preferableExtension },
|
||||
otherwise = { ".${randomString()}" }
|
||||
)
|
||||
|
||||
return "${randomString()}$extension"
|
||||
}
|
||||
|
||||
internal fun randomFilePathMutator(filePath: String, preferableExtension: String): String {
|
||||
randomAction(0.2F) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
val file = File(filePath)
|
||||
|
||||
val mutatedDirectory = mutatePath(file.parentFile)
|
||||
|
||||
return File(
|
||||
mutatedDirectory,
|
||||
randomFileNameMutator(file.name, preferableExtension)
|
||||
).absolutePath
|
||||
}
|
||||
|
||||
internal fun randomDirectoryPathMutator(baseDirectory: String): String {
|
||||
return mutatePath(File(baseDirectory)).absolutePath
|
||||
}
|
||||
Reference in New Issue
Block a user