Skip to content

Commit c32a6e6

Browse files
Vassiliy-Kudryashovtamarinvs19
authored andcommitted
Better test source root suggestion: remove standard "src/main/" suffix (#1439)
1 parent cab6ef4 commit c32a6e6

File tree

5 files changed

+150
-88
lines changed

5 files changed

+150
-88
lines changed

utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.intellij.psi.PsiClass
99
import org.jetbrains.kotlin.idea.core.getPackage
1010
import org.jetbrains.kotlin.idea.util.projectStructure.allModules
1111
import org.utbot.framework.plugin.api.CodegenLanguage
12-
import org.utbot.intellij.plugin.ui.utils.TestSourceRoot
12+
import org.utbot.intellij.plugin.ui.utils.ITestSourceRoot
1313
import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle
1414
import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots
1515

@@ -48,7 +48,7 @@ open class BaseTestsModel(
4848
srcClasses.map { it.packageName }.distinct().size != 1
4949
}
5050

51-
fun getAllTestSourceRoots() : MutableList<TestSourceRoot> {
51+
fun getAllTestSourceRoots() : MutableList<out ITestSourceRoot> {
5252
with(if (project.isBuildWithGradle) project.allModules() else potentialTestModules) {
5353
return this.flatMap { it.suitableTestSourceRoots().toList() }.toMutableList()
5454
}

utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.intellij.openapi.project.guessProjectDir
88
import com.intellij.openapi.ui.ComboBox
99
import com.intellij.openapi.ui.ComponentWithBrowseButton
1010
import com.intellij.openapi.ui.FixedSizeButton
11-
import com.intellij.openapi.util.text.StringUtil
1211
import com.intellij.openapi.vfs.VirtualFile
1312
import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile
1413
import com.intellij.ui.ColoredListCellRenderer
@@ -21,15 +20,16 @@ import javax.swing.JList
2120
import org.jetbrains.kotlin.idea.util.rootManager
2221
import org.utbot.common.PathUtil
2322
import org.utbot.intellij.plugin.models.BaseTestsModel
24-
import org.utbot.intellij.plugin.ui.utils.TestSourceRoot
23+
import org.utbot.intellij.plugin.ui.utils.ITestSourceRoot
2524
import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot
26-
import org.utbot.intellij.plugin.ui.utils.dedicatedTestSourceRootName
25+
import org.utbot.intellij.plugin.ui.utils.getSortedTestRoots
2726
import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle
2827

28+
private const val SET_TEST_FOLDER = "set test folder"
29+
2930
class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) :
3031
ComponentWithBrowseButton<ComboBox<Any>>(ComboBox(), null) {
3132

32-
private val SET_TEST_FOLDER = "set test folder"
3333

3434
init {
3535
if (model.project.isBuildWithGradle) {
@@ -58,7 +58,11 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) :
5858
}
5959
}
6060

61-
val testRoots = model.getSortedTestRoots()
61+
val testRoots = getSortedTestRoots(
62+
model.getAllTestSourceRoots(),
63+
model.srcModule.rootManager.sourceRoots.map { file: VirtualFile -> file.toNioPath().toString() },
64+
model.codegenLanguage
65+
)
6266

6367
// this method is blocked for Gradle, where multiple test modules can exist
6468
model.testModule.addDedicatedTestRoot(testRoots, model.codegenLanguage)
@@ -86,33 +90,6 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) :
8690
}
8791
}
8892

