Skip to content

Utbot-Python refactoring #2366

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
merged 45 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e694ba6
Remove unused code
tamarinvs19 Jun 26, 2023
fade414
Add new requirements installer
tamarinvs19 Jun 26, 2023
f0b72a8
Previous version of base test generator
tamarinvs19 Jun 26, 2023
a3afda0
New test generation processor for intellij-python
tamarinvs19 Jun 27, 2023
c13163d
New test generation processor for cli-python and refactor
tamarinvs19 Jun 28, 2023
a7ae3bb
Rename PythonTestGenerationProcessor
tamarinvs19 Jun 28, 2023
b5ce816
Refactor PythonEngine and PythonTestCaseGenerator
tamarinvs19 Jun 28, 2023
c765749
Change isEqual codegen for bool and None
tamarinvs19 Jun 28, 2023
9127738
Fix installation complete notification
tamarinvs19 Jun 29, 2023
09ce912
Filter unused imports builtins and types
tamarinvs19 Jun 29, 2023
e8610b8
Refactor structure
tamarinvs19 Jun 29, 2023
3af1401
Working on optional arguments
tamarinvs19 Jun 29, 2023
7af0581
Remove additionalVars
tamarinvs19 Jun 30, 2023
74f8e33
Working on named arguments
tamarinvs19 Jul 3, 2023
ad6633e
Working on named arguments
tamarinvs19 Jul 3, 2023
5a122dc
Supported named arguments
tamarinvs19 Jul 4, 2023
a02ca1d
Supported self checking
tamarinvs19 Jul 4, 2023
a26d4cf
Update sys path rendering
tamarinvs19 Jul 11, 2023
bec549a
Fix named arguments in magic methods
tamarinvs19 Jul 12, 2023
d0c1b1f
Refactor example testset
tamarinvs19 Jul 12, 2023
1e49085
Fix comparePythonTree
tamarinvs19 Jul 12, 2023
5e67969
Fix timeout group separation
tamarinvs19 Jul 13, 2023
549870c
Fix constructor building
tamarinvs19 Jul 13, 2023
6b14e87
Fix constructor building
tamarinvs19 Jul 17, 2023
ba40767
Add more test examples
tamarinvs19 Jul 18, 2023
ae455a7
Add draft of run test script
tamarinvs19 Jul 18, 2023
8ae8990
Update run_tests.py
tamarinvs19 Jul 18, 2023
7c34a13
Update tests and finalize run test script
tamarinvs19 Jul 18, 2023
24f725c
Add example command
tamarinvs19 Jul 18, 2023
92f1b5e
Merge branch 'main' into tamarinvs19/utbot-python-refactoring
tamarinvs19 Jul 18, 2023
92a8d95
Update version of utbot-executor
tamarinvs19 Jul 19, 2023
33f4bba
Fix annotation import for python
tamarinvs19 Jul 20, 2023
efc8fe6
Used setting runtimeExceptionTestsBehaviour
tamarinvs19 Jul 21, 2023
091a719
update
tamarinvs19 Jul 21, 2023
ad03cf6
Update run_tests
tamarinvs19 Jul 24, 2023
b03b695
Add coverage checker
tamarinvs19 Jul 24, 2023
3cb62b7
Add coverage info
tamarinvs19 Jul 24, 2023
c4afc4f
Fix builtins module rendering
tamarinvs19 Jul 25, 2023
3588baf
Fix codegen
tamarinvs19 Jul 25, 2023
b234aff
Fix empty test set rendering
tamarinvs19 Jul 25, 2023
e088396
Add try before manager.shutdown()
tamarinvs19 Jul 25, 2023
2895029
Generate field state assertions only for success tests
tamarinvs19 Jul 25, 2023
e3451d7
Update config and test set
tamarinvs19 Jul 25, 2023
94d293b
Add generate-regression-suite param for cli
tamarinvs19 Jul 25, 2023
715c481
Invert regression suite param
tamarinvs19 Jul 26, 2023
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
16 changes: 10 additions & 6 deletions utbot-cli-python/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

- Required Java version: 11.

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

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/).

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

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

## Basic usage

Expand Down Expand Up @@ -66,10 +66,6 @@ Run generated tests:

Turn off Python requirements check (to speed up).

- `--visit-only-specified-source`

Do not search for classes and imported modules in other Python files from `--sys-path` option.

- `-t, --timeout INT`

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

Test framework to be used.

- `--runtime-exception-behaviour [PASS|FAIL]`

Expected behaviour for runtime exception.

- `--do-not-generate-regression-suite`

Generate regression test suite or not. Regression suite and error suite generation is active by default.

### `run_python` options

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.utbot.cli.language.python

import mu.KLogger
import org.utbot.python.utils.RequirementsInstaller
import org.utbot.python.utils.RequirementsUtils

class CliRequirementsInstaller(
private val installRequirementsIfMissing: Boolean,
private val logger: KLogger,
) : RequirementsInstaller {
override fun checkRequirements(pythonPath: String, requirements: List<String>): Boolean {
return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements)
}

