Skip to content

Commit 52acdb2

Browse files
authored
Test framework installation before test generation #1570 (#1584)
1 parent bb982d8 commit 52acdb2

File tree

11 files changed

+107
-42
lines changed

11 files changed

+107
-42
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
@@ -244,6 +244,12 @@ class PythonGenerateTestsCommand : CliktCommand(
244244
checkingRequirementsAction = {
245245
logger.info("Checking requirements...")
246246
},
247+
installingRequirementsAction = {
248+
logger.info("Installing requirements...")
249+
},
250+
testFrameworkInstallationAction = {
251+
logger.info("Test framework installation...")
252+
},
247253
requirementsAreNotInstalledAction = ::processMissingRequirements,
248254
startedLoadingPythonTypesAction = {
249255
logger.info("Loading information about Python types...")

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

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,40 @@ object PythonDialogProcessor {
5151
focusedMethod: PyFunction?,
5252
file: PyFile
5353
) {
54-
val dialog = createDialog(project, functionsToShow, containingClass, focusedMethod, file)
55-
if (!dialog.showAndGet()) {
56-
return
54+
val pythonPath = getPythonPath(functionsToShow)
55+
if (pythonPath == null) {
56+
showErrorDialogLater(
57+
project,
58+
message = "Couldn't find Python interpreter",
59+
title = "Python test generation error"
60+
)
61+
} else {
62+
val dialog = createDialog(
63+
project,
64+
functionsToShow,
65+
containingClass,
66+
focusedMethod,
67+
file,
68+
pythonPath,
69+
)
70+
if (!dialog.showAndGet()) {
71+
return
72+
}
73+
createTests(project, dialog.model)
5774
}
75+
}
5876

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

6281
private fun createDialog(
6382
project: Project,
6483
functionsToShow: Set<PyFunction>,
6584
containingClass: PyClass?,
6685
focusedMethod: PyFunction?,
67-
file: PyFile
86+
file: PyFile,
87+
pythonPath: String,
6888
): PythonDialogWindow {
6989
val srcModule = findSrcModule(functionsToShow)
7090
val testModules = srcModule.testModules(project)
@@ -85,6 +105,7 @@ object PythonDialogProcessor {
85105
DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS,
86106
visitOnlySpecifiedSource = false,
87107
cgLanguageAssistant = PythonCgLanguageAssistant,
108+
pythonPath = pythonPath,
88109
)
89110
)
90111
}
@@ -119,15 +140,7 @@ object PythonDialogProcessor {
119140
return
120141
}
121142
try {
122-
val pythonPath = model.srcModule.sdk?.homePath
123-
if (pythonPath == null) {
124-
showErrorDialogLater(
125-
project,
126-
message = "Couldn't find Python interpreter",
127-
title = "Python test generation error"
128-
)
129-
return
130-
}
143+
131144
val methods = findSelectedPythonMethods(model)
132145
if (methods == null) {
133146
showErrorDialogLater(
@@ -138,7 +151,7 @@ object PythonDialogProcessor {
138151
return
139152
}
140153
processTestGeneration(
141-
pythonPath = pythonPath,
154+
pythonPath = model.pythonPath,
142155
pythonFilePath = model.file.virtualFile.path,
143156
pythonFileContent = getContentFromPyFile(model.file),
144157
directoriesForSysPath = model.directoriesForSysPath,
@@ -151,8 +164,10 @@ object PythonDialogProcessor {
151164
visitOnlySpecifiedSource = model.visitOnlySpecifiedSource,
152165
isCanceled = { indicator.isCanceled },
153166
checkingRequirementsAction = { indicator.text = "Checking requirements" },
167+
installingRequirementsAction = { indicator.text = "Installing requirements..." },
168+
testFrameworkInstallationAction = { indicator.text = "Test framework installation" },
154169
requirementsAreNotInstalledAction = {
155-
askAndInstallRequirementsLater(model.project, pythonPath)
170+
askAndInstallRequirementsLater(model.project, model.pythonPath)
156171
PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED
157172
},
158173
startedLoadingPythonTypesAction = { indicator.text = "Loading information about Python types" },

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.intellij.openapi.project.Project
44
import com.intellij.openapi.roots.ProjectFileIndex
55
import com.intellij.openapi.vfs.VirtualFile
66
import com.intellij.psi.PsiElement
7+
import org.utbot.python.utils.RequirementsUtils
78
import kotlin.random.Random
89

910
inline fun <reified T : PsiElement> getContainingElement(
@@ -28,3 +29,7 @@ fun generateRandomString(length: Int): String {
2829
.map { Random.nextInt(0, charPool.size).let { charPool[it] } }
2930
.joinToString("")
3031
}
32+
33+
fun checkModuleIsInstalled(pythonPath: String, moduleName: String): Boolean {
34+
return RequirementsUtils.requirementsAreInstalled(pythonPath, listOf(moduleName))
35+
}

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-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import org.utbot.python.typing.MypyAnnotations
2222
import org.utbot.python.typing.PythonTypesStorage
2323
import org.utbot.python.typing.StubFileFinder
2424
import org.utbot.python.utils.Cleaner
25+
import org.utbot.python.utils.RequirementsUtils.installRequirements
2526
import org.utbot.python.utils.RequirementsUtils.requirementsAreInstalled
26-
import org.utbot.python.utils.TemporaryFileManager
2727
import org.utbot.python.utils.getLineOfFunction
2828
import java.io.File
2929
import java.nio.file.Path
@@ -49,6 +49,8 @@ object PythonTestGenerationProcessor {
4949
withMinimization: Boolean = true,
5050
isCanceled: () -> Boolean = { false },
5151
checkingRequirementsAction: () -> Unit = {},
52+
installingRequirementsAction: () -> Unit = {},
53+
testFrameworkInstallationAction: () -> Unit = {},
5254
requirementsAreNotInstalledAction: () -> MissingRequirementsActionResult = {
5355
MissingRequirementsActionResult.NOT_INSTALLED
5456
},
@@ -63,11 +65,14 @@ object PythonTestGenerationProcessor {
6365
Cleaner.restart()
6466

6567
try {
66-
TemporaryFileManager.setup()
67-
68+
if (!testFramework.isInstalled) {
69+
testFrameworkInstallationAction()
70+
installRequirements(pythonPath, listOf(testFramework.mainPackage))
71+
}
6872
if (!doNotCheckRequirements) {
6973
checkingRequirementsAction()
7074
if (!requirementsAreInstalled(pythonPath)) {
75+
installingRequirementsAction()
7176
val result = requirementsAreNotInstalledAction()
7277
if (result == MissingRequirementsActionResult.NOT_INSTALLED)
7378
return

utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ object Pytest : TestFramework(displayName = "pytest", id = "pytest") {
4848
}
4949

5050
object Unittest : TestFramework(displayName = "Unittest", id = "Unittest") {
51+
init {
52+
isInstalled = true
53+
}
54+
5155
override val testSuperClass: ClassId = PythonClassId("unittest.TestCase")
5256
override val mainPackage: String = "unittest"
5357
override val assertionsClass: ClassId = PythonClassId("self")

utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,33 @@ object RequirementsUtils {
1414
?: error("Didn't find /check_requirements.py")
1515

1616
fun requirementsAreInstalled(pythonPath: String): Boolean {
17+
return requirementsAreInstalled(pythonPath, requirements)
18+
}
19+
20+
fun requirementsAreInstalled(pythonPath: String, requirementList: List<String>): Boolean {
1721
val requirementsScript =
1822
TemporaryFileManager.createTemporaryFile(requirementsScriptContent, tag = "requirements")
1923
val result = runCommand(
2024
listOf(
2125
pythonPath,
2226
requirementsScript.path
23-
) + requirements
27+
) + requirementList
2428
)
2529
return result.exitValue == 0
2630
}
2731

2832
fun installRequirements(pythonPath: String): CmdResult {
33+
return installRequirements(pythonPath, requirements)
34+
}
35+
36+
fun installRequirements(pythonPath: String, moduleNames: List<String>): CmdResult {
2937
return runCommand(
3038
listOf(
3139
pythonPath,
3240
"-m",
3341
"pip",
3442
"install"
35-
) + requirements
43+
) + moduleNames
3644
)
3745
}
3846
}

utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import java.nio.file.Path
66
import java.nio.file.Paths
77

88
object TemporaryFileManager {
9-
private lateinit var tmpDirectory: Path
9+
private var tmpDirectory: Path
1010
private var nextId = 0
1111

12-
fun setup() {
12+
init {
1313
tmpDirectory = FileUtil.createTempDirectory("python-test-generation-${nextId++}")
1414
Cleaner.addFunction { tmpDirectory.toFile().deleteRecursively() }
1515
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.utbot.intellij.plugin.ui.utils
2+
3+
import com.intellij.ui.ColoredListCellRenderer
4+
import com.intellij.ui.SimpleTextAttributes
5+
import org.utbot.framework.codegen.domain.TestFramework
6+
import javax.swing.JList
7+
8+
fun createTestFrameworksRenderer(willBeInstalledLabel: String): ColoredListCellRenderer<TestFramework> {
9+
return object : ColoredListCellRenderer<TestFramework>() {
10+
override fun customizeCellRenderer(
11+
list: JList<out TestFramework>, value: TestFramework,
12+
index: Int, selected: Boolean, hasFocus: Boolean
13+
) {
14+
this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES)
15+
if (!value.isInstalled) {
16+
this.append(willBeInstalledLabel, SimpleTextAttributes.ERROR_ATTRIBUTES)
17+
}
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)