89-
private fun BaseTestsModel.getSortedTestRoots(): MutableList<TestSourceRoot> {
90-
var commonModuleSourceDirectory = ""
91-
for ((i, sourceRoot) in srcModule.rootManager.sourceRoots.withIndex()) {
92-
commonModuleSourceDirectory = if (i == 0) {
93-
sourceRoot.toNioPath().toString()
94-
} else {
95-
StringUtil.commonPrefix(commonModuleSourceDirectory, sourceRoot.toNioPath().toString())
96-
}
97-
}
98-
99-
return getAllTestSourceRoots().distinct().toMutableList().sortedWith(
100-
compareByDescending<TestSourceRoot> {
101-
// Heuristics: Dirs with proper code language should go first
102-
it.expectedLanguage == codegenLanguage
103-
}.thenByDescending {
104-
// Heuristics: Dirs from within module 'common' directory should go first
105-
it.dir.toNioPath().toString().startsWith(commonModuleSourceDirectory)
106-
}.thenByDescending {
107-
// Heuristics: dedicated test source root named 'utbot_tests' should go first
108-
it.dir.name == dedicatedTestSourceRootName
109-
}.thenBy {
110-
// ABC-sorting
111-
it.dir.toNioPath()
112-
}
113-
).toMutableList()
114-
}
115-
11693
private fun chooseTestRoot(model: BaseTestsModel): VirtualFile? =
11794
ReadAction.compute<VirtualFile, RuntimeException> {
11895
val desc = object:FileChooserDescriptor(false, true, false, false, false, false) {
@@ -126,12 +103,12 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) :
126103
files.singleOrNull()
127104
}
128105

