diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt index bf517cdca7..abdbda292a 100644 --- a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt @@ -9,7 +9,7 @@ import com.intellij.psi.PsiClass import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.util.projectStructure.allModules import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.intellij.plugin.ui.utils.TestSourceRoot +import org.utbot.intellij.plugin.ui.utils.ITestSourceRoot import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots @@ -48,7 +48,7 @@ open class BaseTestsModel( srcClasses.map { it.packageName }.distinct().size != 1 } - fun getAllTestSourceRoots() : MutableList { + fun getAllTestSourceRoots() : MutableList { with(if (project.isBuildWithGradle) project.allModules() else potentialTestModules) { return this.flatMap { it.suitableTestSourceRoots().toList() }.toMutableList() } diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt index 222d016167..2f933e9e13 100644 --- a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComponentWithBrowseButton import com.intellij.openapi.ui.FixedSizeButton -import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.ui.ColoredListCellRenderer @@ -21,15 +20,16 @@ import javax.swing.JList import org.jetbrains.kotlin.idea.util.rootManager import org.utbot.common.PathUtil import org.utbot.intellij.plugin.models.BaseTestsModel -import org.utbot.intellij.plugin.ui.utils.TestSourceRoot +import org.utbot.intellij.plugin.ui.utils.ITestSourceRoot import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot -import org.utbot.intellij.plugin.ui.utils.dedicatedTestSourceRootName +import org.utbot.intellij.plugin.ui.utils.getSortedTestRoots import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle +private const val SET_TEST_FOLDER = "set test folder" + class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) : ComponentWithBrowseButton>(ComboBox(), null) { - private val SET_TEST_FOLDER = "set test folder" init { if (model.project.isBuildWithGradle) { @@ -58,7 +58,11 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) : } } - val testRoots = model.getSortedTestRoots() + val testRoots = getSortedTestRoots( + model.getAllTestSourceRoots(), + model.srcModule.rootManager.sourceRoots.map { file: VirtualFile -> file.toNioPath().toString() }, + model.codegenLanguage + ) // this method is blocked for Gradle, where multiple test modules can exist model.testModule.addDedicatedTestRoot(testRoots, model.codegenLanguage) @@ -86,33 +90,6 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) : } } - private fun BaseTestsModel.getSortedTestRoots(): MutableList { - var commonModuleSourceDirectory = "" - for ((i, sourceRoot) in srcModule.rootManager.sourceRoots.withIndex()) { - commonModuleSourceDirectory = if (i == 0) { - sourceRoot.toNioPath().toString() - } else { - StringUtil.commonPrefix(commonModuleSourceDirectory, sourceRoot.toNioPath().toString()) - } - } - - return getAllTestSourceRoots().distinct().toMutableList().sortedWith( - compareByDescending { - // Heuristics: Dirs with proper code language should go first - it.expectedLanguage == codegenLanguage - }.thenByDescending { - // Heuristics: Dirs from within module 'common' directory should go first - it.dir.toNioPath().toString().startsWith(commonModuleSourceDirectory) - }.thenByDescending { - // Heuristics: dedicated test source root named 'utbot_tests' should go first - it.dir.name == dedicatedTestSourceRootName - }.thenBy { - // ABC-sorting - it.dir.toNioPath() - } - ).toMutableList() - } - private fun chooseTestRoot(model: BaseTestsModel): VirtualFile? = ReadAction.compute { val desc = object:FileChooserDescriptor(false, true, false, false, false, false) { @@ -126,12 +103,12 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) : files.singleOrNull() } - private fun configureRootsCombo(testRoots: List) { + private fun configureRootsCombo(testRoots: List) { val selectedRoot = testRoots.first() // do not update model.testModule here, because fake test source root could have been chosen model.testSourceRoot = selectedRoot.dir - newItemList(testRoots.map { it.dir }.toSet()) + newItemList(testRoots.mapNotNull { it.dir }.toSet()) } private fun newItemList(comboItems: Set) { diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt index 3af0593f66..242cbbb303 100644 --- a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt @@ -4,8 +4,6 @@ import org.utbot.common.PathUtil.toPath import org.utbot.common.WorkaroundReason import org.utbot.common.workaround import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.intellij.plugin.ui.CommonErrorNotifier -import org.utbot.intellij.plugin.ui.UnsupportedJdkNotifier import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil @@ -14,9 +12,6 @@ import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessModuleDir -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 @@ -29,7 +24,6 @@ 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 @@ -37,18 +31,23 @@ import org.jetbrains.kotlin.platform.TargetPlatformVersion private val logger = KotlinLogging.logger {} -data class TestSourceRoot( - val dir: VirtualFile, - val expectedLanguage: CodegenLanguage -) +interface ITestSourceRoot { + val dirPath: String + val dirName: String + val dir: VirtualFile? + val expectedLanguage : CodegenLanguage +} -/** - * @return jdk version of the module - */ -fun Module.jdkVersion(): JavaSdkVersion { - val moduleRootManager = ModuleRootManager.getInstance(this) - val sdk = moduleRootManager.sdk - return jdkVersionBy(sdk) +class TestSourceRoot(override val dir: VirtualFile, override val expectedLanguage: CodegenLanguage) : ITestSourceRoot { + override val dirPath: String = dir.toNioPath().toString() + override val dirName: String = dir.name + + override fun toString() = dirPath + + override fun equals(other: Any?) = + other is TestSourceRoot && dir == other.dir && expectedLanguage == other.expectedLanguage + + override fun hashCode() = 31 * dir.hashCode() + expectedLanguage.hashCode() } /** @@ -107,11 +106,11 @@ private fun findPotentialModulesForTests(project: Project, srcModule: Module): L if (modules.isNotEmpty()) return modules if (srcModule.suitableTestSourceFolders().isEmpty()) { - val modules = mutableSetOf() - ModuleUtilCore.collectModulesDependsOn(srcModule, modules) - modules.remove(srcModule) + val modulesWithTestRoot = mutableSetOf().also { + ModuleUtilCore.collectModulesDependsOn(srcModule, it) + it.remove(srcModule) + }.filter { it.suitableTestSourceFolders().isNotEmpty() } - val modulesWithTestRoot = modules.filter { it.suitableTestSourceFolders().isNotEmpty() } if (modulesWithTestRoot.size == 1) return modulesWithTestRoot } return listOf(srcModule) @@ -165,11 +164,11 @@ val Project.isBuildWithGradle get() = const val dedicatedTestSourceRootName = "utbot_tests" -fun Module.addDedicatedTestRoot(testSourceRoots: MutableList, language: CodegenLanguage): VirtualFile? { - // Don't suggest new test source roots for Gradle project where 'unexpected' test roots won't work +fun Module.addDedicatedTestRoot(testSourceRoots: MutableList, language: CodegenLanguage): VirtualFile? { + // Don't suggest new test source roots for a Gradle project where 'unexpected' test roots won't work if (project.isBuildWithGradle) return null // Dedicated test root already exists - if (testSourceRoots.any { root -> root.dir.name == dedicatedTestSourceRootName }) return null + if (testSourceRoots.any { root -> root.dir?.name == dedicatedTestSourceRootName }) return null val moduleInstance = ModuleRootManager.getInstance(this) val testFolder = moduleInstance.contentEntries.flatMap { it.sourceFolders.toList() } @@ -256,34 +255,6 @@ fun ContentEntry.addSourceRootIfAbsent( } } -/** - * Obtain JDK version and make sure that it is JDK8 or JDK11 - */ -private fun jdkVersionBy(sdk: Sdk?): JavaSdkVersion { - if (sdk == null) { - CommonErrorNotifier.notify("Failed to obtain JDK version of the project") - } - requireNotNull(sdk) - - val jdkVersion = when (sdk.sdkType) { - is JavaSdk -> { - (sdk.sdkType as JavaSdk).getVersion(sdk) - } - is AndroidSdkType -> { - ((sdk.sdkType as AndroidSdkType).dependencyType as JavaSdk).getVersion(sdk) - } - else -> null - } - if (jdkVersion == null) { - CommonErrorNotifier.notify("Failed to obtain JDK version of the project") - } - requireNotNull(jdkVersion) - if (!jdkVersion.isAtLeast(JavaSdkVersion.JDK_1_8)) { - UnsupportedJdkNotifier.notify(jdkVersion.description) - } - return jdkVersion -} - private val SourceFolder.expectedLanguageForTests: CodegenLanguage? get() { // unfortunately, Gradle creates Kotlin test source root with Java source root type, so type is misleading, diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt index e687656721..76505578e4 100644 --- a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt @@ -1,6 +1,8 @@ package org.utbot.intellij.plugin.ui.utils import com.intellij.openapi.roots.SourceFolder +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil import org.jetbrains.jps.model.java.JavaResourceRootProperties import org.jetbrains.jps.model.java.JavaResourceRootType import org.jetbrains.jps.model.java.JavaSourceRootProperties @@ -55,3 +57,53 @@ fun SourceFolder.isForGeneratedSources(): Boolean { return markedGeneratedSources || androidStudioGeneratedSources } + +const val SRC_MAIN = "src/main/" + +/** + * Sorting test roots, the main idea is to place 'the best' + * test source root the first and to provide readability in general + * @param allTestRoots are all test roots of a project to be sorted + * @param moduleSourcePaths is list of source roots for the module for which we're going to generate tests. + * The first test source root in the resulting list is expected + * to be the closest one to the module based on module source roots. + * @param codegenLanguage is target generation language + */ +fun getSortedTestRoots( + allTestRoots: MutableList, + moduleSourcePaths: List, + codegenLanguage: CodegenLanguage +): MutableList { + var commonModuleSourceDirectory = FileUtil.toSystemIndependentName(moduleSourcePaths.getCommonPrefix()) + //Remove standard suffix that may prevent exact module path matching + commonModuleSourceDirectory = StringUtil.trimEnd(commonModuleSourceDirectory, SRC_MAIN) + + return allTestRoots.distinct().toMutableList().sortedWith( + compareByDescending { + // Heuristics: Dirs with proper code language should go first + it.expectedLanguage == codegenLanguage + }.thenByDescending { + // Heuristics: Dirs from within module 'common' directory should go first + FileUtil.toSystemIndependentName(it.dirPath).startsWith(commonModuleSourceDirectory) + }.thenByDescending { + // Heuristics: dedicated test source root named 'utbot_tests' should go first + it.dirName == dedicatedTestSourceRootName + }.thenBy { + // ABC-sorting + it.dirPath + } + ).toMutableList() +} + + +fun List.getCommonPrefix() : String { + var result = "" + for ((i, s) in withIndex()) { + result = if (i == 0) { + s + } else { + StringUtil.commonPrefix(result, s) + } + } + return result +} diff --git a/utbot-ui-commons/src/test/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtilsTest.kt b/utbot-ui-commons/src/test/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtilsTest.kt new file mode 100644 index 0000000000..ed5593d0e1 --- /dev/null +++ b/utbot-ui-commons/src/test/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtilsTest.kt @@ -0,0 +1,62 @@ +package org.utbot.intellij.plugin.ui.utils + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.CodegenLanguage + +internal class RootUtilsTest { + internal class MockTestSourceRoot(override val dirPath: String) : ITestSourceRoot { + override val dir = null + override val dirName = dirPath.substring(dirPath.lastIndexOf("/") + 1) + override val expectedLanguage = if (dirName == "java") CodegenLanguage.JAVA else CodegenLanguage.KOTLIN + override fun toString()= dirPath + } + + @Test + fun testCommonPrefix() { + val commonPrefix = listOf( + "/UTBotJavaTest/utbot-framework/src/main/java", + "/UTBotJavaTest/utbot-framework/src/main/kotlin", + "/UTBotJavaTest/utbot-framework/src/main/resources" + ).getCommonPrefix() + Assertions.assertEquals("/UTBotJavaTest/utbot-framework/src/main/", commonPrefix) + Assertions.assertTrue(commonPrefix.endsWith(SRC_MAIN)) + } + + @Test + fun testRootSorting() { + val allTestRoots = mutableListOf( + MockTestSourceRoot("/UTBotJavaTest/utbot-analytics/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-analytics/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-analytics-torch/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-cli/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-api/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-api/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-test/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-framework-test/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-fuzzers/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-fuzzers/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-gradle/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation-tests/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation-tests/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-intellij/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-maven/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-sample/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-summary/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-summary-tests/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-core/src/test/java"), + MockTestSourceRoot("/UTBotJavaTest/utbot-core/src/test/kotlin"), + MockTestSourceRoot("/UTBotJavaTest/utbot-rd/src/test/kotlin"), + ) + val moduleSourcePaths = listOf( + "/UTBotJavaTest/utbot-framework/src/main/java", + "/UTBotJavaTest/utbot-framework/src/main/kotlin", + "/UTBotJavaTest/utbot-framework/src/main/resources", + ) + val sortedTestRoots = getSortedTestRoots(allTestRoots, moduleSourcePaths, CodegenLanguage.JAVA) + Assertions.assertEquals("/UTBotJavaTest/utbot-framework/src/test/java", sortedTestRoots.first().toString()) + } +} \ No newline at end of file