diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt index fe673af397..76dab3a430 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt @@ -28,15 +28,14 @@ import org.utbot.intellij.plugin.ui.utils.findFrameworkLibrary import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath import org.utbot.intellij.plugin.ui.utils.kotlinTargetPlatform import org.utbot.intellij.plugin.ui.utils.parseVersion -import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots import org.utbot.intellij.plugin.ui.utils.testResourceRootTypes +import org.utbot.intellij.plugin.ui.utils.addSourceRootIfAbsent import org.utbot.intellij.plugin.ui.utils.testRootType import com.intellij.ide.impl.ProjectNewWindowDoNotAskOption import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service import com.intellij.openapi.options.ShowSettingsUtil -import com.intellij.openapi.projectRoots.JavaSdkVersion import com.intellij.openapi.roots.ContentEntry import com.intellij.openapi.roots.DependencyScope import com.intellij.openapi.roots.ExternalLibraryDescriptor @@ -48,10 +47,12 @@ import com.intellij.openapi.ui.DialogPanel import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.ui.popup.IconButton +import com.intellij.openapi.util.Computable +import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore.urlToPath import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.psi.PsiClass import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod @@ -74,14 +75,9 @@ import com.intellij.ui.layout.Row import com.intellij.ui.layout.panel import com.intellij.util.IncorrectOperationException import com.intellij.util.io.exists -import com.intellij.util.lang.JavaVersion -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.JBUI.Borders.empty -import com.intellij.util.ui.JBUI.Borders.merge -import com.intellij.util.ui.JBUI.scale import com.intellij.util.ui.JBUI.size +import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil -import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout import java.nio.file.Files import java.nio.file.Path @@ -274,16 +270,16 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m private fun getTestRoot() : VirtualFile? { model.testSourceRoot?.let { - if (it.isDirectory) return it + if (it.isDirectory || it is FakeVirtualFile) return it } return null } override fun doValidate(): ValidationInfo? { - if (getTestRoot() == null) { - return ValidationInfo("Test source root is not configured", testSourceFolderField.childComponent) - } - if (getRootDirectoryAndContentEntry() == null) { + val testRoot = getTestRoot() + ?: return ValidationInfo("Test source root is not configured", testSourceFolderField.childComponent) + + if (findReadOnlyContentEntry(testRoot) == null) { return ValidationInfo("Test source root is located out of content entry", testSourceFolderField.childComponent) } @@ -373,18 +369,33 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m * Creates test source root if absent and target packages for tests. */ private fun createTestRootAndPackages(): Boolean { - val (sourceRoot, contentEntry) = getRootDirectoryAndContentEntry() ?: return false - val modifiableModel = ModuleRootManager.getInstance(model.testModule).modifiableModel - VfsUtil.createDirectoryIfMissing(urlToPath(sourceRoot.url)) - contentEntry.addSourceFolder(sourceRoot.url, codegenLanguages.item.testRootType()) - WriteCommandAction.runWriteCommandAction(model.project) { modifiableModel.commit() } + model.testSourceRoot = createDirectoryIfMissing(model.testSourceRoot) + val testSourceRoot = model.testSourceRoot ?: return false + if (model.testSourceRoot?.isDirectory != true) return false + if (getOrCreateTestRoot(testSourceRoot)) { + if (cbSpecifyTestPackage.isSelected) { + createSelectedPackage(testSourceRoot) + } else { + createPackagesByClasses(testSourceRoot) + } + return true + } + return false + } - if (cbSpecifyTestPackage.isSelected) { - createSelectedPackage(sourceRoot) + private fun createDirectoryIfMissing(dir : VirtualFile?): VirtualFile? { + val file = if (dir is FakeVirtualFile) { + WriteCommandAction.runWriteCommandAction(model.project, Computable { + VfsUtil.createDirectoryIfMissing(dir.path) + }) } else { - createPackagesByClasses(sourceRoot) + dir + }?: return null + return if (VfsUtil.virtualToIoFile(file).isFile) { + null + } else { + StandardFileSystems.local().findFileByPath(file.path) } - return true } private fun createPackagesByClasses(testSourceRoot: VirtualFile) { @@ -413,12 +424,33 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m "Generation error" ) - private fun getRootDirectoryAndContentEntry() : Pair? { - val testSourceRoot = getTestRoot()?: return null - val contentEntry = ModuleRootManager.getInstance(model.testModule).contentEntries + private fun findReadOnlyContentEntry(testSourceRoot: VirtualFile?): ContentEntry? { + if (testSourceRoot == null) return null + if (testSourceRoot is FakeVirtualFile) { + return findReadOnlyContentEntry(testSourceRoot.parent) + } + return ModuleRootManager.getInstance(model.testModule).contentEntries .filterNot { it.file == null } - .firstOrNull { VfsUtil.isAncestor(it.file!!, testSourceRoot, true) } ?: return null - return Pair(testSourceRoot, contentEntry) + .firstOrNull { VfsUtil.isAncestor(it.file!!, testSourceRoot, false) } + } + + private fun getOrCreateTestRoot(testSourceRoot: VirtualFile): Boolean { + val modifiableModel = ModuleRootManager.getInstance(model.testModule).modifiableModel + try { + val contentEntry = modifiableModel.contentEntries + .filterNot { it.file == null } + .firstOrNull { VfsUtil.isAncestor(it.file!!, testSourceRoot, true) } + ?: return false + + contentEntry.addSourceRootIfAbsent( + modifiableModel, + testSourceRoot.url, + codegenLanguages.item.testRootType() + ) + return true + } finally { + if (modifiableModel.isWritable && !modifiableModel.isDisposed) modifiableModel.dispose() + } } private fun createPackageWrapper(packageName: String?): PackageWrapper = @@ -648,7 +680,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m itemsToHelpTooltip.forEach { (box, tooltip) -> box.setHelpTooltipTextChanger(tooltip) } testSourceFolderField.childComponent.addActionListener { event -> - model.testSourceRoot = pathToFile((event.source as JComboBox<*>).selectedItem as String) + with((event.source as JComboBox<*>).selectedItem) { + if (this is VirtualFile) { + model.testSourceRoot = this@with + } + else { + model.testSourceRoot = null + } + } } mockStrategies.addActionListener { event -> @@ -700,14 +739,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } - private fun pathToFile(path: String): VirtualFile? { - val relativePath = path.substring(".../".length).replace('\\', '/') - return model.testModule - .suitableTestSourceRoots() - .firstOrNull { it.path.contains(relativePath) } - } - - private lateinit var currentFrameworkItem: TestFramework //We would like to remove JUnit4 from framework list in parametrized mode diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt index b03960bd0c..c473054696 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt @@ -1,17 +1,23 @@ package org.utbot.intellij.plugin.ui.components -import org.utbot.common.PathUtil -import org.utbot.intellij.plugin.ui.GenerateTestsModel -import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots -import com.intellij.ide.ui.laf.darcula.DarculaUIUtil import com.intellij.openapi.application.ReadAction import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile +import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.ComboboxWithBrowseButton +import com.intellij.ui.SimpleTextAttributes import com.intellij.util.ArrayUtil +import java.io.File import javax.swing.DefaultComboBoxModel +import javax.swing.JList +import org.utbot.common.PathUtil +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.intellij.plugin.ui.GenerateTestsModel +import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot +import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : ComboboxWithBrowseButton() { @@ -19,9 +25,29 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : C init { childComponent.isEditable = false + childComponent.renderer = object : ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, + value: Any?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + if (value is String) { + append(value) + return + } + if (value is VirtualFile) { + append(formatUrl(value, model)) + } + if (value is FakeVirtualFile) { + append(" (will be created)", SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } - val testRoots = model.testModule.suitableTestSourceRoots() - + val testRoots = model.testModule.suitableTestSourceRoots(CodegenLanguage.JAVA).toMutableList() + model.testModule.addDedicatedTestRoot(testRoots) if (testRoots.isNotEmpty()) { configureRootsCombo(testRoots) } else { @@ -34,11 +60,11 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : C model.testSourceRoot = it if (childComponent.itemCount == 1 && childComponent.selectedItem == SET_TEST_FOLDER) { - newItemList(setOf(formatUrl(it, model))) + newItemList(setOf(it)) } else { //Prepend and select newly added test root - val testRootItems = linkedSetOf(formatUrl(it, model)) - testRootItems += (0 until childComponent.itemCount).map { i -> childComponent.getItemAt(i) as String} + val testRootItems = linkedSetOf(it) + testRootItems += (0 until childComponent.itemCount).map { i -> childComponent.getItemAt(i) as VirtualFile} newItemList(testRootItems) } } @@ -59,17 +85,19 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : C val selectedRoot = testRoots.first() model.testSourceRoot = selectedRoot - newItemList(testRoots.map { root -> formatUrl(root, model) }.toSet()) + newItemList(testRoots.toSet()) } - private fun newItemList(comboItems: Set) { + private fun newItemList(comboItems: Set) { childComponent.model = DefaultComboBoxModel(ArrayUtil.toObjectArray(comboItems)) - childComponent.putClientProperty("JComponent.outline", - if (comboItems.isNotEmpty() && !comboItems.contains(SET_TEST_FOLDER)) null else DarculaUIUtil.Outline.error) } private fun formatUrl(virtualFile: VirtualFile, model: GenerateTestsModel): String { - var directoryUrl = virtualFile.presentableUrl + var directoryUrl = if (virtualFile is FakeVirtualFile) { + virtualFile.parent.presentableUrl + File.separatorChar + virtualFile.name + } else { + virtualFile.presentableUrl + } @Suppress("DEPRECATION") val projectHomeUrl = model.project.baseDir.presentableUrl diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt index 18600f8eaf..ae2786d0b3 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt @@ -14,16 +14,20 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.JavaSdk import com.intellij.openapi.projectRoots.JavaSdkVersion import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ContentEntry +import com.intellij.openapi.roots.ModifiableRootModel import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.SourceFolder import com.intellij.openapi.roots.TestModuleProperties import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.util.PathUtil.getParentPath import java.nio.file.Path import mu.KotlinLogging import org.jetbrains.android.sdk.AndroidSdkType +import org.jetbrains.jps.model.module.JpsModuleSourceRootType import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider import org.jetbrains.kotlin.config.TestResourceKotlinRootType import org.jetbrains.kotlin.platform.TargetPlatformVersion @@ -112,7 +116,7 @@ private fun findPotentialModuleForTests(project: Project, srcModule: Module): Mo /** * Finds all suitable test root virtual files. */ -private fun Module.suitableTestSourceRoots(codegenLanguage: CodegenLanguage): List { +fun Module.suitableTestSourceRoots(codegenLanguage: CodegenLanguage): List { val sourceRootsInModule = suitableTestSourceFolders(codegenLanguage).mapNotNull { it.file } if (sourceRootsInModule.isNotEmpty()) { @@ -141,53 +145,94 @@ private fun Module.suitableTestSourceFolders(codegenLanguage: CodegenLanguage): .sortedBy { it.url.length } } +private const val dedicatedTestSourceRootName = "utbot_tests" +fun Module.addDedicatedTestRoot(testSourceRoots: MutableList): VirtualFile? { + // Dedicated test root already exists + // OR it looks like standard structure of Gradle project where 'unexpected' test roots won't work + if (testSourceRoots.any { file -> + file.name == dedicatedTestSourceRootName || file.path.endsWith("src/test/java") + }) return null + + val moduleInstance = ModuleRootManager.getInstance(this) + val testFolder = moduleInstance.contentEntries.flatMap { it.sourceFolders.toList() } + .firstOrNull { it.rootType in testSourceRootTypes } + (testFolder?.let { testFolder.file?.parent } ?: (testFolder?.contentEntry + ?: moduleInstance.contentEntries.first()).file ?: moduleFile)?.let { + val file = FakeVirtualFile(it, dedicatedTestSourceRootName) + testSourceRoots.add(file) + // We return "true" IFF it's case of not yet created fake directory + return if (VfsUtil.findRelativeFile(it, dedicatedTestSourceRootName) == null) file else null + } + return null +} + private const val resourcesSuffix = "/resources" private fun getOrCreateTestResourcesUrl(module: Module, testSourceRoot: VirtualFile?): String { - val moduleInstance = ModuleRootManager.getInstance(module) - val sourceFolders = moduleInstance.contentEntries.flatMap { it.sourceFolders.toList() } + val rootModel = ModuleRootManager.getInstance(module).modifiableModel + try { + val sourceFolders = rootModel.contentEntries.flatMap { it.sourceFolders.toList() } - val testResourcesFolder = sourceFolders - .filter { sourceFolder -> - sourceFolder.rootType in testResourceRootTypes && !sourceFolder.isForGeneratedSources() - } - // taking the source folder that has the maximum common prefix - // with `testSourceRoot`, which was selected by the user - .maxBy { sourceFolder -> - val sourceFolderPath = sourceFolder.file?.path ?: "" - val testSourceRootPath = testSourceRoot?.path ?: "" - sourceFolderPath.commonPrefixWith(testSourceRootPath).length + val testResourcesFolder = sourceFolders + .filter { sourceFolder -> + sourceFolder.rootType in testResourceRootTypes && !sourceFolder.isForGeneratedSources() + } + // taking the source folder that has the maximum common prefix + // with `testSourceRoot`, which was selected by the user + .maxBy { sourceFolder -> + val sourceFolderPath = sourceFolder.file?.path ?: "" + val testSourceRootPath = testSourceRoot?.path ?: "" + sourceFolderPath.commonPrefixWith(testSourceRootPath).length + } + if (testResourcesFolder != null) { + return testResourcesFolder.url } - if (testResourcesFolder != null) { - return testResourcesFolder.url - } - val testFolder = sourceFolders.firstOrNull { it.rootType in testSourceRootTypes } - val contentEntry = testFolder?.contentEntry ?: moduleInstance.contentEntries.first() + val testFolder = sourceFolders.firstOrNull { it.rootType in testSourceRootTypes } + val contentEntry = testFolder?.contentEntry ?: rootModel.contentEntries.first() - val parentFolderUrl = testFolder?.let { getParentPath(testFolder.url) } - val testResourcesUrl = - if (parentFolderUrl != null) "${parentFolderUrl}$resourcesSuffix" else "${contentEntry.url}$resourcesSuffix" + val parentFolderUrl = testFolder?.let { getParentPath(testFolder.url) } + val testResourcesUrl = + if (parentFolderUrl != null) "${parentFolderUrl}$resourcesSuffix" else "${contentEntry.url}$resourcesSuffix" - val codegenLanguage = - if (testFolder?.rootType == TestResourceKotlinRootType) CodegenLanguage.KOTLIN else CodegenLanguage.JAVA + val codegenLanguage = + if (testFolder?.rootType == TestResourceKotlinRootType) CodegenLanguage.KOTLIN else CodegenLanguage.JAVA - try { - WriteCommandAction.runWriteCommandAction(module.project) { - contentEntry.addSourceFolder(testResourcesUrl, codegenLanguage.testResourcesRootType()) - moduleInstance.modifiableModel.commit() - VfsUtil.createDirectoryIfMissing(VfsUtilCore.urlToPath(testResourcesUrl)) + try { + contentEntry.addSourceRootIfAbsent(rootModel, testResourcesUrl, codegenLanguage.testResourcesRootType()) + } catch (e: java.lang.IllegalStateException) { + // Hack to avoid unmodifiable ModuleBridge testModule on Android SAT-1536. + workaround(WorkaroundReason.HACK) { + logger.info("Error during SARIF report generation: $e") + return testFolder!!.url + } } + + return testResourcesUrl + } finally { + if (!rootModel.isDisposed && rootModel.isWritable) rootModel.dispose() + } +} + +fun ContentEntry.addSourceRootIfAbsent( + model: ModifiableRootModel, + sourceRootUrl: String, + type: JpsModuleSourceRootType<*> +) { + getSourceFolders(type).find { it.url == sourceRootUrl }?.apply { + model.dispose() + return } - catch (e: java.lang.IllegalStateException) { - // Hack to avoid unmodifiable ModuleBridge testModule on Android SAT-1536. - workaround(WorkaroundReason.HACK) { - logger.info("Error during SARIF report generation: $e") - return testFolder!!.url + VfsUtil.createDirectoryIfMissing(VfsUtilCore.urlToPath(sourceRootUrl)) + addSourceFolder(sourceRootUrl, type) + WriteCommandAction.runWriteCommandAction(rootModel.module.project) { + try { + model.commit() + } catch (e: Exception) { + logger.error { e } + model.dispose() } } - - return testResourcesUrl } /**