Skip to content

Better test source root suggestion: remove standard "src/main/" suffix #1439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -48,7 +48,7 @@ open class BaseTestsModel(
srcClasses.map { it.packageName }.distinct().size != 1
}

fun getAllTestSourceRoots() : MutableList<TestSourceRoot> {
fun getAllTestSourceRoots() : MutableList<out ITestSourceRoot> {
with(if (project.isBuildWithGradle) project.allModules() else potentialTestModules) {
return this.flatMap { it.suitableTestSourceRoots().toList() }.toMutableList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Any>>(ComboBox(), null) {

private val SET_TEST_FOLDER = "set test folder"

init {
if (model.project.isBuildWithGradle) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -86,33 +90,6 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) :
}
}

private fun BaseTestsModel.getSortedTestRoots(): MutableList<TestSourceRoot> {
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<TestSourceRoot> {
// 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<VirtualFile, RuntimeException> {
val desc = object:FileChooserDescriptor(false, true, false, false, false, false) {
Expand All @@ -126,12 +103,12 @@ class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) :
files.singleOrNull()
}

private fun configureRootsCombo(testRoots: List<TestSourceRoot>) {
private fun configureRootsCombo(testRoots: List<ITestSourceRoot>) {
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<Any>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -29,26 +24,30 @@ 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

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()
}

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

if (srcModule.suitableTestSourceFolders().isEmpty()) {
val modules = mutableSetOf<Module>()
ModuleUtilCore.collectModulesDependsOn(srcModule, modules)
modules.remove(srcModule)
val modulesWithTestRoot = mutableSetOf<Module>().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)
Expand Down Expand Up @@ -165,11 +164,11 @@ val Project.isBuildWithGradle get() =

const val dedicatedTestSourceRootName = "utbot_tests"

fun Module.addDedicatedTestRoot(testSourceRoots: MutableList<TestSourceRoot>, 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<ITestSourceRoot>, 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() }
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<out ITestSourceRoot>,
moduleSourcePaths: List<String>,
codegenLanguage: CodegenLanguage
): MutableList<ITestSourceRoot> {
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<ITestSourceRoot> {
// 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<String>.getCommonPrefix() : String {
var result = ""
for ((i, s) in withIndex()) {
result = if (i == 0) {
s
} else {
StringUtil.commonPrefix(result, s)
}
}
return result
}
Original file line number Diff line number Diff line change
@@ -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())
}
}