override fun installRequirements(pythonPath: String, requirements: List<String>) {
if (installRequirementsIfMissing) {
val result = RequirementsUtils.installRequirements(pythonPath, requirements)
if (result.exitValue != 0) {
System.err.println(result.stderr)
logger.error("Failed to install requirements.")
}
} else {
logger.error("Missing some requirements. Please add --install-requirements flag or install them manually.")
}
logger.info("Requirements: ${requirements.joinToString()}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.utbot.cli.language.python

import mu.KLogger
import org.utbot.python.PythonTestGenerationConfig
import org.utbot.python.PythonTestGenerationProcessor
import org.utbot.python.PythonTestSet

class PythonCliProcessor(
override val configuration: PythonTestGenerationConfig,
private val output: String,
private val logger: KLogger,
private val coverageOutput: String?,
) : PythonTestGenerationProcessor() {

override fun saveTests(testsCode: String) {
writeToFileAndSave(output, testsCode)
}

override fun notGeneratedTestsAction(testedFunctions: List<String>) {
logger.error(
"Couldn't generate tests for the following functions: ${testedFunctions.joinToString()}"
)
}

override fun processCoverageInfo(testSets: List<PythonTestSet>) {
val coverageReport = getCoverageInfo(testSets)
val output = coverageOutput ?: return
writeToFileAndSave(output, coverageReport)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.long
import mu.KotlinLogging
import org.parsers.python.PythonParser
import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour
import org.utbot.framework.codegen.domain.TestFramework
import org.utbot.framework.plugin.api.UtExecutionSuccess
import org.utbot.python.PythonMethodHeader
import org.utbot.python.PythonTestGenerationProcessor
import org.utbot.python.PythonTestGenerationProcessor.processTestGeneration
import org.utbot.python.PythonTestGenerationConfig
import org.utbot.python.PythonTestSet
import org.utbot.python.utils.RequirementsInstaller
import org.utbot.python.TestFileInformation
import org.utbot.python.code.PythonCode
import org.utbot.python.framework.api.python.PythonClassId
import org.utbot.python.framework.codegen.model.Pytest
Expand All @@ -19,8 +23,6 @@ import org.utbot.python.newtyping.ast.parseClassDefinition
import org.utbot.python.newtyping.ast.parseFunctionDefinition
import org.utbot.python.newtyping.mypy.dropInitFile
import org.utbot.python.utils.*
import org.utbot.python.utils.RequirementsUtils.installRequirements
import org.utbot.python.utils.RequirementsUtils.requirements
import java.io.File
import java.nio.file.Paths

Expand Down Expand Up @@ -98,6 +100,13 @@ class PythonGenerateTestsCommand : CliktCommand(
.choice(Pytest.toString(), Unittest.toString())
.default(Unittest.toString())

private val runtimeExceptionTestsBehaviour by option("--runtime-exception-behaviour", help = "PASS or FAIL")
.choice("PASS", "FAIL")
.default("FAIL")

private val doNotGenerateRegressionSuite by option("--do-not-generate-regression-suite", help = "Do not generate regression test suite")
.flag(default = false)

private val testFramework: TestFramework
get() =
when (testFrameworkAsString) {
Expand Down Expand Up @@ -200,78 +209,75 @@ class PythonGenerateTestsCommand : CliktCommand(
}
}

private fun processMissingRequirements(): PythonTestGenerationProcessor.MissingRequirementsActionResult {
if (installRequirementsIfMissing) {
logger.info("Installing requirements...")
val result = installRequirements(pythonPath)
if (result.exitValue == 0)
return PythonTestGenerationProcessor.MissingRequirementsActionResult.INSTALLED
System.err.println(result.stderr)
logger.error("Failed to install requirements.")
} else {
logger.error("Missing some requirements. Please add --install-requirements flag or install them manually.")
}
logger.info("Requirements: ${requirements.joinToString()}")
return PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED
}

private fun writeToFileAndSave(filename: String, fileContent: String) {
val file = File(filename)
file.parentFile?.mkdirs()
file.writeText(fileContent)
file.createNewFile()
}


override fun run() {
val status = calculateValues()
if (status is Fail) {
logger.error(status.message)
return
}

processTestGeneration(
logger.info("Checking requirements...")
val installer = CliRequirementsInstaller(installRequirementsIfMissing, logger)
val requirementsAreInstalled = RequirementsInstaller.checkRequirements(
installer,
pythonPath,
if (testFramework.isInstalled) emptyList() else listOf(testFramework.mainPackage)
)
if (!requirementsAreInstalled) {
return
}

val config = PythonTestGenerationConfig(
pythonPath = pythonPath,
pythonFilePath = sourceFile.toAbsolutePath(),
pythonFileContent = sourceFileContent,
directoriesForSysPath = directoriesForSysPath.map { it.toAbsolutePath() }.toSet(),
currentPythonModule = currentPythonModule.dropInitFile(),
pythonMethods = pythonMethods,
containingClassName = pythonClass,
testFileInformation = TestFileInformation(sourceFile.toAbsolutePath(), sourceFileContent, currentPythonModule.dropInitFile()),
sysPathDirectories = directoriesForSysPath.toSet(),
testedMethods = pythonMethods,
timeout = timeout,
testFramework = testFramework,
timeoutForRun = timeoutForRun,
writeTestTextToFile = { generatedCode ->
writeToFileAndSave(output, generatedCode)
},
pythonRunRoot = Paths.get("").toAbsolutePath(),
doNotCheckRequirements = doNotCheckRequirements,
testFramework = testFramework,
testSourceRootPath = Paths.get(output).parent.toAbsolutePath(),
withMinimization = !doNotMinimize,
checkingRequirementsAction = {
logger.info("Checking requirements...")
},
installingRequirementsAction = {
logger.info("Installing requirements...")
},
requirementsAreNotInstalledAction = ::processMissingRequirements,
startedLoadingPythonTypesAction = {
logger.info("Loading information about Python types...")
},
startedTestGenerationAction = {
logger.info("Generating tests...")
},
notGeneratedTestsAction = {
logger.error(
"Couldn't generate tests for the following functions: ${it.joinToString()}"
isCanceled = { false },
runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.valueOf(runtimeExceptionTestsBehaviour)
)

val processor = PythonCliProcessor(
config,
output,
logger,
coverageOutput,
)

logger.info("Loading information about Python types...")
val (mypyStorage, _) = processor.sourceCodeAnalyze()

logger.info("Generating tests...")
var testSets = processor.testGenerate(mypyStorage)
if (testSets.isEmpty()) return
if (doNotGenerateRegressionSuite) {
testSets = testSets.map { testSet ->
PythonTestSet(
testSet.method,
testSet.executions.filterNot { it.result is UtExecutionSuccess },
testSet.errors,
testSet.mypyReport,
testSet.classId
)
},
processMypyWarnings = { messages -> messages.forEach { println(it) } },
processCoverageInfo = { coverageReport ->
val output = coverageOutput ?: return@processTestGeneration
writeToFileAndSave(output, coverageReport)
}
) {
logger.info("Finished test generation for the following functions: ${it.joinToString()}")
}

logger.info("Saving tests...")
val testCode = processor.testCodeGenerate(testSets)
processor.saveTests(testCode)


logger.info("Saving coverage report...")
processor.processCoverageInfo(testSets)

logger.info(
"Finished test generation for the following functions: ${
testSets.joinToString { it.method.name }
}"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,11 @@ fun findCurrentPythonModule(
}

fun String.toAbsolutePath(): String =
File(this).canonicalPath
File(this).canonicalPath

fun writeToFileAndSave(filename: String, fileContent: String) {
val file = File(filename)
file.parentFile?.mkdirs()
file.writeText(fileContent)
file.createNewFile()
}
1 change: 1 addition & 0 deletions utbot-intellij-python/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tasks {
}

dependencies {
implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion)
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
implementation(project(":utbot-ui-commons"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.utbot.intellij.plugin.language.python

import com.intellij.notification.NotificationType
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.dsl.builder.panel
import org.utbot.intellij.plugin.ui.Notifier
import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater
import org.utbot.python.utils.RequirementsInstaller
import org.utbot.python.utils.RequirementsUtils
import javax.swing.JComponent


class IntellijRequirementsInstaller(
val project: Project,
): RequirementsInstaller {
override fun checkRequirements(pythonPath: String, requirements: List<String>): Boolean {
return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements)
}

override fun installRequirements(pythonPath: String, requirements: List<String>) {
invokeLater {
if (InstallRequirementsDialog(requirements).showAndGet()) {
val installResult = RequirementsUtils.installRequirements(pythonPath, requirements)
if (installResult.exitValue != 0) {
showErrorDialogLater(
project,
"Requirements installing failed.<br>" +
"${installResult.stderr}<br><br>" +
"Try to install with pip:<br>" +
" ${requirements.joinToString("<br>")}",
"Requirements error"
)
} else {
invokeLater {
runReadAction {
PythonNotifier.notify("Requirements installation is complete")
}
}
}
}
}
}
}


class InstallRequirementsDialog(private val requirements: List<String>) : DialogWrapper(true) {
init {
title = "Python Requirements Installation"
init()
}

private lateinit var panel: DialogPanel

override fun createCenterPanel(): JComponent {
panel = panel {
row("Some requirements are not installed.") { }
row("Requirements:") { }
indent {
requirements.map { row {text(it)} }
}
row("Install them?") { }
}
return panel
}
}

object PythonNotifier : Notifier() {
override val notificationType: NotificationType = NotificationType.INFORMATION

override val displayId: String = "Python notification"
}
Loading