diff --git a/utbot-cli-python/src/README.md b/utbot-cli-python/src/README.md index bdc5c5e0b0..67c689721d 100644 --- a/utbot-cli-python/src/README.md +++ b/utbot-cli-python/src/README.md @@ -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 @@ -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). @@ -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 diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/CliRequirementsInstaller.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/CliRequirementsInstaller.kt new file mode 100644 index 0000000000..8653520a30 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/CliRequirementsInstaller.kt @@ -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): Boolean { + return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements) + } + + override fun installRequirements(pythonPath: String, requirements: List) { + 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()}") + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonCliProcessor.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonCliProcessor.kt new file mode 100644 index 0000000000..801686e242 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonCliProcessor.kt @@ -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) { + logger.error( + "Couldn't generate tests for the following functions: ${testedFunctions.joinToString()}" + ) + } + + override fun processCoverageInfo(testSets: List) { + val coverageReport = getCoverageInfo(testSets) + val output = coverageOutput ?: return + writeToFileAndSave(output, coverageReport) + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt index 72169f00a1..03361d44f7 100644 --- a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt @@ -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 @@ -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 @@ -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) { @@ -200,29 +209,6 @@ 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) { @@ -230,48 +216,68 @@ class PythonGenerateTestsCommand : CliktCommand( 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 } + }" + ) } } diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Utils.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Utils.kt index 8f3b92f673..8ff7f71457 100644 --- a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Utils.kt +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Utils.kt @@ -19,4 +19,11 @@ fun findCurrentPythonModule( } fun String.toAbsolutePath(): String = - File(this).canonicalPath \ No newline at end of file + File(this).canonicalPath + +fun writeToFileAndSave(filename: String, fileContent: String) { + val file = File(filename) + file.parentFile?.mkdirs() + file.writeText(fileContent) + file.createNewFile() +} diff --git a/utbot-intellij-python/build.gradle.kts b/utbot-intellij-python/build.gradle.kts index 5c737516ef..bf7d404342 100644 --- a/utbot-intellij-python/build.gradle.kts +++ b/utbot-intellij-python/build.gradle.kts @@ -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")) diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/IntellijRequirementsInstaller.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/IntellijRequirementsInstaller.kt new file mode 100644 index 0000000000..e8701bdbe6 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/IntellijRequirementsInstaller.kt @@ -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): Boolean { + return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements) + } + + override fun installRequirements(pythonPath: String, requirements: List) { + invokeLater { + if (InstallRequirementsDialog(requirements).showAndGet()) { + val installResult = RequirementsUtils.installRequirements(pythonPath, requirements) + if (installResult.exitValue != 0) { + showErrorDialogLater( + project, + "Requirements installing failed.
" + + "${installResult.stderr}

