Skip to content

Commit f16d5ca

Browse files
authored
Utbot-Python refactoring (#2366)
1 parent 9f47eed commit f16d5ca

File tree

122 files changed

+2855
-1178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+2855
-1178
lines changed

utbot-cli-python/src/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
- Required Java version: 11.
88

9-
- Prefered Python version: 3.8 or 3.9.
9+
- Prefered Python version: 3.8+.
1010

1111
Make sure that your Python has `pip` installed (this is usually the case). [Read more about pip installation](https://pip.pypa.io/en/stable/installation/).
1212

1313
Before running utbot install pip requirements (or use `--install-requirements` flag in `generate_python` command):
1414

15-
python -m pip install mypy==0.971 astor typeshed-client coverage
15+
python -m pip install mypy==1.0 utbot_executor==0.4.31 utbot_mypy_runner==0.2.8
1616

1717
## Basic usage
1818

@@ -66,10 +66,6 @@ Run generated tests:
6666

6767
Turn off Python requirements check (to speed up).
6868

69-
- `--visit-only-specified-source`
70-
71-
Do not search for classes and imported modules in other Python files from `--sys-path` option.
72-
7369
- `-t, --timeout INT`
7470

7571
Specify the maximum time in milliseconds to spend on generating tests (60000 by default).
@@ -81,6 +77,14 @@ Run generated tests:
8177
- `--test-framework [pytest|Unittest]`
8278

8379
Test framework to be used.
80+
81+
- `--runtime-exception-behaviour [PASS|FAIL]`
82+
83+
Expected behaviour for runtime exception.
84+
85+
- `--do-not-generate-regression-suite`
86+
87+
Generate regression test suite or not. Regression suite and error suite generation is active by default.
8488

8589
### `run_python` options
8690

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.utbot.cli.language.python
2+
3+
import mu.KLogger
4+
import org.utbot.python.utils.RequirementsInstaller
5+
import org.utbot.python.utils.RequirementsUtils
6+
7+
class CliRequirementsInstaller(
8+
private val installRequirementsIfMissing: Boolean,
9+
private val logger: KLogger,
10+
) : RequirementsInstaller {
11+
override fun checkRequirements(pythonPath: String, requirements: List<String>): Boolean {
12+
return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements)
13+
}
14+
15+
override fun installRequirements(pythonPath: String, requirements: List<String>) {
16+
if (installRequirementsIfMissing) {
17+
val result = RequirementsUtils.installRequirements(pythonPath, requirements)
18+
if (result.exitValue != 0) {
19+
System.err.println(result.stderr)
20+
logger.error("Failed to install requirements.")
21+
}
22+
} else {
23+
logger.error("Missing some requirements. Please add --install-requirements flag or install them manually.")
24+
}
25+
logger.info("Requirements: ${requirements.joinToString()}")
26+
}
27+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.utbot.cli.language.python
2+
3+
import mu.KLogger
4+
import org.utbot.python.PythonTestGenerationConfig
5+
import org.utbot.python.PythonTestGenerationProcessor
6+
import org.utbot.python.PythonTestSet
7+
8+
class PythonCliProcessor(
9+
override val configuration: PythonTestGenerationConfig,
10+
private val output: String,
11+
private val logger: KLogger,
12+
private val coverageOutput: String?,
13+
) : PythonTestGenerationProcessor() {
14+
15+
override fun saveTests(testsCode: String) {
16+
writeToFileAndSave(output, testsCode)
17+
}
18+
19+
override fun notGeneratedTestsAction(testedFunctions: List<String>) {
20+
logger.error(
21+
"Couldn't generate tests for the following functions: ${testedFunctions.joinToString()}"
22+
)
23+
}
24+
25+
override fun processCoverageInfo(testSets: List<PythonTestSet>) {
26+
val coverageReport = getCoverageInfo(testSets)
27+
val output = coverageOutput ?: return
28+
writeToFileAndSave(output, coverageReport)
29+
}
30+
}

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

Lines changed: 69 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import com.github.ajalt.clikt.parameters.types.choice
77
import com.github.ajalt.clikt.parameters.types.long
88
import mu.KotlinLogging
99
import org.parsers.python.PythonParser
10+
import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour
1011
import org.utbot.framework.codegen.domain.TestFramework
12+
import org.utbot.framework.plugin.api.UtExecutionSuccess
1113
import org.utbot.python.PythonMethodHeader
12-
import org.utbot.python.PythonTestGenerationProcessor
13-
import org.utbot.python.PythonTestGenerationProcessor.processTestGeneration
14+
import org.utbot.python.PythonTestGenerationConfig
15+
import org.utbot.python.PythonTestSet
16+
import org.utbot.python.utils.RequirementsInstaller
17+
import org.utbot.python.TestFileInformation
1418
import org.utbot.python.code.PythonCode
1519
import org.utbot.python.framework.api.python.PythonClassId
1620
import org.utbot.python.framework.codegen.model.Pytest
@@ -19,8 +23,6 @@ import org.utbot.python.newtyping.ast.parseClassDefinition
1923
import org.utbot.python.newtyping.ast.parseFunctionDefinition
2024
import org.utbot.python.newtyping.mypy.dropInitFile
2125
import org.utbot.python.utils.*
22-
import org.utbot.python.utils.RequirementsUtils.installRequirements
23-
import org.utbot.python.utils.RequirementsUtils.requirements
2426
import java.io.File
2527
import java.nio.file.Paths
2628

@@ -98,6 +100,13 @@ class PythonGenerateTestsCommand : CliktCommand(
98100
.choice(Pytest.toString(), Unittest.toString())
99101
.default(Unittest.toString())
100102

103+
private val runtimeExceptionTestsBehaviour by option("--runtime-exception-behaviour", help = "PASS or FAIL")
104+
.choice("PASS", "FAIL")
105+
.default("FAIL")
106+
107+
private val doNotGenerateRegressionSuite by option("--do-not-generate-regression-suite", help = "Do not generate regression test suite")
108+
.flag(default = false)
109+
101110
private val testFramework: TestFramework
102111
get() =
103112
when (testFrameworkAsString) {
@@ -200,78 +209,75 @@ class PythonGenerateTestsCommand : CliktCommand(
200209
}
201210
}
202211

203-
private fun processMissingRequirements(): PythonTestGenerationProcessor.MissingRequirementsActionResult {
204-
if (installRequirementsIfMissing) {
205-
logger.info("Installing requirements...")
206-
val result = installRequirements(pythonPath)
207-
if (result.exitValue == 0)
208-
return PythonTestGenerationProcessor.MissingRequirementsActionResult.INSTALLED
209-
System.err.println(result.stderr)
210-
logger.error("Failed to install requirements.")
211-
} else {
212-
logger.error("Missing some requirements. Please add --install-requirements flag or install them manually.")
213-
}
214-
logger.info("Requirements: ${requirements.joinToString()}")
215-
return PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED
216-
}
217-
218-
private fun writeToFileAndSave(filename: String, fileContent: String) {
219-
val file = File(filename)
220-
file.parentFile?.mkdirs()
221-
file.writeText(fileContent)
222-
file.createNewFile()
223-
}
224-
225-
226212
override fun run() {
227213
val status = calculateValues()
228214
if (status is Fail) {
229215
logger.error(status.message)
230216
return
231217
}
232218

233-
processTestGeneration(
219+
logger.info("Checking requirements...")
220+
val installer = CliRequirementsInstaller(installRequirementsIfMissing, logger)
221+
val requirementsAreInstalled = RequirementsInstaller.checkRequirements(
222+
installer,
223+
pythonPath,
224+
if (testFramework.isInstalled) emptyList() else listOf(testFramework.mainPackage)
225+
)
226+
if (!requirementsAreInstalled) {
227+
return
228+
}
229+
230+
val config = PythonTestGenerationConfig(
234231
pythonPath = pythonPath,
235-
pythonFilePath = sourceFile.toAbsolutePath(),
236-
pythonFileContent = sourceFileContent,
237-
directoriesForSysPath = directoriesForSysPath.map { it.toAbsolutePath() }.toSet(),
238-
currentPythonModule = currentPythonModule.dropInitFile(),
239-
pythonMethods = pythonMethods,
240-
containingClassName = pythonClass,
232+
testFileInformation = TestFileInformation(sourceFile.toAbsolutePath(), sourceFileContent, currentPythonModule.dropInitFile()),
233+
sysPathDirectories = directoriesForSysPath.toSet(),
234+
testedMethods = pythonMethods,
241235
timeout = timeout,
242-
testFramework = testFramework,
243236
timeoutForRun = timeoutForRun,
244-
writeTestTextToFile = { generatedCode ->
245-
writeToFileAndSave(output, generatedCode)
246-
},
247-
pythonRunRoot = Paths.get("").toAbsolutePath(),
248-
doNotCheckRequirements = doNotCheckRequirements,
237+
testFramework = testFramework,
238+
testSourceRootPath = Paths.get(output).parent.toAbsolutePath(),
249239
withMinimization = !doNotMinimize,
250-
checkingRequirementsAction = {
251-
logger.info("Checking requirements...")
252-
},
253-
installingRequirementsAction = {
254-
logger.info("Installing requirements...")
255-
},
256-
requirementsAreNotInstalledAction = ::processMissingRequirements,
257-
startedLoadingPythonTypesAction = {
258-
logger.info("Loading information about Python types...")
259-
},
260-
startedTestGenerationAction = {
261-
logger.info("Generating tests...")
262-
},
263-
notGeneratedTestsAction = {
264-
logger.error(
265-
"Couldn't generate tests for the following functions: ${it.joinToString()}"
240+
isCanceled = { false },
241+
runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.valueOf(runtimeExceptionTestsBehaviour)
242+
)
243+
244+
val processor = PythonCliProcessor(
245+
config,
246+
output,
247+
logger,
248+
coverageOutput,
249+
)
250+
251+
logger.info("Loading information about Python types...")
252+
val (mypyStorage, _) = processor.sourceCodeAnalyze()
253+
254+
logger.info("Generating tests...")
255+
var testSets = processor.testGenerate(mypyStorage)
256+
if (testSets.isEmpty()) return
257+
if (doNotGenerateRegressionSuite) {
258+
testSets = testSets.map { testSet ->
259+
PythonTestSet(
260+
testSet.method,
261+
testSet.executions.filterNot { it.result is UtExecutionSuccess },
262+
testSet.errors,
263+
testSet.mypyReport,
264+
testSet.classId
266265
)
267-
},
268-
processMypyWarnings = { messages -> messages.forEach { println(it) } },
269-
processCoverageInfo = { coverageReport ->
270-
val output = coverageOutput ?: return@processTestGeneration
271-
writeToFileAndSave(output, coverageReport)
272266
}
273-
) {
274-
logger.info("Finished test generation for the following functions: ${it.joinToString()}")
275267
}
268+
269+
logger.info("Saving tests...")
270+
val testCode = processor.testCodeGenerate(testSets)
271+
processor.saveTests(testCode)
272+
273+
274+
logger.info("Saving coverage report...")
275+
processor.processCoverageInfo(testSets)
276+
277+
logger.info(
278+
"Finished test generation for the following functions: ${
279+
testSets.joinToString { it.method.name }
280+
}"
281+
)
276282
}
277283
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,11 @@ fun findCurrentPythonModule(
1919
}
2020

2121
fun String.toAbsolutePath(): String =
22-
File(this).canonicalPath
22+
File(this).canonicalPath
23+
24+
fun writeToFileAndSave(filename: String, fileContent: String) {
25+
val file = File(filename)
26+
file.parentFile?.mkdirs()
27+
file.writeText(fileContent)
28+
file.createNewFile()
29+
}

utbot-intellij-python/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ tasks {
3333
}
3434

3535
dependencies {
36+
implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion)
3637
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
3738
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
3839
implementation(project(":utbot-ui-commons"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.utbot.intellij.plugin.language.python
2+
3+
import com.intellij.notification.NotificationType
4+
import com.intellij.openapi.application.invokeLater
5+
import com.intellij.openapi.application.runReadAction
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.ui.DialogPanel
8+
import com.intellij.openapi.ui.DialogWrapper
9+
import com.intellij.ui.dsl.builder.panel
10+
import org.utbot.intellij.plugin.ui.Notifier
11+
import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater
12+
import org.utbot.python.utils.RequirementsInstaller
13+
import org.utbot.python.utils.RequirementsUtils
14+
import javax.swing.JComponent
15+
16+
17+
class IntellijRequirementsInstaller(
18+
val project: Project,
19+
): RequirementsInstaller {
20+
override fun checkRequirements(pythonPath: String, requirements: List<String>): Boolean {
21+
return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements)
22+
}
23+
24+
override fun installRequirements(pythonPath: String, requirements: List<String>) {
25+
invokeLater {
26+
if (InstallRequirementsDialog(requirements).showAndGet()) {
27+
val installResult = RequirementsUtils.installRequirements(pythonPath, requirements)
28+
if (installResult.exitValue != 0) {
29+
showErrorDialogLater(
30+
project,
31+
"Requirements installing failed.<br>" +
32+
"${installResult.stderr}<br><br>" +
33+
"Try to install with pip:<br>" +
34+
" ${requirements.joinToString("<br>")}",
35+
"Requirements error"
36+
)
37+
} else {
38+
invokeLater {
39+
runReadAction {
40+
PythonNotifier.notify("Requirements installation is complete")
41+
}
42+
}
43+
}
44+
}
45+
}
46+
}
47+
}
48+
49+
50+
class InstallRequirementsDialog(private val requirements: List<String>) : DialogWrapper(true) {
51+
init {
52+
title = "Python Requirements Installation"
53+
init()
54+
}
55+
56+
private lateinit var panel: DialogPanel
57+
58+
override fun createCenterPanel(): JComponent {
59+
panel = panel {
60+
row("Some requirements are not installed.") { }
61+
row("Requirements:") { }
62+
indent {
63+
requirements.map { row {text(it)} }
64+
}
65+
row("Install them?") { }
66+
}
67+
return panel
68+
}
69+
}
70+
71+
object PythonNotifier : Notifier() {
72+
override val notificationType: NotificationType = NotificationType.INFORMATION
73+
74+
override val displayId: String = "Python notification"
75+
}

0 commit comments

Comments
 (0)