Skip to content

Commit 83c0955

Browse files
authored
Merge branch 'main' into tamarinvs19/utbot-python-stable
2 parents b2f15ff + d2edb45 commit 83c0955

File tree

17 files changed

+181
-72
lines changed

17 files changed

+181
-72
lines changed

utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,12 @@ class PythonGenerateTestsCommand : CliktCommand(
249249
checkingRequirementsAction = {
250250
logger.info("Checking requirements...")
251251
},
252+
installingRequirementsAction = {
253+
logger.info("Installing requirements...")
254+
},
255+
testFrameworkInstallationAction = {
256+
logger.info("Test framework installation...")
257+
},
252258
requirementsAreNotInstalledAction = ::processMissingRequirements,
253259
startedLoadingPythonTypesAction = {
254260
logger.info("Loading information about Python types...")

utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.intellij.openapi.roots.ProjectFileIndex
1313
import com.intellij.openapi.ui.Messages
1414
import com.intellij.openapi.vfs.VfsUtil
1515
import com.intellij.openapi.vfs.VfsUtilCore
16+
import com.intellij.openapi.vfs.VirtualFile
1617
import com.intellij.psi.PsiDirectory
1718
import com.intellij.psi.PsiFileFactory
1819
import com.jetbrains.python.psi.PyClass
@@ -37,7 +38,6 @@ import org.utbot.python.framework.codegen.PythonCgLanguageAssistant
3738
import org.utbot.python.utils.RequirementsUtils.installRequirements
3839
import org.utbot.python.utils.RequirementsUtils.requirements
3940
import org.utbot.python.utils.camelToSnakeCase
40-
import java.io.File
4141
import java.nio.file.Path
4242
import java.nio.file.Paths
4343
import kotlin.io.path.Path
@@ -52,20 +52,40 @@ object PythonDialogProcessor {
5252
focusedMethod: PyFunction?,
5353
file: PyFile
5454
) {
55-
val dialog = createDialog(project, functionsToShow, containingClass, focusedMethod, file)
56-
if (!dialog.showAndGet()) {
57-
return
55+
val pythonPath = getPythonPath(functionsToShow)
56+
if (pythonPath == null) {
57+
showErrorDialogLater(
58+
project,
59+
message = "Couldn't find Python interpreter",
60+
title = "Python test generation error"
61+
)
62+
} else {
63+
val dialog = createDialog(
64+
project,
65+
functionsToShow,
66+
containingClass,
67+
focusedMethod,
68+
file,
69+
pythonPath,
70+
)
71+
if (!dialog.showAndGet()) {
72+
return
73+
}
74+
createTests(project, dialog.model)
5875
}
76+
}
5977

60-
createTests(project, dialog.model)
78+
private fun getPythonPath(functionsToShow: Set<PyFunction>): String? {
79+
return findSrcModule(functionsToShow).sdk?.homePath
6180
}
6281

6382
private fun createDialog(
6483
project: Project,
6584
functionsToShow: Set<PyFunction>,
6685
containingClass: PyClass?,
6786
focusedMethod: PyFunction?,
68-
file: PyFile
87+
file: PyFile,
88+
pythonPath: String,
6989
): PythonDialogWindow {
7090
val srcModule = findSrcModule(functionsToShow)
7191
val testModules = srcModule.testModules(project)
@@ -86,6 +106,7 @@ object PythonDialogProcessor {
86106
DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS,
87107
visitOnlySpecifiedSource = false,
88108
cgLanguageAssistant = PythonCgLanguageAssistant,
109+
pythonPath = pythonPath,
89110
)
90111
)
91112
}
@@ -134,19 +155,11 @@ object PythonDialogProcessor {
134155
return
135156
}
136157
try {
137-
val pythonPath = model.srcModule.sdk?.homePath
138-
if (pythonPath == null) {
139-
showErrorDialogLater(
140-
project,
141-
message = "Couldn't find Python interpreter",
142-
title = "Python test generation error"
143-
)
144-
return
145-
}
158+
146159
val methods = findSelectedPythonMethods(model)
147160

148161
processTestGeneration(
149-
pythonPath = pythonPath,
162+
pythonPath = model.pythonPath,
150163
pythonFilePath = model.file.virtualFile.path,
151164
pythonFileContent = getContentFromPyFile(model.file),
152165
directoriesForSysPath = model.directoriesForSysPath,
@@ -162,8 +175,10 @@ object PythonDialogProcessor {
162175
pythonRunRoot = Path(model.testSourceRootPath),
163176
isCanceled = { indicator.isCanceled },
164177
checkingRequirementsAction = { indicator.text = "Checking requirements" },
178+
installingRequirementsAction = { indicator.text = "Installing requirements..." },
179+
testFrameworkInstallationAction = { indicator.text = "Test framework installation" },
165180
requirementsAreNotInstalledAction = {
166-
askAndInstallRequirementsLater(model.project, pythonPath)
181+
askAndInstallRequirementsLater(model.project, model.pythonPath)
167182
PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED
168183
},
169184
startedLoadingPythonTypesAction = { indicator.text = "Loading information about Python types" },
@@ -272,43 +287,72 @@ fun findSrcModule(functions: Collection<PyFunction>): Module {
272287

273288
fun getContentFromPyFile(file: PyFile) = file.viewProvider.contents.toString()
274289

290+
fun getPyCodeFromPyFile(file: PyFile, pythonModule: String): PythonCode? {
291+
val content = getContentFromPyFile(file)
292+
return getFromString(content, file.virtualFile.path, pythonModule = pythonModule)
293+
}
294+
295+
/*
296+
* Returns set of sys paths and tested file import path
297+
*/
298+
275299
fun getDirectoriesForSysPath(
276300
srcModule: Module,
277301
file: PyFile
278302
): Pair<Set<String>, String> {
279303
val sources = ModuleRootManager.getInstance(srcModule).getSourceRoots(false).toMutableList()
280304
val ancestor = ProjectFileIndex.getInstance(file.project).getContentRootForFile(file.virtualFile)
281-
if (ancestor != null && !sources.contains(ancestor))
305+
if (ancestor != null)
282306
sources.add(ancestor)
283307

284308
// Collect sys.path directories with imported modules
309+
val importedPaths = emptyList<VirtualFile>().toMutableList()
310+
311+
// 1. import <module>
285312
file.importTargets.forEach { importTarget ->
286313
importTarget.multiResolve().forEach {
287314
val element = it.element
288315
if (element != null) {
289316
val directory = element.parent
290317
if (directory is PsiDirectory) {
291-
if (sources.any { source ->
292-
val sourcePath = source.canonicalPath
293-
if (source.isDirectory && sourcePath != null) {
294-
directory.virtualFile.canonicalPath?.startsWith(sourcePath) ?: false
295-
} else {
296-
false
297-
}
298-
}) {
299-
sources.add(directory.virtualFile)
300-
}
318+
importedPaths.add(directory.virtualFile)
301319
}
302320
}
303321
}
304322
}
305323

306-
var importPath = ancestor?.let { VfsUtil.getParentDir(VfsUtilCore.getRelativeLocation(file.virtualFile, it)) } ?: ""
307-
if (importPath != "")
308-
importPath += "."
324+
// 2. from <module> import ...
325+
file.fromImports.forEach { importTarget ->
326+
importTarget.resolveImportSourceCandidates().forEach {
327+
val directory = it.parent
328+
if (directory is PsiDirectory ) {
329+
importedPaths.add(directory.virtualFile)
330+
}
331+
}
332+
}
333+
334+
// Select modules only from this project
335+
importedPaths.forEach {
336+
if (it.isProjectSubmodule(ancestor)) {
337+
sources.add(it)
338+
}
339+
}
340+
341+
val fileName = file.name.removeSuffix(".py")
342+
val importPath = ancestor?.let {
343+
VfsUtil.getParentDir(
344+
VfsUtilCore.getRelativeLocation(file.virtualFile, it)
345+
)
346+
} ?: ""
347+
val importStringPath = listOf(
348+
importPath.toPath().joinToString("."),
349+
fileName
350+
)
351+
.filterNot { it.isEmpty() }
352+
.joinToString(".")
309353

310354
return Pair(
311-
sources.map { it.path.replace("\\", "\\\\") }.toSet(),
312-
"${importPath}${file.name}".removeSuffix(".py").toPath().joinToString(".").replace("/", File.separator)
355+
sources.map { it.path }.toSet(),
356+
importStringPath
313357
)
314358
}

utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import com.intellij.openapi.ui.ComboBox
55
import com.intellij.openapi.ui.DialogPanel
66
import com.intellij.openapi.ui.DialogWrapper
77
import com.intellij.openapi.ui.ValidationInfo
8+
import com.intellij.ui.ColoredListCellRenderer
89
import com.intellij.ui.ContextHelpLabel
910
import com.intellij.ui.JBIntSpinner
11+
import com.intellij.ui.SimpleTextAttributes
1012
import com.intellij.ui.components.Panel
1113
import com.intellij.ui.layout.CellBuilder
1214
import com.intellij.ui.layout.Row
@@ -17,16 +19,15 @@ import com.jetbrains.python.refactoring.classes.PyMemberInfoStorage
1719
import com.jetbrains.python.refactoring.classes.membersManager.PyMemberInfo
1820
import com.jetbrains.python.refactoring.classes.ui.PyMemberSelectionTable
1921
import org.utbot.framework.UtSettings
20-
import org.utbot.framework.plugin.api.CodeGenerationSettingItem
22+
import org.utbot.framework.codegen.domain.TestFramework
2123
import java.awt.BorderLayout
2224
import java.util.concurrent.TimeUnit
23-
import javax.swing.DefaultComboBoxModel
24-
import javax.swing.JCheckBox
25-
import javax.swing.JComponent
26-
import javax.swing.JPanel
2725
import org.utbot.intellij.plugin.ui.components.TestSourceDirectoryChooser
26+
import org.utbot.intellij.plugin.ui.utils.createTestFrameworksRenderer
27+
import javax.swing.*
2828

2929

30+
private const val WILL_BE_INSTALLED_LABEL = " (will be installed)"
3031
private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1
3132

3233
class PythonDialogWindow(val model: PythonTestsModel) : DialogWrapper(model.project) {
@@ -55,12 +56,16 @@ class PythonDialogWindow(val model: PythonTestsModel) : DialogWrapper(model.proj
5556
private lateinit var panel: DialogPanel
5657

5758
init {
58-
title = "Generate Tests With UtBot"
59+
title = "Generate Tests with UnitTestBot"
5960
isResizable = false
61+
62+
model.cgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks.forEach {
63+
it.isInstalled = it.isInstalled || checkModuleIsInstalled(model.pythonPath, it.mainPackage)
64+
}
65+
6066
init()
6167
}
6268

63-
@Suppress("UNCHECKED_CAST")
6469
override fun createCenterPanel(): JComponent {
6570

6671
panel = panel {
@@ -69,7 +74,7 @@ class PythonDialogWindow(val model: PythonTestsModel) : DialogWrapper(model.proj
6974
}
7075
row("Test framework:") {
7176
makePanelWithHelpTooltip(
72-
testFrameworks as ComboBox<CodeGenerationSettingItem>,
77+
testFrameworks,
7378
null
7479
)
7580
}
@@ -100,9 +105,14 @@ class PythonDialogWindow(val model: PythonTestsModel) : DialogWrapper(model.proj
100105
}
101106

102107
updateFunctionsTable()
108+
updateTestFrameworksList()
103109
return panel
104110
}
105111

112+
private fun updateTestFrameworksList() {
113+
testFrameworks.renderer = createTestFrameworksRenderer(WILL_BE_INSTALLED_LABEL)
114+
}
115+
106116
private fun globalPyFunctionsToPyMemberInfo(
107117
project: Project,
108118
functions: Collection<PyFunction>

utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class PythonTestsModel(
2323
var timeoutForRun: Long,
2424
var visitOnlySpecifiedSource: Boolean,
2525
val cgLanguageAssistant: CgLanguageAssistant,
26+
val pythonPath: String,
2627
) : BaseTestsModel(
2728
project,
2829
srcModule,

utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/Utils.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package org.utbot.intellij.plugin.language.python
22

33
import com.intellij.openapi.project.Project
44
import com.intellij.openapi.roots.ProjectFileIndex
5+
import com.intellij.openapi.vfs.VfsUtil
56
import com.intellij.openapi.vfs.VirtualFile
67
import com.intellij.psi.PsiElement
8+
import org.utbot.python.utils.RequirementsUtils
79
import kotlin.random.Random
810

911
inline fun <reified T : PsiElement> getContainingElement(
@@ -27,4 +29,12 @@ fun generateRandomString(length: Int): String {
2729
return (0..length)
2830
.map { Random.nextInt(0, charPool.size).let { charPool[it] } }
2931
.joinToString("")
30-
}
32+
}
33+
34+
fun VirtualFile.isProjectSubmodule(ancestor: VirtualFile?): Boolean {
35+
return VfsUtil.isUnder(this, setOf(ancestor).toMutableSet())
36+
}
37+
38+
fun checkModuleIsInstalled(pythonPath: String, moduleName: String): Boolean {
39+
return RequirementsUtils.requirementsAreInstalled(pythonPath, listOf(moduleName))
40+
}

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import org.utbot.intellij.plugin.ui.components.TestFolderComboWithBrowseButton
131131
import org.utbot.intellij.plugin.ui.utils.LibrarySearchScope
132132
import org.utbot.intellij.plugin.ui.utils.addSourceRootIfAbsent
133133
import org.utbot.intellij.plugin.ui.utils.allLibraries
134+
import org.utbot.intellij.plugin.ui.utils.createTestFrameworksRenderer
134135
import org.utbot.intellij.plugin.ui.utils.findFrameworkLibrary
135136
import org.utbot.intellij.plugin.ui.utils.findParametrizedTestsLibrary
136137
import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath
@@ -984,17 +985,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
984985

985986
testFrameworks.model = DefaultComboBoxModel(enabledTestFrameworks.toTypedArray())
986987
testFrameworks.item = if (currentFrameworkItem in enabledTestFrameworks) currentFrameworkItem else defaultItem
987-
testFrameworks.renderer = object : ColoredListCellRenderer<TestFramework>() {
988-
override fun customizeCellRenderer(
989-
list: JList<out TestFramework>, value: TestFramework,
990-
index: Int, selected: Boolean, hasFocus: Boolean
991-
) {
992-
this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES)
993-
if (!value.isInstalled) {
994-
this.append(WILL_BE_INSTALLED_LABEL, SimpleTextAttributes.ERROR_ATTRIBUTES)
995-
}
996-
}
997-
}
988+
testFrameworks.renderer = createTestFrameworksRenderer(WILL_BE_INSTALLED_LABEL)
998989

999990
currentFrameworkItem = testFrameworks.item
1000991
}

utbot-js/src/main/kotlin/framework/api/js/JsApi.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package framework.api.js
22

3-
import java.lang.reflect.Modifier
3+
import framework.api.js.util.toJsClassId
44
import org.utbot.framework.plugin.api.ClassId
55
import org.utbot.framework.plugin.api.ConstructorId
66
import org.utbot.framework.plugin.api.MethodId
77
import org.utbot.framework.plugin.api.UtModel
8-
import framework.api.js.util.toJsClassId
98
import org.utbot.framework.plugin.api.primitiveModelValueToClassId
9+
import java.lang.reflect.Modifier
1010

1111
open class JsClassId(
1212
private val jsName: String,
@@ -19,6 +19,9 @@ open class JsClassId(
1919
override val simpleName: String
2020
get() = jsName
2121

22+
override val simpleNameWithEnclosingClasses: String
23+
get() = jsName
24+
2225
override val allMethods: Sequence<JsMethodId>
2326
get() = methods
2427

@@ -119,4 +122,4 @@ data class JsPrimitiveModel(
119122
}
120123

121124
private fun jsPrimitiveModelValueToClassId(value: Any) =
122-
primitiveModelValueToClassId(value).toJsClassId()
125+
primitiveModelValueToClassId(value).toJsClassId()

utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ class MochaManager(context: CgContext) : TestFrameworkManager(context) {
4444
get() = TODO("Not yet implemented")
4545
override val annotationForNestedClasses: CgAnnotation
4646
get() = TODO("Not yet implemented")
47-
override val annotationForOuterClasses: CgAnnotation
48-
get() = TODO("Not yet implemented")
4947

5048
override fun assertEquals(expected: CgValue, actual: CgValue) {
5149
+assertions[jsAssertEquals](expected, actual)

utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ fun startEvaluationProcess(input: EvaluationInput): EvaluationProcess {
106106
input.directoriesForSysPath,
107107
input.moduleToImport,
108108
input.additionalModulesToImport,
109-
fileForOutput.path.replace("\\", "\\\\"),
110-
coverageDatabasePath.absolutePath.replace("\\", "\\\\")
109+
fileForOutput.path,
110+
coverageDatabasePath.path,
111111
)
112112
val fileWithCode = TemporaryFileManager.createTemporaryFile(
113113
runCode,

0 commit comments

Comments
 (0)