" + + "Try to install with pip:
" + + " ${requirements.joinToString("
")}", + "Requirements error" + ) + } else { + invokeLater { + runReadAction { + PythonNotifier.notify("Requirements installation is complete") + } + } + } + } + } + } +} + + +class InstallRequirementsDialog(private val requirements: List) : 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" +} diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt index 05b0853bfa..b769ccceef 100644 --- a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt @@ -1,8 +1,8 @@ package org.utbot.intellij.plugin.language.python -import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.application.readAction +import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileDocumentManager @@ -14,45 +14,98 @@ import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDirectory -import com.intellij.psi.PsiFileFactory +import com.intellij.util.concurrency.AppExecutorUtil import com.jetbrains.python.psi.PyClass import com.jetbrains.python.psi.PyElement import com.jetbrains.python.psi.PyFile import com.jetbrains.python.psi.PyFunction import com.jetbrains.python.psi.resolve.QualifiedNameFinder -import kotlinx.coroutines.runBlocking -import org.jetbrains.kotlin.idea.util.application.runWriteAction +import mu.KotlinLogging import org.jetbrains.kotlin.idea.util.module import org.jetbrains.kotlin.idea.util.projectStructure.sdk -import org.jetbrains.kotlin.j2k.getContainingClass import org.utbot.common.PathUtil.toPath -import org.utbot.common.appendHtmlLine import org.utbot.framework.plugin.api.util.LockFile import org.utbot.intellij.plugin.settings.Settings -import org.utbot.intellij.plugin.ui.WarningTestsReportNotifier import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater import org.utbot.intellij.plugin.ui.utils.testModules 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.utils.RequirementsInstaller +import org.utbot.python.TestFileInformation import org.utbot.python.framework.api.python.PythonClassId import org.utbot.python.framework.codegen.PythonCgLanguageAssistant import org.utbot.python.newtyping.mypy.dropInitFile -import org.utbot.python.utils.RequirementsUtils.installRequirements -import org.utbot.python.utils.RequirementsUtils.requirements -import org.utbot.python.utils.camelToSnakeCase -import java.nio.file.Path -import java.nio.file.Paths +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit import kotlin.io.path.Path -const val DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS = 2000L - object PythonDialogProcessor { + private val logger = KotlinLogging.logger {} + + enum class ProgressRange(val from : Double, val to: Double) { + ANALYZE(from = 0.0, to = 0.1), + SOLVING(from = 0.1, to = 0.95), + CODEGEN(from = 0.95, to = 1.0), + } + + private fun updateIndicator( + indicator: ProgressIndicator, + range: ProgressRange, + text: String, + fraction: Double, + stepCount: Int, + stepNumber: Int = 0, + ) { + assert(stepCount > stepNumber) + val maxValue = 1.0 / stepCount + val shift = stepNumber.toDouble() + invokeLater { + if (indicator.isCanceled) return@invokeLater + text.let { indicator.text = it } + indicator.fraction = indicator.fraction + .coerceAtLeast((shift + range.from + (range.to - range.from) * fraction.coerceIn(0.0, 1.0)) * maxValue) + logger.debug("Phase ${indicator.text} with progress ${String.format("%.2f",indicator.fraction)}") + } + } + + private fun runIndicatorWithTimeHandler(indicator: ProgressIndicator, range: ProgressRange, text: String, globalCount: Int, globalShift: Int, timeout: Long): ScheduledFuture<*> { + val startTime = System.currentTimeMillis() + return AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({ + val innerTimeoutRatio = + ((System.currentTimeMillis() - startTime).toDouble() / timeout) + .coerceIn(0.0, 1.0) + updateIndicator( + indicator, + range, + text, + innerTimeoutRatio, + globalCount, + globalShift, + ) + }, 0, 100, TimeUnit.MILLISECONDS) + } + + private fun updateIndicatorTemplate( + indicator: ProgressIndicator, + stepCount: Int, + stepNumber: Int + ): (ProgressRange, String, Double) -> Unit { + return { range: ProgressRange, text: String, fraction: Double -> + updateIndicator( + indicator, + range, + text, + fraction, + stepCount, + stepNumber + ) + } + } + fun createDialogAndGenerateTests( project: Project, elementsToShow: Set, @@ -87,10 +140,6 @@ object PythonDialogProcessor { } } - private fun getPythonPath(elementsToShow: Set): String? { - return findSrcModules(elementsToShow).first().sdk?.homePath - } - private fun createDialog( project: Project, elementsToShow: Set, @@ -111,7 +160,7 @@ object PythonDialogProcessor { elementsToShow, focusedElements, project.service().generationTimeoutInMillis, - DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS, + project.service().hangingTestsTimeout.timeoutMs, cgLanguageAssistant = PythonCgLanguageAssistant, pythonPath = pythonPath, names = elementsToShow.associateBy { Pair(it.fileName()!!, it.name!!) }, @@ -120,8 +169,7 @@ object PythonDialogProcessor { } private fun findSelectedPythonMethods(model: PythonTestLocalModel): List { - return runBlocking { - readAction { + return ReadAction.nonBlocking> { model.selectedElements .filter { model.selectedElements.contains(it) } .flatMap { @@ -136,7 +184,7 @@ object PythonDialogProcessor { val functionName = it.name ?: return@mapNotNull null val moduleFilename = it.containingFile.virtualFile?.canonicalPath ?: "" val containingClassId = it.containingClass?.name?.let{ cls -> PythonClassId(cls) } - return@mapNotNull PythonMethodHeader( + PythonMethodHeader( functionName, moduleFilename, containingClassId, @@ -144,55 +192,54 @@ object PythonDialogProcessor { } .toSet() .toList() - } - } + }.executeSynchronously() ?: emptyList() } private fun groupPyElementsByModule(model: PythonTestsModel): Set { - return runBlocking { - readAction { - model.selectedElements - .groupBy { it.containingFile } - .flatMap { fileGroup -> - fileGroup.value - .groupBy { it is PyClass }.values - } - .filter { it.isNotEmpty() } - .map { - val realElements = it.map { member -> model.names[Pair(member.fileName(), member.name)]!! } - val file = realElements.first().containingFile as PyFile - val srcModule = getSrcModule(realElements.first()) - - val (directoriesForSysPath, moduleToImport) = getDirectoriesForSysPath(srcModule, file) - PythonTestLocalModel( - model.project, - model.timeout, - model.timeoutForRun, - model.cgLanguageAssistant, - model.pythonPath, - model.testSourceRootPath, - model.testFramework, - realElements.toSet(), - model.runtimeExceptionTestsBehaviour, - directoriesForSysPath, - moduleToImport.dropInitFile(), - file, - realElements.first().getContainingClass() as PyClass? - ) - } - .toSet() - } - } - } + return ReadAction.nonBlocking> { + model.selectedElements + .groupBy { it.containingFile } + .flatMap { fileGroup -> + fileGroup.value + .groupBy { it is PyClass }.values + } + .flatMap { fileGroup -> + val classes = fileGroup.filterIsInstance() + val functions = fileGroup.filterIsInstance() + val groups: List> = classes.map { listOf(it) } + listOf(functions) + groups + } + .filter { it.isNotEmpty() } + .map { + val realElements = it.map { member -> model.names[Pair(member.fileName(), member.name)]!! } + val file = realElements.first().containingFile as PyFile + val srcModule = getSrcModule(realElements.first()) - private fun getOutputFileName(model: PythonTestLocalModel): String { - val moduleName = model.currentPythonModule.camelToSnakeCase().replace('.', '_') - return if (model.containingClass == null) { - "test_$moduleName.py" - } else { - val className = model.containingClass.name?.camelToSnakeCase()?.replace('.', '_') - "test_${moduleName}_$className.py" - } + val (directoriesForSysPath, moduleToImport) = getDirectoriesForSysPath(srcModule, file) + PythonTestLocalModel( + model.project, + model.timeout, + model.timeoutForRun, + model.cgLanguageAssistant, + model.pythonPath, + model.testSourceRootPath, + model.testFramework, + realElements.toSet(), + model.runtimeExceptionTestsBehaviour, + directoriesForSysPath, + moduleToImport.dropInitFile(), + file, + realElements.first().let { pyElement -> + if (pyElement is PyFunction) { + pyElement.containingClass + } else { + null + } + } + ) + } + .toSet() + }.executeSynchronously() ?: emptySet() } private fun createTests(project: Project, baseModel: PythonTestsModel) { @@ -202,138 +249,95 @@ object PythonDialogProcessor { return } try { - groupPyElementsByModule(baseModel).forEach { model -> - val methods = findSelectedPythonMethods(model) - val requirementsList = requirements.toMutableList() - if (!model.testFramework.isInstalled) { - requirementsList += model.testFramework.mainPackage - } + indicator.text = "Checking requirements..." + indicator.isIndeterminate = false - val content = runBlocking { - readAction { - getContentFromPyFile(model.file) - } - } + val installer = IntellijRequirementsInstaller(project) + + val requirementsAreInstalled = RequirementsInstaller.checkRequirements( + installer, + baseModel.pythonPath, + if (baseModel.testFramework.isInstalled) emptyList() else listOf(baseModel.testFramework.mainPackage) + ) + if (!requirementsAreInstalled) { + return + } + + val modelGroups = groupPyElementsByModule(baseModel) + val totalModules = modelGroups.size + + modelGroups.forEachIndexed { index, model -> + val localUpdateIndicator = updateIndicatorTemplate(indicator, totalModules, index) + localUpdateIndicator(ProgressRange.ANALYZE, "Analyze code: read files", 0.1) + + val methods = findSelectedPythonMethods(model) + val content = getContentFromPyFile(model.file) - processTestGeneration( + val config = PythonTestGenerationConfig( pythonPath = model.pythonPath, - pythonFilePath = model.file.virtualFile.path, - pythonFileContent = content, - directoriesForSysPath = model.directoriesForSysPath, - currentPythonModule = model.currentPythonModule, - pythonMethods = methods, - containingClassName = model.containingClass?.name, + testFileInformation = TestFileInformation(model.file.virtualFile.path, content, model.currentPythonModule), + sysPathDirectories = model.directoriesForSysPath, + testedMethods = methods, timeout = model.timeout, - testFramework = model.testFramework, timeoutForRun = model.timeoutForRun, - writeTestTextToFile = { generatedCode -> - writeGeneratedCodeToPsiDocument(generatedCode, model) - }, - pythonRunRoot = Path(model.testSourceRootPath), + testFramework = model.testFramework, + testSourceRootPath = Path(model.testSourceRootPath), + withMinimization = true, isCanceled = { indicator.isCanceled }, - checkingRequirementsAction = { indicator.text = "Checking requirements" }, - installingRequirementsAction = { indicator.text = "Installing requirements..." }, - requirementsAreNotInstalledAction = { - askAndInstallRequirementsLater(model.project, model.pythonPath, requirementsList) - PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED - }, - startedLoadingPythonTypesAction = { indicator.text = "Loading information about Python types" }, - startedTestGenerationAction = { indicator.text = "Generating tests" }, - notGeneratedTestsAction = { - showErrorDialogLater( - project, - message = "Cannot create tests for the following functions: " + it.joinToString(), - title = "Python test generation error" - ) - }, - processMypyWarnings = { - val message = it.fold(StringBuilder()) { acc, line -> acc.appendHtmlLine(line) } - WarningTestsReportNotifier.notify(message.toString()) - }, - runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour, - startedCleaningAction = { indicator.text = "Cleaning up..." } + runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour + ) + val processor = PythonIntellijProcessor( + config, + project, + model ) - } - } finally { - LockFile.unlock() - } - } - }) - } - private fun getDirectoriesFromRoot(root: Path, path: Path): List { - if (path == root || path.parent == null) - return emptyList() - return getDirectoriesFromRoot(root, path.parent) + listOf(path.fileName.toString()) - } + localUpdateIndicator(ProgressRange.ANALYZE, "Analyze module ${model.currentPythonModule}", 0.5) - private fun createPsiDirectoryForTestSourceRoot(model: PythonTestLocalModel): PsiDirectory { - val root = getContentRoot(model.project, model.file.virtualFile) - val paths = getDirectoriesFromRoot( - Paths.get(root.path), - Paths.get(model.testSourceRootPath) - ) - val rootPSI = getContainingElement(model.file) { it.virtualFile == root }!! - return paths.fold(rootPSI) { acc, folderName -> - acc.findSubdirectory(folderName) ?: acc.createSubdirectory(folderName) - } - } + val (mypyStorage, _) = processor.sourceCodeAnalyze() - private fun writeGeneratedCodeToPsiDocument(generatedCode: String, model: PythonTestLocalModel) { - invokeLater { - runWriteAction { - val testDir = createPsiDirectoryForTestSourceRoot(model) - val testFileName = getOutputFileName(model) - val testPsiFile = PsiFileFactory.getInstance(model.project) - .createFileFromText(testFileName, PythonLanguageAssistant.language, generatedCode) - testDir.findFile(testPsiFile.name)?.delete() - testDir.add(testPsiFile) - val file = testDir.findFile(testPsiFile.name)!! - CodeInsightUtil.positionCursor(model.project, file, file) - } - } - } + localUpdateIndicator(ProgressRange.ANALYZE, "Analyze module ${model.currentPythonModule}", 1.0) - private fun askAndInstallRequirementsLater(project: Project, pythonPath: String, requirementsList: List) { - val message = """ - Some requirements are not installed. - Requirements:
- ${requirementsList.joinToString("
")} -
- Install them? - """.trimIndent() - invokeLater { - val result = Messages.showOkCancelDialog( - project, - message, - "Requirements Error", - "Install", - "Cancel", - null - ) - if (result == Messages.CANCEL) - return@invokeLater + val timerHandler = runIndicatorWithTimeHandler( + indicator, + ProgressRange.SOLVING, + "Generate test cases for module ${model.currentPythonModule}", + totalModules, + index, + model.timeout, + ) + try { + val testSets = processor.testGenerate(mypyStorage) + timerHandler.cancel(true) + if (testSets.isEmpty()) return@forEachIndexed - ProgressManager.getInstance().run(object : Backgroundable(project, "Installing requirements") { - override fun run(indicator: ProgressIndicator) { - val installResult = installRequirements(pythonPath, requirementsList) + localUpdateIndicator(ProgressRange.CODEGEN, "Generate tests code for module ${model.currentPythonModule}", 0.0) + val testCode = processor.testCodeGenerate(testSets) - if (installResult.exitValue != 0) { - showErrorDialogLater( - project, - "Requirements installing failed.
" + - "${installResult.stderr}

" + - "Try to install with pip:
" + - " ${requirementsList.joinToString("
")}", - "Requirements error" - ) + localUpdateIndicator(ProgressRange.CODEGEN, "Saving tests module ${model.currentPythonModule}", 0.9) + processor.saveTests(testCode) + + logger.info( + "Finished test generation for the following functions: ${ + testSets.joinToString { it.method.name } + }" + ) + } finally { + timerHandler.cancel(true) + } } + } finally { + LockFile.unlock() } - }) - } + } + }) } } +fun getPythonPath(elementsToShow: Set): String? { + return findSrcModules(elementsToShow).first().sdk?.homePath +} + fun findSrcModules(elements: Collection): List { return elements.mapNotNull { it.module }.distinct() } @@ -346,7 +350,10 @@ fun getFullName(element: PyElement): String { return QualifiedNameFinder.getQualifiedName(element) ?: error("Name for source class or function not found") } -fun getContentFromPyFile(file: PyFile) = file.viewProvider.contents.toString() +fun getContentFromPyFile(file: PyFile) = + ReadAction.nonBlocking { + file.viewProvider.contents.toString() + }.executeSynchronously() ?: error("Cannot read file $file") /* * Returns set of sys paths and tested file import path @@ -355,76 +362,74 @@ fun getDirectoriesForSysPath( srcModule: Module, file: PyFile ): Pair, String> { - return runBlocking { - readAction { - val sources = ModuleRootManager.getInstance(srcModule).getSourceRoots(false).toMutableList() - val ancestor = ProjectFileIndex.getInstance(file.project).getContentRootForFile(file.virtualFile) - if (ancestor != null) - sources.add(ancestor) - - // Collect sys.path directories with imported modules - val importedPaths = emptyList().toMutableList() - - // 1. import - file.importTargets.forEach { importTarget -> - importTarget.multiResolve().forEach { - val element = it.element - if (element != null) { - val directory = element.parent - if (directory is PsiDirectory) { - // If we have `import a.b.c` we need to add syspath to module `a` only - val additionalLevel = importTarget.importedQName?.componentCount?.dec() ?: 0 - directory.topParent(additionalLevel)?.let { dir -> - importedPaths.add(dir.virtualFile) - } - } - } - } - } + return ReadAction.nonBlocking, String>> { + val sources = ModuleRootManager.getInstance(srcModule).getSourceRoots(false).toMutableList() + val ancestor = ProjectFileIndex.getInstance(file.project).getContentRootForFile(file.virtualFile) + if (ancestor != null) + sources.add(ancestor) + + // Collect sys.path directories with imported modules + val importedPaths = emptyList().toMutableList() - // 2. from import ... - file.fromImports.forEach { importTarget -> - importTarget.resolveImportSourceCandidates().forEach { - val directory = it.parent - val isRelativeImport = - importTarget.relativeLevel > 0 // If we have `from . import a` we don't need to add syspath - if (directory is PsiDirectory && !isRelativeImport) { - // If we have `from a.b.c import d` we need to add syspath to module `a` only - val additionalLevel = importTarget.importSourceQName?.componentCount?.dec() ?: 0 + // 1. import + file.importTargets.forEach { importTarget -> + importTarget.multiResolve().forEach { + val element = it.element + if (element != null) { + val directory = element.parent + if (directory is PsiDirectory) { + // If we have `import a.b.c` we need to add syspath to module `a` only + val additionalLevel = importTarget.importedQName?.componentCount?.dec() ?: 0 directory.topParent(additionalLevel)?.let { dir -> importedPaths.add(dir.virtualFile) } } } } + } - // Select modules only from this project but not from installation directory - importedPaths.forEach { - val path = it.toNioPath() - val hasSitePackages = - (0 until (path.nameCount)).any { i -> path.subpath(i, i + 1).toString() == "site-packages" } - if (it.isProjectSubmodule(ancestor) && !hasSitePackages) { - sources.add(it) + // 2. from import ... + file.fromImports.forEach { importTarget -> + importTarget.resolveImportSourceCandidates().forEach { + val directory = it.parent + val isRelativeImport = + importTarget.relativeLevel > 0 // If we have `from . import a` we don't need to add syspath + if (directory is PsiDirectory && !isRelativeImport) { + // If we have `from a.b.c import d` we need to add syspath to module `a` only + val additionalLevel = importTarget.importSourceQName?.componentCount?.dec() ?: 0 + directory.topParent(additionalLevel)?.let { dir -> + importedPaths.add(dir.virtualFile) + } } } + } - val fileName = file.name.removeSuffix(".py") - val importPath = ancestor?.let { - VfsUtil.getParentDir( - VfsUtilCore.getRelativeLocation(file.virtualFile, it) - ) - } ?: "" - val importStringPath = listOf( - importPath.toPath().joinToString("."), - fileName - ) - .filterNot { it.isEmpty() } - .joinToString(".") + // Select modules only from this project but not from installation directory + importedPaths.forEach { + val path = it.toNioPath() + val hasSitePackages = + (0 until (path.nameCount)).any { i -> path.subpath(i, i + 1).toString() == "site-packages" } + if (it.isProjectSubmodule(ancestor) && !hasSitePackages) { + sources.add(it) + } + } - Pair( - sources.map { it.path }.toSet(), - importStringPath + val fileName = file.name.removeSuffix(".py") + val importPath = ancestor?.let { + VfsUtil.getParentDir( + VfsUtilCore.getRelativeLocation(file.virtualFile, it) ) - } - } + } ?: "" + val importStringPath = listOf( + importPath.toPath().joinToString("."), + fileName + ) + .filterNot { it.isEmpty() } + .joinToString(".") + + Pair( + sources.map { it.path }.toSet(), + importStringPath + ) + }.executeSynchronously() ?: error("Cannot collect sys path directories") } \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt index 7f349c0cfd..32b83945a9 100644 --- a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt @@ -76,9 +76,9 @@ class PythonDialogWindow(val model: PythonTestsModel) : DialogWrapper(model.proj row("Test generation timeout:") { cell(BorderLayoutPanel().apply { addToLeft(timeoutSpinnerForTotalTimeout) - addToRight(JBLabel("seconds per module")) + addToRight(JBLabel("seconds per group")) }) - contextHelp("Set the timeout for all test generation processes per module to complete.") + contextHelp("Set the timeout for all test generation processes per class or top level functions in one module to complete.") } row("Generate tests for:") {} row { diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonIntellijProcessor.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonIntellijProcessor.kt new file mode 100644 index 0000000000..89f2c498ec --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonIntellijProcessor.kt @@ -0,0 +1,78 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiFileFactory +import com.jetbrains.python.psi.PyClass +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.python.PythonTestGenerationConfig +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestSet +import org.utbot.python.utils.camelToSnakeCase +import java.nio.file.Path +import java.nio.file.Paths + +class PythonIntellijProcessor( + override val configuration: PythonTestGenerationConfig, + val project: Project, + val model: PythonTestLocalModel, +) : PythonTestGenerationProcessor() { + override fun saveTests(testsCode: String) { + invokeLater { + runWriteAction { + val testDir = createPsiDirectoryForTestSourceRoot(model) + val testFileName = getOutputFileName(model) + val testPsiFile = PsiFileFactory.getInstance(model.project) + .createFileFromText(testFileName, PythonLanguageAssistant.language, testsCode) + testDir.findFile(testPsiFile.name)?.delete() + testDir.add(testPsiFile) + val file = testDir.findFile(testPsiFile.name)!! + CodeInsightUtil.positionCursor(model.project, file, file) + } + } + } + + private fun getDirectoriesFromRoot(root: Path, path: Path): List { + if (path == root || path.parent == null) + return emptyList() + return getDirectoriesFromRoot(root, path.parent) + listOf(path.fileName.toString()) + } + + private fun createPsiDirectoryForTestSourceRoot(model: PythonTestLocalModel): PsiDirectory { + val root = getContentRoot(model.project, model.file.virtualFile) + val paths = getDirectoriesFromRoot( + Paths.get(root.path), + Paths.get(model.testSourceRootPath) + ) + val rootPSI = getContainingElement(model.file) { it.virtualFile == root }!! + return paths.fold(rootPSI) { acc, folderName -> + acc.findSubdirectory(folderName) ?: acc.createSubdirectory(folderName) + } + } + + private fun getOutputFileName(model: PythonTestLocalModel): String { + val moduleName = model.currentPythonModule.camelToSnakeCase().replace('.', '_') + return if (model.selectedElements.size == 1 && model.selectedElements.first() is PyClass) { + val className = model.selectedElements.first().name?.camelToSnakeCase()?.replace('.', '_') + "test_${moduleName}_$className.py" + } else if (model.containingClass == null) { + "test_$moduleName.py" + } else { + val className = model.containingClass.name?.camelToSnakeCase()?.replace('.', '_') + "test_${moduleName}_$className.py" + } + } + + override fun notGeneratedTestsAction(testedFunctions: List) { + showErrorDialogLater( + project, + message = "Cannot create tests for the following functions: " + testedFunctions.joinToString(), + title = "Python test generation error" + ) + } + + override fun processCoverageInfo(testSets: List) { } +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/settings/Settings.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/settings/Settings.kt index 931a462a6e..5c46f4d53b 100644 --- a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/settings/Settings.kt +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/settings/Settings.kt @@ -1,5 +1,6 @@ package org.utbot.intellij.plugin.language.python.settings +import org.utbot.framework.codegen.domain.HangingTestsTimeout import org.utbot.intellij.plugin.language.python.PythonTestsModel import org.utbot.intellij.plugin.settings.Settings @@ -13,5 +14,7 @@ private fun fromGenerateTestsModel(model: PythonTestsModel): Settings.State { testFramework = model.testFramework, generationTimeoutInMillis = model.timeout, enableExperimentalLanguagesSupport = true, + hangingTestsTimeout = HangingTestsTimeout(model.timeoutForRun), + runtimeExceptionTestsBehaviour = model.runtimeExceptionTestsBehaviour, ) } diff --git a/utbot-python/samples/.gitignore b/utbot-python/samples/.gitignore index 5f40fc7fee..42d795037d 100644 --- a/utbot-python/samples/.gitignore +++ b/utbot-python/samples/.gitignore @@ -1,5 +1,6 @@ .tmp/ utbot_tests/ +cli_test_dir/ utbot-cli.jar __pycache__/ .venv/ diff --git a/utbot-python/samples/easy_samples/.gitignore b/utbot-python/samples/easy_samples/.gitignore deleted file mode 100644 index aef5dc69ef..0000000000 --- a/utbot-python/samples/easy_samples/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -utbot_tests -.tmp -.pytest_cache \ No newline at end of file diff --git a/utbot-python/samples/easy_samples/import_test.py b/utbot-python/samples/easy_samples/import_test.py deleted file mode 100644 index 3edca6b8da..0000000000 --- a/utbot-python/samples/easy_samples/import_test.py +++ /dev/null @@ -1,10 +0,0 @@ -import importlib.machinery as im -import collections as c -from collections import deque -import importlib - -importlib.machinery.PathFinder - - -class A: - pass diff --git a/utbot-python/samples/easy_samples/regex.py b/utbot-python/samples/easy_samples/regex.py deleted file mode 100644 index 96c8f092f3..0000000000 --- a/utbot-python/samples/easy_samples/regex.py +++ /dev/null @@ -1,18 +0,0 @@ -import re -import time - - -def check_regex(string: str) -> bool: - pattern = r"'(''|\\\\|\\'|[^'])*'" - if re.match(pattern, string): - return True - return False - - -def timetest(): - t = time.time() - print(check_regex("\'\J/\\\\\\'\\N''''P'''\'x'L''\'';'\'N\$'\\\'\'\`''D\\''�='''m\\\\\'\'\\–H\\No'F'(''U]\'V")) - print((time.time() - t)) - -if __name__ == '__main__': - timetest() diff --git a/utbot-python/samples/easy_samples/sample_classes.py b/utbot-python/samples/easy_samples/sample_classes.py deleted file mode 100644 index 7d33528a08..0000000000 --- a/utbot-python/samples/easy_samples/sample_classes.py +++ /dev/null @@ -1,36 +0,0 @@ -from dataclasses import dataclass - - -class A: - def __init__(self, val: int): - self.description = val - - -class B: - def __init__(self, val: complex): - self.description = val - - def sqrt(self): - return self.description ** 0.5 - - -@dataclass -class C: - counter: int = 0 - - def inc(self): - self.counter += 1 - - -class Outer: - class Inner: - a = 1 - - def inc(self): - self.a += 1 - - def __init__(self): - self.inner = Outer.Inner() - - def inc1(self): - self.inner.inc() diff --git a/utbot-python/samples/easy_samples/test_package/__init__.py b/utbot-python/samples/easy_samples/test_package/__init__.py deleted file mode 100644 index bf9748c947..0000000000 --- a/utbot-python/samples/easy_samples/test_package/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -def init_function(x: int): - return x ** 2 diff --git a/utbot-python/samples/run_tests.py b/utbot-python/samples/run_tests.py new file mode 100644 index 0000000000..7cb0bf4acd --- /dev/null +++ b/utbot-python/samples/run_tests.py @@ -0,0 +1,141 @@ +""" +Example command + run_tests.py generate -c test_configuration.json + -p -o + + run_tests.py run -p -t + -c +""" +import argparse +import json +import os +import typing +import pathlib + + +def parse_arguments(): + parser = argparse.ArgumentParser( + prog='UtBot Python test', + description='Generage tests for example files' + ) + subparsers = parser.add_subparsers(dest="command") + parser_generate = subparsers.add_parser('generate', help='Generate tests') + parser_generate.add_argument('java') + parser_generate.add_argument('jar') + parser_generate.add_argument('path_to_test_dir') + parser_generate.add_argument('-c', '--config_file') + parser_generate.add_argument('-p', '--python_path') + parser_generate.add_argument('-o', '--output_dir') + parser_generate.add_argument('-i', '--coverage_output_dir') + parser_run = subparsers.add_parser('run', help='Run tests') + parser_run.add_argument('-p', '--python_path') + parser_run.add_argument('-t', '--test_directory') + parser_run.add_argument('-c', '--code_directory') + parser_coverage = subparsers.add_parser('check_coverage', help='Check coverage') + parser_coverage.add_argument('-i', '--coverage_output_dir') + parser_coverage.add_argument('-c', '--config_file') + return parser.parse_args() + + +def parse_config(config_path: str): + with open(config_path, "r") as fin: + return json.loads(fin.read()) + + +def generate_tests( + java: str, + jar_path: str, + sys_paths: list[str], + python_path: str, + file_under_test: str, + timeout: int, + output: str, + coverage_output: str, + class_names: typing.Optional[list[str]] = None, + method_names: typing.Optional[list[str]] = None + ): + command = f"{java} -jar {jar_path} generate_python {file_under_test}.py -p {python_path} -o {output} -s {' '.join(sys_paths)} --timeout {timeout * 1000} --install-requirements --runtime-exception-behaviour PASS --coverage={coverage_output}" + if class_names is not None: + command += f" -c {','.join(class_names)}" + if method_names is not None: + command += f" -m {','.join(method_names)}" + print(command) + code = os.system(command) + print(code) + return code + + +def run_tests( + python_path: str, + tests_dir: str, + samples_dir: str, +): + command = f'{python_path} -m coverage run --source={samples_dir} -m unittest {tests_dir} -p "utbot_*"' + print(command) + code = os.system(command) + return code + + +def check_coverage( + config_file: str, + coverage_output_dir: str, +): + config = parse_config(config_file) + report: typing.Dict[str, bool] = {} + coverage: typing.Dict[str, typing.Tuple[float, float]] = {} + for part in config['parts']: + for file in part['files']: + for group in file['groups']: + expected_coverage = group.get('coverage', 0) + + file_suffix = f"{part['path'].replace('/', '_')}_{file['name']}" + coverage_output_file = pathlib.PurePath(coverage_output_dir, f"coverage_{file_suffix}.json") + with open(coverage_output_file, "rt") as fin: + actual_coverage_json = json.loads(fin.readline()) + actual_covered = sum(lines['end'] - lines['start'] + 1 for lines in actual_coverage_json['covered']) + actual_not_covered = sum(lines['end'] - lines['start'] + 1 for lines in actual_coverage_json['notCovered']) + actual_coverage = round(actual_covered / (actual_not_covered + actual_covered) * 100) + + coverage[file_suffix] = (actual_coverage, expected_coverage) + report[file_suffix] = actual_coverage >= expected_coverage + if all(report.values()): + return True + print("-------------") + print("Bad coverage:") + print("-------------") + for file, good_coverage in report.items(): + if not good_coverage: + print(f"{file}: {coverage[file][0]}/{coverage[file][1]}") + return False + + +def main_test_generation(args): + config = parse_config(args.config_file) + for part in config['parts']: + for file in part['files']: + for group in file['groups']: + full_name = pathlib.PurePath(args.path_to_test_dir, part['path'], file['name']) + output_file = pathlib.PurePath(args.output_dir, f"utbot_tests_{part['path'].replace('/', '_')}_{file['name']}.py") + coverage_output_file = pathlib.PurePath(args.coverage_output_dir, f"coverage_{part['path'].replace('/', '_')}_{file['name']}.json") + generate_tests( + args.java, + args.jar, + [args.path_to_test_dir], + args.python_path, + str(full_name), + group['timeout'], + str(output_file), + str(coverage_output_file), + group['classes'], + group['methods'] + ) + + +if __name__ == '__main__': + arguments = parse_arguments() + if arguments.command == 'generate': + main_test_generation(arguments) + elif arguments.command == 'run': + run_tests(arguments.python_path, arguments.test_directory, arguments.code_directory) + elif arguments.command == 'check_coverage': + check_coverage(arguments.config_file, arguments.coverage_output_dir) diff --git a/utbot-python/samples/samples.md b/utbot-python/samples/samples.md deleted file mode 100644 index b3cf2794e8..0000000000 --- a/utbot-python/samples/samples.md +++ /dev/null @@ -1,27 +0,0 @@ -## Соответствие файлов и сгенерированных тестов - -Примеры в `/samples`, сгенерированный код в `/cli_utbot_tests`. - -Команда по умолчанию -```bash -java -jar utbot-cli.jar generate_python samples/.py -p -o cli_utbot_tests/.py -s samples/ ----timeout-for-run 500 --timeout 10000 --visit-only-specified-source -``` - -| Пример | Тесты | Дополнительные аргументы | -|--------------------------|-------------------------------------------|-------------------------------------------| -| `arithmetic.py` | `generated_tests__arithmetic.py` | | -| `deep_equals.py` | `generated_tests__deep_equals.py` | | -| `dicts.py` | `generated_tests__dicts.py` | `-c Dictionary -m translate` | -| `deque.py` | `generated_tests__deque.py` | | -| `dummy_with_eq.py` | `generated_tests__dummy_with_eq.py` | `-c Dummy -m propogate` | -| `dummy_without_eq.py` | `generated_tests__dummy_without_eq.py` | `-c Dummy -m propogate` | -| `graph.py` | `generated_tests__graph.py` | | -| `lists.py` | `generated_tests__lists.py` | | -| `list_of_datetime.py` | `generated_tests__list_of_datetime.py` | | -| `longest_subsequence.py` | `generated_tests__longest_subsequence.py` | | -| `matrix.py` | `generated_tests__matrix.py` | `-c Matrix -m __add__,__mul__,__matmul__` | -| `primitive_types.py` | `generated_tests__primitive_types.py` | | -| `quick_sort.py` | `generated_tests__quick_sort.py` | | -| `test_coverage.py` | `generated_tests__test_coverage.py` | | -| `type_inhibition.py` | `generated_tests__type_inhibition.py` | | -| `using_collections.py` | `generated_tests__using_collections.py` | | diff --git a/utbot-python/samples/easy_samples/empty_file.py b/utbot-python/samples/samples/__init__.py similarity index 100% rename from utbot-python/samples/easy_samples/empty_file.py rename to utbot-python/samples/samples/__init__.py diff --git a/utbot-python/samples/samples/algorithms/__init__.py b/utbot-python/samples/samples/algorithms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/graph.py b/utbot-python/samples/samples/algorithms/bfs.py similarity index 96% rename from utbot-python/samples/samples/graph.py rename to utbot-python/samples/samples/algorithms/bfs.py index 9ef845a919..727bcb7ba2 100644 --- a/utbot-python/samples/samples/graph.py +++ b/utbot-python/samples/samples/algorithms/bfs.py @@ -18,7 +18,7 @@ def __eq__(self, other): return False -def bfs(nodes): +def bfs(nodes: List[Node]): if len(nodes) == 0: return [] diff --git a/utbot-python/samples/samples/longest_subsequence.py b/utbot-python/samples/samples/algorithms/longest_subsequence.py similarity index 100% rename from utbot-python/samples/samples/longest_subsequence.py rename to utbot-python/samples/samples/algorithms/longest_subsequence.py diff --git a/utbot-python/samples/samples/quick_sort.py b/utbot-python/samples/samples/algorithms/quick_sort.py similarity index 100% rename from utbot-python/samples/samples/quick_sort.py rename to utbot-python/samples/samples/algorithms/quick_sort.py diff --git a/utbot-python/samples/samples/classes/__init__.py b/utbot-python/samples/samples/classes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/classes/constructors.py b/utbot-python/samples/samples/classes/constructors.py new file mode 100644 index 0000000000..aac8cd71c1 --- /dev/null +++ b/utbot-python/samples/samples/classes/constructors.py @@ -0,0 +1,36 @@ +class EmptyClass: + pass + + +class WithInitClass: + def __init__(self, x: int): + self.x = x + + +class EmptyInitClass: + def __init__(self): + pass + + +class BasicNewCass: + def __new__(cls, *args, **kwargs): + super().__new__(cls, *args, **kwargs) + + +class ParentEmptyInitClass(EmptyClass): + pass + + +class ParentWithInitClass(WithInitClass): + pass + + +class ParentEmptyClass(EmptyClass): + pass + + +def func(a: EmptyClass, b: WithInitClass, c: EmptyInitClass, d: BasicNewCass, e: ParentEmptyClass, + f: ParentWithInitClass, g: ParentEmptyInitClass): + return a.__class__.__name__ + str( + b.x) + c.__class__.__name__ + d.__class__.__name__ + e.__class__.__name__ + str( + f.x) + g.__class__.__name__ diff --git a/utbot-python/samples/samples/classes/dataclass.py b/utbot-python/samples/samples/classes/dataclass.py new file mode 100644 index 0000000000..0fd03ee182 --- /dev/null +++ b/utbot-python/samples/samples/classes/dataclass.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class C: + counter: int = 0 + + def inc(self): + self.counter += 1 diff --git a/utbot-python/samples/samples/dicts.py b/utbot-python/samples/samples/classes/dicts.py similarity index 80% rename from utbot-python/samples/samples/dicts.py rename to utbot-python/samples/samples/classes/dicts.py index 6c63404587..3b0e25a549 100644 --- a/utbot-python/samples/samples/dicts.py +++ b/utbot-python/samples/samples/classes/dicts.py @@ -5,6 +5,9 @@ class Word: def __init__(self, translations: Dict[str, str]): self.translations = translations + def __eq__(self, other): + return self.translations == other.translations + def keys(self): return list(self.translations.keys()) @@ -18,6 +21,9 @@ def __init__( self.languages = languages self.words = [Word(translations) for translations in words] + def __eq__(self, other): + return self.languages == other.languages and self.words == other.words + def translate(self, word: str, language: Optional[str]): if language is not None: for word_ in self.words: diff --git a/utbot-python/samples/samples/classes/easy_class.py b/utbot-python/samples/samples/classes/easy_class.py new file mode 100644 index 0000000000..2e3d679714 --- /dev/null +++ b/utbot-python/samples/samples/classes/easy_class.py @@ -0,0 +1,9 @@ +class B: + def __init__(self, val: complex): + self.description = val + + def __eq__(self, other): + return self.description == other.description + + def sqrt(self): + return self.description ** 0.5 \ No newline at end of file diff --git a/utbot-python/samples/samples/classes/equals.py b/utbot-python/samples/samples/classes/equals.py new file mode 100644 index 0000000000..6ed34ec5fa --- /dev/null +++ b/utbot-python/samples/samples/classes/equals.py @@ -0,0 +1,23 @@ +class WithEqual: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class WithoutEqual: + def __init__(self, x: int): + self.x = x + + +class WithoutEqualChild(WithoutEqual): + def __init__(self, x: int): + super().__init__(x) + + def __eq__(self, other): + return self.x == other.x + + +def f1(a: WithEqual, b: WithoutEqual, c: WithoutEqualChild): + return a.x + b.x + c.x diff --git a/utbot-python/samples/easy_samples/field.py b/utbot-python/samples/samples/classes/field.py similarity index 100% rename from utbot-python/samples/easy_samples/field.py rename to utbot-python/samples/samples/classes/field.py diff --git a/utbot-python/samples/samples/classes/inner_class.py b/utbot-python/samples/samples/classes/inner_class.py new file mode 100644 index 0000000000..e77d866b1a --- /dev/null +++ b/utbot-python/samples/samples/classes/inner_class.py @@ -0,0 +1,12 @@ +class Outer: + class Inner: + a = 1 + + def inc(self): + self.a += 1 + + def __init__(self): + self.inner = Outer.Inner() + + def inc1(self): + self.inner.inc() \ No newline at end of file diff --git a/utbot-python/samples/easy_samples/corner_cases.py b/utbot-python/samples/samples/classes/rename_self.py similarity index 89% rename from utbot-python/samples/easy_samples/corner_cases.py rename to utbot-python/samples/samples/classes/rename_self.py index 2805e84108..b4081820fe 100644 --- a/utbot-python/samples/easy_samples/corner_cases.py +++ b/utbot-python/samples/samples/classes/rename_self.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -import sample_classes as s @dataclass diff --git a/utbot-python/samples/easy_samples/setstate_test.py b/utbot-python/samples/samples/classes/setstate_test.py similarity index 100% rename from utbot-python/samples/easy_samples/setstate_test.py rename to utbot-python/samples/samples/classes/setstate_test.py diff --git a/utbot-python/samples/samples/collections/__init__.py b/utbot-python/samples/samples/collections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/collections/dicts.py b/utbot-python/samples/samples/collections/dicts.py new file mode 100644 index 0000000000..52651e8893 --- /dev/null +++ b/utbot-python/samples/samples/collections/dicts.py @@ -0,0 +1,24 @@ +import typing + + +class MyClass: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + def __hash__(self): + return hash(self.x) + + +def f(x: typing.Dict[int, int]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def g(x: typing.Dict[MyClass, int]): + if len(x) == 0: + return "Empty!" + return len(x) diff --git a/utbot-python/samples/samples/lists.py b/utbot-python/samples/samples/collections/lists.py similarity index 87% rename from utbot-python/samples/samples/lists.py rename to utbot-python/samples/samples/collections/lists.py index daa1472ac5..005f16401b 100644 --- a/utbot-python/samples/samples/lists.py +++ b/utbot-python/samples/samples/collections/lists.py @@ -18,6 +18,12 @@ def find_articles_with_author(articles: List[Article], author: str) -> List[Arti ] +def f(x: List[int]): + if len(x) == 0: + return "Empty!" + return sum(x) + + if __name__ == '__main__': print(find_articles_with_author([ Article('a', 'a1', 'jfls', datetime.datetime.today()), diff --git a/utbot-python/samples/samples/collections/sets.py b/utbot-python/samples/samples/collections/sets.py new file mode 100644 index 0000000000..2913fbcc58 --- /dev/null +++ b/utbot-python/samples/samples/collections/sets.py @@ -0,0 +1,24 @@ +import typing + + +class MyClass: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + def __hash__(self): + return hash(self.x) + + +def f(x: typing.Set[int]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def g(x: typing.Set[MyClass]): + if len(x) == 0: + return "Empty!" + return len(x) diff --git a/utbot-python/samples/samples/collections/tuples.py b/utbot-python/samples/samples/collections/tuples.py new file mode 100644 index 0000000000..2ea141806e --- /dev/null +++ b/utbot-python/samples/samples/collections/tuples.py @@ -0,0 +1,36 @@ +import typing + + +class MyClass: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + def __hash__(self): + return hash(self.x) + + +def f(x: typing.Tuple[int, ...]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def f1(x: typing.Tuple[int, int, int]): + if len(x) != 3: + return "Very bad input!!!" + return x[0] + x[1] + x[2] + + +def g(x: typing.Tuple[MyClass, ...]): + if len(x) == 0: + return "Empty!" + return len(x) + + +def g1(x: typing.Tuple[MyClass, MyClass]): + if len(x) != 2: + return "Very bad input!!!" + return x[0].x + x[1].x diff --git a/utbot-python/samples/samples/using_collections.py b/utbot-python/samples/samples/collections/using_collections.py similarity index 100% rename from utbot-python/samples/samples/using_collections.py rename to utbot-python/samples/samples/collections/using_collections.py diff --git a/utbot-python/samples/samples/controlflow/__init__.py b/utbot-python/samples/samples/controlflow/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/arithmetic.py b/utbot-python/samples/samples/controlflow/arithmetic.py similarity index 59% rename from utbot-python/samples/samples/arithmetic.py rename to utbot-python/samples/samples/controlflow/arithmetic.py index a43ca21442..6f26aa94f4 100644 --- a/utbot-python/samples/samples/arithmetic.py +++ b/utbot-python/samples/samples/controlflow/arithmetic.py @@ -15,23 +15,3 @@ def calculate_function_value(x, y): return (3*x**2 - 2*x*y + y**2) / math.sin(x) else: return (0.01 * x) ** math.log2(y) - - -def f(x): - if x == 10: - return 100 - if x == 20: - return 200 - if x == 30: - return 300 - if x == 40: - return 400 - if x == 50: - return 500 - if x == 60: - return 600 - if x < 0: - return x ** 2 - if x > 0: - return 239 - return 100500 \ No newline at end of file diff --git a/utbot-python/samples/samples/controlflow/conditions.py b/utbot-python/samples/samples/controlflow/conditions.py new file mode 100644 index 0000000000..3897f578b1 --- /dev/null +++ b/utbot-python/samples/samples/controlflow/conditions.py @@ -0,0 +1,18 @@ +def f(x): + if x == 10: + return 100 + if x == 20: + return 200 + if x == 30: + return 300 + if x == 40: + return 400 + if x == 50: + return 500 + if x == 60: + return 600 + if x < 0: + return x ** 2 + if x > 0: + return 239 + return 100500 diff --git a/utbot-python/samples/samples/test_coverage.py b/utbot-python/samples/samples/controlflow/inner_conditions.py similarity index 100% rename from utbot-python/samples/samples/test_coverage.py rename to utbot-python/samples/samples/controlflow/inner_conditions.py diff --git a/utbot-python/samples/samples/easy_samples/__init__.py b/utbot-python/samples/samples/easy_samples/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/easy_samples/deep_equals.py b/utbot-python/samples/samples/easy_samples/deep_equals.py similarity index 100% rename from utbot-python/samples/easy_samples/deep_equals.py rename to utbot-python/samples/samples/easy_samples/deep_equals.py diff --git a/utbot-python/samples/samples/deep_equals.py b/utbot-python/samples/samples/easy_samples/deep_equals_2.py similarity index 100% rename from utbot-python/samples/samples/deep_equals.py rename to utbot-python/samples/samples/easy_samples/deep_equals_2.py diff --git a/utbot-python/samples/samples/dummy_with_eq.py b/utbot-python/samples/samples/easy_samples/dummy_with_eq.py similarity index 100% rename from utbot-python/samples/samples/dummy_with_eq.py rename to utbot-python/samples/samples/easy_samples/dummy_with_eq.py diff --git a/utbot-python/samples/samples/dummy_without_eq.py b/utbot-python/samples/samples/easy_samples/dummy_without_eq.py similarity index 100% rename from utbot-python/samples/samples/dummy_without_eq.py rename to utbot-python/samples/samples/easy_samples/dummy_without_eq.py diff --git a/utbot-python/samples/easy_samples/fully_annotated.py b/utbot-python/samples/samples/easy_samples/fully_annotated.py similarity index 100% rename from utbot-python/samples/easy_samples/fully_annotated.py rename to utbot-python/samples/samples/easy_samples/fully_annotated.py diff --git a/utbot-python/samples/easy_samples/general.py b/utbot-python/samples/samples/easy_samples/general.py similarity index 93% rename from utbot-python/samples/easy_samples/general.py rename to utbot-python/samples/samples/easy_samples/general.py index eae1ffaa6b..40630c2550 100644 --- a/utbot-python/samples/easy_samples/general.py +++ b/utbot-python/samples/samples/easy_samples/general.py @@ -4,9 +4,6 @@ from socket import socket from typing import List, Dict, Set, Optional, AbstractSet from dataclasses import dataclass -import logging -import datetime -import sample_classes class Dummy: @@ -28,17 +25,6 @@ def dict_f(x, a, b, c): return 2 -class A: - x = 4 - y = 5 - - def func(self): - n = 0 - for i in range(self.x): - n += self.y - return n - - def fact(n): ans = 1 for i in range(1, n + 1): diff --git a/utbot-python/samples/easy_samples/long_function_coverage.py b/utbot-python/samples/samples/easy_samples/long_function_coverage.py similarity index 100% rename from utbot-python/samples/easy_samples/long_function_coverage.py rename to utbot-python/samples/samples/easy_samples/long_function_coverage.py diff --git a/utbot-python/samples/samples/exceptions/__init__.py b/utbot-python/samples/samples/exceptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/exceptions/exception_examples.py b/utbot-python/samples/samples/exceptions/exception_examples.py new file mode 100644 index 0000000000..c052935f66 --- /dev/null +++ b/utbot-python/samples/samples/exceptions/exception_examples.py @@ -0,0 +1,53 @@ +import typing + +from samples.exceptions.my_checked_exception import MyCheckedException + + +def init_array(n: int): + try: + a: typing.List[typing.Optional[int]] = [None] * n + a[n-1] = n + 1 + a[n-2] = n + 2 + return a[n-1] + a[n-2] + except ImportError: + return -1 + except IndexError: + return -2 + + +def nested_exceptions(i: int): + try: + return check_all(i) + except IndexError: + return 100 + except ValueError: + return -100 + + +def check_positive(i: int): + if i > 0: + raise IndexError("Positive") + return 0 + + +def check_all(i: int): + if i < 0: + raise ValueError("Negative") + return check_positive(i) + + +def throw_exception(i: int): + r = 1 + if i > 0: + r += 10 + r -= (i + r) / 0 + else: + r += 100 + return r + + +def throw_my_exception(i: int): + if i > 0: + raise MyCheckedException("i > 0") + return i ** 2 + diff --git a/utbot-python/samples/samples/exceptions/my_checked_exception.py b/utbot-python/samples/samples/exceptions/my_checked_exception.py new file mode 100644 index 0000000000..3be517e3a4 --- /dev/null +++ b/utbot-python/samples/samples/exceptions/my_checked_exception.py @@ -0,0 +1,6 @@ +class MyCheckedException(Exception): + def __init__(self, x: str): + self.x = x + + def method(self, y: str): + return self.x == y diff --git a/utbot-python/samples/samples/imports/__init__.py b/utbot-python/samples/samples/imports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/builtins_module/__init__.py b/utbot-python/samples/samples/imports/builtins_module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/builtins_module/crypto.py b/utbot-python/samples/samples/imports/builtins_module/crypto.py new file mode 100644 index 0000000000..5315d61e37 --- /dev/null +++ b/utbot-python/samples/samples/imports/builtins_module/crypto.py @@ -0,0 +1,15 @@ +import hashlib +from collections import Counter + + +def f(word: str): + m = hashlib.sha256() + m.update(word.encode()) + code = m.hexdigest() + if len(code) > len(word): + return Counter(code) + return Counter(word) + + +if __name__ == '__main__': + print(f("fjasld")) diff --git a/utbot-python/samples/samples/imports/pack_1/__init__.py b/utbot-python/samples/samples/imports/pack_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_1/__init__.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_1/inner_mod_1.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_1/inner_mod_1.py new file mode 100644 index 0000000000..a8112a5831 --- /dev/null +++ b/utbot-python/samples/samples/imports/pack_1/inner_pack_1/inner_mod_1.py @@ -0,0 +1,7 @@ +from ..inner_pack_2 import inner_mod_2 + + +def inner_func_1(a: int): + if a > 1: + return inner_mod_2.inner_func_2(a) + return a ** 2 diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_2.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_2.py new file mode 100644 index 0000000000..8d349164a7 --- /dev/null +++ b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_2.py @@ -0,0 +1,5 @@ +from ..inner_pack_2 import inner_mod_3 + + +def inner_func_2(a: int): + return inner_mod_3.inner_func_3(a) diff --git a/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_3.py b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_3.py new file mode 100644 index 0000000000..60d7ab5ea2 --- /dev/null +++ b/utbot-python/samples/samples/imports/pack_1/inner_pack_2/inner_mod_3.py @@ -0,0 +1,2 @@ +def inner_func_3(x: float): + return int(x) diff --git a/utbot-python/samples/samples/math/__init__.py b/utbot-python/samples/samples/math/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/named_arguments/__init__.py b/utbot-python/samples/samples/named_arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/named_arguments/method_named_arguments.py b/utbot-python/samples/samples/named_arguments/method_named_arguments.py new file mode 100644 index 0000000000..10a61f8f63 --- /dev/null +++ b/utbot-python/samples/samples/named_arguments/method_named_arguments.py @@ -0,0 +1,28 @@ +from samples.named_arguments.named_arguments import g + + +class A: + def __init__(self, x: int): + self.x = x + + def __eq__(self, other): + return self.x == other + + def __round__(self, n=None): + if n is not None: + return round(self.x, n) + return round(self.x) + + def __pow__(self, power, modulo=None): + if modulo is None: + return self.x ** power + return pow(self.x, power, modulo) + + def f1(self, x, y=1, *, z, t=2): + if y == 0: + return 100 * x + t + else: + x *= y + if x % 2 == 0: + y = g(x) + z + return x + y + 100 / y \ No newline at end of file diff --git a/utbot-python/samples/samples/named_arguments/named_arguments.py b/utbot-python/samples/samples/named_arguments/named_arguments.py new file mode 100644 index 0000000000..57f7c35b84 --- /dev/null +++ b/utbot-python/samples/samples/named_arguments/named_arguments.py @@ -0,0 +1,16 @@ +def f(x, y=1, *, z, t=2): + if y == 0: + return 100 * x + t + else: + x *= y + if x % 2 == 0: + y = g(x) + z + return x + y + 100 / y + + +def g(x): + return x ** 2 + + +if __name__ == '__main__': + print(f(1, y=2, z=3)) diff --git a/utbot-python/samples/samples/primitives/__init__.py b/utbot-python/samples/samples/primitives/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/primitives/bytes_example.py b/utbot-python/samples/samples/primitives/bytes_example.py new file mode 100644 index 0000000000..f23a7fa92b --- /dev/null +++ b/utbot-python/samples/samples/primitives/bytes_example.py @@ -0,0 +1,7 @@ +def neg_bytes(b: bytes): + return not b + + +def sum_bytes(a: bytes, b: bytes): + c = a + b + return c diff --git a/utbot-python/samples/samples/primitives/numbers.py b/utbot-python/samples/samples/primitives/numbers.py new file mode 100644 index 0000000000..1305a369ab --- /dev/null +++ b/utbot-python/samples/samples/primitives/numbers.py @@ -0,0 +1,34 @@ +import copy +import math +import typing + + +def summ(a: typing.SupportsInt, b: typing.SupportsInt): + return int(a) + int(b) + + +def create_table(a: int): + table = [] + for i in range(a): + row = [] + for j in range(a): + row.append(i * j) + table.append(copy.deepcopy(row)) + return table + + +def operations(a: int): + if a > 1024: + return math.log2(a) + if a > 512: + return math.exp(a) + if a > 256: + return math.isinf(a) + if a > 128: + return math.e * a + return math.sqrt(a) + + +def check_order(a: int, b: int, c: int): + return a < b < c + diff --git a/utbot-python/samples/samples/primitive_types.py b/utbot-python/samples/samples/primitives/primitive_types.py similarity index 100% rename from utbot-python/samples/samples/primitive_types.py rename to utbot-python/samples/samples/primitives/primitive_types.py diff --git a/utbot-python/samples/samples/primitives/regex.py b/utbot-python/samples/samples/primitives/regex.py new file mode 100644 index 0000000000..acee35cc7b --- /dev/null +++ b/utbot-python/samples/samples/primitives/regex.py @@ -0,0 +1,8 @@ +import re + + +def check_regex(string: str) -> bool: + pattern = r"'(''|\\\\|\\'|[^'])*'" + if re.match(pattern, string): + return True + return False diff --git a/utbot-python/samples/samples/primitives/str_example.py b/utbot-python/samples/samples/primitives/str_example.py new file mode 100644 index 0000000000..834f5ca477 --- /dev/null +++ b/utbot-python/samples/samples/primitives/str_example.py @@ -0,0 +1,51 @@ +import dataclasses +import typing + + +@dataclasses.dataclass +class IntPair: + fst: int + snd: int + + +def concat(a: str, b: str): + return a + b + + +def concat_pair(pair: IntPair): + return pair.fst + pair.snd + + +def string_constants(s: str): + return "String('" + s + "')" + + +def contains(s: str, t: str): + return t in s + + +def const_contains(s: str): + return "ab" in s + + +def to_str(a: int, b: int): + if a > b: + return str(a) + else: + return str(b) + + +def starts_with(s: str): + if s.startswith("1234567890"): + s = s.replace("3", "A") + else: + s = s.strip() + + if s[0] == "x": + return s + else: + return s.upper() + + +def join_str(strings: typing.List[str]): + return "--".join(strings) \ No newline at end of file diff --git a/utbot-python/samples/samples/recursion/__init__.py b/utbot-python/samples/samples/recursion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/samples/recursion/recursion.py b/utbot-python/samples/samples/recursion/recursion.py new file mode 100644 index 0000000000..09c25521c6 --- /dev/null +++ b/utbot-python/samples/samples/recursion/recursion.py @@ -0,0 +1,41 @@ +def factorial(n: int): + if n < 0: + raise ValueError() + if n == 0: + return 1 + return n * factorial(n-1) + + +def fib(n: int): + if n < 0: + raise ValueError + if n == 0: + return 0 + if n == 1: + return 1 + return fib(n-1) + fib(n-2) + + +def summ(fst: int, snd: int): + def signum(x: int): + return 0 if x == 0 else 1 if x > 0 else -1 + if snd == 0: + return fst + return summ(fst + signum(snd), snd - signum(snd)) + + +def recursion_with_exception(n: int): + if n < 42: + recursion_with_exception(n+1) + if n > 42: + recursion_with_exception(n-1) + raise ValueError + + +def first(n: int): + def second(n: int): + first(n) + if n < 4: + return + second(n) + diff --git a/utbot-python/samples/samples/structures/__init__.py b/utbot-python/samples/samples/structures/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/easy_samples/boruvka.py b/utbot-python/samples/samples/structures/boruvka.py similarity index 99% rename from utbot-python/samples/easy_samples/boruvka.py rename to utbot-python/samples/samples/structures/boruvka.py index 8f406b6012..1e757abcf8 100644 --- a/utbot-python/samples/easy_samples/boruvka.py +++ b/utbot-python/samples/samples/structures/boruvka.py @@ -1,6 +1,3 @@ -from typing import Any - - class Graph: def __init__(self, num_of_nodes: int) -> None: """ diff --git a/utbot-python/samples/samples/deque.py b/utbot-python/samples/samples/structures/deque.py similarity index 100% rename from utbot-python/samples/samples/deque.py rename to utbot-python/samples/samples/structures/deque.py diff --git a/utbot-python/samples/samples/structures/graph_matrix.py b/utbot-python/samples/samples/structures/graph_matrix.py new file mode 100644 index 0000000000..785c295724 --- /dev/null +++ b/utbot-python/samples/samples/structures/graph_matrix.py @@ -0,0 +1,40 @@ +# An island in matrix is a group of linked areas, all having the same value. +# This code counts number of islands in a given matrix, with including diagonal +# connections. + + +class Matrix: # Public class to implement a graph + def __init__(self, row: int, col: int, graph: list[list[bool]]) -> None: + self.ROW = row + self.COL = col + self.graph = graph + + def __eq__(self, other): + return self.graph == other.graph + + def is_safe(self, i: int, j: int, visited: list[list[bool]]) -> bool: + return ( + 0 <= i < self.ROW + and 0 <= j < self.COL + and not visited[i][j] + and self.graph[i][j] + ) + + def diffs(self, i: int, j: int, visited: list[list[bool]]) -> None: + # Checking all 8 elements surrounding nth element + row_nbr = [-1, -1, -1, 0, 0, 1, 1, 1] # Coordinate order + col_nbr = [-1, 0, 1, -1, 1, -1, 0, 1] + visited[i][j] = True # Make those cells visited + for k in range(8): + if self.is_safe(i + row_nbr[k], j + col_nbr[k], visited): + self.diffs(i + row_nbr[k], j + col_nbr[k], visited) + + def count_islands(self) -> int: # And finally, count all islands. + visited = [[False for j in range(self.COL)] for i in range(self.ROW)] + count = 0 + for i in range(self.ROW): + for j in range(self.COL): + if visited[i][j] is False and self.graph[i][j] == 1: + self.diffs(i, j, visited) + count += 1 + return count \ No newline at end of file diff --git a/utbot-python/samples/samples/matrix.py b/utbot-python/samples/samples/structures/matrix.py similarity index 100% rename from utbot-python/samples/samples/matrix.py rename to utbot-python/samples/samples/structures/matrix.py diff --git a/utbot-python/samples/samples/structures/multi_level_feedback_queue.py b/utbot-python/samples/samples/structures/multi_level_feedback_queue.py new file mode 100644 index 0000000000..284c758b8f --- /dev/null +++ b/utbot-python/samples/samples/structures/multi_level_feedback_queue.py @@ -0,0 +1,312 @@ +from collections import deque + + +class Process: + def __init__(self, process_name: str, arrival_time: int, burst_time: int) -> None: + self.process_name = process_name # process name + self.arrival_time = arrival_time # arrival time of the process + # completion time of finished process or last interrupted time + self.stop_time = arrival_time + self.burst_time = burst_time # remaining burst time + self.waiting_time = 0 # total time of the process wait in ready queue + self.turnaround_time = 0 # time from arrival time to completion time + + +class MLFQ: + """ + MLFQ(Multi Level Feedback Queue) + https://en.wikipedia.org/wiki/Multilevel_feedback_queue + MLFQ has a lot of queues that have different priority + In this MLFQ, + The first Queue(0) to last second Queue(N-2) of MLFQ have Round Robin Algorithm + The last Queue(N-1) has First Come, First Served Algorithm + """ + + def __init__( + self, + number_of_queues: int, + time_slices: list[int], + queue: deque[Process], + current_time: int, + ) -> None: + # total number of mlfq's queues + self.number_of_queues = number_of_queues + # time slice of queues that round robin algorithm applied + self.time_slices = time_slices + # unfinished process is in this ready_queue + self.ready_queue = queue + # current time + self.current_time = current_time + # finished process is in this sequence queue + self.finish_queue: deque[Process] = deque() + + def calculate_sequence_of_finish_queue(self) -> list[str]: + """ + This method returns the sequence of finished processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_sequence_of_finish_queue() + ['P2', 'P4', 'P1', 'P3'] + """ + sequence = [] + for i in range(len(self.finish_queue)): + sequence.append(self.finish_queue[i].process_name) + return sequence + + def calculate_waiting_time(self, queue: list[Process]) -> list[int]: + """ + This method calculates waiting time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_waiting_time([P1, P2, P3, P4]) + [83, 17, 94, 101] + """ + waiting_times = [] + for i in range(len(queue)): + waiting_times.append(queue[i].waiting_time) + return waiting_times + + def calculate_turnaround_time(self, queue: list[Process]) -> list[int]: + """ + This method calculates turnaround time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_turnaround_time([P1, P2, P3, P4]) + [136, 34, 162, 125] + """ + turnaround_times = [] + for i in range(len(queue)): + turnaround_times.append(queue[i].turnaround_time) + return turnaround_times + + def calculate_completion_time(self, queue: list[Process]) -> list[int]: + """ + This method calculates completion time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_turnaround_time([P1, P2, P3, P4]) + [136, 34, 162, 125] + """ + completion_times = [] + for i in range(len(queue)): + completion_times.append(queue[i].stop_time) + return completion_times + + def calculate_remaining_burst_time_of_processes( + self, queue: deque[Process] + ) -> list[int]: + """ + This method calculate remaining burst time of processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> finish_queue, ready_queue = mlfq.round_robin(deque([P1, P2, P3, P4]), 17) + >>> mlfq.calculate_remaining_burst_time_of_processes(mlfq.finish_queue) + [0] + >>> mlfq.calculate_remaining_burst_time_of_processes(ready_queue) + [36, 51, 7] + >>> finish_queue, ready_queue = mlfq.round_robin(ready_queue, 25) + >>> mlfq.calculate_remaining_burst_time_of_processes(mlfq.finish_queue) + [0, 0] + >>> mlfq.calculate_remaining_burst_time_of_processes(ready_queue) + [11, 26] + """ + return [q.burst_time for q in queue] + + def update_waiting_time(self, process: Process) -> int: + """ + This method updates waiting times of unfinished processes + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> mlfq.current_time = 10 + >>> P1.stop_time = 5 + >>> mlfq.update_waiting_time(P1) + 5 + """ + process.waiting_time += self.current_time - process.stop_time + return process.waiting_time + + def first_come_first_served(self, ready_queue: deque[Process]) -> deque[Process]: + """ + FCFS(First Come, First Served) + FCFS will be applied to MLFQ's last queue + A first came process will be finished at first + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> _ = mlfq.first_come_first_served(mlfq.ready_queue) + >>> mlfq.calculate_sequence_of_finish_queue() + ['P1', 'P2', 'P3', 'P4'] + """ + finished: deque[Process] = deque() # sequence deque of finished process + while len(ready_queue) != 0: + cp = ready_queue.popleft() # current process + + # if process's arrival time is later than current time, update current time + if self.current_time < cp.arrival_time: + self.current_time += cp.arrival_time + + # update waiting time of current process + self.update_waiting_time(cp) + # update current time + self.current_time += cp.burst_time + # finish the process and set the process's burst-time 0 + cp.burst_time = 0 + # set the process's turnaround time because it is finished + cp.turnaround_time = self.current_time - cp.arrival_time + # set the completion time + cp.stop_time = self.current_time + # add the process to queue that has finished queue + finished.append(cp) + + self.finish_queue.extend(finished) # add finished process to finish queue + # FCFS will finish all remaining processes + return finished + + def round_robin( + self, ready_queue: deque[Process], time_slice: int + ) -> tuple[deque[Process], deque[Process]]: + """ + RR(Round Robin) + RR will be applied to MLFQ's all queues except last queue + All processes can't use CPU for time more than time_slice + If the process consume CPU up to time_slice, it will go back to ready queue + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> finish_queue, ready_queue = mlfq.round_robin(mlfq.ready_queue, 17) + >>> mlfq.calculate_sequence_of_finish_queue() + ['P2'] + """ + finished: deque[Process] = deque() # sequence deque of terminated process + # just for 1 cycle and unfinished processes will go back to queue + for _ in range(len(ready_queue)): + cp = ready_queue.popleft() # current process + + # if process's arrival time is later than current time, update current time + if self.current_time < cp.arrival_time: + self.current_time += cp.arrival_time + + # update waiting time of unfinished processes + self.update_waiting_time(cp) + # if the burst time of process is bigger than time-slice + if cp.burst_time > time_slice: + # use CPU for only time-slice + self.current_time += time_slice + # update remaining burst time + cp.burst_time -= time_slice + # update end point time + cp.stop_time = self.current_time + # locate the process behind the queue because it is not finished + ready_queue.append(cp) + else: + # use CPU for remaining burst time + self.current_time += cp.burst_time + # set burst time 0 because the process is finished + cp.burst_time = 0 + # set the finish time + cp.stop_time = self.current_time + # update the process' turnaround time because it is finished + cp.turnaround_time = self.current_time - cp.arrival_time + # add the process to queue that has finished queue + finished.append(cp) + + self.finish_queue.extend(finished) # add finished process to finish queue + # return finished processes queue and remaining processes queue + return finished, ready_queue + + def multi_level_feedback_queue(self) -> deque[Process]: + """ + MLFQ(Multi Level Feedback Queue) + >>> P1 = Process("P1", 0, 53) + >>> P2 = Process("P2", 0, 17) + >>> P3 = Process("P3", 0, 68) + >>> P4 = Process("P4", 0, 24) + >>> mlfq = MLFQ(3, [17, 25], deque([P1, P2, P3, P4]), 0) + >>> finish_queue = mlfq.multi_level_feedback_queue() + >>> mlfq.calculate_sequence_of_finish_queue() + ['P2', 'P4', 'P1', 'P3'] + """ + + # all queues except last one have round_robin algorithm + for i in range(self.number_of_queues - 1): + finished, self.ready_queue = self.round_robin( + self.ready_queue, self.time_slices[i] + ) + # the last queue has first_come_first_served algorithm + self.first_come_first_served(self.ready_queue) + + return self.finish_queue + + +if __name__ == "__main__": + import doctest + + P1 = Process("P1", 0, 53) + P2 = Process("P2", 0, 17) + P3 = Process("P3", 0, 68) + P4 = Process("P4", 0, 24) + number_of_queues = 3 + time_slices = [17, 25] + queue = deque([P1, P2, P3, P4]) + + if len(time_slices) != number_of_queues - 1: + raise SystemExit(0) + + doctest.testmod(extraglobs={"queue": deque([P1, P2, P3, P4])}) + + P1 = Process("P1", 0, 53) + P2 = Process("P2", 0, 17) + P3 = Process("P3", 0, 68) + P4 = Process("P4", 0, 24) + number_of_queues = 3 + time_slices = [17, 25] + queue = deque([P1, P2, P3, P4]) + mlfq = MLFQ(number_of_queues, time_slices, queue, 0) + finish_queue = mlfq.multi_level_feedback_queue() + + # print total waiting times of processes(P1, P2, P3, P4) + print( + f"waiting time:\ + \t\t\t{MLFQ.calculate_waiting_time(mlfq, [P1, P2, P3, P4])}" + ) + # print completion times of processes(P1, P2, P3, P4) + print( + f"completion time:\ + \t\t{MLFQ.calculate_completion_time(mlfq, [P1, P2, P3, P4])}" + ) + # print total turnaround times of processes(P1, P2, P3, P4) + print( + f"turnaround time:\ + \t\t{MLFQ.calculate_turnaround_time(mlfq, [P1, P2, P3, P4])}" + ) + # print sequence of finished processes + print( + f"sequence of finished processes:\ + {mlfq.calculate_sequence_of_finish_queue()}" + ) \ No newline at end of file diff --git a/utbot-python/samples/samples/type_inference/__init__.py b/utbot-python/samples/samples/type_inference/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/easy_samples/annotations2.py b/utbot-python/samples/samples/type_inference/annotations.py similarity index 100% rename from utbot-python/samples/easy_samples/annotations2.py rename to utbot-python/samples/samples/type_inference/annotations.py diff --git a/utbot-python/samples/easy_samples/annotation_tests.py b/utbot-python/samples/samples/type_inference/annotations2.py similarity index 84% rename from utbot-python/samples/easy_samples/annotation_tests.py rename to utbot-python/samples/samples/type_inference/annotations2.py index d9412617e6..5dfa3d36ee 100644 --- a/utbot-python/samples/easy_samples/annotation_tests.py +++ b/utbot-python/samples/samples/type_inference/annotations2.py @@ -1,7 +1,6 @@ +from __future__ import annotations from typing import * -import collections from enum import Enum -import datetime XXX = TypeVar("XXX", "A", int) @@ -10,7 +9,7 @@ class A(Generic[XXX]): self_: XXX - def f(self, a, b: 'A'[int]): + def f(self, a, b: A[int]): self.y = b self.self_.x = b pass @@ -70,9 +69,3 @@ def supports_abs(x: SupportsAbs): def tuple_(x: Tuple[int, str]): return x[1] + str(x[0]) - - -if __name__ == "__main__": - # print(square(collections.defaultdict(int))) - # enum_literal(Color.BLUE) - pass diff --git a/utbot-python/samples/easy_samples/generics.py b/utbot-python/samples/samples/type_inference/generics.py similarity index 100% rename from utbot-python/samples/easy_samples/generics.py rename to utbot-python/samples/samples/type_inference/generics.py diff --git a/utbot-python/samples/samples/list_of_datetime.py b/utbot-python/samples/samples/type_inference/list_of_datetime.py similarity index 100% rename from utbot-python/samples/samples/list_of_datetime.py rename to utbot-python/samples/samples/type_inference/list_of_datetime.py diff --git a/utbot-python/samples/easy_samples/subtypes.py b/utbot-python/samples/samples/type_inference/subtypes.py similarity index 82% rename from utbot-python/samples/easy_samples/subtypes.py rename to utbot-python/samples/samples/type_inference/subtypes.py index 8c3a480de2..24eb1c18e6 100644 --- a/utbot-python/samples/easy_samples/subtypes.py +++ b/utbot-python/samples/samples/type_inference/subtypes.py @@ -1,5 +1,4 @@ import collections -import numpy from typing import * @@ -18,11 +17,11 @@ def f(self, x: Union[int, str]) -> object: return collections.Counter([x]) -def func_for_P(x: P) -> None: +def func_for_p(x: P) -> None: return None -func_for_P(S()) +# func_for_p(S()) class R(Protocol): @@ -35,11 +34,11 @@ def f(self) -> 'RImpl': return self -def func_for_R(x: R) -> None: +def func_for_r(x: R) -> None: return None -func_for_R(RImpl()) +# func_for_r(RImpl()) a: List[int] = [] @@ -51,5 +50,3 @@ def func_for_R(x: R) -> None: def func_abs(x: SupportsAbs[T]): return abs(x) - -b: int = 10 diff --git a/utbot-python/samples/samples/type_inference.py b/utbot-python/samples/samples/type_inference/type_inference.py similarity index 100% rename from utbot-python/samples/samples/type_inference.py rename to utbot-python/samples/samples/type_inference/type_inference.py diff --git a/utbot-python/samples/samples/type_inference_2.py b/utbot-python/samples/samples/type_inference/type_inference_2.py similarity index 100% rename from utbot-python/samples/samples/type_inference_2.py rename to utbot-python/samples/samples/type_inference/type_inference_2.py diff --git a/utbot-python/samples/test_configuration.json b/utbot-python/samples/test_configuration.json new file mode 100644 index 0000000000..1746e07ba0 --- /dev/null +++ b/utbot-python/samples/test_configuration.json @@ -0,0 +1,635 @@ +{ + "parts": [ + { + "path": "samples/algorithms", + "files": [ + { + "name": "bfs", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "longest_subsequence", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "quick_sort", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 85 + } + ] + } + ] + }, + { + "path": "samples/classes", + "files": [ + { + "name": "constructors", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 80 + } + ] + }, + { + "name": "dataclass", + "groups": [ + { + "classes": ["C"], + "methods": ["inc"], + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "dicts", + "groups": [ + { + "classes": ["Dictionary"], + "methods": ["translate"], + "timeout": 10, + "coverage": 89 + } + ] + }, + { + "name": "easy_class", + "groups": [ + { + "classes": ["B"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "equals", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "field", + "groups": [ + { + "classes": ["NoTestsProblem"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "inner_class", + "groups": [ + { + "classes": ["Outer"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "rename_self", + "groups": [ + { + "classes": ["A"], + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "setstate_test", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/collections", + "files": [ + { + "name": "dicts", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "lists", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "sets", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "tuples", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 10, + "coverage": 100 + } + ] + }, + { + "name": "using_collections", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 60, + "coverage": 88 + } + ] + } + ] + }, + { + "path": "samples/controlflow", + "files": [ + { + "name": "arithmetic", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 100, + "coverage": 100 + } + ] + }, + { + "name": "conditions", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 72 + } + ] + }, + { + "name": "inner_conditions", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 75 + } + ] + } + ] + }, + { + "path": "samples/easy_samples", + "files": [ + { + "name": "deep_equals", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 90, + "coverage": 100 + } + ] + }, + { + "name": "deep_equals_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + }, + { + "name": "fully_annotated", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 300, + "coverage": 100 + } + ] + }, + { + "name": "dummy_with_eq", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + }, + { + "name": "dummy_without_eq", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + }, + { + "name": "long_function_coverage", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/exceptions", + "files": [ + { + "name": "exception_examples", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/imports/builtins_module", + "files": [ + { + "name": "crypto", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 86 + } + ] + } + ] + }, + { + "path": "samples/imports/pack_1/inner_pack_1", + "files": [ + { + "name": "inner_mod_1", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/imports/pack_1/inner_pack_2", + "files": [ + { + "name": "inner_mod_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/imports/pack_1/inner_pack_2", + "files": [ + { + "name": "inner_mod_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 20, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/primitives", + "files": [ + { + "name": "bytes_example", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 100 + } + ] + }, + { + "name": "numbers", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 80, + "coverage": 100 + } + ] + }, + { + "name": "primitive_types", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 60, + "coverage": 100 + } + ] + }, + { + "name": "regex", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 40, + "coverage": 100 + } + ] + }, + { + "name": "str_example", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 160, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/recursion", + "files": [ + { + "name": "recursion", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 150, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/structures", + "files": [ + { + "name": "boruvka", + "groups": [ + { + "classes": ["Graph"], + "methods": null, + "timeout": 150, + "coverage": 100 + } + ] + }, + { + "name": "deque", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 150, + "coverage": 100 + } + ] + }, + { + "name": "graph_matrix", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 120, + "coverage": 100 + } + ] + }, + { + "name": "matrix", + "groups": [ + { + "classes": ["Matrix"], + "methods": null, + "timeout": 240, + "coverage": 100 + } + ] + }, + { + "name": "multi_level_feedback_queue", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 180, + "coverage": 100 + } + ] + } + ] + }, + { + "path": "samples/type_inference", + "files": [ + { + "name": "annotations", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 140, + "coverage": 100 + } + ] + }, + { + "name": "annotations2", + "groups": [ + { + "classes": ["A"], + "methods": null, + "timeout": 40, + "coverage": 100 + } + ] + }, + { + "name": "annotations2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 120, + "coverage": 100 + } + ] + }, + { + "name": "generics", + "groups": [ + { + "classes": ["LoggedVar"], + "methods": null, + "timeout": 30, + "coverage": 80 + } + ] + }, + { + "name": "generics", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 100 + } + ] + }, + { + "name": "list_of_datetime", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 30, + "coverage": 50 + } + ] + }, + { + "name": "subtypes", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 90, + "coverage": 100 + } + ] + }, + { + "name": "type_inference", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 40, + "coverage": 100 + } + ] + }, + { + "name": "type_inference_2", + "groups": [ + { + "classes": null, + "methods": null, + "timeout": 40, + "coverage": 62 + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt index 17fda466fb..5c00195421 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt @@ -106,7 +106,6 @@ class PythonEngine( val coveredInstructions = coveredLinesToInstructions(coveredLines, methodUnderTest) val coverage = Coverage(coveredInstructions) - val utFuzzedExecution = PythonUtExecution( stateInit = EnvironmentModels(beforeThisObject, beforeModelList, emptyMap()), stateBefore = EnvironmentModels(beforeThisObject, beforeModelList, emptyMap()), @@ -116,7 +115,8 @@ class PythonEngine( coverage = coverage, testMethodName = testMethodName.testName?.camelToSnakeCase(), displayName = testMethodName.displayName, - summary = summary.map { DocRegularStmt(it) } + summary = summary.map { DocRegularStmt(it) }, + arguments = methodUnderTest.argumentsWithoutSelf ) return ValidExecution(utFuzzedExecution) } @@ -170,7 +170,8 @@ class PythonEngine( coverage = evaluationResult.coverage, testMethodName = testMethodName.testName?.camelToSnakeCase(), displayName = testMethodName.displayName, - summary = summary.map { DocRegularStmt(it) } + summary = summary.map { DocRegularStmt(it) }, + arguments = methodUnderTest.argumentsWithoutSelf, ) return ValidExecution(utFuzzedExecution) } @@ -186,9 +187,94 @@ class PythonEngine( ) } - fun fuzzing(parameters: List, isCancelled: () -> Boolean, until: Long): Flow = flow { + private fun fuzzingResultHandler( + description: PythonMethodDescription, + arguments: List, + parameters: List, + manager: PythonWorkerManager, + ): PythonExecutionResult? { val additionalModules = parameters.flatMap { it.pythonModules() } + val argumentValues = arguments.map { PythonTreeModel(it.tree, it.tree.type) } + logger.debug(argumentValues.map { it.tree } .toString()) + val argumentModules = argumentValues + .flatMap { it.allContainingClassIds } + .map { it.moduleName } + .filterNot { it.startsWith(moduleToImport) } + val localAdditionalModules = (additionalModules + argumentModules + moduleToImport).toSet() + + val (thisObject, modelList) = + if (methodUnderTest.hasThisArgument) + Pair(argumentValues[0], argumentValues.drop(1)) + else + Pair(null, argumentValues) + val functionArguments = FunctionArguments( + thisObject, + methodUnderTest.thisObjectName, + modelList, + methodUnderTest.argumentsNames + ) + try { + val coverageId = CoverageIdGenerator.createId() + return when (val evaluationResult = + manager.runWithCoverage(functionArguments, localAdditionalModules, coverageId)) { + is PythonEvaluationError -> { + val utError = UtError( + "Error evaluation: ${evaluationResult.status}, ${evaluationResult.message}", + Throwable(evaluationResult.stackTrace.joinToString("\n")) + ) + logger.debug(evaluationResult.stackTrace.joinToString("\n")) + PythonExecutionResult(InvalidExecution(utError), PythonFeedback(control = Control.PASS)) + } + + is PythonEvaluationTimeout -> { + val coveredLines = + manager.coverageReceiver.coverageStorage.getOrDefault(coverageId, mutableSetOf()) + val utTimeoutException = handleTimeoutResult(arguments, description, coveredLines) + val coveredInstructions = coveredLinesToInstructions(coveredLines, methodUnderTest) + val trieNode: Trie.Node = + if (coveredInstructions.isEmpty()) + Trie.emptyNode() + else + description.tracer.add(coveredInstructions) + PythonExecutionResult( + utTimeoutException, + PythonFeedback(control = Control.PASS, result = trieNode) + ) + } + + is PythonEvaluationSuccess -> { + val coveredInstructions = evaluationResult.coverage.coveredInstructions + + when (val result = handleSuccessResult( + arguments, + parameters, + evaluationResult, + description, + )) { + is ValidExecution -> { + val trieNode: Trie.Node = description.tracer.add(coveredInstructions) + PythonExecutionResult( + result, + PythonFeedback(control = Control.CONTINUE, result = trieNode) + ) + } + is InvalidExecution -> { + PythonExecutionResult(result, PythonFeedback(control = Control.CONTINUE)) + } + else -> { + PythonExecutionResult(result, PythonFeedback(control = Control.PASS)) + } + } + } + } + } catch (_: TimeoutException) { + logger.info { "Fuzzing process was interrupted by timeout" } + return null + } + } + + fun fuzzing(parameters: List, isCancelled: () -> Boolean, until: Long): Flow = flow { ServerSocket(0).use { serverSocket -> logger.info { "Server port: ${serverSocket.localPort}" } val manager = try { @@ -202,90 +288,6 @@ class PythonEngine( } logger.info { "Executor manager was created successfully" } - fun fuzzingResultHandler( - description: PythonMethodDescription, - arguments: List - ): PythonExecutionResult? { - val argumentValues = arguments.map { PythonTreeModel(it.tree, it.tree.type) } - logger.debug(argumentValues.map { it.tree } .toString()) - val argumentModules = argumentValues - .flatMap { it.allContainingClassIds } - .map { it.moduleName } - .filterNot { it.startsWith(moduleToImport) } - val localAdditionalModules = (additionalModules + argumentModules + moduleToImport).toSet() - - val (thisObject, modelList) = - if (methodUnderTest.hasThisArgument) - Pair(argumentValues[0], argumentValues.drop(1)) - else - Pair(null, argumentValues) - val functionArguments = FunctionArguments( - thisObject, - methodUnderTest.thisObjectName, - modelList, - methodUnderTest.argumentsNames - ) - try { - val coverageId = CoverageIdGenerator.createId() - return when (val evaluationResult = - manager.runWithCoverage(functionArguments, localAdditionalModules, coverageId)) { - is PythonEvaluationError -> { - val utError = UtError( - "Error evaluation: ${evaluationResult.status}, ${evaluationResult.message}", - Throwable(evaluationResult.stackTrace.joinToString("\n")) - ) - logger.debug(evaluationResult.stackTrace.joinToString("\n")) - PythonExecutionResult(InvalidExecution(utError), PythonFeedback(control = Control.PASS)) - } - - is PythonEvaluationTimeout -> { - val coveredLines = - manager.coverageReceiver.coverageStorage.getOrDefault(coverageId, mutableSetOf()) - val utTimeoutException = handleTimeoutResult(arguments, description, coveredLines) - val coveredInstructions = coveredLinesToInstructions(coveredLines, methodUnderTest) - val trieNode: Trie.Node = - if (coveredInstructions.isEmpty()) - Trie.emptyNode() - else description.tracer.add( - coveredInstructions - ) - PythonExecutionResult( - utTimeoutException, - PythonFeedback(control = Control.PASS, result = trieNode) - ) - } - - is PythonEvaluationSuccess -> { - val coveredInstructions = evaluationResult.coverage.coveredInstructions - - when (val result = handleSuccessResult( - arguments, - parameters, - evaluationResult, - description, - )) { - is ValidExecution -> { - val trieNode: Trie.Node = description.tracer.add(coveredInstructions) - PythonExecutionResult( - result, - PythonFeedback(control = Control.CONTINUE, result = trieNode) - ) - } - is InvalidExecution -> { - PythonExecutionResult(result, PythonFeedback(control = Control.CONTINUE)) - } - else -> { - PythonExecutionResult(result, PythonFeedback(control = Control.PASS)) - } - } - } - } - } catch (_: TimeoutException) { - logger.info { "Fuzzing process was interrupted by timeout" } - return null - } - } - val pmd = PythonMethodDescription( methodUnderTest.name, parameters, @@ -295,53 +297,56 @@ class PythonEngine( Random(0), ) - if (parameters.isEmpty()) { - val result = fuzzingResultHandler(pmd, emptyList()) - result?.let { - emit(it.fuzzingExecutionFeedback) - } - } else { - try { - PythonFuzzing(pmd.pythonTypeStorage) { description, arguments -> - if (isCancelled()) { - logger.info { "Fuzzing process was interrupted" } - manager.disconnect() - return@PythonFuzzing PythonFeedback(control = Control.STOP) - } - if (System.currentTimeMillis() >= until) { - logger.info { "Fuzzing process was interrupted by timeout" } - manager.disconnect() - return@PythonFuzzing PythonFeedback(control = Control.STOP) - } + try { + if (parameters.isEmpty()) { + val result = fuzzingResultHandler(pmd, emptyList(), parameters, manager) + result?.let { + emit(it.fuzzingExecutionFeedback) + } + } else { + try { + PythonFuzzing(pmd.pythonTypeStorage) { description, arguments -> + if (isCancelled()) { + logger.info { "Fuzzing process was interrupted" } + manager.disconnect() + return@PythonFuzzing PythonFeedback(control = Control.STOP) + } + if (System.currentTimeMillis() >= until) { + logger.info { "Fuzzing process was interrupted by timeout" } + manager.disconnect() + return@PythonFuzzing PythonFeedback(control = Control.STOP) + } - if (arguments.any { PythonTree.containsFakeNode(it.tree) }) { - logger.debug("FakeNode in Python model") - emit(FakeNodeFeedback) - return@PythonFuzzing PythonFeedback(control = Control.CONTINUE) - } + if (arguments.any { PythonTree.containsFakeNode(it.tree) }) { + logger.debug("FakeNode in Python model") + emit(FakeNodeFeedback) + return@PythonFuzzing PythonFeedback(control = Control.CONTINUE) + } - val pair = Pair(description, arguments.map { PythonTreeWrapper(it.tree) }) - val mem = cache.get(pair) - if (mem != null) { - logger.debug("Repeat in fuzzing") - emit(CachedExecutionFeedback(mem.fuzzingExecutionFeedback)) - return@PythonFuzzing mem.fuzzingPlatformFeedback - } - val result = fuzzingResultHandler(description, arguments) - if (result == null) { // timeout - manager.disconnect() - return@PythonFuzzing PythonFeedback(control = Control.STOP) - } + val pair = Pair(description, arguments.map { PythonTreeWrapper(it.tree) }) + val mem = cache.get(pair) + if (mem != null) { + logger.debug("Repeat in fuzzing") + emit(CachedExecutionFeedback(mem.fuzzingExecutionFeedback)) + return@PythonFuzzing mem.fuzzingPlatformFeedback + } + val result = fuzzingResultHandler(description, arguments, parameters, manager) + if (result == null) { // timeout + manager.disconnect() + return@PythonFuzzing PythonFeedback(control = Control.STOP) + } - cache.add(pair, result) - emit(result.fuzzingExecutionFeedback) - return@PythonFuzzing result.fuzzingPlatformFeedback - }.fuzz(pmd) - } catch (_: NoSeedValueException) { // e.g. NoSeedValueException - logger.info { "Cannot fuzz values for types: ${parameters.map { it.pythonTypeRepresentation() }}" } + cache.add(pair, result) + emit(result.fuzzingExecutionFeedback) + return@PythonFuzzing result.fuzzingPlatformFeedback + }.fuzz(pmd) + } catch (_: NoSeedValueException) { + logger.info { "Cannot fuzz values for types: ${parameters.map { it.pythonTypeRepresentation() }}" } + } } + } finally { + manager.shutdown() } - manager.shutdown() } } } diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt index 6131dcc98c..e19454598c 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt @@ -22,7 +22,8 @@ import org.utbot.python.newtyping.mypy.MypyAnnotationStorage import org.utbot.python.newtyping.mypy.MypyReportLine import org.utbot.python.newtyping.mypy.getErrorNumber import org.utbot.python.newtyping.utils.getOffsetLine -import org.utbot.python.typing.MypyAnnotations +import org.utbot.python.newtyping.mypy.MypyAnnotations +import org.utbot.python.newtyping.utils.isRequired import org.utbot.python.utils.ExecutionWithTimeoutMode import org.utbot.python.utils.TestGenerationLimitManager import org.utbot.python.utils.PriorityCartesianProduct @@ -32,6 +33,7 @@ import java.io.File private val logger = KotlinLogging.logger {} private const val RANDOM_TYPE_FREQUENCY = 6 private const val MAX_EMPTY_COVERAGE_TESTS = 5 +private const val MAX_SUBSTITUTIONS = 10 class PythonTestCaseGenerator( private val withMinimization: Boolean = true, @@ -86,22 +88,20 @@ class PythonTestCaseGenerator( } } - private val maxSubstitutions = 10 - private fun generateTypesAfterSubstitution(type: Type, typeStorage: PythonTypeStorage): List { val params = type.getBoundedParameters() return PriorityCartesianProduct(params.map { getCandidates(it, typeStorage) }).getSequence().map { subst -> DefaultSubstitutionProvider.substitute(type, (params zip subst).associate { it }) - }.take(maxSubstitutions).toList() + }.take(MAX_SUBSTITUTIONS).toList() } - private fun substituteTypeParameters(method: PythonMethod, typeStorage: PythonTypeStorage): List { - val newClasses = - if (method.containingPythonClass != null) { - generateTypesAfterSubstitution(method.containingPythonClass, typeStorage) - } else { - listOf(null) - } + private fun substituteTypeParameters( + method: PythonMethod, + typeStorage: PythonTypeStorage, + ): List { + val newClasses = method.containingPythonClass?.let { + generateTypesAfterSubstitution(it, typeStorage) + } ?: listOf(null) return newClasses.flatMap { newClass -> val funcType = newClass?.getPythonAttributeByName(typeStorage, method.name)?.type as? FunctionType ?: method.definition.type @@ -117,7 +117,7 @@ class PythonTestCaseGenerator( method.ast ) } - }.take(maxSubstitutions) + }.take(MAX_SUBSTITUTIONS) } private fun methodHandler( @@ -128,7 +128,7 @@ class PythonTestCaseGenerator( executions: MutableList, initMissingLines: Set?, until: Long, - additionalVars: String = "" + additionalVars: String = "", ): Set? { // returns missing lines val limitManager = TestGenerationLimitManager( ExecutionWithTimeoutMode, @@ -138,79 +138,77 @@ class PythonTestCaseGenerator( val (hintCollector, constantCollector) = constructCollectors(mypyStorage, typeStorage, method) val constants = constantCollector.result.map { (type, value) -> - logger.debug("Collected constant: ${type.pythonTypeRepresentation()}: $value") + logger.debug { "Collected constant: ${type.pythonTypeRepresentation()}: $value" } PythonFuzzedConcreteValue(type, value) } - substituteTypeParameters(method, typeStorage).forEach { newMethod -> - inferAnnotations( - newMethod, - mypyStorage, - typeStorage, - hintCollector, - mypyReportLine, - mypyConfigFile, - limitManager, - additionalVars - ) { functionType -> - val args = (functionType as FunctionType).arguments - - logger.info { "Inferred annotations: ${args.joinToString { it.pythonTypeRepresentation() }}" } - - val engine = PythonEngine( - newMethod, - directoriesForSysPath, - curModule, - pythonPath, - constants, - timeoutForRun, - PythonTypeStorage.get(mypyStorage) - ) + inferAnnotations( + method, + mypyStorage, + typeStorage, + hintCollector, + mypyReportLine, + mypyConfigFile, + limitManager, + additionalVars + ) { functionType -> + val args = (functionType as FunctionType).arguments - var feedback: InferredTypeFeedback = SuccessFeedback + logger.info { "Inferred annotations: ${args.joinToString { it.pythonTypeRepresentation() }}" } - val fuzzerCancellation = { isCancelled() || limitManager.isCancelled() } + val engine = PythonEngine( + method, + directoriesForSysPath, + curModule, + pythonPath, + constants, + timeoutForRun, + PythonTypeStorage.get(mypyStorage) + ) - engine.fuzzing(args, fuzzerCancellation, until).collect { - when (it) { - is ValidExecution -> { - executions += it.utFuzzedExecution - missingLines = updateCoverage(it.utFuzzedExecution, coveredLines, missingLines) - feedback = SuccessFeedback - limitManager.addSuccessExecution() - } - is InvalidExecution -> { - errors += it.utError - feedback = InvalidTypeFeedback - limitManager.addInvalidExecution() - } - is ArgumentsTypeErrorFeedback -> { - feedback = InvalidTypeFeedback - limitManager.addInvalidExecution() - } - is TypeErrorFeedback -> { - feedback = InvalidTypeFeedback - limitManager.addInvalidExecution() - } - is CachedExecutionFeedback -> { - when (it.cachedFeedback) { - is ValidExecution -> { - limitManager.addSuccessExecution() - } - else -> { - limitManager.addInvalidExecution() - } + var feedback: InferredTypeFeedback = SuccessFeedback + + val fuzzerCancellation = { isCancelled() || limitManager.isCancelled() } + + engine.fuzzing(args, fuzzerCancellation, until).collect { + when (it) { + is ValidExecution -> { + executions += it.utFuzzedExecution + missingLines = updateMissingLines(it.utFuzzedExecution, coveredLines, missingLines) + feedback = SuccessFeedback + limitManager.addSuccessExecution() + } + is InvalidExecution -> { + errors += it.utError + feedback = InvalidTypeFeedback + limitManager.addInvalidExecution() + } + is ArgumentsTypeErrorFeedback -> { + feedback = InvalidTypeFeedback + limitManager.addInvalidExecution() + } + is TypeErrorFeedback -> { + feedback = InvalidTypeFeedback + limitManager.addInvalidExecution() + } + is CachedExecutionFeedback -> { + when (it.cachedFeedback) { + is ValidExecution -> { + limitManager.addSuccessExecution() + } + else -> { + limitManager.addInvalidExecution() } } - is FakeNodeFeedback -> { - limitManager.addFakeNodeExecutions() - } } - limitManager.missedLines = missingLines?.size + is FakeNodeFeedback -> { + limitManager.addFakeNodeExecutions() + } } - limitManager.restart() - feedback + limitManager.missedLines = missingLines?.size } + limitManager.restart() + feedback } return missingLines } @@ -226,37 +224,27 @@ class PythonTestCaseGenerator( logger.info("Start test generation for ${method.name}") try { - val meta = method.definition.type.pythonDescription() as PythonCallableTypeDescription - val argKinds = meta.argumentKinds - if (argKinds.any { it != PythonCallableTypeDescription.ArgKind.ARG_POS }) { - val now = System.currentTimeMillis() - val firstUntil = (until - now) / 2 + now - val originalDef = method.definition - val shortType = meta.removeNonPositionalArgs(originalDef.type) - val shortMeta = PythonFuncItemDescription( - originalDef.meta.name, - originalDef.meta.args.take(shortType.arguments.size) - ) - val additionalVars = originalDef.meta.args - .drop(shortType.arguments.size) - .joinToString(separator = "\n", prefix = "\n") { arg -> - "${arg.name}: ${pythonAnyType.pythonTypeRepresentation()}" // TODO: better types - } - method.definition = PythonFunctionDefinition(shortMeta, shortType) - val missingLines = methodHandler( + val methodModifications = mutableSetOf>() // Set of pairs + + substituteTypeParameters(method, typeStorage).forEach { newMethod -> + createShortForm(newMethod)?.let { methodModifications.add(it) } + methodModifications.add(newMethod to "") + } + + val now = System.currentTimeMillis() + val timeout = (until - now) / methodModifications.size + var missingLines: Set? = null + methodModifications.forEach { (method, additionalVars) -> + missingLines = methodHandler( method, typeStorage, coveredLines, errors, executions, - null, - firstUntil, - additionalVars + missingLines, + minOf(until, System.currentTimeMillis() + timeout), + additionalVars, ) - method.definition = originalDef - methodHandler(method, typeStorage, coveredLines, errors, executions, missingLines, until) - } else { - methodHandler(method, typeStorage, coveredLines, errors, executions, null, until) } } catch (_: OutOfMemoryError) { logger.info { "Out of memory error. Stop test generation process" } @@ -273,14 +261,16 @@ class PythonTestCaseGenerator( minimizeExecutions(failedExecutions) + emptyCoverageExecutions.take(MAX_EMPTY_COVERAGE_TESTS) else - executions, + coverageExecutions + emptyCoverageExecutions.take(MAX_EMPTY_COVERAGE_TESTS), errors, storageForMypyMessages ) } - // returns new missingLines - private fun updateCoverage( + /** + * Calculate a new set of missing lines in tested function + */ + private fun updateMissingLines( execution: UtExecution, coveredLines: MutableSet, missingLines: Set? @@ -338,11 +328,43 @@ class PythonTestCaseGenerator( val iterationNumber = algo.run(hintCollector.result, typeInferenceCancellation, annotationHandler) - if (iterationNumber == 1) { + if (iterationNumber == 1) { // Initial annotation can't be substituted limitManager.mode = TimeoutMode val existsAnnotation = method.definition.type annotationHandler(existsAnnotation) } } } + + companion object { + fun createShortForm(method: PythonMethod): Pair? { + val meta = method.definition.type.pythonDescription() as PythonCallableTypeDescription + val argKinds = meta.argumentKinds + if (argKinds.any { !isRequired(it) }) { + val originalDef = method.definition + val shortType = meta.removeNotRequiredArgs(originalDef.type) + val shortMeta = PythonFuncItemDescription( + originalDef.meta.name, + originalDef.meta.args.filterIndexed { index, _ -> isRequired(argKinds[index]) } + ) + val additionalVars = originalDef.meta.args + .filterIndexed { index, _ -> !isRequired(argKinds[index]) } + .mapIndexed { index, arg -> + "${arg.name}: ${method.argumentsWithoutSelf[index].annotation ?: pythonAnyType.pythonTypeRepresentation()}" + } + .joinToString(separator = "\n", prefix = "\n") + val shortDef = PythonFunctionDefinition(shortMeta, shortType) + val shortMethod = PythonMethod( + method.name, + method.moduleFilename, + method.containingPythonClass, + method.codeAsString, + shortDef, + method.ast + ) + return Pair(shortMethod, additionalVars) + } + return null + } + } } diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationConfig.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationConfig.kt new file mode 100644 index 0000000000..405a1abf0d --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationConfig.kt @@ -0,0 +1,25 @@ +package org.utbot.python + +import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.domain.TestFramework +import java.nio.file.Path + +data class TestFileInformation( + val testedFilePath: String, + val testedFileContent: String, + val moduleName: String, +) + +class PythonTestGenerationConfig( + val pythonPath: String, + val testFileInformation: TestFileInformation, + val sysPathDirectories: Set, + val testedMethods: List, + val timeout: Long, + val timeoutForRun: Long, + val testFramework: TestFramework, + val testSourceRootPath: Path, + val withMinimization: Boolean, + val isCanceled: () -> Boolean, + val runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour, +) \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt index 8206263588..fd7df3a128 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt @@ -3,11 +3,7 @@ package org.utbot.python import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import org.parsers.python.PythonParser -import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour -import org.utbot.python.framework.codegen.model.PythonSysPathImport -import org.utbot.python.framework.codegen.model.PythonSystemImport -import org.utbot.python.framework.codegen.model.PythonUserImport -import org.utbot.framework.codegen.domain.TestFramework +import org.utbot.framework.codegen.domain.HangingTestsTimeout import org.utbot.framework.codegen.domain.models.CgMethodTestSet import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.UtClusterInfo @@ -20,226 +16,194 @@ import org.utbot.python.framework.api.python.PythonMethodId import org.utbot.python.framework.api.python.PythonModel import org.utbot.python.framework.api.python.PythonUtExecution import org.utbot.python.framework.api.python.RawPythonAnnotation +import org.utbot.python.framework.api.python.pythonBuiltinsModuleName import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId import org.utbot.python.framework.codegen.model.PythonCodeGenerator +import org.utbot.python.framework.codegen.model.PythonImport +import org.utbot.python.framework.codegen.model.PythonSysPathImport +import org.utbot.python.framework.codegen.model.PythonSystemImport +import org.utbot.python.framework.codegen.model.PythonUserImport import org.utbot.python.newtyping.PythonFunctionDefinition import org.utbot.python.newtyping.general.CompositeType import org.utbot.python.newtyping.getPythonAttributes import org.utbot.python.newtyping.mypy.MypyAnnotationStorage +import org.utbot.python.newtyping.mypy.MypyReportLine import org.utbot.python.newtyping.mypy.readMypyAnnotationStorageAndInitialErrors import org.utbot.python.newtyping.mypy.setConfigFile -import org.utbot.python.typing.MypyAnnotations -import org.utbot.python.utils.Cleaner -import org.utbot.python.utils.RequirementsUtils.requirementsAreInstalled -import org.utbot.python.utils.getLineOfFunction +import org.utbot.python.newtyping.pythonName import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.pathString -object PythonTestGenerationProcessor { - fun processTestGeneration( - pythonPath: String, - pythonFilePath: String, - pythonFileContent: String, - directoriesForSysPath: Set, - currentPythonModule: String, - pythonMethods: List, - containingClassName: String?, - timeout: Long, - testFramework: TestFramework, - timeoutForRun: Long, - writeTestTextToFile: (String) -> Unit, - pythonRunRoot: Path, - doNotCheckRequirements: Boolean = false, - withMinimization: Boolean = true, - runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.FAIL, - isCanceled: () -> Boolean = { false }, - checkingRequirementsAction: () -> Unit = {}, - installingRequirementsAction: () -> Unit = {}, - requirementsAreNotInstalledAction: () -> MissingRequirementsActionResult = { - MissingRequirementsActionResult.NOT_INSTALLED - }, - startedLoadingPythonTypesAction: () -> Unit = {}, - startedTestGenerationAction: () -> Unit = {}, - notGeneratedTestsAction: (List) -> Unit = {}, // take names of functions without tests - processMypyWarnings: (List) -> Unit = {}, - processCoverageInfo: (String) -> Unit = {}, - startedCleaningAction: () -> Unit = {}, - finishedAction: (List) -> Unit = {}, // take names of functions with generated tests - ) { - Cleaner.restart() - - try { - if (!doNotCheckRequirements) { - checkingRequirementsAction() - if (!requirementsAreInstalled(pythonPath)) { - installingRequirementsAction() - val result = requirementsAreNotInstalledAction() - if (result == MissingRequirementsActionResult.NOT_INSTALLED) - return - } - } +// TODO: add asserts that one or less of containing classes and only one file +abstract class PythonTestGenerationProcessor { + abstract val configuration: PythonTestGenerationConfig + + fun sourceCodeAnalyze(): Pair> { + val mypyConfigFile = setConfigFile(configuration.sysPathDirectories) + return readMypyAnnotationStorageAndInitialErrors( + configuration.pythonPath, + configuration.testFileInformation.testedFilePath, + configuration.testFileInformation.moduleName, + mypyConfigFile + ) + } - startedLoadingPythonTypesAction() + fun testGenerate(mypyStorage: MypyAnnotationStorage): List { + val mypyConfigFile = setConfigFile(configuration.sysPathDirectories) + val startTime = System.currentTimeMillis() + + val testCaseGenerator = PythonTestCaseGenerator( + withMinimization = configuration.withMinimization, + directoriesForSysPath = configuration.sysPathDirectories, + curModule = configuration.testFileInformation.moduleName, + pythonPath = configuration.pythonPath, + fileOfMethod = configuration.testFileInformation.testedFilePath, + isCancelled = configuration.isCanceled, + timeoutForRun = configuration.timeoutForRun, + sourceFileContent = configuration.testFileInformation.testedFileContent, + mypyStorage = mypyStorage, + mypyReportLine = emptyList(), + mypyConfigFile = mypyConfigFile, + ) - val mypyConfigFile = setConfigFile(directoriesForSysPath) - val (mypyStorage, report) = readMypyAnnotationStorageAndInitialErrors( - pythonPath, - pythonFilePath, - currentPythonModule, - mypyConfigFile + val until = startTime + configuration.timeout + val tests = configuration.testedMethods.mapIndexed { index, methodHeader -> + val methodsLeft = configuration.testedMethods.size - index + val localUntil = (until - System.currentTimeMillis()) / methodsLeft + System.currentTimeMillis() + val method = findMethodByHeader( + mypyStorage, + methodHeader, + configuration.testFileInformation.moduleName, + configuration.testFileInformation.testedFileContent ) + testCaseGenerator.generate(method, localUntil) + } + val (notEmptyTests, emptyTestSets) = tests.partition { it.executions.isNotEmpty() } - startedTestGenerationAction() + if (emptyTestSets.isNotEmpty()) { + notGeneratedTestsAction(emptyTestSets.map { it.method.name }) + } - val startTime = System.currentTimeMillis() + return notEmptyTests + } - val testCaseGenerator = PythonTestCaseGenerator( - withMinimization = withMinimization, - directoriesForSysPath = directoriesForSysPath, - curModule = currentPythonModule, - pythonPath = pythonPath, - fileOfMethod = pythonFilePath, - isCancelled = isCanceled, - timeoutForRun = timeoutForRun, - sourceFileContent = pythonFileContent, - mypyStorage = mypyStorage, - mypyReportLine = report, - mypyConfigFile = mypyConfigFile, + fun testCodeGenerate(testSets: List): String { + val containingClassName = getContainingClassName(testSets) + val classId = PythonClassId(configuration.testFileInformation.moduleName, containingClassName) + + val methodIds = testSets.associate { testSet -> + testSet.method to PythonMethodId( + classId, + testSet.method.name, + RawPythonAnnotation(pythonAnyClassId.name), + testSet.method.arguments.map { argument -> + argument.annotation?.let { annotation -> + RawPythonAnnotation(annotation) + } ?: pythonAnyClassId + }, ) + } - val until = startTime + timeout - val tests = pythonMethods.mapIndexed { index, methodHeader -> - val methodsLeft = pythonMethods.size - index - val localUntil = (until - System.currentTimeMillis()) / methodsLeft + System.currentTimeMillis() - val method = findMethodByHeader(mypyStorage, methodHeader, currentPythonModule, pythonFileContent) - testCaseGenerator.generate(method, localUntil) + val paramNames = testSets.associate { testSet -> + var params = testSet.method.arguments.map { it.name } + if (testSet.method.hasThisArgument) { + params = params.drop(1) } + methodIds[testSet.method] as ExecutableId to params + }.toMutableMap() + + val allImports = collectImports(testSets) + + val context = UtContext(this::class.java.classLoader) + withUtContext(context) { + val codegen = PythonCodeGenerator( + classId, + paramNames = paramNames, + testFramework = configuration.testFramework, + testClassPackageName = "", + hangingTestsTimeout = HangingTestsTimeout(configuration.timeoutForRun), + runtimeExceptionTestsBehaviour = configuration.runtimeExceptionTestsBehaviour, + ) + val testCode = codegen.pythonGenerateAsStringWithTestReport( + testSets.map { testSet -> + val intRange = testSet.executions.indices + val clusterInfo = listOf(Pair(UtClusterInfo("FUZZER"), intRange)) + CgMethodTestSet( + executableId = methodIds[testSet.method] as ExecutableId, + executions = testSet.executions, + clustersInfo = clusterInfo, + ) + }, + allImports + ).generatedCode + return testCode + } + } - val (notEmptyTests, emptyTestSets) = tests.partition { it.executions.isNotEmpty() } - - if (emptyTestSets.isNotEmpty()) { - notGeneratedTestsAction(emptyTestSets.map { it.method.name }) - } + abstract fun saveTests(testsCode: String) - if (notEmptyTests.isEmpty()) - return + abstract fun notGeneratedTestsAction(testedFunctions: List) - val classId = - if (containingClassName == null) - PythonClassId(currentPythonModule, "TopLevelFunctions") - else - PythonClassId(currentPythonModule, containingClassName) + abstract fun processCoverageInfo(testSets: List) - val methodIds = notEmptyTests.associate { - it.method to PythonMethodId( - classId, - it.method.name, - RawPythonAnnotation(pythonAnyClassId.name), - it.method.arguments.map { argument -> - argument.annotation?.let { annotation -> - RawPythonAnnotation(annotation) - } ?: pythonAnyClassId - } - ) - } + private fun getContainingClassName(testSets: List): String { + val containingClasses = testSets.map { it.method.containingPythonClass?.pythonName() ?: "TopLevelFunctions" } + return containingClasses.toSet().first() + } - val paramNames = notEmptyTests.associate { testSet -> - var params = testSet.method.arguments.map { it.name } - if (testSet.method.hasThisArgument) { - params = params.drop(1) + private fun collectImports(notEmptyTests: List): Set { + val importParamModules = notEmptyTests.flatMap { testSet -> + testSet.executions.flatMap { execution -> + val params = (execution.stateAfter.parameters + execution.stateBefore.parameters).toMutableSet() + val self = mutableListOf(execution.stateBefore.thisInstance, execution.stateAfter.thisInstance) + if (execution is PythonUtExecution) { + params.addAll(execution.stateInit.parameters) + self.add(execution.stateInit.thisInstance) } - methodIds[testSet.method] as ExecutableId to params - }.toMutableMap() - - val importParamModules = notEmptyTests.flatMap { testSet -> - testSet.executions.flatMap { execution -> - val params = (execution.stateAfter.parameters + execution.stateBefore.parameters).toMutableSet() - val self = mutableListOf(execution.stateBefore.thisInstance, execution.stateAfter.thisInstance) - if (execution is PythonUtExecution) { - params.addAll(execution.stateInit.parameters) - self.add(execution.stateInit.thisInstance) - } - (params + self) - .filterNotNull() - .flatMap { utModel -> + (params + self) + .filterNotNull() + .flatMap { utModel -> (utModel as PythonModel).let { - it.allContainingClassIds.map { classId -> - PythonUserImport(importName_ = classId.moduleName) - } + it.allContainingClassIds + .filterNot { classId -> classId == pythonNoneClassId } + .map { classId -> PythonUserImport(importName_ = classId.moduleName) } } } - } } - val importResultModules = notEmptyTests.flatMap { testSet -> - testSet.executions.mapNotNull { execution -> - if (execution.result is UtExecutionSuccess) { - (execution.result as UtExecutionSuccess).let { result -> - (result.model as PythonModel).let { - it.allContainingClassIds.map { classId -> - PythonUserImport(importName_ = classId.moduleName) - } - } + } + val importResultModules = notEmptyTests.flatMap { testSet -> + testSet.executions.mapNotNull { execution -> + if (execution.result is UtExecutionSuccess) { + (execution.result as UtExecutionSuccess).let { result -> + (result.model as PythonModel).let { + it.allContainingClassIds + .filterNot { classId -> classId == pythonNoneClassId } + .map { classId -> PythonUserImport(importName_ = classId.moduleName) } } - } else null - }.flatten() - } - val testRootModules = notEmptyTests.mapNotNull { testSet -> - methodIds[testSet.method]?.rootModuleName?.let { PythonUserImport(importName_ = it) } - } - val sysImport = PythonSystemImport("sys") - val sysPathImports = relativizePaths(pythonRunRoot, directoriesForSysPath).map { PythonSysPathImport(it) } - - val testFrameworkModule = - testFramework.testSuperClass?.let { PythonUserImport(importName_ = (it as PythonClassId).rootModuleName) } - - val allImports = ( - importParamModules + importResultModules + testRootModules + sysPathImports + listOf( - testFrameworkModule, - sysImport - ) - ) - .filterNotNull() -// .filterNot { it.importName == pythonBuiltinsModuleName } - .toSet() - - val context = UtContext(this::class.java.classLoader) - withUtContext(context) { - val codegen = PythonCodeGenerator( - classId, - paramNames = paramNames, - testFramework = testFramework, - testClassPackageName = "", - runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, - ) - val testCode = codegen.pythonGenerateAsStringWithTestReport( - notEmptyTests.map { testSet -> - val intRange = testSet.executions.indices - val clusterInfo = listOf(Pair(UtClusterInfo("FUZZER"), intRange)) - CgMethodTestSet( - executableId = methodIds[testSet.method] as ExecutableId, - executions = testSet.executions, - clustersInfo = clusterInfo, - ) - }, - allImports - ).generatedCode - writeTestTextToFile(testCode) - } - - val coverageInfo = getCoverageInfo(notEmptyTests) - processCoverageInfo(coverageInfo) - - val mypyReport = getMypyReport(notEmptyTests, pythonFileContent) - if (mypyReport.isNotEmpty()) - processMypyWarnings(mypyReport) - - finishedAction(notEmptyTests.map { it.method.name }) - - } finally { - startedCleaningAction() - Cleaner.doCleaning() + } + } else null + }.flatten() } + val rootModule = configuration.testFileInformation.moduleName.split(".").first() + val testRootModule = PythonUserImport(importName_ = rootModule) + val sysImport = PythonSystemImport("sys") + val osImport = PythonSystemImport("os") + val sysPathImports = relativizePaths( + configuration.testSourceRootPath, + configuration.sysPathDirectories + ).map { PythonSysPathImport(it) } + + val testFrameworkModule = + configuration.testFramework.testSuperClass?.let { PythonUserImport(importName_ = (it as PythonClassId).rootModuleName) } + + return (importParamModules + importResultModules + testRootModule + sysPathImports + listOf( + testFrameworkModule, osImport, sysImport + )) + .filterNotNull() + .filterNot { it.rootModuleName == pythonBuiltinsModuleName } + .toSet() } private fun findMethodByHeader( @@ -273,26 +237,13 @@ object PythonTestGenerationProcessor { ) } - enum class MissingRequirementsActionResult { - INSTALLED, NOT_INSTALLED - } - - private fun getMypyReport(notEmptyTests: List, pythonFileContent: String): List = - notEmptyTests.flatMap { testSet -> - val lineOfFunction = getLineOfFunction(pythonFileContent, testSet.method.name) - val msgLines = testSet.mypyReport.mapNotNull { - if (it.file != MypyAnnotations.TEMPORARY_MYPY_FILE) - null - else if (lineOfFunction != null && it.line >= 0) - ":${it.line + lineOfFunction}: ${it.type}: ${it.message}" - else - "${it.type}: ${it.message}" - } - if (msgLines.isNotEmpty()) { - listOf("MYPY REPORT (function ${testSet.method.name})") + msgLines - } else { - emptyList() - } + private fun relativizePaths(rootPath: Path?, paths: Set): Set = + if (rootPath != null) { + paths.map { path -> + rootPath.relativize(Path(path)).pathString + }.toSet() + } else { + paths } data class InstructionSet( @@ -318,8 +269,7 @@ object PythonTestGenerationProcessor { else acc + listOf(InstructionSet(lineNumber, lineNumber)) } - - private fun getCoverageInfo(testSets: List): String { + protected fun getCoverageInfo(testSets: List): String { val covered = mutableSetOf() val missed = mutableSetOf>() testSets.forEach { testSet -> @@ -336,15 +286,12 @@ object PythonTestGenerationProcessor { else getInstructionSetList(missed.reduce { a, b -> a intersect b }) - return jsonAdapter.toJson(CoverageInfo(coveredInstructionSets, missedInstructionSets)) + return jsonAdapter.toJson( + CoverageInfo( + coveredInstructionSets, + missedInstructionSets + ) + ) } - private fun relativizePaths(rootPath: Path?, paths: Set): Set = - if (rootPath != null) { - paths.map { path -> - rootPath.relativize(Path(path)).pathString - }.toSet() - } else { - paths - } -} +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt b/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt index 9914345deb..e2a7e10037 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt @@ -8,9 +8,14 @@ import org.utbot.python.framework.api.python.PythonTreeModel import org.utbot.python.framework.api.python.util.pythonAnyClassId import org.utbot.python.newtyping.* import org.utbot.python.newtyping.general.CompositeType -import org.utbot.python.typing.MypyAnnotations +import org.utbot.python.newtyping.mypy.MypyAnnotations +import org.utbot.python.newtyping.utils.isNamed -data class PythonArgument(val name: String, val annotation: String?) +data class PythonArgument( + val name: String, + val annotation: String?, + val isNamed: Boolean = false, +) class PythonMethodHeader( val name: String, @@ -26,25 +31,33 @@ class PythonMethod( var definition: PythonFunctionDefinition, val ast: Block ) { + fun methodSignature(): String = "$name(" + arguments.joinToString(", ") { "${it.name}: ${it.annotation ?: pythonAnyClassId.name}" } + ")" /* Check that the first argument is `self` of `cls`. - TODO: Now we think that all class methods has `self` argument! We should support `@property` decorator + TODO: We should support `@property` decorator */ val hasThisArgument: Boolean - get() = containingPythonClass != null + get() = containingPythonClass != null && definition.meta.args.any { it.isSelf } val arguments: List get() { - val paramNames = definition.meta.args.map { it.name } - return (definition.type.arguments zip paramNames).map { - PythonArgument(it.second, it.first.pythonTypeRepresentation()) + val meta = definition.type.pythonDescription() as PythonCallableTypeDescription + return (definition.type.arguments).mapIndexed { index, type -> + PythonArgument( + meta.argumentNames[index]!!, + type.pythonTypeRepresentation(), // TODO: improve pythonTypeRepresentation + isNamed(meta.argumentKinds[index]) + ) } } + val argumentsWithoutSelf: List + get() = if (hasThisArgument) arguments.drop(1) else arguments + val thisObjectName: String? get() = if (hasThisArgument) arguments[0].name else null diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt index cd2450c602..ccecb68e13 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt @@ -11,34 +11,6 @@ import org.utbot.python.newtyping.general.Type object PythonCodeGenerator { - fun generateRunFunctionCode( - method: PythonMethod, - methodArguments: List, - directoriesForSysPath: Set, - moduleToImport: String, - additionalModules: Set = emptySet(), - fileForOutputName: String, - coverageDatabasePath: String, - ): String { - val context = UtContext(this::class.java.classLoader) - withUtContext(context) { - val codegen = org.utbot.python.framework.codegen.model.PythonCodeGenerator( - PythonClassId("TopLevelFunction"), - paramNames = emptyMap>().toMutableMap(), - testFramework = PythonCgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks[0], - testClassPackageName = "", - ) - return codegen.generateFunctionCall( - method, - methodArguments, - directoriesForSysPath, - moduleToImport, - additionalModules, - fileForOutputName, - coverageDatabasePath, - ) - } - } fun generateMypyCheckCode( method: PythonMethod, diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCodeSocketExecutor.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCodeSocketExecutor.kt index 7d8bb6a454..be259a046b 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCodeSocketExecutor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCodeSocketExecutor.kt @@ -14,8 +14,12 @@ import org.utbot.python.evaluation.serialiation.SuccessExecution import org.utbot.python.evaluation.serialiation.serializeObjects import org.utbot.python.evaluation.utils.CoverageIdGenerator import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.newtyping.PythonCallableTypeDescription +import org.utbot.python.newtyping.pythonDescription import org.utbot.python.newtyping.pythonTypeName import org.utbot.python.newtyping.pythonTypeRepresentation +import org.utbot.python.newtyping.utils.isNamed +import org.utbot.python.newtyping.utils.isRequired import java.net.SocketException private val logger = KotlinLogging.logger {} @@ -61,6 +65,17 @@ class PythonCodeSocketExecutor( ): PythonEvaluationResult { val (arguments, memory) = serializeObjects(fuzzedValues.allArguments.map { it.tree }) + val meta = method.definition.type.pythonDescription() as PythonCallableTypeDescription + val argKinds = meta.argumentKinds + val namedArgs = meta.argumentNames + .filterIndexed { index, _ -> !isNamed(argKinds[index]) } + + val (positionalArguments, namedArguments) = arguments + .zip(meta.argumentNames) + .partition { (_, name) -> + namedArgs.contains(name) + } + val containingClass = method.containingPythonClass val functionTextName = if (containingClass == null) @@ -75,8 +90,8 @@ class PythonCodeSocketExecutor( moduleToImport, additionalModulesToImport.toList(), syspathDirectories.toList(), - arguments, - emptyMap(), // here can be only-kwargs arguments + positionalArguments.map { it.first }, + namedArguments.associate { it.second!! to it.first }, // here can be only-kwargs arguments memory, method.moduleFilename, coverageId, diff --git a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCoverageReceiver.kt b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCoverageReceiver.kt index 6c46215d13..0707d347af 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCoverageReceiver.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/evaluation/PythonCoverageReceiver.kt @@ -29,12 +29,11 @@ class PythonCoverageReceiver( val request = DatagramPacket(buf, buf.size) socket.receive(request) val (id, line) = request.data.decodeToString().take(request.length).split(":") - logger.debug { "Got coverage: $id, $line" } val lineNumber = line.toInt() coverageStorage.getOrPut(id) { mutableSetOf() } .add(lineNumber) } } catch (ex: SocketException) { - logger.error("Socket error: " + ex.message) + logger.error(ex.message) } catch (ex: IOException) { logger.error("IO error: " + ex.message) } diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt index be50cf7f7c..d76d1a04cb 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt @@ -1,6 +1,7 @@ package org.utbot.python.framework.api.python import org.utbot.framework.plugin.api.* +import org.utbot.python.PythonArgument import org.utbot.python.framework.api.python.util.comparePythonTree import org.utbot.python.framework.api.python.util.moduleOfType @@ -93,11 +94,24 @@ class PythonUtExecution( stateAfter: EnvironmentModels, val diffIds: List, result: UtExecutionResult, + val arguments: List, coverage: Coverage? = null, summary: List? = null, testMethodName: String? = null, - displayName: String? = null + displayName: String? = null, ) : UtExecution(stateBefore, stateAfter, result, coverage, summary, testMethodName, displayName) { + init { + stateInit.parameters.zip(stateBefore.parameters).map { (init, before) -> + if (init is PythonTreeModel && before is PythonTreeModel) { + init.tree.comparable = before.tree.comparable + } + } + val init = stateInit.thisInstance + val before = stateBefore.thisInstance + if (init is PythonTreeModel && before is PythonTreeModel) { + init.tree.comparable = before.tree.comparable + } + } override fun copy( stateBefore: EnvironmentModels, stateAfter: EnvironmentModels, @@ -116,7 +130,8 @@ class PythonUtExecution( coverage = coverage, summary = summary, testMethodName = testMethodName, - displayName = displayName + displayName = displayName, + arguments = arguments ) } } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonTreeComparator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonTreeComparator.kt index 7d5ac0637f..e6a03ee171 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonTreeComparator.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonTreeComparator.kt @@ -26,7 +26,7 @@ fun comparePythonTree( return false } if (visitedLeft[left.id] == VisitStatus.CLOSED) { - return equals[left.id to right.id]!! + return equals[left.id to right.id] ?: false } if (visitedLeft[left.id] == VisitStatus.OPENED) { return true diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt index fc809207a0..dcff4c4dc2 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt @@ -8,42 +8,23 @@ import org.utbot.framework.codegen.domain.ProjectType import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour import org.utbot.framework.codegen.domain.StaticsMocking import org.utbot.framework.codegen.domain.TestFramework -import org.utbot.framework.codegen.domain.models.CgAssignment -import org.utbot.framework.codegen.domain.models.CgLiteral import org.utbot.framework.codegen.domain.models.CgMethodTestSet -import org.utbot.framework.codegen.domain.models.CgVariable import org.utbot.framework.codegen.domain.models.SimpleTestClassModel import org.utbot.framework.codegen.generator.CodeGenerator import org.utbot.framework.codegen.renderer.CgAbstractRenderer import org.utbot.framework.codegen.renderer.CgPrinterImpl import org.utbot.framework.codegen.renderer.CgRendererContext -import org.utbot.framework.codegen.tree.CgComponents.clearContextRelatedStorage import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.UtModel import org.utbot.python.PythonMethod -import org.utbot.python.framework.api.python.PythonClassId -import org.utbot.python.framework.api.python.PythonTreeModel -import org.utbot.python.framework.api.python.util.pythonAnyClassId -import org.utbot.python.framework.api.python.util.pythonNoneClassId -import org.utbot.python.framework.api.python.util.pythonStrClassId import org.utbot.python.framework.codegen.PythonCgLanguageAssistant -import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgStatementConstructorImpl import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgTestClassConstructor -import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgVariableConstructor import org.utbot.python.framework.codegen.model.constructor.visitor.CgPythonRenderer -import org.utbot.python.framework.codegen.model.tree.CgPythonDict -import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall -import org.utbot.python.framework.codegen.model.tree.CgPythonList -import org.utbot.python.framework.codegen.model.tree.CgPythonRepr -import org.utbot.python.framework.codegen.model.tree.CgPythonTree import org.utbot.python.newtyping.general.Type import org.utbot.python.newtyping.pythonAnyType import org.utbot.python.newtyping.pythonModules import org.utbot.python.newtyping.pythonTypeRepresentation -import org.utbot.python.framework.codegen.toPythonRawString -import org.utbot.python.newtyping.pythonName class PythonCodeGenerator( classUnderTest: ClassId, @@ -95,119 +76,6 @@ class PythonCodeGenerator( } } - fun generateFunctionCall( - method: PythonMethod, - methodArguments: List, - directoriesForSysPath: Set, - functionModule: String, - additionalModules: Set = emptySet(), - fileForOutputName: String, - coverageDatabasePath: String, - ): String = withCustomContext(testClassCustomName = null) { - context.withTestClassFileScope { - val cgStatementConstructor = - context.cgLanguageAssistant.getStatementConstructorBy(context) as PythonCgStatementConstructorImpl - with(cgStatementConstructor) { - clearContextRelatedStorage() - (context.cgLanguageAssistant as PythonCgLanguageAssistant).clear() - - val renderer = CgAbstractRenderer.makeRenderer(context) as CgPythonRenderer - - val executorModuleName = "utbot_executor.executor" - val executorModuleNameAlias = "__utbot_executor" - val executorFunctionName = "$executorModuleNameAlias.run_calculate_function_value" - val failArgumentsFunctionName = "$executorModuleNameAlias.fail_argument_initialization" - - val importExecutor = PythonUserImport(executorModuleName, alias_ = executorModuleNameAlias) - val importSys = PythonSystemImport("sys") - val importSysPaths = directoriesForSysPath.map { PythonSysPathImport(it) } - val importFunction = PythonUserImport(functionModule) - val imports = - listOf(importSys) + importSysPaths + listOf( - importExecutor, - importFunction - ) + additionalModules.map { PythonUserImport(it) } - imports.toSet().forEach { - context.cgLanguageAssistant.getNameGeneratorBy(context).variableName(it.moduleName ?: it.importName) - renderer.renderPythonImport(it) - } - - val fullpath = CgLiteral(pythonStrClassId, method.moduleFilename.toPythonRawString()) - val outputPath = CgLiteral(pythonStrClassId, fileForOutputName.toPythonRawString()) - val databasePath = CgLiteral(pythonStrClassId, coverageDatabasePath.toPythonRawString()) - - val containingClass = method.containingPythonClass - var functionTextName = - if (containingClass == null) - method.name - else - "${containingClass.pythonName()}.${method.name}" - if (functionModule.isNotEmpty()) { - functionTextName = "$functionModule.$functionTextName" - } - - val functionName = CgLiteral(pythonStrClassId, functionTextName) - - val arguments = method.arguments.map { argument -> - CgVariable(argument.name, argument.annotation?.let { PythonClassId(it) } ?: pythonAnyClassId) - } - - if (method.arguments.isNotEmpty()) { - var argumentsTryCatch = tryBlock { - methodArguments.zip(arguments).map { (model, argument) -> - if (model is PythonTreeModel) { - val obj = - (context.cgLanguageAssistant.getVariableConstructorBy(context) as PythonCgVariableConstructor) - .getOrCreateVariable(model) - +CgAssignment( - argument, - (obj as CgPythonTree).value - ) - } else { - +CgAssignment(argument, CgLiteral(model.classId, model.toString())) - } - } - } - argumentsTryCatch = argumentsTryCatch.catch(PythonClassId("builtins.Exception")) { exception -> - +CgPythonFunctionCall( - pythonNoneClassId, - failArgumentsFunctionName, - listOf( - outputPath, - exception, - ) - ) - emptyLine() - +CgPythonRepr(pythonAnyClassId, "sys.exit()") - } - argumentsTryCatch.accept(renderer) - } - - val args = CgPythonList(emptyList()) - val kwargs = CgPythonDict( - arguments.associateBy { argument -> CgLiteral(pythonStrClassId, "'${argument.name}'") } - ) - - val executorCall = CgPythonFunctionCall( - pythonNoneClassId, - executorFunctionName, - listOf( - databasePath, - functionName, - args, - kwargs, - fullpath, - outputPath, - ) - ) - - executorCall.accept(renderer) - - renderer.toString() - } - } - } - fun generateMypyCheckCode( method: PythonMethod, methodAnnotations: Map, @@ -250,6 +118,6 @@ class PythonCodeGenerator( "", functionName, ) + method.codeAsString.split("\n").map { " $it" } - return mypyCheckCode.joinToString("\n") + return mypyCheckCode.joinToString("\n") } } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt index e4eae18db6..7830ad22a8 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt @@ -50,26 +50,13 @@ class PythonCgCallableAccessManagerImpl(val context: CgContext) : CgCallableAcce override fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { val resolvedArgs = emptyList().toMutableList() args.forEach { arg -> + // if arg is named argument we must use this name if (arg is CgPythonTree) { resolvedArgs.add(arg.value) -// arg.children.forEach { +it } } else { resolvedArgs.add(arg as CgExpression) } } -// resolvedArgs.forEach { -// if (it is CgPythonTree) { -// it.children.forEach { child -> -// if (child is CgAssignment) { -// if (!existingVariableNames.contains(child.lValue.toString())) { -// +child -// } -// } else { -// +child -// } -// } -// } -// } val methodCall = CgMethodCall(caller, method, resolvedArgs) if (method is PythonMethodId) newMethodCall(method) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt index 623ebefe15..88ef4feb91 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt @@ -22,11 +22,18 @@ import org.utbot.python.framework.api.python.util.pythonExceptionClassId import org.utbot.python.framework.api.python.util.pythonIntClassId import org.utbot.python.framework.api.python.util.pythonNoneClassId import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded import org.utbot.python.framework.codegen.model.tree.* class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(context) { private val maxDepth: Int = 5 + private fun CgVariable.deepcopy(): CgVariable { + val classId = PythonClassId("copy.deepcopy") + importIfNeeded(classId) + return newVar(this.type) { CgPythonFunctionCall(classId, "copy.deepcopy", listOf(this)) } + } + override fun assertEquality(expected: CgValue, actual: CgVariable) { pythonDeepEquals(expected, actual) } @@ -83,7 +90,7 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex // build arguments val stateAssertions = emptyMap>().toMutableMap() for ((index, param) in constructorState.parameters.withIndex()) { - val name = paramNames[executableId]?.get(index) + val name = execution.arguments[index].name var argument = variableConstructor.getOrCreateVariable(param, name) val beforeValue = execution.stateBefore.parameters[index] @@ -96,6 +103,9 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex stateAssertions[index] = Pair(argument, afterValue) } } + if (execution.arguments[index].isNamed) { + argument = CgPythonNamedArgument(name, argument) + } methodArguments += argument } @@ -103,12 +113,17 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex if (it is CgPythonTree) { context.currentBlock.addAll(it.arguments) } + if (it is CgPythonNamedArgument && it.value is CgPythonTree) { + context.currentBlock.addAll(it.value.arguments) + } } recordActualResult() generateResultAssertions() - generateFieldStateAssertions(stateAssertions, assertThisObject, executableId) + if (methodType == CgTestMethodType.PASSED_EXCEPTION) { + generateFieldStateAssertions(stateAssertions, assertThisObject, executableId) + } } if (statics.isNotEmpty()) { @@ -245,9 +260,9 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex private fun assertIsInstance(expected: CgValue, actual: CgVariable) { when (testFrameworkManager) { is PytestManager -> - (testFrameworkManager as PytestManager).assertIsInstance(listOf(expected.type), actual) + (testFrameworkManager as PytestManager).assertIsinstance(listOf(expected.type as PythonClassId), actual) is UnittestManager -> - (testFrameworkManager as UnittestManager).assertIsInstance(listOf(expected.type), actual) + (testFrameworkManager as UnittestManager).assertIsinstance(listOf(expected.type as PythonClassId), actual) else -> testFrameworkManager.assertEquals(expected, actual) } } @@ -270,7 +285,7 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex val firstChild = elements.first() // TODO: We can use only structure => we should use another element if the first is empty - emptyLine() + emptyLineIfNeeded() if (elementsHaveSameStructure) { val index = newVar(pythonNoneClassId, keyName) { CgLiteral(pythonNoneClassId, "None") @@ -293,7 +308,7 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex index ) } - pythonDeepTreeEquals(firstChild, indexExpected, indexActual) + pythonDeepTreeEquals(firstChild, indexExpected, indexActual, useExpectedAsValue = true) statements = currentBlock } } @@ -323,12 +338,17 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex expectedNode: PythonTree.PythonTreeNode, expected: CgValue, actual: CgVariable, - depth: Int = maxDepth + depth: Int = maxDepth, + useExpectedAsValue: Boolean = false ) { if (expectedNode.comparable || depth == 0) { - emptyLineIfNeeded() + val expectedValue = if (useExpectedAsValue) { + expected + } else { + variableConstructor.getOrCreateVariable(PythonTreeModel(expectedNode)) + } testFrameworkManager.assertEquals( - expected, + expectedValue, actual, ) return diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt index c0e87a5b87..64de3da058 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt @@ -34,8 +34,10 @@ import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.python.framework.api.python.PythonClassId import org.utbot.python.framework.codegen.PythonCgLanguageAssistant.getCallableAccessManagerBy import org.utbot.python.framework.codegen.PythonCgLanguageAssistant.getNameGeneratorBy +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded import org.utbot.python.framework.codegen.model.constructor.util.plus import java.util.* @@ -253,7 +255,10 @@ class PythonCgStatementConstructorImpl(context: CgContext) : AnnotationTarget.Field -> error("Annotation ${annotation.target} is not supported in Python") } - importIfNeeded(annotation.classId) + val classId = annotation.classId + if (classId is PythonClassId) { + importIfNeeded(classId) + } } override fun returnStatement(expression: () -> CgExpression) { diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt index e1de6bd0e4..7410ea4fab 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt @@ -38,7 +38,7 @@ class PythonCgVariableConstructor(cgContext: CgContext) : CgVariableConstructor( private fun pythonBuildObject(objectNode: PythonTree.PythonTreeNode, baseName: String? = null): Pair> { return when (objectNode) { is PythonTree.PrimitiveNode -> { - Pair(CgLiteral(objectNode.type.dropBuiltins(), objectNode.repr), emptyList()) + Pair(CgLiteral(objectNode.type, objectNode.repr), emptyList()) } is PythonTree.ListNode -> { @@ -84,7 +84,7 @@ class PythonCgVariableConstructor(cgContext: CgContext) : CgVariableConstructor( getOrCreateVariable(PythonTreeModel(it, it.type)) } val constructor = ConstructorId( - objectNode.constructor.dropBuiltins(), + objectNode.constructor, initArgs.map { it.type } ) val constructorCall = CgConstructorCall(constructor, initArgs) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt index 5f1a98bd08..6cbfbb5096 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt @@ -81,7 +81,7 @@ internal class PytestManager(context: CgContext) : TestFrameworkManager(context) ) } - fun assertIsInstance(types: List, actual: CgVariable) { + fun assertIsinstance(types: List, actual: CgVariable) { +CgPythonAssertEquals( CgPythonFunctionCall( pythonBoolClassId, @@ -89,9 +89,9 @@ internal class PytestManager(context: CgContext) : TestFrameworkManager(context) listOf( actual, if (types.size == 1) - CgLiteral(pythonAnyClassId, types[0].name) + CgLiteral(pythonAnyClassId, types[0].prettyName) else - CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.name) }) + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.prettyName) }) ), ), ) @@ -152,7 +152,7 @@ internal class UnittestManager(context: CgContext) : TestFrameworkManager(contex ) } - fun assertIsInstance(types: List, actual: CgVariable) { + fun assertIsinstance(types: List, actual: CgVariable) { +assertions[assertTrue]( CgPythonFunctionCall( pythonBoolClassId, @@ -160,9 +160,9 @@ internal class UnittestManager(context: CgContext) : TestFrameworkManager(contex listOf( actual, if (types.size == 1) - CgLiteral(pythonAnyClassId, types[0].name) + CgLiteral(pythonAnyClassId, types[0].prettyName) else - CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.name) }) + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.prettyName) }) ), ), ) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt index 06e0b3ba63..0feb5e99d8 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -3,10 +3,12 @@ package org.utbot.python.framework.codegen.model.constructor.util import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentSet import org.utbot.framework.codegen.domain.context.CgContextOwner +import org.utbot.framework.codegen.domain.models.CgVariable import org.utbot.python.framework.api.python.PythonClassId import org.utbot.python.framework.api.python.PythonMethodId import org.utbot.python.framework.api.python.pythonBuiltinsModuleName import org.utbot.python.framework.codegen.model.PythonUserImport +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall internal fun CgContextOwner.importIfNeeded(method: PythonMethodId) { collectedImports += PythonUserImport(method.moduleName) @@ -40,3 +42,11 @@ internal fun PythonClassId.dropBuiltins(): PythonClassId { this } +internal fun String.dropBuiltins(): String { + val builtinsPrefix = "$pythonBuiltinsModuleName." + return if (this.startsWith(builtinsPrefix)) + this.drop(builtinsPrefix.length) + else + this +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt index b041c4c585..5c91f5389d 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt @@ -66,9 +66,10 @@ import org.utbot.framework.plugin.api.WildcardTypeParameter import org.utbot.python.framework.api.python.PythonClassId import org.utbot.python.framework.api.python.pythonBuiltinsModuleName import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.codegen.model.constructor.util.dropBuiltins import org.utbot.python.framework.codegen.model.tree.* import java.lang.StringBuilder -import org.utbot.python.framework.codegen.toPythonRawString +import org.utbot.python.framework.codegen.utils.toRelativeRawPath internal class CgPythonRenderer( context: CgRendererContext, @@ -234,7 +235,12 @@ internal class CgPythonRenderer( override fun visit(element: CgEqualTo) { element.left.accept(this) - print(" == ") + val isCompareTypes = listOf("builtins.bool", "types.NoneType") + if (isCompareTypes.contains(element.right.type.canonicalName)) { + print(" is ") + } else { + print(" == ") + } element.right.accept(this) } @@ -295,7 +301,7 @@ internal class CgPythonRenderer( } override fun visit(element: CgConstructorCall) { - print(element.executableId.classId.name) + print(element.executableId.classId.name.dropBuiltins()) renderExecutableCallArguments(element) } @@ -318,7 +324,7 @@ internal class CgPythonRenderer( fun renderPythonImport(pythonImport: PythonImport) { val importBuilder = StringBuilder() if (pythonImport is PythonSysPathImport) { - importBuilder.append("sys.path.append(${pythonImport.sysPath.toPythonRawString()})") + importBuilder.append("sys.path.append(${pythonImport.sysPath.toRelativeRawPath()})") } else if (pythonImport.moduleName == null) { importBuilder.append("import ${pythonImport.importName}") } else { @@ -499,7 +505,7 @@ internal class CgPythonRenderer( } override fun visit(element: CgPythonRepr) { - print(element.content) + print(element.content.dropBuiltins()) } override fun visit(element: CgPythonIndex) { @@ -510,7 +516,7 @@ internal class CgPythonRenderer( } override fun visit(element: CgPythonFunctionCall) { - print(element.name) + print(element.name.dropBuiltins()) print("(") val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine element.parameters.renderSeparated(newLinesNeeded) @@ -573,6 +579,11 @@ internal class CgPythonRenderer( withIndent { element.statements.forEach { it.accept(this) } } } + override fun visit(element: CgPythonNamedArgument) { + element.name?.let { print("$it=") } + element.value.accept(this) + } + override fun visit(element: CgPythonDict) { print("{") element.elements.map { (key, value) -> @@ -594,7 +605,7 @@ internal class CgPythonRenderer( } override fun visit(element: CgLiteral) { - print(element.value.toString()) + print(element.value.toString().dropBuiltins()) } override fun visit(element: CgFormattedString) { diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt index 904012191a..de93c4de42 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt @@ -16,4 +16,5 @@ interface CgPythonVisitor : CgVisitor { fun visit(element: CgPythonSet): R fun visit(element: CgPythonTree): R fun visit(element: CgPythonWith): R + fun visit(element: CgPythonNamedArgument): R } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt index 8617f88f12..140eed8183 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt @@ -1,12 +1,20 @@ package org.utbot.python.framework.codegen.model.tree +import org.utbot.framework.codegen.domain.models.CgAnnotation +import org.utbot.framework.codegen.domain.models.CgDocumentationComment import org.utbot.framework.codegen.domain.models.CgElement import org.utbot.framework.codegen.domain.models.CgExpression import org.utbot.framework.codegen.domain.models.CgLiteral +import org.utbot.framework.codegen.domain.models.CgMethod +import org.utbot.framework.codegen.domain.models.CgMethodCall +import org.utbot.framework.codegen.domain.models.CgParameterDeclaration import org.utbot.framework.codegen.domain.models.CgStatement +import org.utbot.framework.codegen.domain.models.CgTestMethod +import org.utbot.framework.codegen.domain.models.CgTestMethodType import org.utbot.framework.codegen.domain.models.CgValue import org.utbot.framework.codegen.domain.models.CgVariable import org.utbot.framework.codegen.renderer.CgVisitor +import org.utbot.framework.codegen.tree.VisibilityModifier import org.utbot.framework.plugin.api.ClassId import org.utbot.python.framework.api.python.PythonClassId import org.utbot.python.framework.api.python.PythonTree @@ -28,6 +36,7 @@ interface CgPythonElement : CgElement { is CgPythonTuple -> visitor.visit(element) is CgPythonTree -> visitor.visit(element) is CgPythonWith -> visitor.visit(element) + is CgPythonNamedArgument -> visitor.visit(element) else -> throw IllegalArgumentException("Can not visit element of type ${element::class}") } } else { @@ -115,3 +124,10 @@ data class CgPythonWith( val target: CgExpression?, val statements: List, ) : CgStatement, CgPythonElement + +class CgPythonNamedArgument( + val name: String?, + val value: CgExpression, +) : CgValue, CgPythonElement { + override val type: ClassId = value.type +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/utils/StringUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/utils/StringUtils.kt index 9a3653722f..409b7acc85 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/utils/StringUtils.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/utils/StringUtils.kt @@ -1,6 +1,8 @@ -package org.utbot.python.framework.codegen +package org.utbot.python.framework.codegen.utils +import java.nio.file.FileSystems -fun String.toPythonRawString(): String { - return "r'${this}'" + +fun String.toRelativeRawPath(): String { + return "os.path.dirname(__file__) + r'${FileSystems.getDefault().separator}${this}'" } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ConstantValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ConstantValueProvider.kt index 1faf11d80f..a6b7af69ce 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ConstantValueProvider.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ConstantValueProvider.kt @@ -9,7 +9,7 @@ import org.utbot.python.fuzzing.PythonMethodDescription import org.utbot.python.fuzzing.provider.utils.isAny import org.utbot.python.newtyping.general.Type import org.utbot.python.newtyping.pythonTypeName -import org.utbot.python.typing.TypesFromJSONStorage +import org.utbot.python.fuzzing.value.TypesFromJSONStorage object ConstantValueProvider : ValueProvider { override fun accept(type: Type): Boolean { diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ReduceValueProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ReduceValueProvider.kt index 41d5e738aa..4e0c8831af 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ReduceValueProvider.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/provider/ReduceValueProvider.kt @@ -31,7 +31,7 @@ object ReduceValueProvider : ValueProvider !(attr.meta.name.startsWith("__") && attr.meta.name.endsWith("__") && attr.meta.name.length >= 4) && diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/PreprocessedValueStorage.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/PreprocessedValueStorage.kt similarity index 97% rename from utbot-python/src/main/kotlin/org/utbot/python/typing/PreprocessedValueStorage.kt rename to utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/PreprocessedValueStorage.kt index e1188545a3..25f5a87e03 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/typing/PreprocessedValueStorage.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/PreprocessedValueStorage.kt @@ -1,4 +1,4 @@ -package org.utbot.python.typing +package org.utbot.python.fuzzing.value import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi diff --git a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/UndefValue.kt b/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/UndefValue.kt deleted file mode 100644 index 73d61c594b..0000000000 --- a/utbot-python/src/main/kotlin/org/utbot/python/fuzzing/value/UndefValue.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.utbot.python.fuzzing.value - -import org.utbot.fuzzing.Mutation -import org.utbot.fuzzing.seeds.KnownValue - -class ObjectValue : KnownValue diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/PythonType.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/PythonType.kt index 8d7d936f4c..c9f94260e8 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/PythonType.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/PythonType.kt @@ -2,6 +2,7 @@ package org.utbot.python.newtyping import org.utbot.python.newtyping.general.* import org.utbot.python.newtyping.general.Name +import org.utbot.python.newtyping.utils.isRequired sealed class PythonTypeDescription(name: Name) : TypeMetaDataWithName(name) { open fun castToCompatibleTypeApi(type: Type): Type = type @@ -185,9 +186,9 @@ class PythonCallableTypeDescription( argumentKinds, argumentNames ) { self -> - val oldToNewParameters = (like.parameters zip self.parameters).associate { - (it.first as TypeParameter) to it.second - } + val oldToNewParameters = (self.parameters zip like.parameters).mapNotNull { + if (it.second is TypeParameter) it else null + }.toMap() val newArgs = args.map { DefaultSubstitutionProvider.substitute(it, oldToNewParameters) } @@ -226,6 +227,25 @@ class PythonCallableTypeDescription( ) } } + + fun removeNotRequiredArgs(type: Type): FunctionType { + val functionType = castToCompatibleTypeApi(type) + return createPythonCallableType( + functionType.parameters.size, + argumentKinds.filter { isRequired(it) }, + argumentNames.filterIndexed { index, _ -> isRequired(argumentKinds[index]) } + ) { self -> + val substitution = (functionType.parameters zip self.parameters).associate { + Pair(it.first as TypeParameter, it.second) + } + FunctionTypeCreator.InitializationData( + functionType.arguments + .filterIndexed { index, _ -> isRequired(argumentKinds[index]) } + .map { DefaultSubstitutionProvider.substitute(it, substitution) }, + DefaultSubstitutionProvider.substitute(functionType.returnValue, substitution) + ) + } + } } // Special Python annotations diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyAnnotations.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyAnnotations.kt index 769a254bc1..358c8bcadb 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyAnnotations.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyAnnotations.kt @@ -105,7 +105,7 @@ class FunctionNode( val returnType: MypyAnnotation, val typeVars: List, val argKinds: List, - val argNames: List, + var argNames: List, ): MypyAnnotationNode() { override val children: List get() = super.children + argTypes + listOf(returnType) @@ -249,4 +249,15 @@ val annotationAdapter: PolymorphicJsonAdapterFactory = .withSubtype(PythonTuple::class.java, AnnotationType.Tuple.name) .withSubtype(PythonNoneType::class.java, AnnotationType.NoneType.name) .withSubtype(TypeAliasNode::class.java, AnnotationType.TypeAlias.name) - .withSubtype(UnknownAnnotationNode::class.java, AnnotationType.Unknown.name) \ No newline at end of file + .withSubtype(UnknownAnnotationNode::class.java, AnnotationType.Unknown.name) + +object MypyAnnotations { + + data class MypyReportLine( + val line: Int, + val type: String, + val message: String, + val file: String + ) + +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyStorage.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyStorage.kt index 8ad7cb48fb..c8641f99b9 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyStorage.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/mypy/MypyStorage.kt @@ -40,6 +40,19 @@ class MypyAnnotationStorage( annotation.initialized = true annotation.args?.forEach { initAnnotation(it) } } + private fun fillArgNames(definition: MypyDefinition) { + val node = definition.type.node + if (node is ConcreteAnnotation) { + node.members.filterIsInstance().forEach { funcDef -> + val nodeInfo = nodeStorage[funcDef.type.nodeId] + if (nodeInfo is FunctionNode && nodeInfo.argNames.contains(null)) { + nodeInfo.argNames = nodeInfo.argNames.zip(funcDef.args).map { + it.first ?: (it.second as Variable).name + } + } + } + } + } val nodeToUtBotType: MutableMap = mutableMapOf() fun getUtBotTypeOfNode(node: MypyAnnotationNode): Type { //println("entering $node") @@ -57,6 +70,7 @@ class MypyAnnotationStorage( definitions.values.forEach { defsInModule -> defsInModule.forEach { initAnnotation(it.value.type) + fillArgNames(it.value) } } types.values.flatten().forEach { diff --git a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/utils/Utils.kt b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/utils/Utils.kt index bf6c906b98..d1e305d9d4 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/newtyping/utils/Utils.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/newtyping/utils/Utils.kt @@ -1,6 +1,7 @@ package org.utbot.python.newtyping.utils import org.utbot.fuzzing.utils.chooseOne +import org.utbot.python.newtyping.PythonCallableTypeDescription import kotlin.random.Random fun getOffsetLine(sourceFileContent: String, offset: Int): Int { @@ -10,4 +11,10 @@ fun getOffsetLine(sourceFileContent: String, offset: Int): Int { fun weightedRandom(elems: List, weights: List, random: Random): T { val index = random.chooseOne(weights.toDoubleArray()) return elems[index] -} \ No newline at end of file +} + +fun isRequired(kind: PythonCallableTypeDescription.ArgKind) = + listOf(PythonCallableTypeDescription.ArgKind.ARG_POS, PythonCallableTypeDescription.ArgKind.ARG_NAMED).contains(kind) + +fun isNamed(kind: PythonCallableTypeDescription.ArgKind) = + listOf(PythonCallableTypeDescription.ArgKind.ARG_NAMED_OPT, PythonCallableTypeDescription.ArgKind.ARG_NAMED).contains(kind) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt deleted file mode 100644 index 42cf1d7660..0000000000 --- a/utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.utbot.python.typing - -object MypyAnnotations { - const val TEMPORARY_MYPY_FILE = "" - - data class MypyReportLine( - val line: Int, - val type: String, - val message: String, - val file: String - ) - -} - diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsInstaller.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsInstaller.kt new file mode 100644 index 0000000000..2aace97dec --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsInstaller.kt @@ -0,0 +1,18 @@ +package org.utbot.python.utils + +interface RequirementsInstaller { + fun checkRequirements(pythonPath: String, requirements: List): Boolean + fun installRequirements(pythonPath: String, requirements: List) + + companion object { + fun checkRequirements(requirementsInstaller: RequirementsInstaller, pythonPath: String, additionalRequirements: List): Boolean { + val requirements = RequirementsUtils.requirements + additionalRequirements + if (!requirementsInstaller.checkRequirements(pythonPath, requirements)) { + requirementsInstaller.installRequirements(pythonPath, requirements) + return requirementsInstaller.checkRequirements(pythonPath, requirements) + } + return true + } + + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt index 25489323bc..008439aa7f 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt @@ -3,8 +3,8 @@ package org.utbot.python.utils object RequirementsUtils { val requirements: List = listOf( "mypy==1.0.0", - "utbot-executor==1.4.31", - "utbot-mypy-runner==0.2.8", + "utbot-executor==1.4.32", + "utbot-mypy-runner==0.2.11", ) private val requirementsScriptContent: String =