129-
private fun configureRootsCombo(testRoots: List<TestSourceRoot>) {
106+
private fun configureRootsCombo(testRoots: List<ITestSourceRoot>) {
130107
val selectedRoot = testRoots.first()
131108

132109
// do not update model.testModule here, because fake test source root could have been chosen
133110
model.testSourceRoot = selectedRoot.dir
134-
newItemList(testRoots.map { it.dir }.toSet())
111+
newItemList(testRoots.mapNotNull { it.dir }.toSet())
135112
}
136113

137114
private fun newItemList(comboItems: Set<Any>) {

utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import org.utbot.common.PathUtil.toPath
44
import org.utbot.common.WorkaroundReason
55
import org.utbot.common.workaround
66
import org.utbot.framework.plugin.api.CodegenLanguage
7-
import org.utbot.intellij.plugin.ui.CommonErrorNotifier
8-
import org.utbot.intellij.plugin.ui.UnsupportedJdkNotifier
97
import com.intellij.openapi.command.WriteCommandAction
108
import com.intellij.openapi.externalSystem.model.ProjectSystemId
119
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
@@ -14,9 +12,6 @@ import com.intellij.openapi.module.ModuleManager
1412
import com.intellij.openapi.module.ModuleUtilCore
1513
import com.intellij.openapi.project.Project
1614
import com.intellij.openapi.project.guessModuleDir
17-
import com.intellij.openapi.projectRoots.JavaSdk
18-
import com.intellij.openapi.projectRoots.JavaSdkVersion
19-
import com.intellij.openapi.projectRoots.Sdk
2015
import com.intellij.openapi.roots.ContentEntry
2116
import com.intellij.openapi.roots.ModifiableRootModel
2217
import com.intellij.openapi.roots.ModuleRootManager
@@ -29,26 +24,30 @@ import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile
2924
import com.intellij.util.PathUtil.getParentPath
3025
import java.nio.file.Path
3126
import mu.KotlinLogging
32-
import org.jetbrains.android.sdk.AndroidSdkType
3327
import org.jetbrains.jps.model.module.JpsModuleSourceRootType
3428
import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider
3529
import org.jetbrains.kotlin.config.TestResourceKotlinRootType
3630
import org.jetbrains.kotlin.platform.TargetPlatformVersion
3731

3832
private val logger = KotlinLogging.logger {}
3933

40-
data class TestSourceRoot(
41-
val dir: VirtualFile,
42-
val expectedLanguage: CodegenLanguage
43-
)
34+
interface ITestSourceRoot {
35+
val dirPath: String
36+
val dirName: String
37+
val dir: VirtualFile?
38+
val expectedLanguage : CodegenLanguage
39+
}
4440

45-
/**
46-
* @return jdk version of the module
47-
*/
48-
fun Module.jdkVersion(): JavaSdkVersion {
49-
val moduleRootManager = ModuleRootManager.getInstance(this)
50-
val sdk = moduleRootManager.sdk
51-
return jdkVersionBy(sdk)
41+
class TestSourceRoot(override val dir: VirtualFile, override val expectedLanguage: CodegenLanguage) : ITestSourceRoot {
42+
override val dirPath: String = dir.toNioPath().toString()
43+
override val dirName: String = dir.name
44+
45+
override fun toString() = dirPath
46+
47+
override fun equals(other: Any?) =
48+
other is TestSourceRoot && dir == other.dir && expectedLanguage == other.expectedLanguage
49+
50+
override fun hashCode() = 31 * dir.hashCode() + expectedLanguage.hashCode()
5251
}
5352

5453
/**
@@ -107,11 +106,11 @@ private fun findPotentialModulesForTests(project: Project, srcModule: Module): L
107106
if (modules.isNotEmpty()) return modules
108107

109108
if (srcModule.suitableTestSourceFolders().isEmpty()) {
110-
val modules = mutableSetOf<Module>()
111-
ModuleUtilCore.collectModulesDependsOn(srcModule, modules)
112-
modules.remove(srcModule)
109+
val modulesWithTestRoot = mutableSetOf<Module>().also {
110+
ModuleUtilCore.collectModulesDependsOn(srcModule, it)
111+
it.remove(srcModule)
112+
}.filter { it.suitableTestSourceFolders().isNotEmpty() }
113113

114-
val modulesWithTestRoot = modules.filter { it.suitableTestSourceFolders().isNotEmpty() }
115114
if (modulesWithTestRoot.size == 1) return modulesWithTestRoot
116115
}
117116
return listOf(srcModule)
@@ -165,11 +164,11 @@ val Project.isBuildWithGradle get() =
165164

166165
const val dedicatedTestSourceRootName = "utbot_tests"
167166

168-
fun Module.addDedicatedTestRoot(testSourceRoots: MutableList<TestSourceRoot>, language: CodegenLanguage): VirtualFile? {
169-
// Don't suggest new test source roots for Gradle project where 'unexpected' test roots won't work
167+
fun Module.addDedicatedTestRoot(testSourceRoots: MutableList<ITestSourceRoot>, language: CodegenLanguage): VirtualFile? {
168+
// Don't suggest new test source roots for a Gradle project where 'unexpected' test roots won't work
170169
if (project.isBuildWithGradle) return null
171170
// Dedicated test root already exists
172-
if (testSourceRoots.any { root -> root.dir.name == dedicatedTestSourceRootName }) return null
171+
if (testSourceRoots.any { root -> root.dir?.name == dedicatedTestSourceRootName }) return null
173172

174173
val moduleInstance = ModuleRootManager.getInstance(this)
175174
val testFolder = moduleInstance.contentEntries.flatMap { it.sourceFolders.toList() }
@@ -256,34 +255,6 @@ fun ContentEntry.addSourceRootIfAbsent(
256255
}
257256
}
258257

259-
/**
260-
* Obtain JDK version and make sure that it is JDK8 or JDK11
261-
*/
262-
private fun jdkVersionBy(sdk: Sdk?): JavaSdkVersion {
263-
if (sdk == null) {
264-
CommonErrorNotifier.notify("Failed to obtain JDK version of the project")
265-
}
266-
requireNotNull(sdk)
267-
268-
val jdkVersion = when (sdk.sdkType) {
269-
is JavaSdk -> {
270-
(sdk.sdkType as JavaSdk).getVersion(sdk)
271-
}
272-
is AndroidSdkType -> {
273-
((sdk.sdkType as AndroidSdkType).dependencyType as JavaSdk).getVersion(sdk)
274-
}
275-
else -> null
276-
}
277-
if (jdkVersion == null) {
278-
CommonErrorNotifier.notify("Failed to obtain JDK version of the project")
279-
}
280-
requireNotNull(jdkVersion)
281-
if (!jdkVersion.isAtLeast(JavaSdkVersion.JDK_1_8)) {
282-
UnsupportedJdkNotifier.notify(jdkVersion.description)
283-
}
284-
return jdkVersion
285-
}
286-
287258
private val SourceFolder.expectedLanguageForTests: CodegenLanguage?
288259
get() {
289260
// unfortunately, Gradle creates Kotlin test source root with Java source root type, so type is misleading,

utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.utbot.intellij.plugin.ui.utils
22

33
import com.intellij.openapi.roots.SourceFolder
4+
import com.intellij.openapi.util.io.FileUtil
5+
import com.intellij.openapi.util.text.StringUtil
46
import org.jetbrains.jps.model.java.JavaResourceRootProperties
57
import org.jetbrains.jps.model.java.JavaResourceRootType
68
import org.jetbrains.jps.model.java.JavaSourceRootProperties
@@ -55,3 +57,53 @@ fun SourceFolder.isForGeneratedSources(): Boolean {
5557

5658
return markedGeneratedSources || androidStudioGeneratedSources
5759
}
60+
61+
const val SRC_MAIN = "src/main/"
62+
63+
/**
64+
* Sorting test roots, the main idea is to place 'the best'
65+
* test source root the first and to provide readability in general
66+
* @param allTestRoots are all test roots of a project to be sorted
67+
* @param moduleSourcePaths is list of source roots for the module for which we're going to generate tests.
68+
* The first test source root in the resulting list is expected
69+
* to be the closest one to the module based on module source roots.
70+
* @param codegenLanguage is target generation language
71+
*/
72+
fun getSortedTestRoots(
73+
allTestRoots: MutableList<out ITestSourceRoot>,
74+
moduleSourcePaths: List<String>,
75+
codegenLanguage: CodegenLanguage
76+
): MutableList<ITestSourceRoot> {
77+
var commonModuleSourceDirectory = FileUtil.toSystemIndependentName(moduleSourcePaths.getCommonPrefix())
78+
//Remove standard suffix that may prevent exact module path matching
79+
commonModuleSourceDirectory = StringUtil.trimEnd(commonModuleSourceDirectory, SRC_MAIN)
80+
81+
return allTestRoots.distinct().toMutableList().sortedWith(
82+
compareByDescending<ITestSourceRoot> {
83+
// Heuristics: Dirs with proper code language should go first
84+
it.expectedLanguage == codegenLanguage
85+
}.thenByDescending {
86+
// Heuristics: Dirs from within module 'common' directory should go first
87+
FileUtil.toSystemIndependentName(it.dirPath).startsWith(commonModuleSourceDirectory)
88+
}.thenByDescending {
89+
// Heuristics: dedicated test source root named 'utbot_tests' should go first
90+
it.dirName == dedicatedTestSourceRootName
91+
}.thenBy {
92+
// ABC-sorting
93+
it.dirPath
94+
}
95+
).toMutableList()
96+
}
97+
98+
99+
fun List<String>.getCommonPrefix() : String {
100+
var result = ""
101+
for ((i, s) in withIndex()) {
102+
result = if (i == 0) {
103+
s
104+
} else {
105+
StringUtil.commonPrefix(result, s)
106+
}
107+
}
108+
return result
109+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.utbot.intellij.plugin.ui.utils
2+
3+
import org.junit.jupiter.api.Assertions
4+
import org.junit.jupiter.api.Test
5+
import org.utbot.framework.plugin.api.CodegenLanguage
6+
7+
internal class RootUtilsTest {
8+
internal class MockTestSourceRoot(override val dirPath: String) : ITestSourceRoot {
9+
override val dir = null
10+
override val dirName = dirPath.substring(dirPath.lastIndexOf("/") + 1)
11+
override val expectedLanguage = if (dirName == "java") CodegenLanguage.JAVA else CodegenLanguage.KOTLIN
12+
override fun toString()= dirPath
13+
}
14+
15+
@Test
16+
fun testCommonPrefix() {
17+
val commonPrefix = listOf(
18+
"/UTBotJavaTest/utbot-framework/src/main/java",
19+
"/UTBotJavaTest/utbot-framework/src/main/kotlin",
20+
"/UTBotJavaTest/utbot-framework/src/main/resources"
21+
).getCommonPrefix()
22+
Assertions.assertEquals("/UTBotJavaTest/utbot-framework/src/main/", commonPrefix)
23+
Assertions.assertTrue(commonPrefix.endsWith(SRC_MAIN))
24+
}
25+
26+
@Test
27+
fun testRootSorting() {
28+
val allTestRoots = mutableListOf(
29+
MockTestSourceRoot("/UTBotJavaTest/utbot-analytics/src/test/java"),
30+
MockTestSourceRoot("/UTBotJavaTest/utbot-analytics/src/test/kotlin"),
31+
MockTestSourceRoot("/UTBotJavaTest/utbot-analytics-torch/src/test/kotlin"),
32+
MockTestSourceRoot("/UTBotJavaTest/utbot-cli/src/test/kotlin"),
33+
MockTestSourceRoot("/UTBotJavaTest/utbot-framework/src/test/java"),
34+
MockTestSourceRoot("/UTBotJavaTest/utbot-framework-api/src/test/java"),
35+
MockTestSourceRoot("/UTBotJavaTest/utbot-framework-api/src/test/kotlin"),
36+
MockTestSourceRoot("/UTBotJavaTest/utbot-framework-test/src/test/java"),
37+
MockTestSourceRoot("/UTBotJavaTest/utbot-framework-test/src/test/kotlin"),
38+
MockTestSourceRoot("/UTBotJavaTest/utbot-fuzzers/src/test/java"),
39+
MockTestSourceRoot("/UTBotJavaTest/utbot-fuzzers/src/test/kotlin"),
40+
MockTestSourceRoot("/UTBotJavaTest/utbot-gradle/src/test/kotlin"),
41+
MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation/src/test/java"),
42+
MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation/src/test/kotlin"),
43+
MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation-tests/src/test/java"),
44+
MockTestSourceRoot("/UTBotJavaTest/utbot-instrumentation-tests/src/test/kotlin"),
45+
MockTestSourceRoot("/UTBotJavaTest/utbot-intellij/src/test/java"),
46+
MockTestSourceRoot("/UTBotJavaTest/utbot-maven/src/test/kotlin"),
47+
MockTestSourceRoot("/UTBotJavaTest/utbot-sample/src/test/java"),
48+
MockTestSourceRoot("/UTBotJavaTest/utbot-summary/src/test/kotlin"),
49+
MockTestSourceRoot("/UTBotJavaTest/utbot-summary-tests/src/test/kotlin"),
50+
MockTestSourceRoot("/UTBotJavaTest/utbot-core/src/test/java"),
51+
MockTestSourceRoot("/UTBotJavaTest/utbot-core/src/test/kotlin"),
52+
MockTestSourceRoot("/UTBotJavaTest/utbot-rd/src/test/kotlin"),
53+
)
54+
val moduleSourcePaths = listOf(
55+
"/UTBotJavaTest/utbot-framework/src/main/java",
56+
"/UTBotJavaTest/utbot-framework/src/main/kotlin",
57+
"/UTBotJavaTest/utbot-framework/src/main/resources",
58+
)
59+
val sortedTestRoots = getSortedTestRoots(allTestRoots, moduleSourcePaths, CodegenLanguage.JAVA)
60+
Assertions.assertEquals("/UTBotJavaTest/utbot-framework/src/test/java", sortedTestRoots.first().toString())
61+
}
62+
}

0 commit comments

Comments
 (0)