diff --git a/docs/ChoosingLanguageSpecificIDE.md b/docs/ChoosingLanguageSpecificIDE.md new file mode 100644 index 0000000000..6512898ecc --- /dev/null +++ b/docs/ChoosingLanguageSpecificIDE.md @@ -0,0 +1,23 @@ +# Choosing language specific IDE + +Some language-specific modules depends on specific IntelliJ IDE: +* Python can work with IntelliJ Community, IntelliJ Ultimate, PyCharm Community, PyCharm Professional +* JavaScript can work with IntelliJ Ultimate, PyCharm Professional and WebStorm +* Java and Kotlin - IntelliJ Community and IntelliJ Ultimate + +You should select correct IDE in `gradle.properties` file: +``` +ideType= +ideVersion=<222.4167.29> +``` + +### IDE marking + +| Mark | Full name | Supported plugin | +|------|----------------------|----------------------------------------| +| IC | IntelliJ Community | JVM, Python, AndroidStudio | +| IU | IntelliJ Ultimate | JVM, Python, JavaScript, AndroidStudio | +| PC | PyCharm Community | Python | +| PY | PyCharm Professional | Python, JavaScript | + +[IntelliJ Platform Plugin SDK documentation](https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#tasks-runpluginverifier) \ No newline at end of file diff --git a/docs/UtbotFamilyChanges.md b/docs/UtbotFamilyChanges.md new file mode 100644 index 0000000000..e739aab937 --- /dev/null +++ b/docs/UtbotFamilyChanges.md @@ -0,0 +1,87 @@ +# Changes + +## Main settings + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `gradle.properties` | Add version parameters and IDE dependencies | +| `settings.gradle` | Rewrite to kts | + +## utbot-framework-api + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt` | Make `UtModel` open | +| `utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt` | Add field `missedInstructions` to class `Coverage` (default empty) | + +## utbot-framework + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt` | Add default else branch for Python and JS models in method `construct` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt` | Add default else branch for Python and JS models in method `assembleModel` | + +## utbot-framework > codegen + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt` | Make class `Import` abstract (for python imports), make class `TestFramework` open, field `assertEquals` and methdod `assertionId` open. Add nullable field `testSuperClass` to `TestFramework` (contains superclass for test class). | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt` | Move function `getLanguageKeywords` into `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt` | Make class `CodeGenerator`, field `context` and methods open. Swap fields in class `CodeGeneratorResult`. | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt` | Add new constructors for `CgMethodTestSet` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt` | Remove internal from data class `TestClassContext` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt` | Remove internal from class `UtilMethodProvider` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt` | Remove internal from classes `Context` and `CgContextOwner`. Add field `cgLanguageAssistant` into `CgContextOwner`. Move logic from `CgContext.__outerMostTestClassContext` and `CgContext.outerMostTestClass` to `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt` | Remove internal from `CgNameGenerator` and `CgNameGeneratorImpl`, change `codegenLanguage` argument to `cgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt` | Remove internal from interface `CgFieldStateManager` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt` | Change private to protected and add empty else branches in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt` | Change private to open or protected, add `cgLanguageAssistant` call instead standard implementations | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt` | Change private to open and add else branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt` | Remove internal and add else branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt` | Remove internal from `TestFrameworkManager` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt` | Remove internal from `EnvironmentFieldStateCache`, `FieldStateCache`, `CgFieldState`, `CgContextOwner.importIfNeeded` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt` | Add visit for `CgForEachLoop` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt` | Add else-branch in `TestFramework`-whens | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt` | Remove internal from `buildExceptionHandler` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt` | Add `CgForEachLoop` visit function, change private to protected some methods, move `makeRender` logic to `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt` | Remove `language` field | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt` | Remove `language` field, change `context.codegenLanugage` to `context.cgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt` | Remove internal and add `cgLanguageAssistant` field | +| `utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt` | Add visit for `CgForEachLoop` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt` | Add else-branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt` | Remove internal | +| `utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt` | Add else-branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt` | Add else-branch in `UtModel`-when | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CgLanguageAssistant.kt` | New file with `CgLanguageAssistant` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCgLanguageAssistant.kt` | Implementation `CgLanguageAssistant` for Java | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCgLanguageAssistant.kt` | Implementation `CgLanguageAssistant` for Kotlin | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/LanguageTestFrameworkManager.kt` | New file with `LanguageTestFrameworkManager` | +| `utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt` | Implementation `LanguageTestFrameworkManager` for JVM (Java + Kotlin) | + +## utbot-fuzzers + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt` | Make class `FuzzedMethodDescription` open | + + +## utbot-intellij and utbot-ui-commons + +| File | Changes | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt` | Move `GenerateTestsModel.getAllTestSourceRoots()` to `BaseTestModel` method, add empty else branch to `insertImports` | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt` | Implementation `LanguageAssistant` for Java | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt` | Move common logic to `BaseTestModel` | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt` | Add else-branches in `TestFramework`-whens | +| `utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt` | Move all logic to `JvmLanguageAssistant` | +| `utbot-intellij/src/main/resources/META-INF/` | Add config xml files for Java, Kotlin, Android, Python, JS | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt` | New class for Actions logic | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt` | New parent class for TestModels | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt` | New file, moved from `utbot-intellij` | +| `utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt` | New file, moved from `utbot-intellij` | \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 1e1b95cccb..202ade54a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,23 @@ kotlin.code.style=official -# IU, IC, PC, PY, WS... +# IU, IC, PC, PY # IC for AndroidStudio ideType=IC +ideVersion=222.4167.29 + +pythonIde=IC,IU,PC,PY +jsIde=IU,PY,WS # In order to run Android Studion instead of Intellij Community, # specify the path to your Android Studio installation //androidStudioPath=your_path_to_android_studio -pythonCommunityPluginVersion=222.4167.37 #Version numbers: https://plugins.jetbrains.com/plugin/631-python/versions +pythonCommunityPluginVersion=222.4167.37 pythonUltimatePluginVersion=222.4167.37 +kotlinPluginVersion=222-1.7.20-release-201-IJ4167.29 + junit5Version=5.8.0-RC1 junit4Version=4.13.2 junit4PlatformVersion=1.9.0 diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 7375c1636b..0000000000 --- a/settings.gradle +++ /dev/null @@ -1,34 +0,0 @@ -pluginManagement { - resolutionStrategy { - eachPlugin { - if (requested.id.name == "rdgen") { - useModule("com.jetbrains.rd:rd-gen:${requested.version}") - } - } - } -} - -rootProject.name = 'utbot' - -include 'utbot-core' -include 'utbot-framework' -include 'utbot-framework-api' -include 'utbot-intellij' -include 'utbot-sample' -include 'utbot-fuzzers' -include 'utbot-junit-contest' -include 'utbot-analytics' -include 'utbot-analytics-torch' -include 'utbot-cli' -include 'utbot-api' -include 'utbot-instrumentation' -include 'utbot-instrumentation-tests' - -include 'utbot-summary' -include 'utbot-gradle' -include 'utbot-maven' -include 'utbot-summary-tests' -include 'utbot-framework-test' -include 'utbot-rd' -include 'utbot-android-studio' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000000..4e30ca7d33 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,54 @@ +val ideType: String by settings + +val pythonIde: String by settings +val jsIde: String by settings + +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.name == "rdgen") { + useModule("com.jetbrains.rd:rd-gen:${requested.version}") + } + } + } +} + +rootProject.name = "utbot" + +include("utbot-core") +include("utbot-framework") +include("utbot-framework-api") +include("utbot-intellij") +include("utbot-sample") +include("utbot-fuzzers") +include("utbot-junit-contest") +include("utbot-analytics") +include("utbot-analytics-torch") + +include("utbot-cli") + +include("utbot-api") +include("utbot-instrumentation") +include("utbot-instrumentation-tests") + +include("utbot-summary") +include("utbot-gradle") +include("utbot-maven") +include("utbot-summary-tests") +include("utbot-framework-test") +include("utbot-rd") +include("utbot-android-studio") + +include("utbot-ui-commons") + +if (pythonIde.split(",").contains(ideType)) { + include("utbot-python") + include("utbot-cli-python") + include("utbot-intellij-python") +} + +if (jsIde.split(",").contains(ideType)) { + include("utbot-js") + include("utbot-cli-js") + include("utbot-intellij-js") +} diff --git a/utbot-cli-js/build.gradle b/utbot-cli-js/build.gradle new file mode 100644 index 0000000000..e0aad8dce9 --- /dev/null +++ b/utbot-cli-js/build.gradle @@ -0,0 +1,75 @@ +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +tasks.withType(JavaCompile) { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 +} + +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework') + implementation project(':utbot-cli') + implementation project(':utbot-js') + + // Without this dependency testng tests do not run. + implementation group: 'com.beust', name: 'jcommander', version: '1.48' + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: cliktVersion + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.json', name: 'json', version: '20220320' + //noinspection GroovyAssignabilityCheck + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + manifest { + attributes 'Main-Class': 'org.utbot.cli.js.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.js' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot JavaScript CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-js/src/README.md b/utbot-cli-js/src/README.md new file mode 100644 index 0000000000..b0363028fa --- /dev/null +++ b/utbot-cli-js/src/README.md @@ -0,0 +1,87 @@ +## Build + +.jar file can be built in GitHub Actions with script publish-plugin-and-cli-from-branch. + +## Requirements + +* NodeJs 10.0.0 or higher (available to download https://nodejs.org/en/download/) +* Java 11 or higher (available to download https://www.oracle.com/java/technologies/downloads/) +* Nyc 15.1.0 or higher (`> npm install -g nyc`) +* Mocha 10.0.0 or higher (`> npm install -g mocha`) + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_js --source="dir/file_with_sources.js" --output="dir/generated_tests.js" + +This will generate tests for top-level functions from `file_with_sources.js`. + +Run generated tests: + + java -jar utbot-cli.jar run_js --fileOrDir="generated_tests.js" + +This will run generated tests from file or directory. + +Generate coverage report: + + java -jar utbot-cli.jar coverage_js --source=dir/generated_tests.js + +This will generate coverage report from generated tests and print in `StdOut` + +## `generate_js` options + +- `-s, --source ` + + (required) Source code file for a test generation. +- `-c, --class ` + + If not specified, tests for top-level functions or single class are generated, otherwise for the specified class. + +- `-o, --output ` + + File for generated tests. +- `-p, --print-test` + + Specifies whether test should be printed out to `StdOut` (default = false) +- `-t, --timeout ` + + Timeout for a single test case to generate in seconds (default = 15) +- `--coverage-mode ` + + Specifies the coverage mode for test generation. Fast mode can't find timeouts, but works faster (default = FAST) +- `--path-to-node ` + + Sets path to Node.js executable (default = "node") +- `--path-to-nyc ` + + Sets path to nyc executable (default = "nyc") +- `--path-to-npm ` + + Sets path to npm executable (default = "npm") + +## `run_js` options + +- `-f, --fileOrDir` + + (required) File or directory with tests. +- `-o, --output` + + Specifies output of .txt file for test framework result (If empty prints to `StdOut`) + +- `-t, --test-framework ` + + Test framework of tests to run. (default = "Mocha") + +## `coverage_js` options + +- `-s, --source ` + + (required) File with tests to generate a report. + +- `-o, --output` + + Specifies output .json file for generated tests (If empty prints .json to `StdOut`) +- `--path-to-nyc ` + + Sets path to nyc executable (default = "nyc") diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt new file mode 100644 index 0000000000..9f1ed44f6c --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt @@ -0,0 +1,35 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import org.slf4j.event.Level +import org.utbot.cli.getVersion +import org.utbot.cli.setVerbosity +import kotlin.system.exitProcess + +class UtBotJsCli : CliktCommand(name = "UnitTestBot JavaScript Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotJsCli().subcommands( + JsCoverageCommand(), + JsGenerateTestsCommand(), + JsRunTestsCommand(), + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt new file mode 100644 index 0000000000..bab5de98ff --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt @@ -0,0 +1,163 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.check +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import mu.KotlinLogging +import org.json.JSONArray +import org.json.JSONObject +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import org.w3c.dom.Document +import org.w3c.dom.Element +import utils.JsCmdExec +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import javax.xml.parsers.DocumentBuilderFactory + +private val logger = KotlinLogging.logger {} + +class JsCoverageCommand : CliktCommand(name = "coverage_js", help = "Get tests coverage for the specified file.") { + + private val testFile by option( + "-s", "--source", + help = "Target test file path." + ).required() + .check("Must exist and ends with .js suffix") { + it.endsWith(".js") && Files.exists(Paths.get(it)) + } + + private val output by option( + "-o", "--output", + help = "Specifies output .json file for generated tests." + ).check("Must end with .json suffix") { + it.endsWith(".json") + } + + private val pathToNYC by option( + "--path-to-nyc", + help = "Sets path to nyc executable, defaults to \"nyc\" shortcut. " + + "As there are many nyc files in the global npm directory, choose one without file extension." + ).default("nyc") + + override fun run() { + val testFileAbsolutePath = makeAbsolutePath(testFile) + val workingDir = testFileAbsolutePath.substringBeforeLast("/") + val coverageDataPath = "$workingDir/coverage" + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + JsCmdExec.runCommand( + dir = workingDir, + shouldWait = true, + timeout = 20, + cmd = arrayOf( + pathToNYC, + "--report-dir=$coverageDataPath", + "--reporter=\"clover\"", + "--temp-dir=${workingDir}/cache", + "mocha", + testFileAbsolutePath + ) + ) + val coveredList = mutableListOf() + val partiallyCoveredList = mutableListOf() + val uncoveredList = mutableListOf() + val db = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val xmlFile = File("$coverageDataPath/clover.xml") + val doc = db.parse(xmlFile) + buildCoverageLists( + coveredList, + partiallyCoveredList, + uncoveredList, + doc, + ) + val json = createJson( + coveredList, + partiallyCoveredList, + uncoveredList, + ) + processResult(json, outputAbsolutePath) + } + + private fun buildCoverageLists( + coveredList: MutableList, + partiallyCoveredList: MutableList, + uncoveredList: MutableList, + doc: Document, + ) { + doc.documentElement.normalize() + val lineList = try { + (((doc.getElementsByTagName("project").item(0) as Element) + .getElementsByTagName("package").item(0) as Element) + .getElementsByTagName("file").item(0) as Element) + .getElementsByTagName("line") + } catch (e: Exception) { + ((doc.getElementsByTagName("project").item(0) as Element) + .getElementsByTagName("file").item(0) as Element) + .getElementsByTagName("line") + } + for (i in 0 until lineList.length) { + val lineInfo = lineList.item(i) as Element + val num = lineInfo.getAttribute("num").toInt() + val count = lineInfo.getAttribute("count").toInt() + when (lineInfo.getAttribute("type")) { + "stmt" -> { + if (count > 0) coveredList += num + else uncoveredList += num + } + + "cond" -> { + val trueCount = lineInfo.getAttribute("truecount").toInt() + val falseCount = lineInfo.getAttribute("falsecount").toInt() + when { + trueCount == 2 && falseCount == 0 -> coveredList += num + trueCount == 1 && falseCount == 1 -> partiallyCoveredList += num + trueCount == 0 && falseCount == 2 -> uncoveredList += num + } + } + } + } + } + + private fun createJson( + coveredList: List, + partiallyCoveredList: List, + uncoveredList: List, + ): JSONObject { + val coveredArray = JSONArray() + coveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + coveredArray.put(obj) + } + val partiallyCoveredArray = JSONArray() + partiallyCoveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + partiallyCoveredArray.put(obj) + } + val uncoveredArray = JSONArray() + uncoveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + uncoveredArray.put(obj) + } + val json = JSONObject() + json.put("covered", coveredArray) + json.put("notCovered", uncoveredArray) + json.put("partlyCovered", partiallyCoveredArray) + return json + } + + private fun processResult(json: JSONObject, output: String?) { + output?.let { fileName -> + val file = File(fileName) + file.createNewFile() + file.writeText(json.toString()) + } ?: logger.info { json.toString() } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt new file mode 100644 index 0000000000..110c2e121d --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt @@ -0,0 +1,153 @@ +package org.utbot.cli.js + +import api.JsTestGenerator +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.choice +import mu.KotlinLogging +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import service.CoverageMode +import settings.JsDynamicSettings +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.startComment +import settings.JsTestGenerationSettings.defaultTimeout +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +private val logger = KotlinLogging.logger {} + + +class JsGenerateTestsCommand : + CliktCommand(name = "generate_js", help = "Generates tests for the specified class or toplevel functions.") { + + private val sourceCodeFile by option( + "-s", "--source", + help = "Specifies source code file for a generated test." + ) + .required() + .check("Must exist and ends with .js suffix") { + it.endsWith(".js") && Files.exists(Paths.get(it)) + } + + private val targetClass by option("-c", "--class", help = "Specifies target class to generate tests for.") + + private val output by option("-o", "--output", help = "Specifies output file for generated tests.") + .check("Must end with .js suffix") { + it.endsWith(".js") + } + + private val printToStdOut by option( + "-p", + "--print-test", + help = "Specifies whether test should be printed out to StdOut." + ) + .flag(default = false) + + private val timeout by option( + "-t", + "--timeout", + help = "Timeout for Node.js to run scripts in seconds." + ).default("$defaultTimeout") + + private val coverageMode by option( + "--coverage-mode", + help = "Specifies the coverage mode for test generation. Check docs for more info." + ).choice( + CoverageMode.BASIC.toString() to CoverageMode.BASIC, + CoverageMode.FAST.toString() to CoverageMode.FAST + ).default(CoverageMode.FAST) + + private val pathToNode by option( + "--path-to-node", + help = "Sets path to Node.js executable, defaults to \"node\" shortcut." + ).default("node") + + private val pathToNYC by option( + "--path-to-nyc", + help = "Sets path to nyc executable, defaults to \"nyc\" shortcut. " + + "As there are many nyc files in the global npm directory, choose one without file extension." + ).default("nyc") + + private val pathToNPM by option( + "--path-to-npm", + help = "Sets path to npm executable, defaults to \"npm\" shortcut." + ).default("npm") + + override fun run() { + val started = LocalDateTime.now() + try { + val sourceFileAbsolutePath = makeAbsolutePath(sourceCodeFile) + logger.info { "Generating tests for [$sourceFileAbsolutePath] - started" } + val fileText = File(sourceCodeFile).readText() + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + val testGenerator = JsTestGenerator( + fileText = fileText, + sourceFilePath = sourceFileAbsolutePath, + parentClassName = targetClass, + outputFilePath = outputAbsolutePath, + exportsManager = ::manageExports, + settings = JsDynamicSettings( + pathToNode = pathToNode, + pathToNYC = pathToNYC, + pathToNPM = pathToNPM, + timeout = timeout.toLong(), + coverageMode = coverageMode, + + ) + ) + val testCode = testGenerator.run() + + if (printToStdOut || (outputAbsolutePath == null && !printToStdOut)) { + logger.info { "\n$testCode" } + } + outputAbsolutePath?.let { filePath -> + val outputFile = File(filePath) + outputFile.createNewFile() + outputFile.writeText(testCode) + } + + } catch (t: Throwable) { + logger.error { "An error has occurred while generating tests for file $sourceCodeFile : $t" } + throw t + } finally { + val duration = ChronoUnit.MILLIS.between(started, LocalDateTime.now()) + logger.debug { "Generating test for [$sourceCodeFile] - completed in [$duration] (ms)" } + } + } + + private fun manageExports(exports: List) { + val exportSection = exports.joinToString("\n") { "exports.$it = $it" } + val file = File(sourceCodeFile) + val fileText = file.readText() + when { + fileText.contains(exportSection) -> {} + + fileText.contains(startComment) && !fileText.contains(exportSection) -> { + val regex = Regex("$startComment((\\r\\n|\\n|\\r|.)*)$endComment") + regex.find(fileText)?.groups?.get(1)?.value?.let { existingSection -> + val exportRegex = Regex("exports[.](.*) =") + val existingExports = existingSection.split("\n").filter { it.contains(exportRegex) } + val existingExportsSet = existingExports.map { rawLine -> + exportRegex.find(rawLine)?.groups?.get(1)?.value ?: throw IllegalStateException() + }.toSet() + val resultSet = existingExportsSet + exports.toSet() + val resSection = resultSet.joinToString("\n") { "exports.$it = $it" } + val swappedText = fileText.replace(existingSection, "\n$resSection\n") + file.writeText(swappedText) + } + } + + else -> { + val line = buildString { + append("\n$startComment\n") + append(exportSection) + append("\n$endComment") + } + file.appendText(line) + } + } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt new file mode 100644 index 0000000000..d08075bbd2 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt @@ -0,0 +1,60 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.check +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.choice +import mu.KotlinLogging +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import utils.JsCmdExec +import java.io.File + +private val logger = KotlinLogging.logger {} + +class JsRunTestsCommand : CliktCommand(name = "run_js", help = "Runs tests for the specified file or directory.") { + + private val fileWithTests by option( + "--fileOrDir", "-f", + help = "Specifies a file or directory with tests." + ).required() + + private val output by option( + "-o", "--output", + help = "Specifies an output .txt file for test framework result." + ).check("Must end with .txt suffix") { + it.endsWith(".txt") + } + + private val testFramework by option("--test-framework", "-t", help = "Test framework to be used.") + .choice("mocha") + .default("mocha") + + + override fun run() { + val fileWithTestsAbsolutePath = makeAbsolutePath(fileWithTests) + val dir = if (fileWithTestsAbsolutePath.endsWith(".js")) + fileWithTestsAbsolutePath.substringBeforeLast("/") else fileWithTestsAbsolutePath + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + when (testFramework) { + "mocha" -> { + val (textReader, error) = JsCmdExec.runCommand( + dir = dir, + cmd = arrayOf("mocha", fileWithTestsAbsolutePath) + ) + val errorText = error.readText() + if (errorText.isNotEmpty()) { + logger.error { "An error has occurred while running tests for $fileWithTests : $errorText" } + } else { + val text = textReader.readText() + outputAbsolutePath?.let { + val file = File(it) + file.createNewFile() + file.writeText(text) + } ?: logger.info { "\n$text" } + } + } + } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt new file mode 100644 index 0000000000..87b135a11a --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt @@ -0,0 +1,14 @@ +package org.utbot.cli.js + +import java.io.File + +internal object JsUtils { + + fun makeAbsolutePath(path: String): String { + val rawPath = when { + File(path).isAbsolute -> path + else -> System.getProperty("user.dir") + "/" + path + } + return rawPath.replace("\\", "/") + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/resources/log4j2.xml b/utbot-cli-js/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..d0f20b10bc --- /dev/null +++ b/utbot-cli-js/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-js/src/main/resources/version.properties b/utbot-cli-js/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-js/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-cli-python/build.gradle b/utbot-cli-python/build.gradle new file mode 100644 index 0000000000..9fd9d4fe24 --- /dev/null +++ b/utbot-cli-python/build.gradle @@ -0,0 +1,76 @@ +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +tasks.withType(JavaCompile) { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 +} + +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework') + implementation project(':utbot-cli') + implementation project(':utbot-python') + + implementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + // Without this dependency testng tests do not run. + implementation group: 'com.beust', name: 'jcommander', version: '1.48' + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: cliktVersion + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion + //noinspection GroovyAssignabilityCheck + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration:'instrumentationArchive') +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + manifest { + attributes 'Main-Class': 'org.utbot.cli.language.python.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.language.python' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot Python CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-python/src/README.md b/utbot-cli-python/src/README.md new file mode 100644 index 0000000000..bdc5c5e0b0 --- /dev/null +++ b/utbot-cli-python/src/README.md @@ -0,0 +1,101 @@ +## Build + +.jar file can be built in Github Actions with script `publish-plugin-and-cli-from-branch`. + +## Requirements + + - Required Java version: 11. + + - Prefered Python version: 3.8 or 3.9. + + 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 + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_python dir/file_with_sources.py -p -o generated_tests.py -s dir + +This will generate tests for top-level functions from `file_with_sources.py`. + +Run generated tests: + + java -jar utbot-cli.jar run_python generated_tests.py -p + +### `generate_python` options + +- `-s, --sys-path ,` + + (required) Directories to add to `sys.path`. One of directories must contain the file with the methods under test. + + `sys.path` is a list of strings that specifies the search path for modules. It must include paths for all user modules that are used in imports. + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `-o, --output ` + + (required) File for generated tests. + +- `--coverage ` + + File to write coverage report. + +- `-c, --class ` + + Specify top-level (ordinary, not nested) class under test. Without this option tests will be generated for top-level functions. + +- `-m, --methods ,` + + Specify methods under test. + +- `--install-requirements` + + Install Python requirements if missing. + +- `--do-not-minimize` + + Turn off minimization of the number of generated tests. + +- `--do-not-check-requirements` + + 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). + +- `--timeout-for-run INT` + + Specify the maximum time in milliseconds to spend on one function run (2000 by default). + +- `--test-framework [pytest|Unittest]` + + Test framework to be used. + +### `run_python` options + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `--test-framework [pytest|Unittest]` + + Test framework of tests to run. + +- `-o, --output ` + + Specify file for report. + +## Problems + +- Unittest can not run tests from parent directories diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt new file mode 100644 index 0000000000..4b486ae560 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt @@ -0,0 +1,34 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import org.slf4j.event.Level +import org.utbot.cli.getVersion +import org.utbot.cli.setVerbosity +import kotlin.system.exitProcess + +class UtBotPythonCli : CliktCommand(name = "UnitTestBot Python Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotPythonCli().subcommands( + PythonGenerateTestsCommand(), + PythonRunTestsCommand() + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Optional.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Optional.kt new file mode 100644 index 0000000000..3f66a46172 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Optional.kt @@ -0,0 +1,25 @@ +package org.utbot.cli.language.python + +sealed class Optional +class Fail(val message: String) : Optional() +class Success(val value: A) : Optional() + +fun bind( + value: Optional, + f: (A) -> Optional +): Optional = + when (value) { + is Fail -> Fail(value.message) + is Success -> f(value.value) + } + +fun pack(vararg values: Optional): Optional> { + val result = mutableListOf() + for (elem in values) { + when (elem) { + is Fail -> return Fail(elem.message) + is Success -> result.add(elem.value) + } + } + return Success(result) +} \ 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 new file mode 100644 index 0000000000..23495af577 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt @@ -0,0 +1,273 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.choice +import com.github.ajalt.clikt.parameters.types.long +import mu.KotlinLogging +import org.utbot.framework.codegen.TestFramework +import org.utbot.python.PythonMethod +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestGenerationProcessor.processTestGeneration +import org.utbot.python.code.PythonClass +import org.utbot.python.code.PythonCode +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.utils.RequirementsUtils.installRequirements +import org.utbot.python.utils.RequirementsUtils.requirements +import org.utbot.python.utils.getModuleName +import java.io.File +import java.nio.file.Paths + +private const val DEFAULT_TIMEOUT_IN_MILLIS = 60000L +private const val DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS = 2000L + +private val logger = KotlinLogging.logger {} + +class PythonGenerateTestsCommand : CliktCommand( + name = "generate_python", + help = "Generate tests for a specified Python class or top-level functions from a specified file." +) { + private val sourceFile by argument( + help = "File with Python code to generate tests for." + ) + + private val pythonClass by option( + "-c", "--class", + help = "Specify top-level (ordinary, not nested) class under test. " + + "Without this option tests will be generated for top-level functions." + ) + + private val methods by option( + "-m", "--methods", + help = "Specify methods under test." + ).split(",") + + private val directoriesForSysPath by option( + "-s", "--sys-path", + help = "(required) Directories to add to sys.path. " + + "One of directories must contain the file with the methods under test." + ).split(",").required() + + private val pythonPath by option( + "-p", "--python-path", + help = "(required) Path to Python interpreter." + ).required() + + private val output by option( + "-o", "--output", + help = "(required) File for generated tests." + ).required() + + private val coverageOutput by option( + "--coverage", + help = "File to write coverage report." + ) + + private val installRequirementsIfMissing by option( + "--install-requirements", + help = "Install Python requirements if missing." + ).flag(default = false) + + private val doNotMinimize by option( + "--do-not-minimize", + help = "Turn off minimization of the number of generated tests." + ).flag(default = false) + + private val doNotCheckRequirements by option( + "--do-not-check-requirements", + help = "Turn off Python requirements check (to speed up)." + ).flag(default = false) + + private val visitOnlySpecifiedSource by option( + "--visit-only-specified-source", + help = "Do not search for classes and imported modules in other Python files from sys.path." + ).flag(default = false) + + private val timeout by option( + "-t", "--timeout", + help = "Specify the maximum time in milliseconds to spend on generating tests ($DEFAULT_TIMEOUT_IN_MILLIS by default)." + ).long().default(DEFAULT_TIMEOUT_IN_MILLIS) + + private val timeoutForRun by option( + "--timeout-for-run", + help = "Specify the maximum time in milliseconds to spend on one function run ($DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS by default)." + ).long().default(DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS) + + private val testFrameworkAsString by option("--test-framework", help = "Test framework to be used.") + .choice(Pytest.toString(), Unittest.toString()) + .default(Unittest.toString()) + + private val testFramework: TestFramework + get() = + when (testFrameworkAsString) { + Unittest.toString() -> Unittest + Pytest.toString() -> Pytest + else -> error("Not reachable") + } + + private fun findCurrentPythonModule(): Optional { + directoriesForSysPath.forEach { path -> + val module = getModuleName(path.toAbsolutePath(), sourceFile.toAbsolutePath()) + if (module != null) + return Success(module) + } + return Fail("Couldn't find path for $sourceFile in --sys-path option. Please, specify it.") + } + + private val forbiddenMethods = listOf("__init__", "__new__") + + private fun getClassMethods(pythonClassFromSources: PythonClass): List = + pythonClassFromSources.methods.filter { method -> method.name !in forbiddenMethods } + + private fun getPythonMethods(sourceCodeContent: String, currentModule: String): Optional> { + val code = PythonCode.getFromString( + sourceCodeContent, + sourceFile.toAbsolutePath(), + pythonModule = currentModule + ) + ?: return Fail("Couldn't parse source file. Maybe it contains syntax error?") + + val topLevelFunctions = code.getToplevelFunctions() + val topLevelClasses = code.getToplevelClasses() + val selectedMethods = methods + if (pythonClass == null && methods == null) { + return if (topLevelFunctions.isNotEmpty()) + Success(topLevelFunctions) + else { + val topLevelClassMethods = topLevelClasses.flatMap { getClassMethods(it) } + if (topLevelClassMethods.isNotEmpty()) { + Success(topLevelClassMethods) + } else + Fail("No top-level functions and top-level classes in the source file to test.") + } + } else if (pythonClass == null && selectedMethods != null) { + val pythonMethodsOpt = selectedMethods.map { functionName -> + topLevelFunctions + .find { it.name == functionName } + ?.let { Success(it) } + ?: Fail("Couldn't find top-level function $functionName in the source file.") + } + return pack(*pythonMethodsOpt.toTypedArray()) + } + + val pythonClassFromSources = code.getToplevelClasses().find { it.name == pythonClass } + ?.let { Success(it) } + ?: Fail("Couldn't find class $pythonClass in the source file.") + + val methods = bind(pythonClassFromSources) { + val fineMethods: List = it.methods.filter { method -> method.name !in forbiddenMethods } + if (fineMethods.isNotEmpty()) + Success(fineMethods) + else + Fail("No methods in definition of class $pythonClass to test.") + } + + if (selectedMethods == null) + return methods + + return bind(methods) { classFineMethods -> + pack( + *(selectedMethods.map { methodName -> + classFineMethods.find { it.name == methodName }?.let { Success(it) } + ?: Fail("Couldn't find method $methodName of class $pythonClass") + }).toTypedArray() + ) + } + } + + private lateinit var currentPythonModule: String + private lateinit var pythonMethods: List + private lateinit var sourceFileContent: String + + @Suppress("UNCHECKED_CAST") + private fun calculateValues(): Optional { + val currentPythonModuleOpt = findCurrentPythonModule() + sourceFileContent = File(sourceFile).readText() + val pythonMethodsOpt = bind(currentPythonModuleOpt) { getPythonMethods(sourceFileContent, it) } + + return bind(pack(currentPythonModuleOpt, pythonMethodsOpt)) { + currentPythonModule = it[0] as String + pythonMethods = it[1] as List + Success(Unit) + } + } + + private fun processMissingRequirements(): PythonTestGenerationProcessor.MissingRequirementsActionResult { + if (installRequirementsIfMissing) { + logger.info("Installing requirements...") + val result = installRequirements(pythonPath) + if (result.exitValue == 0) + return PythonTestGenerationProcessor.MissingRequirementsActionResult.INSTALLED + System.err.println(result.stderr) + logger.error("Failed to install requirements.") + } else { + logger.error("Missing some requirements. Please add --install-requirements flag or install them manually.") + } + logger.info("Requirements: ${requirements.joinToString()}") + return PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED + } + + private fun writeToFileAndSave(filename: String, fileContent: String) { + val file = File(filename) + file.parentFile?.mkdirs() + file.writeText(fileContent) + file.createNewFile() + } + + + override fun run() { + val status = calculateValues() + if (status is Fail) { + logger.error(status.message) + return + } + + processTestGeneration( + pythonPath = pythonPath, + pythonFilePath = sourceFile.toAbsolutePath(), + pythonFileContent = sourceFileContent, + directoriesForSysPath = directoriesForSysPath.map { it.toAbsolutePath() }.toSet(), + currentPythonModule = currentPythonModule, + pythonMethods = pythonMethods, + containingClassName = pythonClass, + timeout = timeout, + testFramework = testFramework, + timeoutForRun = timeoutForRun, + withMinimization = !doNotMinimize, + doNotCheckRequirements = doNotCheckRequirements, + visitOnlySpecifiedSource = visitOnlySpecifiedSource, + writeTestTextToFile = { generatedCode -> + writeToFileAndSave(output, generatedCode) + }, + checkingRequirementsAction = { + logger.info("Checking 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()}" + ) + }, + processMypyWarnings = { messages -> messages.forEach { println(it) } }, + finishedAction = { + logger.info("Finished test generation for the following functions: ${it.joinToString()}") + }, + processCoverageInfo = { coverageReport -> + val output = coverageOutput ?: return@processTestGeneration + writeToFileAndSave(output, coverageReport) + }, + pythonRunRoot = Paths.get("").toAbsolutePath() + ) + } + + private fun String.toAbsolutePath(): String = + File(this).canonicalPath +} diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt new file mode 100644 index 0000000000..ffa9d08462 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt @@ -0,0 +1,82 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.choice +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.utils.CmdResult +import org.utbot.python.utils.runCommand +import java.io.File +import java.nio.file.Paths + +class PythonRunTestsCommand : CliktCommand(name = "run_python", help = "Run tests in the specified file") { + + private val sourceFile by argument( + help = "File with Python tests to run." + ) + + private val pythonPath by option( + "-p", "--python-path", + help = "Path to Python interpreter." + ).required() + + private val output by option( + "-o", "--output", + help = "Specify file for report." + ) + + private val testFrameworkAsString by option("--test-framework", help = "Test framework of tests to run") + .choice(Pytest.toString(), Unittest.toString()) + .default(Unittest.toString()) + + private fun runUnittest(): CmdResult { + val currentPath = Paths.get("").toAbsolutePath().toString() + val sourceFilePath = Paths.get(sourceFile).toAbsolutePath().toString() + return if (sourceFilePath.startsWith(currentPath)) { + runCommand( + listOf( + pythonPath, + "-m", + "unittest", + sourceFile + ) + ) + } else CmdResult( + "", + "File $sourceFile can not be imported from Unittest. Move test file to child directory or use pytest.", + 1 + ) + } + + private fun runPytest(): CmdResult = + runCommand( + listOf( + pythonPath, + "-m", + "pytest", + sourceFile + ) + ) + + override fun run() { + val result = + when (testFrameworkAsString) { + Unittest.toString() -> runUnittest() + Pytest.toString() -> runPytest() + else -> error("Not reachable") + } + + output?.let { + val file = File(it) + file.writeText(result.stderr + result.stdout) + file.parentFile?.mkdirs() + file.createNewFile() + } + println(result.stderr) + println(result.stdout) + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/resources/log4j2.xml b/utbot-cli-python/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..3d6ee82bcf --- /dev/null +++ b/utbot-cli-python/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-python/src/main/resources/version.properties b/utbot-cli-python/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-python/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt index a4d6b59f8d..c6d03794be 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt @@ -249,7 +249,7 @@ data class UtError( * * UtNullModel represents nulls, other models represent not-nullable entities. */ -sealed class UtModel( +open class UtModel( open val classId: ClassId ) diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 6ab14e5aac..f40f5848a7 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -24,9 +24,11 @@ data class Instruction( * * @param coveredInstructions a list of the covered instructions in the order they are visited. * @param instructionsCount a number of all instructions in the current class. + * @param missedInstructions a list of the missed instructions. * */ data class Coverage( val coveredInstructions: List = emptyList(), - val instructionsCount: Long? = null + val instructionsCount: Long? = null, + val missedInstructions: List = emptyList(), ) \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt index ff436d10ff..d044d0111e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt @@ -193,6 +193,8 @@ class ValueConstructor { is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model)) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) + // Python, JavaScript are supposed to be here as well + else -> throw UnsupportedOperationException("UtModel $model cannot construct UtConcreteValue") } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt index 0e24ba829f..dc9fc6b1ad 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt @@ -192,6 +192,8 @@ class AssembleModelGenerator(private val basePackageName: String) { is UtArrayModel -> assembleArrayModel(utModel) is UtCompositeModel -> assembleCompositeModel(utModel) is UtAssembleModel -> assembleAssembleModel(utModel) + // Python, JavaScript are supposed to be here as well + else -> utModel } } catch (e: AssembleException) { utModel diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt index 43c85062c4..b3c1889a08 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt @@ -35,7 +35,7 @@ import org.utbot.framework.plugin.api.util.voidWrapperClassId data class TestClassFile(val packageName: String, val imports: List, val testClass: String) -sealed class Import(internal val order: Int) : Comparable { +abstract class Import(val order: Int) : Comparable { abstract val qualifiedName: String override fun compareTo(other: Import) = importComparator.compare(this, other) @@ -169,7 +169,7 @@ object MockitoStaticMocking : StaticsMocking(id = "Mockito static mocking", disp ) } -sealed class TestFramework( +abstract class TestFramework( override val id: String, override val displayName: String, override val description: String = "Use $displayName as test framework", @@ -191,7 +191,9 @@ sealed class TestFramework( abstract val nestedClassesShouldBeStatic: Boolean abstract val argListClassId: ClassId - val assertEquals by lazy { assertionId("assertEquals", objectClassId, objectClassId) } + open val testSuperClass: ClassId? = null + + open val assertEquals by lazy { assertionId("assertEquals", objectClassId, objectClassId) } val assertFloatEquals by lazy { assertionId("assertEquals", floatClassId, floatClassId, floatClassId) } @@ -225,10 +227,10 @@ sealed class TestFramework( val assertNotEquals by lazy { assertionId("assertNotEquals", objectClassId, objectClassId) } - protected fun assertionId(name: String, vararg params: ClassId): MethodId = + protected open fun assertionId(name: String, vararg params: ClassId): MethodId = builtinStaticMethodId(assertionsClass, name, voidClassId, *params) private fun arrayAssertionId(name: String, vararg params: ClassId): MethodId = - builtinStaticMethodId(arraysAssertionsClass, name, voidClassId, *params) + builtinStaticMethodId(arraysAssertionsClass, name, voidClassId, *params) abstract fun getRunTestsCommand( executionInvoke: String, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt index b4f248ddef..693c305156 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt @@ -1,6 +1,6 @@ package org.utbot.framework.codegen -import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.CgLanguageAssistant private val javaKeywords = setOf( "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", @@ -29,13 +29,8 @@ private val kotlinModifierKeywords = setOf( "private", "protected", "public", "reified", "sealed", "suspend", "tailrec", "vararg" ) -// For now we check only hard keywords because others can be used as methods and variables identifiers +// For now, we check only hard keywords because others can be used as methods and variables identifiers private val kotlinKeywords = kotlinHardKeywords -private fun getLanguageKeywords(codegenLanguage: CodegenLanguage): Set = when(codegenLanguage) { - CodegenLanguage.JAVA -> javaKeywords - CodegenLanguage.KOTLIN -> kotlinKeywords -} - -fun isLanguageKeyword(word: String, codegenLanguage: CodegenLanguage): Boolean = - word in getLanguageKeywords(codegenLanguage) +fun isLanguageKeyword(word: String, codegenLanguageAssistant: CgLanguageAssistant): Boolean = + word in codegenLanguageAssistant.languageKeywords diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt index 850863e4b9..0fe271fead 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt @@ -17,6 +17,7 @@ import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport import org.utbot.framework.codegen.model.tree.CgClassFile import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CgLanguageAssistant import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.MockFramework @@ -26,8 +27,8 @@ import org.utbot.framework.codegen.model.tree.CgDocRegularStmt import org.utbot.framework.codegen.model.tree.CgDocumentationComment import java.util.* -class CodeGenerator( - private val classUnderTest: ClassId, +open class CodeGenerator( + val classUnderTest: ClassId, paramNames: MutableMap> = mutableMapOf(), generateUtilClassFile: Boolean = false, testFramework: TestFramework = TestFramework.defaultItem, @@ -43,13 +44,14 @@ class CodeGenerator( testClassPackageName: String = classUnderTest.packageName, ) { - private var context: CgContext = CgContext( + open var context: CgContext = CgContext( classUnderTest = classUnderTest, generateUtilClassFile = generateUtilClassFile, paramNames = paramNames, testFramework = testFramework, mockFramework = mockFramework, codegenLanguage = codegenLanguage, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage), parametrizedTestSource = parameterizedTestSource, staticsMocking = staticsMocking, forceStaticMocking = forceStaticMocking, @@ -93,7 +95,7 @@ class CodeGenerator( * - turns on imports optimization in code generator * - passes a custom test class name if there is one */ - private fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { + fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { val prevContext = context return try { context = prevContext.copy( @@ -106,7 +108,7 @@ class CodeGenerator( } } - private fun renderClassFile(file: CgClassFile): String { + fun renderClassFile(file: CgClassFile): String { val renderer = CgAbstractRenderer.makeRenderer(context) file.accept(renderer) return renderer.toString() @@ -115,14 +117,14 @@ class CodeGenerator( /** * @property generatedCode the source code of the test class - * @property utilClassKind the kind of util class if it is required, otherwise - null * @property testsGenerationReport some info about test generation process + * @property utilClassKind the kind of util class if it is required, otherwise - null */ data class CodeGeneratorResult( val generatedCode: String, + val testsGenerationReport: TestsGenerationReport, // null if no util class needed, e.g. when we are generating utils directly into test class - val utilClassKind: UtilClassKind?, - val testsGenerationReport: TestsGenerationReport + val utilClassKind: UtilClassKind? = null, ) /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt index e241083e8c..435e85b5e4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt @@ -14,7 +14,7 @@ import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.fuzzer.UtFuzzedExecution import soot.jimple.JimpleBody -data class CgMethodTestSet private constructor( +data class CgMethodTestSet constructor( val executableId: ExecutableId, val jimpleBody: JimpleBody? = null, val errors: Map = emptyMap(), @@ -31,6 +31,34 @@ data class CgMethodTestSet private constructor( ) { executions = from.executions } + /** + * For JavaScript purposes. + */ + constructor( + executableId: ExecutableId, + execs: List = emptyList(), + errors: Map = emptyMap() + ) : this( + executableId, + null, + errors, + listOf(null to execs.indices) + ) { + executions = execs + } + + constructor( + executableId: ExecutableId, + executions: List = emptyList(), + ) : this( + executableId, + null, + emptyMap(), + + listOf(null to executions.indices) + ) { + this.executions = executions + } fun prepareTestSetsForParameterizedTestGeneration(): List { val testSetList = mutableListOf() diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt index 5cfaf2925c..86ee8e9b28 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt @@ -10,7 +10,7 @@ import org.utbot.framework.codegen.model.tree.CgClass * This class stores context information needed to build [CgClass]. * Should only be used in [CgContextOwner]. */ -internal data class TestClassContext( +data class TestClassContext( // set of interfaces that the test class must inherit val collectedTestClassInterfaces: MutableSet = mutableSetOf(), diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt index 1c07a0df13..74e1850dbd 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt @@ -34,7 +34,7 @@ import java.lang.reflect.Method * The class may actually not have some of these methods if they * are not required in the process of code generation (this is the case for [TestClassUtilMethodProvider]). */ -internal abstract class UtilMethodProvider(val utilClassId: ClassId) { +abstract class UtilMethodProvider(val utilClassId: ClassId) { val utilMethodIds: Set get() = setOf( getUnsafeInstanceMethodId, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt index d37de756c0..ce461e3014 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt @@ -34,6 +34,7 @@ import org.utbot.framework.codegen.model.constructor.TestClassModel import org.utbot.framework.codegen.model.tree.CgParameterKind import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CgLanguageAssistant import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.FieldId @@ -62,7 +63,7 @@ import org.utbot.framework.plugin.api.util.jClass * * @see [CgContextOwner.withNameScope] */ -internal interface CgContextOwner { +interface CgContextOwner { // current class under test val classUnderTest: ClassId @@ -131,6 +132,8 @@ internal interface CgContextOwner { val codegenLanguage: CodegenLanguage + val cgLanguageAssistant: CgLanguageAssistant + val parametrizedTestSource: ParametrizedTestSource /** @@ -427,9 +430,9 @@ internal interface CgContextOwner { /** * Context with current code generation info */ -internal data class CgContext( +data class CgContext( override val classUnderTest: ClassId, - val generateUtilClassFile: Boolean, + val generateUtilClassFile: Boolean = false, override var currentExecutable: ExecutableId? = null, override val collectedExceptions: MutableSet = mutableSetOf(), override val collectedMethodAnnotations: MutableSet = mutableSetOf(), @@ -448,6 +451,7 @@ internal data class CgContext( override val forceStaticMocking: ForceStaticMocking, override val generateWarningsForStaticMocking: Boolean, override val codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, + override val cgLanguageAssistant: CgLanguageAssistant, override val parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.DO_NOT_PARAMETRIZE, override var mockFrameworkUsed: Boolean = false, override var currentBlock: PersistentList = persistentListOf(), @@ -478,7 +482,7 @@ internal data class CgContext( override val outerMostTestClassContext: TestClassContext get() = _outerMostTestClassContext ?: error("Accessing outerMostTestClassInfo out of class file scope") - private var _outerMostTestClassContext: TestClassContext? = null + private var _outerMostTestClassContext: TestClassContext? = cgLanguageAssistant.outerMostTestClassContent /** * This property cannot be accessed outside of test class scope @@ -490,9 +494,9 @@ internal data class CgContext( private var _currentTestClassContext: TestClassContext? = null override val outerMostTestClass: ClassId by lazy { - val packagePrefix = if (testClassPackageName.isNotEmpty()) "$testClassPackageName." else "" - val simpleName = testClassCustomName ?: "${classUnderTest.simpleName}Test" - val name = "$packagePrefix$simpleName" + val (name, simpleName) = cgLanguageAssistant.testClassName( + testClassCustomName, testClassPackageName, classUnderTest + ) BuiltinClassId( canonicalName = name, simpleName = simpleName, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt index 50254afc03..107b664386 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt @@ -14,7 +14,7 @@ import org.utbot.framework.plugin.api.util.isArray /** * Interface for method and variable name generators */ -internal interface CgNameGenerator { +interface CgNameGenerator { /** * Generate a variable name given a [base] name. * @param isMock denotes whether a variable represents a mock object or not @@ -67,7 +67,7 @@ internal interface CgNameGenerator { * Class that generates names for methods and variables * To avoid name collisions it uses existing names information from CgContext */ -internal class CgNameGeneratorImpl(private val context: CgContext) +class CgNameGeneratorImpl(val context: CgContext) : CgNameGenerator, CgContextOwner by context { override fun variableName(base: String, isMock: Boolean, isStatic: Boolean): String { @@ -78,7 +78,7 @@ internal class CgNameGeneratorImpl(private val context: CgContext) } return when { baseName in existingVariableNames -> nextIndexedVarName(baseName) - isLanguageKeyword(baseName, codegenLanguage) -> createNameFromKeyword(baseName) + isLanguageKeyword(baseName, context.cgLanguageAssistant) -> createNameFromKeyword(baseName) else -> baseName }.also { existingVariableNames = existingVariableNames.add(it) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt index 1f0f70ff40..447fcd7356 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt @@ -33,7 +33,7 @@ import org.utbot.framework.util.hasThisInstance import org.utbot.fuzzer.UtFuzzedExecution import java.lang.reflect.Array -internal interface CgFieldStateManager { +interface CgFieldStateManager { fun rememberInitialEnvironmentState(info: StateModificationInfo) fun rememberFinalEnvironmentState(info: StateModificationInfo) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt index 191876b223..10e83236ca 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt @@ -165,14 +165,14 @@ import org.utbot.framework.plugin.api.util.isStatic private const val DEEP_EQUALS_MAX_DEPTH = 5 // TODO move it to plugin settings? -internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by context, +open class CgMethodConstructor(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by getCallableAccessManagerBy(context), CgStatementConstructor by getStatementConstructorBy(context) { - private val nameGenerator = getNameGeneratorBy(context) - private val testFrameworkManager = getTestFrameworkManagerBy(context) + protected val nameGenerator = getNameGeneratorBy(context) + protected val testFrameworkManager = getTestFrameworkManagerBy(context) - private val variableConstructor = getVariableConstructorBy(context) + protected val variableConstructor = getVariableConstructorBy(context) private val mockFrameworkManager = getMockFrameworkManagerBy(context) private val floatDelta: Float = 1e-6f @@ -180,9 +180,9 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c // a model for execution result (it is lateinit because execution can fail, // and we need it only on assertions generation stage - private lateinit var resultModel: UtModel + lateinit var resultModel: UtModel - private lateinit var methodType: CgTestMethodType + lateinit var methodType: CgTestMethodType private val fieldsOfExecutionResults = mutableMapOf, MutableList>() @@ -192,7 +192,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c */ private var containsStreamConsumingFailureForParametrizedTests: Boolean = false - private fun setupInstrumentation() { + protected fun setupInstrumentation() { if (currentExecution is UtSymbolicExecution) { val execution = currentExecution as UtSymbolicExecution val instrumentation = execution.instrumentation @@ -237,7 +237,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c * Thus, this method only caches an actual initial static fields state in order to recover it * at the end of the test, and it has nothing to do with the 'before' and 'after' caches. */ - private fun rememberInitialStaticFields(statics: Map) { + protected fun rememberInitialStaticFields(statics: Map) { val accessibleStaticFields = statics.accessibleFields() for ((field, _) in accessibleStaticFields) { val declaringClass = field.declaringClass @@ -262,7 +262,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - private fun substituteStaticFields(statics: Map, isParametrized: Boolean = false) { + protected fun substituteStaticFields(statics: Map, isParametrized: Boolean = false) { val accessibleStaticFields = statics.accessibleFields() for ((field, model) in accessibleStaticFields) { val declaringClass = field.declaringClass @@ -285,7 +285,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - private fun recoverStaticFields() { + protected fun recoverStaticFields() { for ((field, prevValue) in prevStaticFieldValues.accessibleFields()) { if (field.canBeSetFrom(context)) { field.declaringClass[field] `=` prevValue @@ -301,7 +301,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c /** * Generates result assertions for unit tests. */ - private fun generateResultAssertions() { + protected open fun generateResultAssertions() { when (currentExecutable) { is ConstructorId -> generateConstructorCall(currentExecutable!!, currentExecution!!) is BuiltinMethodId -> error("Unexpected BuiltinMethodId $currentExecutable while generating result assertions") @@ -409,7 +409,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return { +actual[streamConsumingMethodId]() } } - private fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { + protected fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { if (exception is AccessControlException) return false // tests with timeout or crash should be processed differently if (exception is TimeoutException || exception is ConcreteExecutionFailureException) return false @@ -420,11 +420,11 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return exceptionRequiresAssert || exceptionIsExplicit } - private fun shouldTestPassWithTimeoutException(execution: UtExecution, exception: Throwable): Boolean { + protected fun shouldTestPassWithTimeoutException(execution: UtExecution, exception: Throwable): Boolean { return execution.result is UtTimeoutException || exception is TimeoutException } - private fun writeWarningAboutTimeoutExceeding() { + protected fun writeWarningAboutTimeoutExceeding() { +CgMultilineComment( listOf( "This execution may take longer than the ${hangingTestsTimeout.timeoutMs} ms timeout", @@ -433,7 +433,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c ) } - private fun writeWarningAboutFailureTest(exception: Throwable) { + protected fun writeWarningAboutFailureTest(exception: Throwable) { require(currentExecutable is ExecutableId) val executableName = "${currentExecutable!!.classId.name}.${currentExecutable!!.name}" @@ -460,7 +460,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return this.replace("\b", "\\b").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r").replace("\\u","\\\\u") } - private fun writeWarningAboutCrash() { + protected fun writeWarningAboutCrash() { +CgSingleLineComment("This invocation possibly crashes JVM") } @@ -511,7 +511,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c * * Note: not supported in parameterized tests. */ - private fun generateFieldStateAssertions() { + protected fun generateFieldStateAssertions() { val thisInstanceCache = statesCache.thisInstance for (path in thisInstanceCache.paths) { assertStatesByPath(thisInstanceCache, path) @@ -763,6 +763,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c // Unit result is considered in generateResultAssertions method error("Unexpected UtVoidModel in deep equals") } + else -> {} } } } @@ -1026,6 +1027,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c is UtVoidModel -> { // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse } + else -> {} } } } @@ -1067,6 +1069,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c is UtVoidModel -> { // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse } + else -> {} } } @@ -1138,7 +1141,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return ClassIdArrayInfo(classId, nestedElementClassId, dimensions) } - private fun assertEquality(expected: CgValue, actual: CgVariable) { + open fun assertEquality(expected: CgValue, actual: CgVariable) { when { expected.type.isArray -> { // TODO: How to compare arrays of Float and Double wrappers? @@ -1303,7 +1306,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c ) } - private fun recordActualResult() { + protected fun recordActualResult() { val executionResult = currentExecution!!.result executionResult.onSuccess { resultModel -> @@ -1357,7 +1360,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = + open fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = withTestMethodScope(execution) { val testMethodName = nameGenerator.testMethodNameFor(executableId, execution.testMethodName) if (execution.testMethodName == null) { @@ -1697,7 +1700,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return arguments } - private fun withTestMethodScope(execution: UtExecution, block: () -> R): R { + protected fun withTestMethodScope(execution: UtExecution, block: () -> R): R { clearTestMethodScope() currentExecution = execution determineExecutionType() @@ -1788,7 +1791,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - private fun testMethod( + protected fun testMethod( methodName: String, displayName: String?, params: List = emptyList(), @@ -1938,7 +1941,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c * in order to wrap these calls in a try-catch block that will handle [InvocationTargetException] * that may be thrown by these calls. */ - private fun CgExecutableCall.intercepted() { + protected fun CgExecutableCall.intercepted() { val executableToWrap = when (executableId) { is MethodId -> invoke is ConstructorId -> newInstance diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt index 3925c14d80..85d1b16168 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt @@ -1,24 +1,19 @@ package org.utbot.framework.codegen.model.constructor.tree import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.Junit4 -import org.utbot.framework.codegen.Junit5 import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.TestNg import org.utbot.framework.codegen.model.constructor.CgMethodTestSet import org.utbot.framework.codegen.model.constructor.TestClassModel import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.name.CgNameGenerator -import org.utbot.framework.codegen.model.constructor.name.CgNameGeneratorImpl import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.clearContextRelatedStorage import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getMethodConstructorBy import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getTestFrameworkManagerBy import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructorImpl import org.utbot.framework.codegen.model.tree.CgAuxiliaryClass import org.utbot.framework.codegen.model.tree.CgMethodsCluster import org.utbot.framework.codegen.model.tree.CgMethod @@ -46,7 +41,7 @@ import org.utbot.framework.plugin.api.util.description import org.utbot.framework.plugin.api.util.humanReadableName import org.utbot.fuzzer.UtFuzzedExecution -internal class CgTestClassConstructor(val context: CgContext) : +open class CgTestClassConstructor(val context: CgContext) : CgContextOwner by context, CgStatementConstructor by getStatementConstructorBy(context) { @@ -58,12 +53,12 @@ internal class CgTestClassConstructor(val context: CgContext) : private val nameGenerator = getNameGeneratorBy(context) private val testFrameworkManager = getTestFrameworkManagerBy(context) - private val testsGenerationReport: TestsGenerationReport = TestsGenerationReport() + protected val testsGenerationReport: TestsGenerationReport = TestsGenerationReport() /** * Given a testClass model constructs CgTestClass */ - fun construct(testClassModel: TestClassModel): CgTestClassFile { + open fun construct(testClassModel: TestClassModel): CgTestClassFile { return buildTestClassFile { this.declaredClass = withTestClassScope { constructTestClass(testClassModel) } imports += context.collectedImports @@ -71,7 +66,7 @@ internal class CgTestClassConstructor(val context: CgContext) : } } - private fun constructTestClass(testClassModel: TestClassModel): CgClass { + open fun constructTestClass(testClassModel: TestClassModel): CgClass { return buildClass { id = currentTestClass @@ -132,7 +127,7 @@ internal class CgTestClassConstructor(val context: CgContext) : } } - private fun constructTestSet(testSet: CgMethodTestSet): List>? { + fun constructTestSet(testSet: CgMethodTestSet): List>? { if (testSet.executions.isEmpty()) { return null } @@ -328,10 +323,10 @@ internal class CgTestClassConstructor(val context: CgContext) : /** * Engine errors + codegen errors for a given [UtMethodTestSet] */ - private val CgMethodTestSet.allErrors: Map + protected val CgMethodTestSet.allErrors: Map get() = errors + codeGenerationErrors.getOrDefault(this, mapOf()) - internal object CgComponents { + object CgComponents { /** * Clears all stored data for current [CgContext]. * As far as context is created per class under test, @@ -356,19 +351,26 @@ internal class CgTestClassConstructor(val context: CgContext) : private val variableConstructors: MutableMap = mutableMapOf() private val methodConstructors: MutableMap = mutableMapOf() - fun getNameGeneratorBy(context: CgContext) = nameGenerators.getOrPut(context) { CgNameGeneratorImpl(context) } - fun getCallableAccessManagerBy(context: CgContext) = callableAccessManagers.getOrPut(context) { CgCallableAccessManagerImpl(context) } - fun getStatementConstructorBy(context: CgContext) = statementConstructors.getOrPut(context) { CgStatementConstructorImpl(context) } - - fun getTestFrameworkManagerBy(context: CgContext) = when (context.testFramework) { - is Junit4 -> testFrameworkManagers.getOrPut(context) { Junit4Manager(context) } - is Junit5 -> testFrameworkManagers.getOrPut(context) { Junit5Manager(context) } - is TestNg -> testFrameworkManagers.getOrPut(context) { TestNgManager(context) } + fun getNameGeneratorBy(context: CgContext) = nameGenerators.getOrPut(context) { + context.cgLanguageAssistant.getNameGeneratorBy(context) + } + fun getCallableAccessManagerBy(context: CgContext) = callableAccessManagers.getOrPut(context) { + context.cgLanguageAssistant.getCallableAccessManagerBy(context) + } + fun getStatementConstructorBy(context: CgContext) = statementConstructors.getOrPut(context) { + context.cgLanguageAssistant.getStatementConstructorBy(context) } + fun getTestFrameworkManagerBy(context: CgContext) = + testFrameworkManagers.getOrDefault(context, context.cgLanguageAssistant.getLanguageTestFrameworkManager().managerByFramework(context)) + fun getMockFrameworkManagerBy(context: CgContext) = mockFrameworkManagers.getOrPut(context) { MockFrameworkManager(context) } - fun getVariableConstructorBy(context: CgContext) = variableConstructors.getOrPut(context) { CgVariableConstructor(context) } - fun getMethodConstructorBy(context: CgContext) = methodConstructors.getOrPut(context) { CgMethodConstructor(context) } + fun getVariableConstructorBy(context: CgContext) = variableConstructors.getOrPut(context) { + context.cgLanguageAssistant.getVariableConstructorBy(context) + } + fun getMethodConstructorBy(context: CgContext) = methodConstructors.getOrPut(context) { + context.cgLanguageAssistant.getMethodConstructorBy(context) + } } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt index 6f13d036ba..56cd08a28b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt @@ -74,7 +74,7 @@ import org.utbot.framework.plugin.api.util.wrapperByPrimitive * Constructs CgValue or CgVariable given a UtModel */ @Suppress("unused") -internal class CgVariableConstructor(val context: CgContext) : +open class CgVariableConstructor(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by getCallableAccessManagerBy(context), CgStatementConstructor by getStatementConstructorBy(context) { @@ -105,7 +105,7 @@ internal class CgVariableConstructor(val context: CgContext) : * We use [valueByModelId] for [UtReferenceModel] by id to not create new variable in case state before * was not transformed. */ - fun getOrCreateVariable(model: UtModel, name: String? = null): CgValue { + open fun getOrCreateVariable(model: UtModel, name: String? = null): CgValue { // name could be taken from existing names, or be specified manually, or be created from generator val baseName = name ?: nameGenerator.nameFrom(model.classId) return if (model is UtReferenceModel) valueByModelId.getOrPut(model.id) { @@ -123,6 +123,7 @@ internal class CgVariableConstructor(val context: CgContext) : is UtPrimitiveModel -> CgLiteral(model.classId, model.value) is UtReferenceModel -> error("Unexpected UtReferenceModel: ${model::class}") is UtVoidModel -> error("Unexpected UtVoidModel: ${model::class}") + else -> error("Unexpected UtModel: ${model::class}") } } } @@ -208,7 +209,7 @@ internal class CgVariableConstructor(val context: CgContext) : return obj } - private fun constructAssemble(model: UtAssembleModel, baseName: String?): CgValue { + fun constructAssemble(model: UtAssembleModel, baseName: String?): CgValue { val instantiationCall = model.instantiationCall processInstantiationStatement(model, instantiationCall, baseName) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt index 2aaa63a50c..981f2e024c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt @@ -65,7 +65,7 @@ import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.voidClassId -internal abstract class CgVariableConstructorComponent(val context: CgContext) : +abstract class CgVariableConstructorComponent(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by CgCallableAccessManagerImpl(context), CgStatementConstructor by CgStatementConstructorImpl(context) { @@ -114,12 +114,13 @@ internal abstract class CgVariableConstructorComponent(val context: CgContext) : argumentMatchersClassId[anyOfClass](getClassOf(id)) } -internal class MockFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) { +class MockFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) { private val objectMocker = MockitoMocker(context) private val staticMocker = when (context.staticsMocking) { is NoStaticMocking -> null is MockitoStaticMocking -> MockitoStaticMocker(context, objectMocker) + else -> null } /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt index fec6458c1a..5f930fcc1b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt @@ -50,7 +50,7 @@ import org.utbot.framework.plugin.api.util.stringClassId import java.util.concurrent.TimeUnit @Suppress("MemberVisibilityCanBePrivate") -internal abstract class TestFrameworkManager(val context: CgContext) +abstract class TestFrameworkManager(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by getCallableAccessManagerBy(context) { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt index c5b937f05d..5f29c0d64f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -53,7 +53,7 @@ import org.utbot.framework.plugin.api.util.methodId import org.utbot.framework.plugin.api.util.objectArrayClassId import org.utbot.framework.plugin.api.util.objectClassId -internal data class EnvironmentFieldStateCache( +data class EnvironmentFieldStateCache( val thisInstance: FieldStateCache, val arguments: Array, val classesWithStaticFields: MutableMap @@ -99,7 +99,7 @@ internal data class EnvironmentFieldStateCache( } } -internal class FieldStateCache { +class FieldStateCache { val before: MutableMap = mutableMapOf() val after: MutableMap = mutableMapOf() @@ -125,7 +125,7 @@ internal class FieldStateCache { } } -internal data class CgFieldState(val variable: CgVariable, val model: UtModel) +data class CgFieldState(val variable: CgVariable, val model: UtModel) data class ExpressionWithType(val type: ClassId, val expression: CgExpression) @@ -213,7 +213,7 @@ internal const val MAX_ARRAY_INITIALIZER_SIZE = 10 private fun CgContextOwner.doesNotHaveSimpleNameClash(type: ClassId): Boolean = importedClasses.none { it.simpleName == type.simpleName } -internal fun CgContextOwner.importIfNeeded(type: ClassId) { +fun CgContextOwner.importIfNeeded(type: ClassId) { // TODO: for now we consider that tests are generated in the same package as CUT, but this may change val underlyingType = type.underlyingType diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt index 20e58a5f8c..05c5c494a4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt @@ -77,6 +77,7 @@ interface CgElement { is CgTryCatch -> visit(element) is CgInnerBlock -> visit(element) is CgForLoop -> visit(element) + is CgForEachLoop -> visit(element) is CgWhileLoop -> visit(element) is CgDoWhileLoop -> visit(element) is CgBreakStatement -> visit(element) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt index 0a905ccdfa..894b9a1fbc 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt @@ -16,11 +16,13 @@ fun TestFramework.patterns(): Patterns { Junit4 -> junit4ModulePatterns Junit5 -> junit5ModulePatterns TestNg -> testNgModulePatterns + else -> throw UnsupportedOperationException() } val libraryPatterns = when (this) { Junit4 -> junit4Patterns Junit5 -> junit5Patterns TestNg -> testNgPatterns + else -> throw UnsupportedOperationException() } return Patterns(moduleLibraryPatterns, libraryPatterns) @@ -32,11 +34,13 @@ fun TestFramework.parametrizedTestsPatterns(): Patterns { Junit4 -> emptyList() Junit5 -> emptyList() // emptyList here because JUnit5 module may not be enough for parametrized tests if :junit-jupiter-params: is not installed TestNg -> testNgModulePatterns + else -> throw UnsupportedOperationException() } val libraryPatterns = when (this) { Junit4 -> emptyList() Junit5 -> junit5ParametrizedTestsPatterns TestNg -> testNgPatterns + else -> throw UnsupportedOperationException() } return Patterns(moduleLibraryPatterns, libraryPatterns) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt index c117ed9768..dc25cfa77d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt @@ -17,6 +17,6 @@ class CgExceptionHandlerBuilder { } } -internal fun buildExceptionHandler(init: CgExceptionHandlerBuilder.() -> Unit): CgExceptionHandler { +fun buildExceptionHandler(init: CgExceptionHandlerBuilder.() -> Unit): CgExceptionHandler { return CgExceptionHandlerBuilder().apply(init).build() } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt index 3ce139983e..4750ff4f49 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt @@ -41,6 +41,7 @@ import org.utbot.framework.codegen.model.tree.CgExecutableCall import org.utbot.framework.codegen.model.tree.CgMethodsCluster import org.utbot.framework.codegen.model.tree.CgExpression import org.utbot.framework.codegen.model.tree.CgFieldAccess +import org.utbot.framework.codegen.model.tree.CgForEachLoop import org.utbot.framework.codegen.model.tree.CgForLoop import org.utbot.framework.codegen.model.tree.CgGreaterThan import org.utbot.framework.codegen.model.tree.CgIfStatement @@ -98,7 +99,7 @@ import org.utbot.framework.plugin.api.util.isRefType import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.shortClassId -internal abstract class CgAbstractRenderer( +abstract class CgAbstractRenderer( val context: CgRendererContext, val printer: CgPrinter = CgPrinterImpl() ) : CgVisitor, @@ -109,12 +110,10 @@ internal abstract class CgAbstractRenderer( protected abstract val logicalAnd: String protected abstract val logicalOr: String - protected val regionStart: String = "///region" - protected val regionEnd: String = "///endregion" + protected open val regionStart: String = "///region" + protected open val regionEnd: String = "///endregion" protected var isInterrupted = false - protected abstract val language: CodegenLanguage - protected abstract val langPackage: String // We may render array elements in initializer in one line or in separate lines: @@ -518,6 +517,8 @@ internal abstract class CgAbstractRenderer( println() } + override fun visit(element: CgForEachLoop) {} + override fun visit(element: CgWhileLoop) { print("while (") element.condition.accept(this) @@ -850,7 +851,7 @@ internal abstract class CgAbstractRenderer( } } - private fun renderClassFileImports(element: CgClassFile) { + protected open fun renderClassFileImports(element: CgClassFile) { val regularImports = element.imports.filterIsInstance() val staticImports = element.imports.filterIsInstance() @@ -873,7 +874,7 @@ internal abstract class CgAbstractRenderer( protected abstract fun renderClassModality(aClass: CgClass) - private fun renderMethodDocumentation(element: CgMethod) { + protected fun renderMethodDocumentation(element: CgMethod) { element.documentation.accept(this) } @@ -944,10 +945,7 @@ internal abstract class CgAbstractRenderer( } private fun makeRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer { - return when (context.codegenLanguage) { - CodegenLanguage.JAVA -> CgJavaRenderer(context, printer) - CodegenLanguage.KOTLIN -> CgKotlinRenderer(context, printer) - } + return context.cgLanguageAssistant.cgRenderer(context, printer) } /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt index 98e2603278..7c1861377c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt @@ -39,7 +39,6 @@ import org.utbot.framework.codegen.model.util.CgPrinter import org.utbot.framework.codegen.model.util.CgPrinterImpl import org.utbot.framework.codegen.model.util.nullLiteral import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.TypeParameters import org.utbot.framework.plugin.api.util.isFinal import org.utbot.framework.plugin.api.util.isPrivate @@ -58,8 +57,6 @@ internal class CgJavaRenderer(context: CgRendererContext, printer: CgPrinter = C override val logicalOr: String get() = "||" - override val language: CodegenLanguage = CodegenLanguage.JAVA - override val langPackage: String = "java.lang" override val ClassId.methodsAreAccessibleAsTopLevel: Boolean diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt index 5bc606069d..4f9790ef9c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt @@ -1,5 +1,6 @@ package org.utbot.framework.codegen.model.visitor +import java.util.Locale import org.apache.commons.text.StringEscapeUtils import org.utbot.common.WorkaroundReason import org.utbot.common.workaround @@ -46,7 +47,6 @@ import org.utbot.framework.codegen.model.util.CgPrinterImpl import org.utbot.framework.codegen.model.util.nullLiteral import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.TypeParameters import org.utbot.framework.plugin.api.WildcardTypeParameter import org.utbot.framework.plugin.api.util.isFinal @@ -71,8 +71,6 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = override val logicalOr: String get() = "or" - override val language: CodegenLanguage = CodegenLanguage.KOTLIN - override val langPackage: String = "kotlin" override val ClassId.methodsAreAccessibleAsTopLevel: Boolean @@ -548,7 +546,7 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = } override fun escapeNamePossibleKeywordImpl(s: String): String = - if (isLanguageKeyword(s, context.codegenLanguage)) "`$s`" else s + if (isLanguageKeyword(s, context.cgLanguageAssistant)) "`$s`" else s override fun renderClassVisibility(classId: ClassId) { when { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt index 3e695b0d42..0fe634db2d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt @@ -8,6 +8,7 @@ import org.utbot.framework.codegen.model.constructor.builtin.utJavaUtilsClassId import org.utbot.framework.codegen.model.constructor.builtin.utKotlinUtilsClassId import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CgLanguageAssistant import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.MockFramework @@ -17,7 +18,7 @@ import org.utbot.framework.plugin.api.MockFramework * Not all the information from [CgContext] is required to render a class, * so this more lightweight context is created for this purpose. */ -internal class CgRendererContext( +class CgRendererContext( val shouldOptimizeImports: Boolean, val importedClasses: Set, val importedStaticMethods: Set, @@ -27,6 +28,7 @@ internal class CgRendererContext( val codegenLanguage: CodegenLanguage, val mockFrameworkUsed: Boolean, val mockFramework: MockFramework, + val cgLanguageAssistant: CgLanguageAssistant, ) { companion object { fun fromCgContext(context: CgContext): CgRendererContext { @@ -38,6 +40,7 @@ internal class CgRendererContext( generatedClass = context.outerMostTestClass, utilMethodProvider = context.utilMethodProvider, codegenLanguage = context.codegenLanguage, + cgLanguageAssistant = context.cgLanguageAssistant, mockFrameworkUsed = context.mockFrameworkUsed, mockFramework = context.mockFramework ) @@ -53,7 +56,8 @@ internal class CgRendererContext( utilMethodProvider = utilClassKind.utilMethodProvider, codegenLanguage = language, mockFrameworkUsed = utilClassKind.mockFrameworkUsed, - mockFramework = utilClassKind.mockFramework + mockFramework = utilClassKind.mockFramework, + cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(language), ) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt index 951dad44b4..a2910032bf 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt @@ -37,6 +37,7 @@ import org.utbot.framework.codegen.model.tree.CgExecutableCall import org.utbot.framework.codegen.model.tree.CgMethodsCluster import org.utbot.framework.codegen.model.tree.CgExpression import org.utbot.framework.codegen.model.tree.CgFieldAccess +import org.utbot.framework.codegen.model.tree.CgForEachLoop import org.utbot.framework.codegen.model.tree.CgForLoop import org.utbot.framework.codegen.model.tree.CgGetJavaClass import org.utbot.framework.codegen.model.tree.CgGetKotlinClass @@ -257,4 +258,6 @@ interface CgVisitor { // Empty line fun visit(element: CgEmptyLine): R + + fun visit(element: CgForEachLoop): R } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt index 182c0c2f9d..b332124ac9 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt @@ -132,6 +132,8 @@ class MockValueConstructor( is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model), model.classId.jClass) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) + // PythonModel, JsUtModel may be here + else -> throw UnsupportedOperationException() } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt index c23fb7d052..8fade3f329 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt @@ -39,7 +39,7 @@ import java.util.stream.BaseStream /** * Represents common interface for model constructors. */ -internal interface UtModelConstructorInterface { +interface UtModelConstructorInterface { /** * Constructs a UtModel from a concrete [value] with a specific [classId]. */ diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt index 196ee58532..149261926d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt @@ -266,5 +266,7 @@ fun UtModel.accept(visitor: UtModelVisitor, data: D) = visitor.run { is UtPrimitiveModel -> visit(element, data) is UtReferenceModel -> visit(element, data) is UtVoidModel -> visit(element, data) + // PythonModel, JsUtModel may be here + else -> throw UnsupportedOperationException() } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt index 3a054916b3..546e0624f2 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt @@ -227,6 +227,8 @@ private fun UtModel.calculateSize(used: MutableSet = mutableSetOf()): I } is UtCompositeModel -> 1 + fields.values.sumOf { it.calculateSize(used) } is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) } + // PythonModel, JsUtModel may be here + else -> 0 } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CgLanguageAssistant.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CgLanguageAssistant.kt new file mode 100644 index 0000000000..5e52fe6571 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CgLanguageAssistant.kt @@ -0,0 +1,51 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.name.CgNameGenerator +import org.utbot.framework.codegen.model.constructor.name.CgNameGeneratorImpl +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManagerImpl +import org.utbot.framework.codegen.model.constructor.tree.CgFieldStateManager +import org.utbot.framework.codegen.model.constructor.tree.CgFieldStateManagerImpl +import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor +import org.utbot.framework.codegen.model.constructor.tree.CgVariableConstructor +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructorImpl +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext + +abstract class CgLanguageAssistant { + + companion object { + fun getByCodegenLanguage(language: CodegenLanguage) = when (language) { + CodegenLanguage.JAVA -> JavaCgLanguageAssistant + CodegenLanguage.KOTLIN -> KotlinCgLanguageAssistant + else -> throw UnsupportedOperationException() + } + } + + open val outerMostTestClassContent: TestClassContext? = null + + abstract val extension: String + + abstract val languageKeywords: Set + + abstract fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair + + open fun getNameGeneratorBy(context: CgContext): CgNameGenerator = CgNameGeneratorImpl(context) + open fun getCallableAccessManagerBy(context: CgContext): CgCallableAccessManager = + CgCallableAccessManagerImpl(context) + open fun getStatementConstructorBy(context: CgContext): CgStatementConstructor = CgStatementConstructorImpl(context) + open fun getVariableConstructorBy(context: CgContext): CgVariableConstructor = CgVariableConstructor(context) + open fun getMethodConstructorBy(context: CgContext): CgMethodConstructor = CgMethodConstructor(context) + open fun getCgFieldStateManager(context: CgContext): CgFieldStateManager = CgFieldStateManagerImpl(context) + + abstract fun getLanguageTestFrameworkManager(): LanguageTestFrameworkManager + abstract fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt new file mode 100644 index 0000000000..5995439c60 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JVMTestFrameworkManager.kt @@ -0,0 +1,24 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.Junit4 +import org.utbot.framework.codegen.Junit5 +import org.utbot.framework.codegen.TestNg +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.Junit4Manager +import org.utbot.framework.codegen.model.constructor.tree.Junit5Manager +import org.utbot.framework.codegen.model.constructor.tree.TestNgManager + +class JVMTestFrameworkManager : LanguageTestFrameworkManager() { + + override fun managerByFramework(context: CgContext) = when (context.testFramework) { + is Junit4 -> Junit4Manager(context) + is Junit5 -> Junit5Manager(context) + is TestNg -> TestNgManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Junit5 + + override val testFrameworks = listOf(Junit4, Junit5, TestNg) + +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCgLanguageAssistant.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCgLanguageAssistant.kt new file mode 100644 index 0000000000..635fc2ae2f --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCgLanguageAssistant.kt @@ -0,0 +1,34 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgJavaRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.utils.testClassNameGenerator + +object JavaCgLanguageAssistant : CgLanguageAssistant() { + + override val extension: String + get() = ".java" + + override val languageKeywords: Set = setOf( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", + "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", + "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "try", "void", "volatile", "while", "null", "false", "true" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return testClassNameGenerator(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun getLanguageTestFrameworkManager() = JVMTestFrameworkManager() + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = + CgJavaRenderer(context, printer) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCgLanguageAssistant.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCgLanguageAssistant.kt new file mode 100644 index 0000000000..e61af08f63 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCgLanguageAssistant.kt @@ -0,0 +1,32 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgKotlinRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.utils.testClassNameGenerator + +object KotlinCgLanguageAssistant : CgLanguageAssistant() { + + override val extension: String + get() = ".kt" + + override val languageKeywords: Set = setOf( + "as", "as?", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", "!in", "interface", + "is", "!is", "null", "object", "package", "return", "super", "this", "throw", "true", "try", "typealias", + "typeof", "val", "var", "when", "while" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return testClassNameGenerator(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun getLanguageTestFrameworkManager() = JVMTestFrameworkManager() + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = + CgKotlinRenderer(context, printer) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/LanguageTestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/LanguageTestFrameworkManager.kt new file mode 100644 index 0000000000..b553753b77 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/LanguageTestFrameworkManager.kt @@ -0,0 +1,12 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager + +abstract class LanguageTestFrameworkManager { + + open val testFrameworks: List = emptyList() + abstract fun managerByFramework(context: CgContext): TestFrameworkManager + abstract val defaultTestFramework: TestFramework +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/CodeLanguageUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/CodeLanguageUtils.kt new file mode 100644 index 0000000000..025204fedb --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/CodeLanguageUtils.kt @@ -0,0 +1,14 @@ +package org.utbot.framework.plugin.api.utils + +import org.utbot.framework.plugin.api.ClassId + +fun testClassNameGenerator( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId +): Pair { + val packagePrefix = if (testClassPackageName.isNotEmpty()) "$testClassPackageName." else "" + val simpleName = testClassCustomName ?: "${classUnderTest.simpleName}Test" + val name = "$packagePrefix$simpleName" + return Pair(name, simpleName) +} diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt index 0d87b21210..f81806a752 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt @@ -13,7 +13,7 @@ import org.utbot.framework.plugin.api.ExecutableId * @param concreteValues any concrete values to be processed by fuzzer * */ -class FuzzedMethodDescription( +open class FuzzedMethodDescription( val name: String, val returnType: ClassId, val parameters: List, diff --git a/utbot-intellij-js/build.gradle.kts b/utbot-intellij-js/build.gradle.kts new file mode 100644 index 0000000000..bafc50275c --- /dev/null +++ b/utbot-intellij-js/build.gradle.kts @@ -0,0 +1,77 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val ideVersion: String? by rootProject +val kotlinPluginVersion: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +plugins { + id("org.jetbrains.intellij") version "1.7.0" +} +project.tasks.asMap["runIde"]?.enabled = false + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + 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")) + + //Family + implementation(project(":utbot-js")) +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = listOf( + "java", + "org.jetbrains.kotlin:$kotlinPluginVersion" + ) + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + plugins.set( + when (ideType) { + "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins // something else, JS? + else -> jvmPlugins + } + ) + + version.set(ideVersion) + type.set(ideType) +} \ No newline at end of file diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/CoverageModeButtons.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/CoverageModeButtons.kt new file mode 100644 index 0000000000..a372deb8b7 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/CoverageModeButtons.kt @@ -0,0 +1,26 @@ +package org.utbot.intellij.plugin.language.js + +import service.CoverageMode +import javax.swing.JToggleButton + +object CoverageModeButtons { + + var mode = CoverageMode.FAST + val baseButton = JToggleButton("Basic") + val fastButton = JToggleButton("Fast") + + init { + val baseButtonModel = baseButton.model + baseButtonModel.addChangeListener { + if (baseButtonModel.isPressed) { + mode = CoverageMode.BASIC + } + } + val fastButtonModel = fastButton.model + fastButtonModel.addChangeListener { + if (baseButtonModel.isPressed) { + mode = CoverageMode.FAST + } + } + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt new file mode 100644 index 0000000000..cb78711a66 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt @@ -0,0 +1,209 @@ +package org.utbot.intellij.plugin.language.js + +import api.JsTestGenerator +import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.lang.ecmascript6.psi.ES6Class +import com.intellij.lang.javascript.psi.JSFile +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.file.PsiDirectoryFactory +import com.intellij.util.concurrency.AppExecutorUtil +import org.jetbrains.kotlin.idea.util.application.invokeLater +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.konan.file.File +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.intellij.plugin.ui.utils.testModules +import settings.JsDynamicSettings +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.startComment +import settings.JsTestGenerationSettings.dummyClassName + +object JsDialogProcessor { + + fun createDialogAndGenerateTests( + project: Project, + srcModule: Module, + fileMethods: Set, + focusedMethod: JSMemberInfo?, + containingFilePath: String, + editor: Editor, + file: JSFile + ) { + createDialog(project, srcModule, fileMethods, focusedMethod, containingFilePath, file)?.let { dialogProcessor -> + if (!dialogProcessor.showAndGet()) return + /* + Since Tern.js accesses containing file, sync with file system required before test generation. + */ + runWriteAction { + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + createTests(dialogProcessor.model, containingFilePath, editor) + } + } + + private fun createDialog( + project: Project, + srcModule: Module, + fileMethods: Set, + focusedMethod: JSMemberInfo?, + filePath: String, + file: JSFile + ): JsDialogWindow? { + val testModules = srcModule.testModules(project) + + if (testModules.isEmpty()) { + val errorMessage = """ + No test source roots found in the project.
+ Please,
create or configure at least one test source root. + """.trimIndent() + showErrorDialogLater(project, errorMessage, "Test source roots not found") + return null + } + + return JsDialogWindow( + JsTestsModel( + project = project, + srcModule = srcModule, + potentialTestModules = testModules, + fileMethods = fileMethods, + selectedMethods = if (focusedMethod != null) setOf(focusedMethod) else emptySet(), + file = file + ).apply { + containingFilePath = filePath + } + ) + } + + private fun unblockDocument(project: Project, document: Document) { + PsiDocumentManager.getInstance(project).apply { + commitDocument(document) + doPostponedOperationsAndUnblockDocument(document) + } + } + + private fun createTests(model: JsTestsModel, containingFilePath: String, editor: Editor) { + val normalizedContainingFilePath = containingFilePath.replace(File.separator, "/") + (object : Task.Backgroundable(model.project, "Generate tests") { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = false + indicator.text = "Generate tests: read classes" + val testDir = PsiDirectoryFactory.getInstance(project).createDirectory( + model.testSourceRoot!! + ) + val testFileName = normalizedContainingFilePath.substringAfterLast("/") + .replace(Regex(".js"), "Test.js") + val testGenerator = JsTestGenerator( + fileText = editor.document.text, + sourceFilePath = normalizedContainingFilePath, + projectPath = model.project.basePath?.replace(File.separator, "/") + ?: throw IllegalStateException("Can't access project path."), + selectedMethods = runReadAction { + model.selectedMethods.map { + it.member.name!! + } + }, + parentClassName = runReadAction { + val name = (model.selectedMethods.first().member.parent as ES6Class).name + if (name == dummyClassName) null else name + }, + outputFilePath = "${testDir.virtualFile.path}/$testFileName".replace(File.separator, "/"), + exportsManager = partialApplication(JsDialogProcessor::manageExports, editor, project), + settings = JsDynamicSettings( + pathToNode = model.pathToNode, + pathToNYC = model.pathToNYC, + pathToNPM = model.pathToNPM, + timeout = model.timeout, + coverageMode = model.coverageMode + ) + ) + + indicator.fraction = indicator.fraction.coerceAtLeast(0.9) + indicator.text = "Generate code for tests" + + val generatedCode = testGenerator.run() + invokeLater { + runWriteAction { + val testPsiFile = + testDir.findFile(testFileName) ?: PsiFileFactory.getInstance(project) + .createFileFromText(testFileName, JsLanguageAssistant.jsLanguage, generatedCode) + val testFileEditor = + CodeInsightUtil.positionCursor(project, testPsiFile, testPsiFile) + unblockDocument(project, testFileEditor.document) + testFileEditor.document.setText(generatedCode) + unblockDocument(project, testFileEditor.document) + testDir.findFile(testFileName) ?: testDir.add(testPsiFile) + } + } + } + }).queue() + } + + private fun partialApplication(f: (A, B, C) -> Unit, a: A, b: B): (C) -> Unit { + return { c: C -> f(a, b, c) } + } + + private fun manageExports(editor: Editor, project: Project, exports: List) { + AppExecutorUtil.getAppExecutorService().submit { + invokeLater { + val exportSection = exports.joinToString("\n") { "exports.$it = $it" } + val fileText = editor.document.text + when { + fileText.contains(exportSection) -> {} + + fileText.contains(startComment) && !fileText.contains(exportSection) -> { + val regex = Regex("$startComment((\\r\\n|\\n|\\r|.)*)$endComment") + regex.find(fileText)?.groups?.get(1)?.value?.let { existingSection -> + val exportRegex = Regex("exports[.](.*) =") + val existingExports = existingSection.split("\n").filter { it.contains(exportRegex) } + val existingExportsSet = existingExports.map { rawLine -> + exportRegex.find(rawLine)?.groups?.get(1)?.value ?: throw IllegalStateException() + }.toSet() + val resultSet = existingExportsSet + exports.toSet() + val resSection = resultSet.joinToString("\n") { "exports.$it = $it" } + val swappedText = fileText.replace(existingSection, "\n$resSection\n") + runWriteAction { + with(editor.document) { + unblockDocument(project, this) + setText(swappedText) + unblockDocument(project, this) + } + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + } + } + + else -> { + val line = buildString { + append("\n$startComment\n") + append(exportSection) + append("\n$endComment") + } + runWriteAction { + with(editor.document) { + unblockDocument(project, this) + setText(fileText + line) + unblockDocument(project, this) + } + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + } + } + } + } + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt new file mode 100644 index 0000000000..fcaef2630f --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt @@ -0,0 +1,186 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreterManager +import com.intellij.lang.javascript.refactoring.ui.JSMemberSelectionTable +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.ui.ContextHelpLabel +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.Panel +import com.intellij.ui.layout.Cell +import com.intellij.ui.layout.panel +import com.intellij.util.ui.JBUI +import framework.codegen.Mocha +import java.awt.BorderLayout +import java.io.File +import java.nio.file.Paths +import javax.swing.DefaultComboBoxModel +import javax.swing.JComboBox +import javax.swing.JComponent +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.intellij.plugin.ui.components.TestSourceDirectoryChooser +import settings.JsTestGenerationSettings.defaultTimeout + +class JsDialogWindow(val model: JsTestsModel) : DialogWrapper(model.project) { + + private val items = model.fileMethods + + private val functionsTable = JSMemberSelectionTable(items, null, null).apply { + val height = this.rowHeight * (items.size.coerceAtMost(12) + 1) + this.preferredScrollableViewportSize = JBUI.size(-1, height) + } + + private val nodeInterp = try { + NodeJsLocalInterpreterManager.getInstance().interpreters.first() + } catch (e: NoSuchElementException) { + throw IllegalStateException("Node.js interpreter is not set in the IDEA settings!") + } + + private val testSourceFolderField = TestSourceDirectoryChooser(model, model.file.virtualFile) + private val testFrameworks = ComboBox(DefaultComboBoxModel(arrayOf(Mocha))) + private val nycSourceFileChooserField = NycSourceFileChooser(model) + private val coverageMode = CoverageModeButtons + +// private var initTestFrameworkPresenceThread: Thread + private lateinit var panel: DialogPanel + + private val timeoutSpinner = + JBIntSpinner( + defaultTimeout.toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + MINIMUM_TIMEOUT_VALUE_IN_SECONDS + ) + + init { + model.pathToNode = nodeInterp.interpreterSystemDependentPath.replace("\\", "/") + model.pathToNPM = model.pathToNode.substringBeforeLast("/") + "/" + "npm" + //TODO: Find out how to find pathToNode from IDEA settings without extra actions from the user + model.pathToNode = "node" + title = "Generate Tests with UtBot" +// initTestFrameworkPresenceThread = thread(start = true) { +// JsCgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks.forEach { +// it.isInstalled = findFrameworkLibrary(it.displayName.lowercase(Locale.getDefault()), model) +// } +// } + isResizable = false + init() + } + + + @Suppress("UNCHECKED_CAST") + override fun createCenterPanel(): JComponent { + panel = panel { + row("Test source root:") { + component(testSourceFolderField) + } + row("Test framework:") { + component( + Panel().apply { + add(testFrameworks as ComboBox, BorderLayout.LINE_START) + } + ) + } + row("Nyc source path:") { + component(nycSourceFileChooserField) + } + row("Coverage mode:") { + panelWithHelpTooltip("Fast mode can't find timeouts, but works faster") { + component(coverageMode.fastButton) + component(coverageMode.baseButton) + } + } + row("Timeout for Node.js (in seconds):") { + panelWithHelpTooltip("The execution timeout") { + component(timeoutSpinner) + component(JBLabel("sec")) + } + } + row("Generate test methods for:") {} + row { + scrollPane(functionsTable) + } + } + updateMembersTable() + setListeners() + return panel + } + + + private inline fun Cell.panelWithHelpTooltip(tooltipText: String?, crossinline init: Cell.() -> Unit): Cell { + init() + tooltipText?.let { component(ContextHelpLabel.create(it)) } + return this + } + + + override fun doOKAction() { + val selected = functionsTable.selectedMemberInfos.toSet() + model.selectedMethods = if (selected.any()) selected else emptySet() + model.testFramework = testFrameworks.item + model.timeout = timeoutSpinner.number.toLong() + model.pathToNYC = nycSourceFileChooserField.text + model.coverageMode = coverageMode.mode + File(testSourceFolderField.text).mkdir() + model.testSourceRoot = + VirtualFileManager.getInstance().refreshAndFindFileByNioPath(Paths.get(testSourceFolderField.text)) + super.doOKAction() + } + + override fun doValidate(): ValidationInfo? { + return testSourceFolderField.validatePath() ?: nycSourceFileChooserField.validateNyc() + } + + private fun updateMembersTable() { + if (items.isEmpty()) isOKActionEnabled = false + val focusedNames = model.selectedMethods.map { it.member.name } + val selectedMethods = items.filter { + focusedNames.contains(it.member.name) + } + if (selectedMethods.isEmpty()) { + checkMembers(items) + } else { + checkMembers(selectedMethods) + } + } + + @Suppress("unused") + private fun configureTestFrameworkIfRequired() { +// initTestFrameworkPresenceThread.join() + val frameworkNotInstalled = !testFrameworks.item.isInstalled + if (frameworkNotInstalled) { + Messages.showErrorDialog( + "Test framework ${testFrameworks.item.displayName} is not installed. " + + "Run \"npm i -g ${testFrameworks.item.displayName}\".", + "Missing Framework" + ) + } + } + + + private fun setListeners() { + + testSourceFolderField.childComponent.addActionListener { event -> + with((event.source as JComboBox<*>).selectedItem) { + if (this is VirtualFile) { + model.setSourceRootAndFindTestModule(this@with) + } else { + model.setSourceRootAndFindTestModule(null) + } + } + } + } + + private fun checkMembers(members: Collection) = members.forEach { it.isChecked = true } + + +} + +private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1 diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsLanguageAssistant.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsLanguageAssistant.kt new file mode 100644 index 0000000000..102a339270 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsLanguageAssistant.kt @@ -0,0 +1,146 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.lang.Language +import com.intellij.lang.ecmascript6.psi.ES6Class +import com.intellij.lang.javascript.psi.JSFile +import com.intellij.lang.javascript.psi.JSFunction +import com.intellij.lang.javascript.psi.ecmal4.JSClass +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.idea.util.projectStructure.module +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import settings.JsTestGenerationSettings.dummyClassName + +object JsLanguageAssistant : LanguageAssistant() { + + private const val jsId = "ECMAScript 6" + val jsLanguage: Language = Language.findLanguageByID(jsId) ?: error("JavaScript language wasn't found") + + private data class PsiTargets( + val methods: Set, + val focusedMethod: JSMemberInfo?, + val module: Module, + val containingFilePath: String, + val editor: Editor, + val file: JSFile + ) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val (methods, focusedMethod, module, containingFilePath, editor, file) = getPsiTargets(e) ?: return + JsDialogProcessor.createDialogAndGenerateTests( + project = project, + srcModule = module, + fileMethods = methods, + focusedMethod = focusedMethod, + containingFilePath = containingFilePath, + editor = editor, + file = file, + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): PsiTargets? { + e.project ?: return null + val virtualFile = (e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return null).path + val editor = e.getData(CommonDataKeys.EDITOR) ?: return null + val file = e.getData(CommonDataKeys.PSI_FILE) as? JSFile ?: return null + val element = findPsiElement(file, editor) ?: return null + val module = element.module ?: return null + val focusedMethod = getContainingMethod(element) + containingClass(element)?.let { + val methods = it.functions + val memberInfos = generateMemberInfo(e.project!!, methods.toList(), it) + val focusedMethodMI = memberInfos.find { member -> + member.member?.name == focusedMethod?.name + } + return PsiTargets( + methods = memberInfos, + focusedMethod = focusedMethodMI, + module = module, + containingFilePath = virtualFile, + editor = editor, + file = file, + ) + } + val memberInfos = generateMemberInfo(e.project!!, file.statements.filterIsInstance()) + val focusedMethodMI = memberInfos.find { member -> + member.member?.name == focusedMethod?.name + } + return PsiTargets( + methods = memberInfos, + focusedMethod = focusedMethodMI, + module = module, + containingFilePath = virtualFile, + editor = editor, + file = file, + ) + } + + private fun getContainingMethod(element: PsiElement): JSFunction? { + if (element is JSFunction) + return element + + val parent = element.parent ?: return null + return getContainingMethod(parent) + } + + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + return element + } + + private fun containingClass(element: PsiElement) = + PsiTreeUtil.getParentOfType(element, ES6Class::class.java, false) + + private fun buildClassStringFromMethods(methods: List): String { + var strBuilder = "\n" + val filteredMethods = methods.filterNot { method -> method.name == "constructor" } + filteredMethods.forEach { + strBuilder += it.text.replace("function ", "") + } + // Creating a class with a random name. It won't affect user's code since it is created in abstract PsiFile. + return "class $dummyClassName {$strBuilder}" + } + + /* + Small hack: generating a string source code of an "impossible" class in order to + generate a PsiFile with it, then extract ES6Class from it, then extract MemberInfos. + Created for top-level functions that don't have a parent class. + */ + private fun generateMemberInfo( + project: Project, + methods: List, + jsClass: JSClass? = null + ): Set { + jsClass?.let { + val res = mutableListOf() + JSMemberInfo.extractClassMembers(it, res) { member -> + member is JSFunction + } + return res.toSet() + } + val strClazz = buildClassStringFromMethods(methods) + val abstractPsiFile = PsiFileFactory.getInstance(project) + .createFileFromText(jsLanguage, strClazz) + val clazz = PsiTreeUtil.getChildOfType(abstractPsiFile, JSClass::class.java) + val res = mutableListOf() + JSMemberInfo.extractClassMembers(clazz!!, res) { true } + return res.toSet() + } +} \ No newline at end of file diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt new file mode 100644 index 0000000000..b2b83da234 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt @@ -0,0 +1,31 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.lang.javascript.psi.JSFile +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import org.utbot.framework.codegen.TestFramework +import org.utbot.intellij.plugin.models.BaseTestsModel +import service.CoverageMode +import settings.JsTestGenerationSettings.defaultTimeout + +class JsTestsModel( + project: Project, + srcModule: Module, + potentialTestModules: List, + val file: JSFile, + val fileMethods: Set, + var selectedMethods: Set, +) : BaseTestsModel( + project, srcModule, potentialTestModules, emptySet() +) { + + var timeout = defaultTimeout + + lateinit var testFramework: TestFramework + lateinit var containingFilePath: String + var pathToNode: String = "node" + var pathToNYC: String = "nyc" + var pathToNPM: String = "npm" + var coverageMode: CoverageMode = CoverageMode.FAST +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/NycSourceFileChooser.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/NycSourceFileChooser.kt new file mode 100644 index 0000000000..11fde43182 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/NycSourceFileChooser.kt @@ -0,0 +1,35 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.ui.TextBrowseFolderListener +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.ui.ValidationInfo +import org.utbot.common.PathUtil.replaceSeparator +import settings.JsDynamicSettings + + +class NycSourceFileChooser(val model: JsTestsModel) : TextFieldWithBrowseButton() { + + + init { + val descriptor = FileChooserDescriptor( + true, + false, + false, + false, + false, + false, + ) + addBrowseFolderListener( + TextBrowseFolderListener(descriptor, model.project) + ) + text = replaceSeparator(getFrameworkLibraryPath(JsDynamicSettings().pathToNYC, model) ?: "Nyc was not found") + } + + fun validateNyc(): ValidationInfo? { + return if (replaceSeparator(text).endsWith("nyc")) + null + else + ValidationInfo("Nyc executable file was not found in the specified directory", this) + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/Utils.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/Utils.kt new file mode 100644 index 0000000000..256a040629 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/Utils.kt @@ -0,0 +1,54 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.openapi.ui.Messages +import utils.JsCmdExec +import utils.OsProvider + +fun getFrameworkLibraryPath(npmPackageName: String, model: JsTestsModel): String? { + val (bufferedReader, errorReader) = JsCmdExec.runCommand( + dir = model.project.basePath!!, + shouldWait = true, + timeout = 10, + cmd = arrayOf(OsProvider.getProviderByOs().getAbstractivePathTool(), model.pathToNYC) + ) + val input = bufferedReader.readText() + val error = errorReader.readText() + + if (error.isNotEmpty() or !input.contains(npmPackageName)) { + if (findFrameworkLibrary(npmPackageName, model)) { + Messages.showErrorDialog( + model.project, + "The following packages were not found, please set it in menu manually:\n $npmPackageName", + "$npmPackageName missing!", + ) + } else { + Messages.showErrorDialog( + model.project, + "The following packages are not installed: $npmPackageName \nPlease install it via npm `> npm i -g $npmPackageName`", + "$npmPackageName missing!", + ) + } + return null + } + return input.substringBefore(npmPackageName) + npmPackageName +} + +fun findFrameworkLibrary(npmPackageName: String, model: JsTestsModel): Boolean { + val (bufferedReader, _) = JsCmdExec.runCommand( + dir = model.project.basePath!!, + shouldWait = true, + timeout = 10, + cmd = arrayOf(model.pathToNPM, "list", "-g") + ) + val checkForPackageText = bufferedReader.readText() + bufferedReader.close() + if (checkForPackageText == "") { + Messages.showErrorDialog( + model.project, + "Node.js is not installed", + "Generation Failed", + ) + return false + } + return checkForPackageText.contains(npmPackageName) +} diff --git a/utbot-intellij-python/build.gradle.kts b/utbot-intellij-python/build.gradle.kts new file mode 100644 index 0000000000..9797f10378 --- /dev/null +++ b/utbot-intellij-python/build.gradle.kts @@ -0,0 +1,77 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val ideVersion: String by rootProject +val kotlinPluginVersion: String by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +plugins { + id("org.jetbrains.intellij") version "1.7.0" +} +project.tasks.asMap["runIde"]?.enabled = false + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + 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")) + + //Family + implementation(project(":utbot-python")) +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = listOf( + "java", + "org.jetbrains.kotlin:222-1.7.20-release-201-IJ4167.29" + ) + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + plugins.set( + when (ideType) { + "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins // something else, JS? + else -> jvmPlugins + } + ) + + version.set(ideVersion) + type.set(ideType) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..f81b66116d --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt @@ -0,0 +1,279 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +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.psi.PsiDirectory +import com.intellij.psi.PsiFileFactory +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.module +import org.jetbrains.kotlin.idea.util.projectStructure.sdk +import org.utbot.common.PathUtil.toPath +import org.utbot.common.appendHtmlLine +import org.utbot.framework.UtSettings +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.PythonMethod +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestGenerationProcessor.processTestGeneration +import org.utbot.python.code.PythonCode +import org.utbot.python.code.PythonCode.Companion.getFromString +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utbot.python.utils.RequirementsUtils.installRequirements +import org.utbot.python.utils.RequirementsUtils.requirements +import org.utbot.python.utils.camelToSnakeCase +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.Path + +const val DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS = 2000L + +object PythonDialogProcessor { + fun createDialogAndGenerateTests( + project: Project, + functionsToShow: Set, + containingClass: PyClass?, + focusedMethod: PyFunction?, + file: PyFile + ) { + val dialog = createDialog(project, functionsToShow, containingClass, focusedMethod, file) + if (!dialog.showAndGet()) { + return + } + + createTests(project, dialog.model) + } + + private fun createDialog( + project: Project, + functionsToShow: Set, + containingClass: PyClass?, + focusedMethod: PyFunction?, + file: PyFile + ): PythonDialogWindow { + val srcModule = findSrcModule(functionsToShow) + val testModules = srcModule.testModules(project) + val (directoriesForSysPath, moduleToImport) = getDirectoriesForSysPath(srcModule, file) + + return PythonDialogWindow( + PythonTestsModel( + project, + srcModule, + testModules, + functionsToShow, + containingClass, + if (focusedMethod != null) setOf(focusedMethod) else null, + file, + directoriesForSysPath, + moduleToImport, + UtSettings.utBotGenerationTimeoutInMillis, + DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS, + visitOnlySpecifiedSource = false, + cgLanguageAssistant = PythonCgLanguageAssistant, + ) + ) + } + + private fun findSelectedPythonMethods(model: PythonTestsModel): List? { + val code = getPyCodeFromPyFile(model.file, model.currentPythonModule) ?: return null + + val shownFunctions: Set = + if (model.containingClass == null) { + code.getToplevelFunctions().toSet() + } else { + val classes = code.getToplevelClasses() + val myClass = classes.find { it.name == model.containingClass.name } + ?: error("Didn't find containing class") + myClass.methods.toSet() + } + + return model.selectedFunctions.map { pyFunction -> + shownFunctions.find { pythonMethod -> + pythonMethod.name == pyFunction.name + } ?: error("Didn't find PythonMethod ${pyFunction.name}") + } + } + + private fun getOutputFileName(model: PythonTestsModel) = + "test_${model.currentPythonModule.camelToSnakeCase().replace('.', '_')}.py" + + private fun createTests(project: Project, model: PythonTestsModel) { + ProgressManager.getInstance().run(object : Backgroundable(project, "Generate python tests") { + override fun run(indicator: ProgressIndicator) { + val pythonPath = model.srcModule.sdk?.homePath + if (pythonPath == null) { + showErrorDialogLater( + project, + message = "Couldn't find Python interpreter", + title = "Python test generation error" + ) + return + } + val methods = findSelectedPythonMethods(model) + if (methods == null) { + showErrorDialogLater( + project, + message = "Couldn't parse file. Maybe it contains syntax error?", + title = "Python test generation error" + ) + return + } + processTestGeneration( + pythonPath = pythonPath, + pythonFilePath = model.file.virtualFile.path, + pythonFileContent = getContentFromPyFile(model.file), + directoriesForSysPath = model.directoriesForSysPath, + currentPythonModule = model.currentPythonModule, + pythonMethods = methods, + containingClassName = model.containingClass?.name, + timeout = model.timeout, + testFramework = model.testFramework, + timeoutForRun = model.timeoutForRun, + visitOnlySpecifiedSource = model.visitOnlySpecifiedSource, + isCanceled = { indicator.isCanceled }, + checkingRequirementsAction = { indicator.text = "Checking requirements" }, + requirementsAreNotInstalledAction = { + askAndInstallRequirementsLater(model.project, pythonPath) + 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" + ) + }, + writeTestTextToFile = { generatedCode -> + writeGeneratedCodeToPsiDocument(generatedCode, model) + }, + processMypyWarnings = { + val message = it.fold(StringBuilder()) { acc, line -> acc.appendHtmlLine(line) } + WarningTestsReportNotifier.notify(message.toString()) + }, + startedCleaningAction = { indicator.text = "Cleaning up..." }, + pythonRunRoot = Path(model.testSourceRootPath) + ) + } + }) + } + + 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: PythonTestsModel): 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 writeGeneratedCodeToPsiDocument(generatedCode: String, model: PythonTestsModel) { + 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) + } + } + } + + private fun askAndInstallRequirementsLater(project: Project, pythonPath: String) { + val message = """ + Some requirements are not installed. + Requirements:
+ ${requirements.joinToString("
")} +
+ Install them? + """.trimIndent() + invokeLater { + val result = Messages.showOkCancelDialog( + project, + message, + "Requirements Error", + "Install", + "Cancel", + null + ) + if (result == Messages.CANCEL) + return@invokeLater + + ProgressManager.getInstance().run(object : Backgroundable(project, "Installing requirements") { + override fun run(indicator: ProgressIndicator) { + val installResult = installRequirements(pythonPath) + + if (installResult.exitValue != 0) { + showErrorDialogLater( + project, + "Requirements installing failed", + "Requirements error" + ) + } + } + }) + } + } +} + +fun findSrcModule(functions: Collection): Module { + val srcModules = functions.mapNotNull { it.module }.distinct() + return when (srcModules.size) { + 0 -> error("Module for source classes not found") + 1 -> srcModules.first() + else -> error("Can not generate tests for classes from different modules") + } +} + +fun getContentFromPyFile(file: PyFile) = file.viewProvider.contents.toString() + +fun getPyCodeFromPyFile(file: PyFile, pythonModule: String): PythonCode? { + val content = getContentFromPyFile(file) + return getFromString(content, file.virtualFile.path, pythonModule = pythonModule) +} + +fun getDirectoriesForSysPath( + srcModule: Module, + file: PyFile +): Pair, String> { + val sources = ModuleRootManager.getInstance(srcModule).getSourceRoots(false).toMutableList() + val ancestor = ProjectFileIndex.getInstance(file.project).getContentRootForFile(file.virtualFile) + if (ancestor != null && !sources.contains(ancestor)) + sources.add(ancestor) + + var importPath = ancestor?.let { VfsUtil.getParentDir(VfsUtilCore.getRelativeLocation(file.virtualFile, it)) } ?: "" + if (importPath != "") + importPath += "." + + return Pair( + sources.map { it.path }.toSet(), + "${importPath}${file.name}".removeSuffix(".py").toPath().joinToString(".").replace("/", File.separator) + ) +} \ 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 new file mode 100644 index 0000000000..1d279c4458 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt @@ -0,0 +1,184 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.ContextHelpLabel +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.Panel +import com.intellij.ui.layout.CellBuilder +import com.intellij.ui.layout.Row +import com.intellij.ui.layout.panel +import com.intellij.util.ui.JBUI +import com.jetbrains.python.psi.* +import com.jetbrains.python.refactoring.classes.PyMemberInfoStorage +import com.jetbrains.python.refactoring.classes.membersManager.PyMemberInfo +import com.jetbrains.python.refactoring.classes.ui.PyMemberSelectionTable +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import java.awt.BorderLayout +import java.util.concurrent.TimeUnit +import javax.swing.DefaultComboBoxModel +import javax.swing.JCheckBox +import javax.swing.JComponent +import javax.swing.JPanel +import org.utbot.intellij.plugin.ui.components.TestSourceDirectoryChooser + + +private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1 + +class PythonDialogWindow(val model: PythonTestsModel) : DialogWrapper(model.project) { + + private val functionsTable = PyMemberSelectionTable(emptyList(), null, false) + private val testSourceFolderField = TestSourceDirectoryChooser(model, model.file.virtualFile) + private val timeoutSpinnerForTotalTimeout = + JBIntSpinner( + TimeUnit.MILLISECONDS.toSeconds(UtSettings.utBotGenerationTimeoutInMillis).toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + MINIMUM_TIMEOUT_VALUE_IN_SECONDS + ) + private val timeoutSpinnerForOneRun = + JBIntSpinner( + TimeUnit.MILLISECONDS.toSeconds(DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS).toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + MINIMUM_TIMEOUT_VALUE_IN_SECONDS + ) + private val testFrameworks = + ComboBox(DefaultComboBoxModel(model.cgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks.toTypedArray())) + + private val visitOnlySpecifiedSource = JCheckBox("Visit only specified source") + + private lateinit var panel: DialogPanel + + init { + title = "Generate Tests With UtBot" + isResizable = false + init() + } + + @Suppress("UNCHECKED_CAST") + override fun createCenterPanel(): JComponent { + + panel = panel { + row("Test source root:") { + component(testSourceFolderField) + } + row("Test framework:") { + makePanelWithHelpTooltip( + testFrameworks as ComboBox, + null + ) + } + row("Timeout for all selected functions:") { + cell { + component(timeoutSpinnerForTotalTimeout) + label("seconds") + component(ContextHelpLabel.create("Set the timeout for all test generation processes.")) + } + } + row("Timeout for one function run:") { + cell { + component(timeoutSpinnerForOneRun) + label("seconds") + component(ContextHelpLabel.create("Set the timeout for one function execution.")) + } + } + row("Generate test methods for:") {} + row { + scrollPane(functionsTable) + } + row { + cell { + component(visitOnlySpecifiedSource) + component(ContextHelpLabel.create("Find argument types only in this file.")) + } + } + } + + updateFunctionsTable() + return panel + } + + private fun globalPyFunctionsToPyMemberInfo( + project: Project, + functions: Collection + ): List> { + val generator = PyElementGenerator.getInstance(project) + val fakeClassName = generateRandomString(15) + val newClass = generator.createFromText( + LanguageLevel.getDefault(), + PyClass::class.java, + "class __FakeWrapperUtBotClass_$fakeClassName:\npass" + ) + functions.forEach { + newClass.add(it) + } + val storage = PyMemberInfoStorage(newClass) + return storage.getClassMemberInfos(newClass) + } + + private fun pyFunctionsToPyMemberInfo( + project: Project, + functions: Collection, + containingClass: PyClass? + ): List> { + if (containingClass == null) { + return globalPyFunctionsToPyMemberInfo(project, functions) + } + return PyMemberInfoStorage(containingClass).getClassMemberInfos(containingClass) + .filter { it.member is PyFunction } + } + + private fun updateFunctionsTable() { + val items = pyFunctionsToPyMemberInfo(model.project, model.functionsToDisplay, model.containingClass) + updateMethodsTable(items) + val height = functionsTable.rowHeight * (items.size.coerceAtMost(12) + 1) + functionsTable.preferredScrollableViewportSize = JBUI.size(-1, height) + } + + private fun updateMethodsTable(allMethods: Collection>) { + val focusedNames = model.focusedMethod?.map { it.name } + val selectedMethods = allMethods.filter { + focusedNames?.contains(it.member.name) ?: false + } + + if (selectedMethods.isEmpty()) { + checkMembers(allMethods) + } else { + checkMembers(selectedMethods) + } + + functionsTable.setMemberInfos(allMethods) + } + + private fun checkMembers(members: Collection>) = members.forEach { it.isChecked = true } + + private fun Row.makePanelWithHelpTooltip( + mainComponent: JComponent, + contextHelpLabel: ContextHelpLabel? + ): CellBuilder = + component(Panel().apply { + add(mainComponent, BorderLayout.LINE_START) + contextHelpLabel?.let { add(it, BorderLayout.LINE_END) } + }) + + override fun doOKAction() { + val selectedMembers = functionsTable.selectedMemberInfos + model.selectedFunctions = selectedMembers.mapNotNull { it.member as? PyFunction }.toSet() + model.testFramework = testFrameworks.item + model.timeout = TimeUnit.SECONDS.toMillis(timeoutSpinnerForTotalTimeout.number.toLong()) + model.timeoutForRun = TimeUnit.SECONDS.toMillis(timeoutSpinnerForOneRun.number.toLong()) + model.visitOnlySpecifiedSource = visitOnlySpecifiedSource.isSelected + model.testSourceRootPath = testSourceFolderField.text + + super.doOKAction() + } + + override fun doValidate(): ValidationInfo? { + return testSourceFolderField.validatePath() + } +} diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonLanguageAssistant.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonLanguageAssistant.kt new file mode 100644 index 0000000000..00f1bf7889 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonLanguageAssistant.kt @@ -0,0 +1,78 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant + +object PythonLanguageAssistant : LanguageAssistant() { + + private const val pythonID = "Python" + val language: Language = Language.findLanguageByID(pythonID) ?: error("Language wasn't found") + + data class Targets( + val functions: Set, + val containingClass: PyClass?, + val focusedFunction: PyFunction?, + val file: PyFile + ) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val (functions, containingClass, focusedFunction, file) = getPsiTargets(e) ?: return + + PythonDialogProcessor.createDialogAndGenerateTests( + project, + functions, + containingClass, + focusedFunction, + file + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): Targets? { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return null + val file = e.getData(CommonDataKeys.PSI_FILE) as? PyFile ?: return null + val element = findPsiElement(file, editor) ?: return null + + val containingFunction = getContainingElement(element) + val containingClass = getContainingElement(element) + + if (containingClass == null) { + val functions = file.topLevelFunctions + if (functions.isEmpty()) + return null + + val focusedFunction = if (functions.contains(containingFunction)) containingFunction else null + return Targets(functions.toSet(), null, focusedFunction, file) + } + + val functions = containingClass.methods + if (functions.isEmpty()) + return null + + val focusedFunction = if (functions.any { it.name == containingFunction?.name }) containingFunction else null + return Targets(functions.toSet(), containingClass, focusedFunction, file) + } + + // this method is copy-paste from GenerateTestsActions.kt + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt new file mode 100644 index 0000000000..eb5af48bac --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt @@ -0,0 +1,34 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.CgLanguageAssistant +import org.utbot.intellij.plugin.models.BaseTestsModel + +class PythonTestsModel( + project: Project, + srcModule: Module, + potentialTestModules: List, + val functionsToDisplay: Set, + val containingClass: PyClass?, + val focusedMethod: Set?, + val file: PyFile, + val directoriesForSysPath: Set, + val currentPythonModule: String, + var timeout: Long, + var timeoutForRun: Long, + var visitOnlySpecifiedSource: Boolean, + val cgLanguageAssistant: CgLanguageAssistant, +) : BaseTestsModel( + project, + srcModule, + potentialTestModules +) { + lateinit var testSourceRootPath: String + lateinit var testFramework: TestFramework + lateinit var selectedFunctions: Set +} diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/Utils.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/Utils.kt new file mode 100644 index 0000000000..235edcfff9 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/Utils.kt @@ -0,0 +1,30 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import kotlin.random.Random + +inline fun getContainingElement( + element: PsiElement, + predicate: (T) -> Boolean = { true } +): T? { + var result = element + while ((result !is T || !predicate(result)) && (result.parent != null)) { + result = result.parent + } + return result as? T +} + +fun getContentRoot(project: Project, file: VirtualFile): VirtualFile { + return ProjectFileIndex.getInstance(project) + .getContentRootForFile(file) ?: error("Source file lies outside of a module") +} + +fun generateRandomString(length: Int): String { + val charPool = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return (0..length) + .map { Random.nextInt(0, charPool.size).let { charPool[it] } } + .joinToString("") +} diff --git a/utbot-intellij/build.gradle.kts b/utbot-intellij/build.gradle.kts index 204604c53c..4539792843 100644 --- a/utbot-intellij/build.gradle.kts +++ b/utbot-intellij/build.gradle.kts @@ -2,9 +2,17 @@ val intellijPluginVersion: String? by rootProject val kotlinLoggingVersion: String? by rootProject val apacheCommonsTextVersion: String? by rootProject val jacksonVersion: String? by rootProject + val ideType: String? by rootProject +val ideVersion: String? by rootProject +val kotlinPluginVersion: String? by rootProject + val pythonCommunityPluginVersion: String? by rootProject val pythonUltimatePluginVersion: String? by rootProject + +val pythonIde: String? by rootProject +val jsIde: String? by rootProject + val sootVersion: String? by rootProject val kryoVersion: String? by rootProject val semVer: String? by rootProject @@ -23,7 +31,7 @@ intellij { val jvmPlugins = mutableListOf( "java", - "org.jetbrains.kotlin:222-1.7.20-release-201-IJ4167.29" + "org.jetbrains.kotlin:$kotlinPluginVersion" ) androidStudioPath?.let { jvmPlugins += androidPlugins } @@ -37,7 +45,7 @@ intellij { ) val jsPlugins = listOf( - "JavaScriptLanguage" + "JavaScript" ) plugins.set( @@ -45,12 +53,12 @@ intellij { "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins "PC" -> pythonCommunityPlugins - "PU" -> pythonUltimatePlugins // something else, JS? + "PY" -> pythonUltimatePlugins // something else, JS? else -> jvmPlugins } ) - version.set("222.4167.29") + version.set(ideVersion) type.set(ideTypeOrAndroidStudio) } @@ -96,5 +104,18 @@ dependencies { testImplementation("org.mock-server:mockserver-netty:5.4.1") testApi(project(":utbot-framework")) + implementation(project(":utbot-ui-commons")) + + //Family + if (pythonIde?.split(',')?.contains(ideType) == true) { + implementation(project(":utbot-python")) + implementation(project(":utbot-intellij-python")) + } + + if (jsIde?.split(',')?.contains(ideType) == true) { + implementation(project(":utbot-js")) + implementation(project(":utbot-intellij-js")) + } + implementation(project(":utbot-android-studio")) } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt index 6def13fd9d..5038079f89 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt @@ -460,12 +460,6 @@ object CodeGenerationController { } } - fun GenerateTestsModel.getAllTestSourceRoots() : MutableList { - with(if (project.isBuildWithGradle) project.allModules() else potentialTestModules) { - return this.flatMap { it.suitableTestSourceRoots().toList() }.toMutableList() - } - } - private val CodegenLanguage.utilClassFileName: String get() = "$UT_UTILS_INSTANCE_NAME${this.extension}" @@ -866,6 +860,7 @@ object CodeGenerationController { } } is RegularImport -> { } + else -> { } } } } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt index 2a2a81f917..52fac9f788 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt @@ -47,7 +47,6 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.TimeUnit import kotlin.io.path.pathString -import org.utbot.intellij.plugin.generator.CodeGenerationController.getAllTestSourceRoots import org.utbot.framework.plugin.api.util.LockFile import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle @@ -358,4 +357,4 @@ object UtTestsDialogProcessor { val pluginJarsPath: List // ^ TODO: Now we collect ALL dependent libs and pass them to the child process. Most of them are redundant. ) -} +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt new file mode 100644 index 0000000000..5fed1159cc --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt @@ -0,0 +1,244 @@ +package org.utbot.intellij.plugin.language + +import com.intellij.openapi.actionSystem.ActionPlaces +import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor +import org.utbot.intellij.plugin.ui.utils.PsiElementHandler +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.util.classMembers.MemberInfo +import org.jetbrains.kotlin.idea.core.getPackage +import org.jetbrains.kotlin.idea.core.util.toPsiDirectory +import org.jetbrains.kotlin.idea.core.util.toPsiFile +import org.jetbrains.kotlin.idea.util.module +import org.utbot.intellij.plugin.util.extractFirstLevelMembers +import org.utbot.intellij.plugin.util.isVisible +import java.util.* +import org.jetbrains.kotlin.j2k.getContainingClass +import org.jetbrains.kotlin.utils.addIfNotNull +import org.utbot.framework.plugin.api.util.LockFile +import org.utbot.intellij.plugin.models.packageName +import org.utbot.intellij.plugin.ui.InvalidClassNotifier +import org.utbot.intellij.plugin.util.isAbstract +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import org.utbot.intellij.plugin.util.findSdkVersionOrNull + +object JvmLanguageAssistant : LanguageAssistant() { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + + val (srcClasses, focusedMethods, extractMembersFromSrcClasses) = getPsiTargets(e) ?: return + val validatedSrcClasses = validateSrcClasses(srcClasses) ?: return + + UtTestsDialogProcessor.createDialogAndGenerateTests(project, validatedSrcClasses, extractMembersFromSrcClasses, focusedMethods) + } + + override fun update(e: AnActionEvent) { + if (LockFile.isLocked()) { + e.presentation.isEnabled = false + return + } + if (e.place == ActionPlaces.POPUP) { + e.presentation.text = "Tests with UnitTestBot..." + } + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): Triple, Set, Boolean>? { + val project = e.project ?: return null + val editor = e.getData(CommonDataKeys.EDITOR) + if (editor != null) { + //The action is being called from editor + val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null + val element = findPsiElement(file, editor) ?: return null + + val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) + + if (psiElementHandler.isCreateTestActionAvailable(element)) { + val srcClass = psiElementHandler.containingClass(element) ?: return null + val srcSourceRoot = srcClass.getSourceRoot() ?: return null + val srcMembers = srcClass.extractFirstLevelMembers(false) + val focusedMethod = focusedMethodOrNull(element, srcMembers, psiElementHandler) + + val module = ModuleUtil.findModuleForFile(srcSourceRoot, project) ?: return null + val matchingRoot = ModuleRootManager.getInstance(module).contentEntries + .flatMap { entry -> entry.sourceFolders.toList() } + .firstOrNull { folder -> folder.file == srcSourceRoot } + if (srcMembers.isEmpty() || matchingRoot == null || matchingRoot.rootType.isForTests) { + return null + } + + return Triple(setOf(srcClass), if (focusedMethod != null) setOf(focusedMethod) else emptySet(), true) + } + } else { + // The action is being called from 'Project' tool window + val srcClasses = mutableSetOf() + val selectedMethods = mutableSetOf() + var extractMembersFromSrcClasses = false + val element = e.getData(CommonDataKeys.PSI_ELEMENT) + if (element is PsiFileSystemItem) { + e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { + srcClasses += getAllClasses(project, it) + } + } else if (element is PsiElement){ + val file = element.containingFile ?: return null + val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) + + if (psiElementHandler.isCreateTestActionAvailable(element)) { + psiElementHandler.containingClass(element)?.let { + srcClasses += setOf(it) + extractMembersFromSrcClasses = true + val memberInfoList = runReadAction> { + it.extractFirstLevelMembers(false) + } + if (memberInfoList.isNullOrEmpty()) + return null + } + + if (element is PsiMethod) { + selectedMethods.add(MemberInfo(element)) + } + } + } else { + val someSelection = e.getData(PlatformDataKeys.SELECTED_ITEMS)?: return null + someSelection.forEach { + when(it) { + is PsiFileSystemItem -> srcClasses += getAllClasses(project, arrayOf(it.virtualFile)) + is PsiClass -> srcClasses.add(it) + is PsiElement -> { + srcClasses.addIfNotNull(it.getContainingClass()) + if (it is PsiMethod) { + selectedMethods.add(MemberInfo(it)) + extractMembersFromSrcClasses = true + } + } + } + } + } + + if (srcClasses.size > 1) { + extractMembersFromSrcClasses = false + } + var commonSourceRoot = null as VirtualFile? + for (srcClass in srcClasses) { + if (commonSourceRoot == null) { + commonSourceRoot = srcClass.getSourceRoot()?: return null + } else if (commonSourceRoot != srcClass.getSourceRoot()) return null + } + if (commonSourceRoot == null) return null + val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null + + if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries) + .flatMap { entry -> Arrays.stream(entry.sourceFolders) } + .filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot} + .findAny().isPresent ) return null + + return Triple(srcClasses.toSet(), selectedMethods.toSet(), extractMembersFromSrcClasses) + } + return null + } + + /** + * Validates that a set of source classes matches some requirements from [isInvalid]. + * If no one of them matches, shows a warning about the first mismatch reason. + */ + private fun validateSrcClasses(srcClasses: Set): Set? { + val filteredClasses = srcClasses + .filterNot { it.isInvalid(withWarnings = false) } + .toSet() + + if (filteredClasses.isEmpty()) { + srcClasses.first().isInvalid(withWarnings = true) + return null + } + + return filteredClasses + } + + private fun PsiClass.isInvalid(withWarnings: Boolean): Boolean { + if (this.module?.let { findSdkVersionOrNull(it) } == null) { + if (withWarnings) InvalidClassNotifier.notify("class out of module or with undefined SDK") + return true + } + + val isAbstractOrInterface = this.isInterface || this.isAbstract + if (isAbstractOrInterface) { + if (withWarnings) InvalidClassNotifier.notify("abstract class or interface ${this.name}") + return true + } + + val isInvisible = !this.isVisible + if (isInvisible) { + if (withWarnings) InvalidClassNotifier.notify("private or protected class ${this.name}") + return true + } + + val packageIsIncorrect = this.packageName.split(".").firstOrNull() == "java" + if (packageIsIncorrect) { + if (withWarnings) InvalidClassNotifier.notify("class ${this.name} located in java.* package") + return true + } + + return false + } + + private fun PsiElement?.getSourceRoot() : VirtualFile? { + val project = this?.project?: return null + val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null + return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile) + } + + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } + + private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { + // getParentOfType might return element which does not correspond to the standard Psi hierarchy. + // Thus, make transition to the Psi if it is required. + val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) + ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } + + return methods.singleOrNull { it.member == currentMethod } + } + + private fun getAllClasses(directory: PsiDirectory): Set { + val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() + for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) + return allClasses + } + + private fun getAllClasses(project: Project, virtualFiles: Array): Set { + val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } + val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } + val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } + + if (!dirsArePackages) { + return emptySet() + } + val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() + for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) + + return allClasses + } + + private fun getClassesFromFile(psiFile: PsiFile): List { + val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile) + return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass) + .map { psiElementHandler.toPsi(it, PsiClass::class.java) } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt index 952eccb8bb..fecabc8708 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt @@ -11,11 +11,7 @@ import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.MockFramework import org.utbot.framework.plugin.api.MockStrategyApi import com.intellij.openapi.module.Module -import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.JavaSdkVersion -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.psi.PsiClass import com.intellij.psi.PsiJavaFile import com.intellij.refactoring.util.classMembers.MemberInfo @@ -23,43 +19,26 @@ import org.jetbrains.kotlin.psi.KtFile import org.utbot.framework.plugin.api.JavaDocCommentStyle import org.utbot.framework.util.ConflictTriggers import org.utbot.intellij.plugin.settings.Settings -import org.utbot.intellij.plugin.ui.utils.jdkVersion -data class GenerateTestsModel( - val project: Project, - val srcModule: Module, - val potentialTestModules: List, - var srcClasses: Set, +class GenerateTestsModel( + project: Project, + srcModule: Module, + potentialTestModules: List, + srcClasses: Set, val extractMembersFromSrcClasses: Boolean, var selectedMembers: Set, var timeout: Long, var generateWarningsForStaticMocking: Boolean = false, var fuzzingValue: Double = 0.05 +): BaseTestsModel( + project, + srcModule, + potentialTestModules, + srcClasses ) { - // GenerateTestsModel is supposed to be created with non-empty list of potentialTestModules. - // Otherwise, the error window is supposed to be shown earlier. - var testModule: Module = potentialTestModules.firstOrNull() ?: error("Empty list of test modules in model") - var testSourceRoot: VirtualFile? = null + override var codegenLanguage = project.service().codegenLanguage - fun setSourceRootAndFindTestModule(newTestSourceRoot: VirtualFile?) { - requireNotNull(newTestSourceRoot) - testSourceRoot = newTestSourceRoot - var target = newTestSourceRoot - while(target != null && target is FakeVirtualFile) { - target = target.parent - } - if (target == null) { - error("Could not find module for $newTestSourceRoot") - } - - testModule = ModuleUtil.findModuleForFile(target, project) - ?: error("Could not find module for $newTestSourceRoot") - } - - val codegenLanguage = project.service().codegenLanguage - - var testPackageName: String? = null lateinit var testFramework: TestFramework lateinit var mockStrategy: MockStrategyApi lateinit var mockFramework: MockFramework @@ -74,9 +53,6 @@ data class GenerateTestsModel( val conflictTriggers: ConflictTriggers = ConflictTriggers() - val isMultiPackage: Boolean by lazy { - srcClasses.map { it.packageName }.distinct().size != 1 - } var runGeneratedTestsWithCoverage : Boolean = false var enableSummariesGeneration : Boolean = true } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt index ac4703526c..f270e5a504 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt @@ -723,6 +723,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m minSupportedSdkVersion -> testNgOldLibraryDescriptor() else -> testNgNewLibraryDescriptor(versionInProject) } + else -> throw UnsupportedOperationException() } selectedTestFramework.isInstalled = true @@ -781,6 +782,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m Junit4 -> error("Parametrized tests are not supported for JUnit 4") Junit5 -> jUnit5ParametrizedTestsLibraryDescriptor(versionInProject) TestNg -> null // Parametrized tests come with TestNG by default + else -> throw UnsupportedOperationException() } selectedTestFramework.isParametrizedTestsConfigured = true diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt index aa764a5328..a8c395b385 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt @@ -1,242 +1,15 @@ package org.utbot.intellij.plugin.ui.actions -import com.intellij.openapi.actionSystem.ActionPlaces -import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor -import org.utbot.intellij.plugin.ui.utils.PsiElementHandler import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.UpdateInBackground -import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.module.ModuleUtil -import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ModuleRootManager -import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.* -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.refactoring.util.classMembers.MemberInfo -import org.jetbrains.kotlin.asJava.findFacadeClass -import org.jetbrains.kotlin.idea.core.getPackage -import org.jetbrains.kotlin.idea.core.util.toPsiDirectory -import org.jetbrains.kotlin.idea.core.util.toPsiFile -import org.jetbrains.kotlin.idea.util.module -import org.utbot.intellij.plugin.util.extractFirstLevelMembers -import org.utbot.intellij.plugin.util.isVisible -import java.util.* -import org.jetbrains.kotlin.j2k.getContainingClass -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.utils.addIfNotNull -import org.utbot.framework.plugin.api.util.LockFile -import org.utbot.intellij.plugin.models.packageName -import org.utbot.intellij.plugin.ui.InvalidClassNotifier -import org.utbot.intellij.plugin.util.findSdkVersionOrNull -import org.utbot.intellij.plugin.util.isAbstract +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant -class GenerateTestsAction : AnAction(), UpdateInBackground { +class GenerateTestsAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { - val project = e.project ?: return - - val (srcClasses, focusedMethods, extractMembersFromSrcClasses) = getPsiTargets(e) ?: return - val validatedSrcClasses = validateSrcClasses(srcClasses) ?: return - - UtTestsDialogProcessor.createDialogAndGenerateTests(project, validatedSrcClasses, extractMembersFromSrcClasses, focusedMethods) + LanguageAssistant.get(e)?.actionPerformed(e) } override fun update(e: AnActionEvent) { - if (LockFile.isLocked()) { - e.presentation.isEnabled = false - return - } - if (e.place == ActionPlaces.POPUP) { - e.presentation.text = "Tests with UnitTestBot..." - } - e.presentation.isEnabled = getPsiTargets(e) != null - } - - private fun getPsiTargets(e: AnActionEvent): Triple, Set, Boolean>? { - val project = e.project ?: return null - val editor = e.getData(CommonDataKeys.EDITOR) - if (editor != null) { - //The action is being called from editor - val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null - val element = findPsiElement(file, editor) ?: return null - - val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) - - if (psiElementHandler.isCreateTestActionAvailable(element)) { - val srcClass = psiElementHandler.containingClass(element) ?: return null - val srcSourceRoot = srcClass.getSourceRoot() ?: return null - val srcMembers = srcClass.extractFirstLevelMembers(false) - val focusedMethod = focusedMethodOrNull(element, srcMembers, psiElementHandler) - - val module = ModuleUtil.findModuleForFile(srcSourceRoot, project) ?: return null - val matchingRoot = ModuleRootManager.getInstance(module).contentEntries - .flatMap { entry -> entry.sourceFolders.toList() } - .firstOrNull { folder -> folder.file == srcSourceRoot } - if (srcMembers.isEmpty() || matchingRoot == null || matchingRoot.rootType.isForTests) { - return null - } - - return Triple(setOf(srcClass), if (focusedMethod != null) setOf(focusedMethod) else emptySet(), true) - } - } else { - // The action is being called from 'Project' tool window - val srcClasses = mutableSetOf() - val selectedMethods = mutableSetOf() - var extractMembersFromSrcClasses = false - val element = e.getData(CommonDataKeys.PSI_ELEMENT) - if (element is PsiFileSystemItem) { - e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { - srcClasses += getAllClasses(project, it) - } - } else if (element is PsiElement){ - val file = element.containingFile ?: return null - val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) - - if (psiElementHandler.isCreateTestActionAvailable(element)) { - psiElementHandler.containingClass(element)?.let { - srcClasses += setOf(it) - extractMembersFromSrcClasses = true - val memberInfoList = runReadAction> { - it.extractFirstLevelMembers(false) - } - if (memberInfoList.isNullOrEmpty()) - return null - } - - if (element is PsiMethod) { - selectedMethods.add(MemberInfo(element)) - } - } - } else { - val someSelection = e.getData(PlatformDataKeys.SELECTED_ITEMS)?: return null - someSelection.forEach { - when(it) { - is PsiFileSystemItem -> srcClasses += getAllClasses(project, arrayOf(it.virtualFile)) - is PsiClass -> srcClasses.add(it) - is PsiElement -> { - srcClasses.addIfNotNull(it.getContainingClass()) - if (it is PsiMethod) { - selectedMethods.add(MemberInfo(it)) - extractMembersFromSrcClasses = true - } - } - } - } - } - - if (srcClasses.size > 1) { - extractMembersFromSrcClasses = false - } - var commonSourceRoot = null as VirtualFile? - for (srcClass in srcClasses) { - if (commonSourceRoot == null) { - commonSourceRoot = srcClass.getSourceRoot()?: return null - } else if (commonSourceRoot != srcClass.getSourceRoot()) return null - } - if (commonSourceRoot == null) return null - val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null - - if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries) - .flatMap { entry -> Arrays.stream(entry.sourceFolders) } - .filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot} - .findAny().isPresent ) return null - - return Triple(srcClasses.toSet(), selectedMethods.toSet(), extractMembersFromSrcClasses) - } - return null - } - - /** - * Validates that a set of source classes matches some requirements from [isInvalid]. - * If no one of them matches, shows a warning about the first mismatch reason. - */ - private fun validateSrcClasses(srcClasses: Set): Set? { - val filteredClasses = srcClasses - .filterNot { it.isInvalid(withWarnings = false) } - .toSet() - - if (filteredClasses.isEmpty()) { - srcClasses.first().isInvalid(withWarnings = true) - return null - } - - return filteredClasses - } - - private fun PsiClass.isInvalid(withWarnings: Boolean): Boolean { - if (this.module?.let { findSdkVersionOrNull(it) } == null) { - if (withWarnings) InvalidClassNotifier.notify("class out of module or with undefined SDK") - return true - } - - val isAbstractOrInterface = this.isInterface || this.isAbstract - if (isAbstractOrInterface) { - if (withWarnings) InvalidClassNotifier.notify("abstract class or interface ${this.name}") - return true - } - - val isInvisible = !this.isVisible - if (isInvisible) { - if (withWarnings) InvalidClassNotifier.notify("private or protected class ${this.name}") - return true - } - - val packageIsIncorrect = this.packageName.split(".").firstOrNull() == "java" - if (packageIsIncorrect) { - if (withWarnings) InvalidClassNotifier.notify("class ${this.name} located in java.* package") - return true - } - - return false - } - - private fun PsiElement?.getSourceRoot() : VirtualFile? { - val project = this?.project?: return null - val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null - return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile) - } - - private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { - val offset = editor.caretModel.offset - var element = file.findElementAt(offset) - if (element == null && offset == file.textLength) { - element = file.findElementAt(offset - 1) - } - - return element - } - - private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { - // getParentOfType might return element which does not correspond to the standard Psi hierarchy. - // Thus, make transition to the Psi if it is required. - val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) - ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } - - return methods.singleOrNull { it.member == currentMethod } - } - - private fun getAllClasses(directory: PsiDirectory): Set { - val allClasses = directory.files.flatMap { PsiElementHandler.makePsiElementHandler(it).getClassesFromFile(it) }.toMutableSet() - for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) - return allClasses - } - - private fun getAllClasses(project: Project, virtualFiles: Array): Set { - val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } - val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } - val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } - - if (!dirsArePackages) { - return emptySet() - } - val allClasses = psiFiles.flatMap { PsiElementHandler.makePsiElementHandler(it).getClassesFromFile(it) }.toMutableSet() - allClasses.addAll(psiFiles.mapNotNull { (it as? KtFile)?.findFacadeClass() }) - for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) - - return allClasses + LanguageAssistant.get(e)?.update(e) } } \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/plugin.xml b/utbot-intellij/src/main/resources/META-INF/plugin.xml index edf01fe9c0..5d2c583673 100644 --- a/utbot-intellij/src/main/resources/META-INF/plugin.xml +++ b/utbot-intellij/src/main/resources/META-INF/plugin.xml @@ -5,11 +5,12 @@ UnitTestBot utbot.org com.intellij.modules.platform - com.intellij.modules.java - org.jetbrains.kotlin - - org.jetbrains.android + com.intellij.modules.java + com.intellij.modules.lang + org.jetbrains.kotlin + com.intellij.modules.python + org.jetbrains.android + + JavaScript + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withJava.xml b/utbot-intellij/src/main/resources/META-INF/withJava.xml new file mode 100644 index 0000000000..2ce2e82cc9 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withJava.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withKotlin.xml b/utbot-intellij/src/main/resources/META-INF/withKotlin.xml new file mode 100644 index 0000000000..07e0e420c3 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withKotlin.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withLang.xml b/utbot-intellij/src/main/resources/META-INF/withLang.xml new file mode 100644 index 0000000000..ed33e791e3 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withLang.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withPython.xml b/utbot-intellij/src/main/resources/META-INF/withPython.xml new file mode 100644 index 0000000000..f272fd7601 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withPython.xml @@ -0,0 +1,4 @@ + + + com.intellij.modules.python + \ No newline at end of file diff --git a/utbot-js/.gitignore b/utbot-js/.gitignore new file mode 100644 index 0000000000..c7708ff1e2 --- /dev/null +++ b/utbot-js/.gitignore @@ -0,0 +1 @@ +samples \ No newline at end of file diff --git a/utbot-js/build.gradle.kts b/utbot-js/build.gradle.kts new file mode 100644 index 0000000000..f868206db9 --- /dev/null +++ b/utbot-js/build.gradle.kts @@ -0,0 +1,55 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + api(project(":utbot-framework")) + implementation(project(":utbot-fuzzers")) + // https://mvnrepository.com/artifact/org.graalvm.js/js + implementation(group = "org.graalvm.js", name = "js", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.graalvm.js/js-scriptengine + implementation(group = "org.graalvm.js", name = "js-scriptengine", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.graalvm.truffle/truffle-api + implementation(group = "org.graalvm.truffle", name = "truffle-api", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.graalvm.sdk/graal-sdk + implementation(group = "org.graalvm.sdk", name = "graal-sdk", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.json/json + implementation(group = "org.json", name = "json", version = "20220320") + + // https://mvnrepository.com/artifact/commons-io/commons-io + implementation(group = "commons-io", name = "commons-io", version = "2.11.0") + + implementation("org.functionaljava:functionaljava:5.0") + implementation("org.functionaljava:functionaljava-quickcheck:5.0") + implementation("org.functionaljava:functionaljava-java-core:5.0") + implementation(group = "org.apache.commons", name = "commons-text", version = apacheCommonsTextVersion) +} \ No newline at end of file diff --git a/utbot-js/docs/CLI.md b/utbot-js/docs/CLI.md new file mode 100644 index 0000000000..ee2c5ff0ae --- /dev/null +++ b/utbot-js/docs/CLI.md @@ -0,0 +1,70 @@ +## Build + +.jar file can be built in GitHub Actions with script publish-plugin-and-cli-from-branch. + +## Requirements + +* NodeJs 10.0.0 or higher (available to download https://nodejs.org/en/download/) +* Java 11 or higher (available to download https://www.oracle.com/java/technologies/downloads/) + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_js --source="dir/file_with_sources.js" --output="dir/generated_tests.js" + +This will generate tests for top-level functions from `file_with_sources.js`. + +Run generated tests: + + java -jar utbot-cli.jar run_js --fileOrDir="generated_tests.js" + +This will run generated tests from file or directory. + +Generate coverage report: + + java -jar utbot-cli.jar coverage_js --source=dir/generated_tests.js + +This will generate coverage report from generated tests and print in `StdOut` + +## `generate_js` options + +- `-s, --source ` + + (required) Source code file for a test generation. +- `-c, --class ` + + If not specified tests for top-level functions are generated, otherwise for the specified class. + +- `-o, --output ` + + File for generated tests. +- `-p, --print-test` + + Specifies whether test should be printed out to `StdOut` (default = false) +- `-t, --timeout ` + + Timeout for Node.js to run scripts in seconds (default = 5) + +## `run_js` options + +- `-f, --fileOrDir` + + (required) File or directory with tests. +- `-o, --output` + + Specifies output of .txt file for test framework result (If empty prints to `StdOut`) + +- `-t, --test-framework [mocha]` + + Test framework of tests to run. + +## `coverage_js` options + +- `-s, --source ` + + (required) File with tests to generate a report. + +- `-o, --output` + + Specifies output .json file for generated tests (If empty prints .json to `StdOut`) diff --git a/utbot-js/samples/bitOperators.js b/utbot-js/samples/bitOperators.js new file mode 100644 index 0000000000..3f69723358 --- /dev/null +++ b/utbot-js/samples/bitOperators.js @@ -0,0 +1,31 @@ +class BitOperators { + + complement(x) { + return (~x) === 1 + } + + xor(x, y) { + return (x ^ y) === 0 + } + + and(x) { + return (x & (x - 1)) === 0 + } + + Not(a, b) { + let d = a && b + let e = !a || b + return d && e ? 100 : 200 + } + + shl(x) { + return (x << 1) === 2 + } + + shlWithBigLongShift(shift) { + if (shift < 40) { + return 1 + } + return (0x77777777 << shift) === 0x77777770 ? 2 : 3 + } +} diff --git a/utbot-js/samples/commonIfStatement.js b/utbot-js/samples/commonIfStatement.js new file mode 100644 index 0000000000..3b21a1c9a1 --- /dev/null +++ b/utbot-js/samples/commonIfStatement.js @@ -0,0 +1,7 @@ +function foo(a,b) { + if (a > 10) { + return a * b + } else { + return -1 + } +} \ No newline at end of file diff --git a/utbot-js/samples/commonLoops.js b/utbot-js/samples/commonLoops.js new file mode 100644 index 0000000000..cd8d0fd9a2 --- /dev/null +++ b/utbot-js/samples/commonLoops.js @@ -0,0 +1,27 @@ +class Loops { + + whileLoop(value) { + let i = 0 + let sum = 0 + while (i < value) { + sum += i + i += 1 + } + return sum + } + + loopInsideLoop(x) { + for (let i = x - 5; i < x; i++) { + if (i < 0) { + return 2 + } else { + for (let j = i; j < x + i; j++) { + if (j === 7) { + return 1 + } + } + } + } + return -1 + } +} \ No newline at end of file diff --git a/utbot-js/samples/commonRecursion.js b/utbot-js/samples/commonRecursion.js new file mode 100644 index 0000000000..4e7cf3c529 --- /dev/null +++ b/utbot-js/samples/commonRecursion.js @@ -0,0 +1,19 @@ +class Recursion { + factorial(n) { + if (n < 0) + return -1 + if (n === 0) + return 1 + return n * this.factorial(n - 1) + } + + fib(n) { + if (n < 0 || n > 25) + return -1 + if (n === 0) + return 0 + if (n === 1) + return 1 + return this.fib(n - 1) + this.fib(n - 2) + } +} \ No newline at end of file diff --git a/utbot-js/samples/commonString.js b/utbot-js/samples/commonString.js new file mode 100644 index 0000000000..ce9a82a7a6 --- /dev/null +++ b/utbot-js/samples/commonString.js @@ -0,0 +1,19 @@ +class StringExamples { + + isNotBlank(cs) { + return cs.length !== 0 + } + + nullableStringBuffer(buffer, i) { + if (i >= 0) { + buffer += "Positive" + } else { + buffer += "Negative" + } + return buffer.toString() + } + + length(cs) { + return cs == null ? 0 : cs.length + } +} \ No newline at end of file diff --git a/utbot-js/samples/functionsThrowExceptionsInRow.js b/utbot-js/samples/functionsThrowExceptionsInRow.js new file mode 100644 index 0000000000..8ab637d052 --- /dev/null +++ b/utbot-js/samples/functionsThrowExceptionsInRow.js @@ -0,0 +1,18 @@ +function customError(a) { + if (a > 5) { + throw Error("MyCustomError") + } else { + return 10 + } +} + +function goodBoy(a) { + switch (a) { + case 5: + return 5 + case 10: + return 10 + default: + return 0 + } +} \ No newline at end of file diff --git a/utbot-js/samples/scenarioMultyClassNoTopLevel.js b/utbot-js/samples/scenarioMultyClassNoTopLevel.js new file mode 100644 index 0000000000..c37ce365e3 --- /dev/null +++ b/utbot-js/samples/scenarioMultyClassNoTopLevel.js @@ -0,0 +1,39 @@ +class Na { + constructor(num) { + this.num = num + } + + double() { + return this.num * 2 + } + + static test(a, b) { + return a + 2 * b + } +} + +class Kek { + foo(a, b) { + return a + b + } + + fString(a, b) { + return a + b + } + + fDel(a, b) { + return a / b + } + + fObj(a, b) { + return a.num + b.num + } + + getDone(a) { + a.done() + } + + done() { + return this.toString() + } +} \ No newline at end of file diff --git a/utbot-js/samples/scenarioObjectParameter.js b/utbot-js/samples/scenarioObjectParameter.js new file mode 100644 index 0000000000..55e510587a --- /dev/null +++ b/utbot-js/samples/scenarioObjectParameter.js @@ -0,0 +1,16 @@ +class ObjectParameter { + + constructor(a) { + this.first = a + } + + performAction(value) { + return 2 * value + } +} + +function functionToTest(obj, v) { + return obj.performAction(v) +} + +functionToTest(new ObjectParameter(5), 5) \ No newline at end of file diff --git a/utbot-js/samples/scenarioStaticMethod.js b/utbot-js/samples/scenarioStaticMethod.js new file mode 100644 index 0000000000..783937e859 --- /dev/null +++ b/utbot-js/samples/scenarioStaticMethod.js @@ -0,0 +1,13 @@ +class Object { + + constructor(a) { + this.first = a + } + + static functionToTest(value) { + if (value > 1024 && value < 1026) { + return 2 * value + } + return value + } +} diff --git a/utbot-js/samples/scenarioThrowError.js b/utbot-js/samples/scenarioThrowError.js new file mode 100644 index 0000000000..416c43d1bb --- /dev/null +++ b/utbot-js/samples/scenarioThrowError.js @@ -0,0 +1,11 @@ +function functionToTest(a) { + if (a === true) { + throw Error("err") + } else if (a === 1) { + while (true) { + } + } else { + return -1 + } + +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt new file mode 100644 index 0000000000..43ab39d189 --- /dev/null +++ b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt @@ -0,0 +1,354 @@ +package api + +import codegen.JsCodeGenerator +import com.oracle.js.parser.ErrorManager +import com.oracle.js.parser.Parser +import com.oracle.js.parser.ScriptEnvironment +import com.oracle.js.parser.Source +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import framework.api.js.JsClassId +import framework.api.js.JsMethodId +import framework.api.js.JsMultipleClassId +import framework.api.js.util.isJsBasic +import framework.api.js.util.jsErrorClassId +import fuzzer.JsFuzzer +import fuzzer.providers.JsObjectModelProvider +import org.graalvm.polyglot.Context +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtExplicitlyThrownException +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.UtFuzzedExecution +import parser.JsClassAstVisitor +import parser.JsFunctionAstVisitor +import parser.JsFuzzerAstVisitor +import parser.JsParserUtils +import parser.JsToplevelFunctionAstVisitor +import service.CoverageServiceProvider +import service.ServiceContext +import service.TernService +import settings.JsDynamicSettings +import settings.JsTestGenerationSettings.dummyClassName +import utils.PathResolver +import utils.constructClass +import utils.toJsAny +import java.io.File + + +class JsTestGenerator( + private val fileText: String, + private var sourceFilePath: String, + private var projectPath: String = sourceFilePath.substringBeforeLast("/"), + private val selectedMethods: List? = null, + private var parentClassName: String? = null, + private var outputFilePath: String?, + private val exportsManager: (List) -> Unit, + private val settings: JsDynamicSettings, +) { + + private val exports = mutableSetOf() + + private lateinit var parsedFile: FunctionNode + + private val utbotDir = "utbotJs" + + init { + fixPathDelims() + } + + private fun fixPathDelims() { + projectPath = projectPath.replace("\\", "/") + outputFilePath = outputFilePath?.replace("\\", "/") + sourceFilePath = sourceFilePath.replace("\\", "/") + } + + /** + * Returns String representation of generated tests. + */ + fun run(): String { + parsedFile = runParser(fileText) + val context = ServiceContext( + utbotDir = utbotDir, + projectPath = projectPath, + filePathToInference = sourceFilePath, + parsedFile = parsedFile, + settings = settings, + ) + val ternService = TernService(context) + val paramNames = mutableMapOf>() + val testSets = mutableListOf() + val classNode = + JsParserUtils.searchForClassDecl( + className = parentClassName, + parsedFile = parsedFile, + strict = selectedMethods?.isNotEmpty() ?: false + ) + parentClassName = classNode?.ident?.name?.toString() + val classId = makeJsClassId(classNode, ternService) + val methods = makeMethodsToTest() + if (methods.isEmpty()) throw IllegalArgumentException("No methods to test were found!") + methods.forEach { funcNode -> + makeTestsForMethod(classId, funcNode, classNode, context, testSets, paramNames) + } + val importPrefix = makeImportPrefix() + val codeGen = JsCodeGenerator( + classUnderTest = classId, + paramNames = paramNames, + importPrefix = importPrefix + ) + return codeGen.generateAsStringWithTestReport(testSets).generatedCode + } + + private fun makeTestsForMethod( + classId: JsClassId, + funcNode: FunctionNode, + classNode: ClassNode?, + context: ServiceContext, + testSets: MutableList, + paramNames: MutableMap> + ) { + val execId = classId.allMethods.find { + it.name == funcNode.name.toString() + } ?: throw IllegalStateException() + manageExports(classNode, funcNode, execId) + val (concreteValues, fuzzedValues) = runFuzzer(funcNode, execId) + val (allCoveredStatements, executionResults) = + CoverageServiceProvider(context).get( + settings.coverageMode, + fuzzedValues, + execId, + classNode + ) + val testsForGenerator = mutableListOf() + val errorsForGenerator = mutableMapOf() + executionResults.forEachIndexed { index, value -> + if (value == "Error:Timeout") { + errorsForGenerator["Timeout in generating test for ${ + fuzzedValues[index] + .joinToString { f -> f.model.toString() } + } parameters"] = 1 + } + } + + analyzeCoverage(allCoveredStatements).forEach { paramIndex -> + val param = fuzzedValues[paramIndex] + val result = + getUtModelResult( + execId = execId, + returnText = executionResults[paramIndex] + ) + val thisInstance = makeThisInstance(execId, classId, concreteValues) + val initEnv = EnvironmentModels(thisInstance, param.map { it.model }, mapOf()) + testsForGenerator.add( + UtFuzzedExecution( + stateBefore = initEnv, + stateAfter = initEnv, + result = result, + ) + ) + } + val testSet = CgMethodTestSet( + execId, + testsForGenerator, + errorsForGenerator + ) + testSets += testSet + paramNames[execId] = funcNode.parameters.map { it.name.toString() } + } + + private fun makeImportPrefix(): String { + return outputFilePath?.let { + PathResolver.getRelativePath( + File(it).parent, + File(sourceFilePath).parent, + ) + } ?: "" + } + + private fun makeThisInstance( + execId: JsMethodId, + classId: JsClassId, + concreteValues: Set + ): UtModel? { + val thisInstance = when { + execId.isStatic -> null + classId.allConstructors.first().parameters.isEmpty() -> { + val id = JsObjectModelProvider.idGenerator.asInt + val constructor = classId.allConstructors.first() + val instantiationCall = UtExecutableCallModel( + instance = null, + executable = constructor, + params = emptyList(), + ) + UtAssembleModel( + id = id, + classId = constructor.classId, + modelName = "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), + instantiationCall = instantiationCall, + ) + } + + else -> { + JsObjectModelProvider.generate( + FuzzedMethodDescription( + name = "thisInstance", + returnType = voidClassId, + parameters = listOf(classId), + concreteValues = concreteValues + ) + ).take(10).toList() + .shuffled().map { it.value.model }.first() + } + } + return thisInstance + } + + private fun getUtModelResult( + execId: JsMethodId, + returnText: String + ): UtExecutionResult { + val (returnValue, valueClassId) = returnText.toJsAny(execId.returnType) + val result = JsUtModelConstructor().construct(returnValue, valueClassId) + val utExecResult = when (result.classId) { + jsErrorClassId -> UtExplicitlyThrownException(Throwable(returnValue.toString()), false) + else -> UtExecutionSuccess(result) + } + return utExecResult + } + + private fun runFuzzer( + funcNode: FunctionNode, + execId: JsMethodId + ): Pair, List>> { + val fuzzerVisitor = JsFuzzerAstVisitor() + funcNode.body.accept(fuzzerVisitor) + val methodUnderTestDescription = + FuzzedMethodDescription(execId, fuzzerVisitor.fuzzedConcreteValues).apply { + compilableName = funcNode.name.toString() + val names = funcNode.parameters.map { it.name.toString() } + parameterNameMap = { index -> names.getOrNull(index) } + } + val fuzzedValues = + JsFuzzer.jsFuzzing(methodUnderTestDescription = methodUnderTestDescription).toList() + return fuzzerVisitor.fuzzedConcreteValues.toSet() to fuzzedValues + } + + private fun manageExports( + classNode: ClassNode?, + funcNode: FunctionNode, + execId: JsMethodId + ) { + val obligatoryExport = (classNode?.ident?.name ?: funcNode.ident.name).toString() + val collectedExports = collectExports(execId) + exports += (collectedExports + obligatoryExport) + exportsManager(exports.toList()) + } + + private fun makeMethodsToTest(): List { + return selectedMethods?.map { + getFunctionNode( + focusedMethodName = it, + parentClassName = parentClassName, + fileText = fileText + ) + } ?: getMethodsToTest() + } + + private fun makeJsClassId( + classNode: ClassNode?, + ternService: TernService + ): JsClassId { + return classNode?.let { + JsClassId(parentClassName!!).constructClass(ternService, classNode) + } ?: JsClassId("undefined").constructClass( + ternService = ternService, + functions = extractToplevelFunctions() + ) + } + + private fun runParser(fileText: String): FunctionNode { + // Fixes problem with Graal.polyglot missing from classpath, resulting in error. + withUtContext(UtContext(Context::class.java.classLoader)) { + val parser = Parser( + ScriptEnvironment.builder().build(), + Source.sourceFor("jsFile", fileText), + ErrorManager.ThrowErrorManager() + ) + return parser.parse() + } + } + + private fun extractToplevelFunctions(): List { + val visitor = JsToplevelFunctionAstVisitor() + parsedFile.body.accept(visitor) + return visitor.extractedMethods + } + + private fun collectExports(methodId: JsMethodId): List { + val res = mutableListOf() + methodId.parameters.forEach { + if (!(it.isJsBasic || it is JsMultipleClassId)) { + res += it.name + } + } + if (!(methodId.returnType.isJsBasic || methodId.returnType is JsMultipleClassId)) res += methodId.returnType.name + return res + } + + private fun analyzeCoverage(coverageList: List>): List { + val allCoveredBranches = mutableSetOf() + val resultList = mutableListOf() + coverageList.forEachIndexed { index, it -> + if (!allCoveredBranches.containsAll(it)) { + resultList += index + allCoveredBranches.addAll(it) + } + } + return resultList + } + + private fun getFunctionNode(focusedMethodName: String, parentClassName: String?, fileText: String): FunctionNode { + val parser = Parser( + ScriptEnvironment.builder().build(), + Source.sourceFor("jsFile", fileText), + ErrorManager.ThrowErrorManager() + ) + val fileNode = parser.parse() + val visitor = JsFunctionAstVisitor( + focusedMethodName, + if (parentClassName != dummyClassName) parentClassName else null + ) + fileNode.accept(visitor) + return visitor.targetFunctionNode + } + + private fun getMethodsToTest() = + parentClassName?.let { + getClassMethods(it) + } ?: extractToplevelFunctions().ifEmpty { + getClassMethods("") + } + + private fun getClassMethods(className: String): List { + val visitor = JsClassAstVisitor(className) + parsedFile.body.accept(visitor) + val classNode = JsParserUtils.searchForClassDecl(className, parsedFile) + return classNode?.classElements?.filter { + it.value is FunctionNode + }?.map { it.value as FunctionNode } ?: throw IllegalStateException("Can't extract methods of class $className") + } +} diff --git a/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt new file mode 100644 index 0000000000..3fc882774e --- /dev/null +++ b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt @@ -0,0 +1,63 @@ +package api + +import fuzzer.providers.JsObjectModelProvider +import org.utbot.framework.concrete.UtModelConstructorInterface +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtStatementModel +import framework.api.js.JsClassId +import framework.api.js.JsEmptyClassId +import framework.api.js.JsNullModel +import framework.api.js.JsPrimitiveModel +import framework.api.js.JsUndefinedModel +import framework.api.js.util.jsErrorClassId +import framework.api.js.util.jsUndefinedClassId + +class JsUtModelConstructor : UtModelConstructorInterface { + + // TODO SEVERE: Requires substantial expansion to other types + @Suppress("NAME_SHADOWING") + override fun construct(value: Any?, classId: ClassId): UtModel { + val classId = classId as JsClassId + when (classId) { + jsUndefinedClassId -> return JsUndefinedModel(classId) + jsErrorClassId -> return UtModel(jsErrorClassId) + } + return when (value) { + null -> JsNullModel(classId) + is Byte, + is Short, + is Char, + is Int, + is Long, + is Float, + is Double, + is String, + is Boolean -> JsPrimitiveModel(value) + + is Map<*, *> -> { + constructObject(classId, value) + } + + else -> JsUndefinedModel(classId) + } + } + + @Suppress("UNCHECKED_CAST") + private fun constructObject(classId: JsClassId, value: Any?): UtModel { + val constructor = classId.allConstructors.first() + val values = (value as Map).values.map { + construct(it, JsEmptyClassId()) + } + val id = JsObjectModelProvider.idGenerator.asInt + val instantiationCall = UtExecutableCallModel(null, constructor, values) + return UtAssembleModel( + id = id, + classId = constructor.classId, + modelName = "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), + instantiationCall = instantiationCall, + ) + } +} diff --git a/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt new file mode 100644 index 0000000000..a5c09204dc --- /dev/null +++ b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt @@ -0,0 +1,86 @@ +package codegen + +import framework.codegen.JsCgLanguageAssistant +import framework.codegen.Mocha +import org.utbot.framework.codegen.ForceStaticMocking +import org.utbot.framework.codegen.HangingTestsTimeout +import org.utbot.framework.codegen.ParametrizedTestSource +import org.utbot.framework.codegen.RegularImport +import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.StaticsMocking +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.model.CodeGeneratorResult +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.tree.CgTestClassFile +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockFramework +import framework.api.js.JsClassId +import settings.JsTestGenerationSettings.fileUnderTestAliases + +class JsCodeGenerator( + private val classUnderTest: JsClassId, + paramNames: MutableMap> = mutableMapOf(), + testFramework: TestFramework = Mocha, + runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + enableTestsTimeout: Boolean = true, + testClassPackageName: String = classUnderTest.packageName, + importPrefix: String, +) { + private var context: CgContext = CgContext( + classUnderTest = classUnderTest, + paramNames = paramNames, + testFramework = testFramework, + mockFramework = MockFramework.MOCKITO, + codegenLanguage = CodegenLanguage.defaultItem, + cgLanguageAssistant = JsCgLanguageAssistant, + parametrizedTestSource = ParametrizedTestSource.defaultItem, + staticsMocking = StaticsMocking.defaultItem, + forceStaticMocking = ForceStaticMocking.defaultItem, + generateWarningsForStaticMocking = true, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + hangingTestsTimeout = hangingTestsTimeout, + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName, + collectedImports = mutableSetOf( + RegularImport("assert", "assert"), + RegularImport( + fileUnderTestAliases, + "./$importPrefix/${classUnderTest.filePath.substringAfterLast("/")}" + ) + ) + ) + + fun generateAsStringWithTestReport( + cgTestSets: List, + testClassCustomName: String? = null, + ): CodeGeneratorResult = withCustomContext(testClassCustomName) { + val testClassModel = TestClassModel(classUnderTest, cgTestSets) + val testClassFile = CgTestClassConstructor(context).construct(testClassModel) + CodeGeneratorResult(renderClassFile(testClassFile), testClassFile.testsGenerationReport) + } + + private fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { + val prevContext = context + return try { + context = prevContext.copy( + shouldOptimizeImports = true, + testClassCustomName = testClassCustomName + ) + block() + } finally { + context = prevContext + } + } + + private fun renderClassFile(file: CgTestClassFile): String { + val renderer = CgAbstractRenderer.makeRenderer(context) + file.accept(renderer) + return renderer.toString() + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt b/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt new file mode 100644 index 0000000000..aca05e313e --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt @@ -0,0 +1,122 @@ +package framework.api.js + +import java.lang.reflect.Modifier +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtModel +import framework.api.js.util.toJsClassId +import org.utbot.framework.plugin.api.primitiveModelValueToClassId + +open class JsClassId( + private val jsName: String, + private val methods: Sequence = emptySequence(), + private val constructor: JsConstructorId? = null, + private val classPackagePath: String = "", + private val classFilePath: String = "", + elementClassId: JsClassId? = null +) : ClassId(jsName, elementClassId) { + override val simpleName: String + get() = jsName + + override val allMethods: Sequence + get() = methods + + override val allConstructors: Sequence + get() = if (constructor == null) emptySequence() else sequenceOf(constructor) + + override val packageName: String + get() = classPackagePath + + override val canonicalName: String + get() = jsName + + override val isAnonymous: Boolean + get() = false + + override val isInDefaultPackage: Boolean + get() = false + + override val isInner: Boolean + get() = false + + override val isLocal: Boolean + get() = false + + override val isNested: Boolean + get() = false + + override val isNullable: Boolean + get() = false + + override val isSynthetic: Boolean + get() = false + + override val outerClass: Class<*>? + get() = null + + val filePath: String + get() = classFilePath + +} + +class JsEmptyClassId : JsClassId("empty") +class JsMethodId( + override var classId: JsClassId, + override val name: String, + private val returnTypeNotLazy: JsClassId, + private val parametersNotLazy: List, + private val staticModifier: Boolean = false, + private val lazyReturnType: Lazy? = null, + private val lazyParameters: Lazy>? = null +) : MethodId(classId, name, returnTypeNotLazy, parametersNotLazy) { + + override val parameters: List + get() = lazyParameters?.value ?: parametersNotLazy + + override val returnType: JsClassId + get() = lazyReturnType?.value ?: returnTypeNotLazy + + override val modifiers: Int + get() = if (staticModifier) Modifier.STATIC else 0 + +} + +class JsConstructorId( + override var classId: JsClassId, + override val parameters: List, +) : ConstructorId(classId, parameters) { + override val modifiers: Int + get() = 0 +} + +class JsMultipleClassId(private val jsJoinedName: String) : JsClassId(jsJoinedName) { + + val types: Sequence + get() = jsJoinedName.split('|').map { JsClassId(it) }.asSequence() +} + +open class JsUtModel( + override val classId: JsClassId +) : UtModel(classId) + +class JsNullModel( + override val classId: JsClassId +) : JsUtModel(classId) { + override fun toString() = "null" +} + +class JsUndefinedModel( + classId: JsClassId +) : JsUtModel(classId) { + override fun toString() = "undefined" +} + +data class JsPrimitiveModel( + val value: Any, +) : JsUtModel(jsPrimitiveModelValueToClassId(value)) { + override fun toString() = value.toString() +} + +private fun jsPrimitiveModelValueToClassId(value: Any) = + primitiveModelValueToClassId(value).toJsClassId() \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt b/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt new file mode 100644 index 0000000000..4e53dbe2d7 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt @@ -0,0 +1,55 @@ +package framework.api.js.util + +import org.utbot.framework.plugin.api.ClassId +import framework.api.js.JsClassId +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.byteClassId +import org.utbot.framework.plugin.api.util.doubleClassId +import org.utbot.framework.plugin.api.util.floatClassId +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.longClassId +import org.utbot.framework.plugin.api.util.shortClassId + +val jsUndefinedClassId = JsClassId("undefined") +val jsNumberClassId = JsClassId("number") +val jsBooleanClassId = JsClassId("bool") +val jsDoubleClassId = JsClassId("double") +val jsStringClassId = JsClassId("string") +val jsErrorClassId = JsClassId("error") + + +val jsPrimitives = setOf( + jsNumberClassId, + jsBooleanClassId, + jsDoubleClassId, +) + +val jsBasic = setOf( + jsNumberClassId, + jsBooleanClassId, + jsDoubleClassId, + jsUndefinedClassId, + jsStringClassId, +) + +fun ClassId.toJsClassId() = + when { + this == intClassId -> jsNumberClassId + this == byteClassId -> jsNumberClassId + this == shortClassId -> jsNumberClassId + this == booleanClassId -> jsBooleanClassId + this == doubleClassId -> jsDoubleClassId + this == floatClassId -> jsDoubleClassId + this.name.lowercase().contains("string") -> jsStringClassId + this == longClassId -> jsNumberClassId + else -> jsUndefinedClassId + } + +val JsClassId.isJsBasic: Boolean + get() = this in jsBasic + +val JsClassId.isJsPrimitive: Boolean + get() = this in jsPrimitives + +val JsClassId.isUndefined: Boolean + get() = this == jsUndefinedClassId \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsCgLanguageAssistant.kt b/utbot-js/src/main/kotlin/framework/codegen/JsCgLanguageAssistant.kt new file mode 100644 index 0000000000..c89ebfa483 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsCgLanguageAssistant.kt @@ -0,0 +1,47 @@ +package framework.codegen + +import framework.codegen.model.constructor.tree.JsCgCallableAccessManager +import framework.codegen.model.constructor.tree.JsCgMethodConstructor +import framework.codegen.model.constructor.tree.JsCgStatementConstructor +import framework.codegen.model.constructor.tree.JsCgVariableConstructor +import framework.codegen.model.constructor.visitor.CgJsRenderer +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CgLanguageAssistant +import org.utbot.framework.plugin.api.utils.testClassNameGenerator + +object JsCgLanguageAssistant : CgLanguageAssistant() { + + override val outerMostTestClassContent: TestClassContext = TestClassContext() + + override val extension: String + get() = ".js" + + override val languageKeywords: Set = setOf( + "abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", + "debugger", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", + "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", + "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var", "void", + "volatile", "while", "with", "yield" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return testClassNameGenerator(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = CgJsRenderer(context, printer) + override fun getCallableAccessManagerBy(context: CgContext) = JsCgCallableAccessManager(context) + override fun getMethodConstructorBy(context: CgContext) = JsCgMethodConstructor(context) + override fun getStatementConstructorBy(context: CgContext) = JsCgStatementConstructor(context) + override fun getVariableConstructorBy(context: CgContext) = JsCgVariableConstructor(context) + override fun getLanguageTestFrameworkManager() = JsTestFrameworkManager() +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt new file mode 100644 index 0000000000..08f571f327 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt @@ -0,0 +1,67 @@ +package framework.codegen + +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId +import framework.api.js.JsClassId +import framework.api.js.util.jsErrorClassId +import framework.api.js.util.jsUndefinedClassId + + +object Mocha : TestFramework(id = "Mocha", displayName = "Mocha") { + override val mainPackage = "" + override val assertionsClass = jsUndefinedClassId + override val arraysAssertionsClass = jsUndefinedClassId + override val testAnnotation = "" + override val testAnnotationFqn = "" + + override val parameterizedTestAnnotation: String + get() = throw UnsupportedOperationException("Parameterized tests are not supported for Mocha") + override val parameterizedTestAnnotationFqn: String + get() = throw UnsupportedOperationException("Parameterized tests are not supported for Mocha") + override val methodSourceAnnotation: String + get() = throw UnsupportedOperationException("Parameterized tests are not supported for Mocha") + override val methodSourceAnnotationFqn: String + get() = throw UnsupportedOperationException("Parameterized tests are not supported for Mocha") + + override val nestedClassesShouldBeStatic: Boolean + get() = false + override val argListClassId: ClassId + get() = jsUndefinedClassId + + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List { + throw UnsupportedOperationException() + } + + override val testAnnotationId = BuiltinClassId( + name = "Mocha", + canonicalName = "Mocha", + simpleName = "Test" + ) + + override val parameterizedTestAnnotationId = jsUndefinedClassId + override val methodSourceAnnotationId = jsUndefinedClassId +} + +internal val jsAssertEquals by lazy { + BuiltinMethodId( + JsClassId("assert.deepEqual"), "assert.deepEqual", jsUndefinedClassId, listOf( + jsUndefinedClassId, jsUndefinedClassId + ) + ) +} + +internal val jsAssertThrows by lazy { + BuiltinMethodId( + JsClassId("assert.throws"), "assert.throws", jsErrorClassId, listOf( + jsUndefinedClassId, jsUndefinedClassId, jsUndefinedClassId + ) + ) +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsTestFrameworkManager.kt b/utbot-js/src/main/kotlin/framework/codegen/JsTestFrameworkManager.kt new file mode 100644 index 0000000000..9523c5e9a5 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsTestFrameworkManager.kt @@ -0,0 +1,17 @@ +package framework.codegen + +import framework.codegen.model.constructor.tree.MochaManager +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.plugin.api.LanguageTestFrameworkManager + +class JsTestFrameworkManager: LanguageTestFrameworkManager() { + + override fun managerByFramework(context: CgContext) = when (context.testFramework) { + is Mocha -> MochaManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Mocha + + override val testFrameworks = listOf(Mocha) +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt new file mode 100644 index 0000000000..1f3595ff16 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt @@ -0,0 +1,56 @@ +package framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.tree.CgIncompleteMethodCall +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId + +class JsCgCallableAccessManager(context: CgContext) : CgCallableAccessManager, + CgContextOwner by context { + + override operator fun CgExpression?.get(methodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(methodId, this) + + override operator fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(staticMethodId, null) + + override fun CgExpression.get(fieldId: FieldId): CgExpression { + TODO("Not yet implemented") + } + + override fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess { + TODO("Not yet implemented") + } + + override operator fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { + val resolvedArgs = args.resolve() + val constructorCall = CgConstructorCall(this, resolvedArgs) + newConstructorCall(this) + return constructorCall + } + + override fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { + val resolvedArgs = args.resolve() + val methodCall = CgMethodCall(caller, method, resolvedArgs) + newMethodCall(method) + return methodCall + } + + private fun newConstructorCall(constructorId: ConstructorId) { + importedClasses += constructorId.classId + } + + private fun newMethodCall(methodId: MethodId) { + if (methodId.classId.name == "undefined") { + importedStaticMethods += methodId + return + } + importedClasses += methodId.classId + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt new file mode 100644 index 0000000000..1d49ab0bb0 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt @@ -0,0 +1,133 @@ +package framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor +import org.utbot.framework.codegen.model.tree.CgTestMethod +import org.utbot.framework.codegen.model.tree.CgTestMethodType +import org.utbot.framework.codegen.model.tree.CgValue +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.plugin.api.ConcreteExecutionFailureException +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtExecution +import framework.api.js.JsClassId +import org.utbot.framework.plugin.api.onFailure +import org.utbot.framework.plugin.api.onSuccess +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.util.isUnit +import java.security.AccessControlException + +class JsCgMethodConstructor(ctx: CgContext) : CgMethodConstructor(ctx) { + + override fun assertEquality(expected: CgValue, actual: CgVariable) { + testFrameworkManager.assertEquals(expected, actual) + } + + override fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = + withTestMethodScope(execution) { + val testMethodName = nameGenerator.testMethodNameFor(executableId, execution.testMethodName) + execution.displayName = execution.displayName?.let { "${executableId.name}: $it" } + testMethod(testMethodName, execution.displayName) { + val statics = currentExecution!!.stateBefore.statics + rememberInitialStaticFields(statics) + val mainBody = { + substituteStaticFields(statics) + // build this instance + thisInstance = execution.stateBefore.thisInstance?.let { + variableConstructor.getOrCreateVariable(it) + } + // build arguments + for ((index, param) in execution.stateBefore.parameters.withIndex()) { + val name = paramNames[executableId]?.get(index) + methodArguments += variableConstructor.getOrCreateVariable(param, name) + } + recordActualResult() + generateResultAssertions() + generateFieldStateAssertions() + } + + if (statics.isNotEmpty()) { + +tryBlock { + mainBody() + }.finally { + recoverStaticFields() + } + } else { + mainBody() + } + } + } + + override fun generateResultAssertions() { + emptyLineIfNeeded() + val currentExecution = currentExecution!! + val method = currentExecutable as MethodId + // build assertions + currentExecution.result + .onSuccess { result -> + methodType = CgTestMethodType.SUCCESSFUL + if (result.isUnit() || method.returnType == voidClassId) { + +thisInstance[method](*methodArguments.toTypedArray()) + } else { + resultModel = result + val expected = variableConstructor.getOrCreateVariable(result, "expected") + assertEquality(expected, actual) + } + } + .onFailure { exception -> + processExecutionFailure(currentExecution, exception) + } + } + + private fun processExecutionFailure(execution: UtExecution, exception: Throwable) { + val methodInvocationBlock = { + with(currentExecutable) { + when (this) { + is MethodId -> thisInstance[this](*methodArguments.toTypedArray()).intercepted() + is ConstructorId -> this(*methodArguments.toTypedArray()).intercepted() + else -> throw IllegalStateException() + } + } + } + + if (shouldTestPassWithException(execution, exception)) { + testFrameworkManager.expectException(JsClassId(exception.message!!)) { + methodInvocationBlock() + } + methodType = CgTestMethodType.SUCCESSFUL + + return + } + + if (shouldTestPassWithTimeoutException(execution, exception)) { + writeWarningAboutTimeoutExceeding() + testFrameworkManager.expectTimeout(hangingTestsTimeout.timeoutMs) { + methodInvocationBlock() + } + methodType = CgTestMethodType.TIMEOUT + + return + } + + when (exception) { + is ConcreteExecutionFailureException -> { + methodType = CgTestMethodType.CRASH + writeWarningAboutCrash() + } + + is AccessControlException -> { + methodType = CgTestMethodType.CRASH + writeWarningAboutFailureTest(exception) + return + } + + else -> { + methodType = CgTestMethodType.FAILING + writeWarningAboutFailureTest(exception) + } + } + + methodInvocationBlock() + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt new file mode 100644 index 0000000000..51c20fadea --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt @@ -0,0 +1,284 @@ +package framework.codegen.model.constructor.tree + +import fj.data.Either +import framework.codegen.model.constructor.util.plus +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor +import org.utbot.framework.codegen.model.constructor.util.ExpressionWithType +import org.utbot.framework.codegen.model.tree.CgAnnotation +import org.utbot.framework.codegen.model.tree.CgAnonymousFunction +import org.utbot.framework.codegen.model.tree.CgComment +import org.utbot.framework.codegen.model.tree.CgDeclaration +import org.utbot.framework.codegen.model.tree.CgEmptyLine +import org.utbot.framework.codegen.model.tree.CgExpression +import org.utbot.framework.codegen.model.tree.CgForEachLoopBuilder +import org.utbot.framework.codegen.model.tree.CgForLoopBuilder +import org.utbot.framework.codegen.model.tree.CgIfStatement +import org.utbot.framework.codegen.model.tree.CgInnerBlock +import org.utbot.framework.codegen.model.tree.CgIsInstance +import org.utbot.framework.codegen.model.tree.CgLogicalAnd +import org.utbot.framework.codegen.model.tree.CgLogicalOr +import org.utbot.framework.codegen.model.tree.CgMultilineComment +import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument +import org.utbot.framework.codegen.model.tree.CgParameterDeclaration +import org.utbot.framework.codegen.model.tree.CgReturnStatement +import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation +import org.utbot.framework.codegen.model.tree.CgSingleLineComment +import org.utbot.framework.codegen.model.tree.CgThrowStatement +import org.utbot.framework.codegen.model.tree.CgTryCatch +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.codegen.model.tree.buildAssignment +import org.utbot.framework.codegen.model.tree.buildDeclaration +import org.utbot.framework.codegen.model.tree.buildDoWhileLoop +import org.utbot.framework.codegen.model.tree.buildForLoop +import org.utbot.framework.codegen.model.tree.buildTryCatch +import org.utbot.framework.codegen.model.tree.buildWhileLoop +import org.utbot.framework.codegen.model.util.buildExceptionHandler +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.UtModel + +class JsCgStatementConstructor(context: CgContext) : + CgStatementConstructor, + CgContextOwner by context, + CgCallableAccessManager by CgTestClassConstructor.CgComponents.getCallableAccessManagerBy(context) { + + private val nameGenerator = CgTestClassConstructor.CgComponents.getNameGeneratorBy(context) + + override fun newVar( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutable: Boolean, + init: () -> CgExpression + ): CgVariable { + val declarationOrVar: Either = + createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType, + model, + baseName, + isMock, + isMutable, + init + ) + + return declarationOrVar.either( + { declaration -> + currentBlock += declaration + + declaration.variable + }, + { variable -> variable } + ) + } + + override fun createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutableVar: Boolean, + init: () -> CgExpression + ): Either { + + val baseExpr = init() + + val name = nameGenerator.variableName(baseType, baseName, isMock) + + // TODO SEVERE: here was import section for CgClassId. Implement it +// importIfNeeded(baseType) +// if ((baseType as JsClassId).name != "undefined") { +// importedClasses += baseType +// } + + val declaration = buildDeclaration { + variableType = baseType + variableName = name + initializer = baseExpr + isMutable = isMutableVar + } + + updateVariableScope(declaration.variable, model) + + return Either.left(declaration) + } + + override fun CgExpression.`=`(value: Any?) { + currentBlock += buildAssignment { + lValue = this@`=` + rValue = value.resolve() + } + } + + override fun CgExpression.and(other: CgExpression): CgLogicalAnd = + CgLogicalAnd(this, other) + + + override fun CgExpression.or(other: CgExpression): CgLogicalOr = + CgLogicalOr(this, other) + + override fun ifStatement( + condition: CgExpression, + trueBranch: () -> Unit, + falseBranch: (() -> Unit)? + ): CgIfStatement { + val trueBranchBlock = block(trueBranch) + val falseBranchBlock = falseBranch?.let { block(it) } + return CgIfStatement(condition, trueBranchBlock, falseBranchBlock).also { + currentBlock += it + } + } + + override fun forLoop(init: CgForLoopBuilder.() -> Unit) { + currentBlock += buildForLoop(init) + } + + override fun whileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun doWhileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildDoWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun forEachLoop(init: CgForEachLoopBuilder.() -> Unit) { + throw UnsupportedOperationException("JavaScript does not have forEach loops") + } + + override fun getClassOf(classId: ClassId): CgExpression { + TODO("Not yet implemented") + } + + override fun createFieldVariable(fieldId: FieldId): CgVariable { + TODO("Not yet implemented") + } + + override fun createExecutableVariable(executableId: ExecutableId, arguments: List): CgVariable { + TODO("Not yet implemented") + } + + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) + + override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = + buildTryCatch { + statements = block(init) + this.resources = resources + } + + override fun CgTryCatch.catch(exception: ClassId, init: (CgVariable) -> Unit): CgTryCatch { + val newHandler = buildExceptionHandler { + val e = declareVariable(exception, nameGenerator.variableName(exception.simpleName.decapitalize())) + this.exception = e + this.statements = block { init(e) } + } + return this.copy(handlers = handlers + newHandler) + } + + override fun CgTryCatch.finally(init: () -> Unit): CgTryCatch { + val finallyBlock = block(init) + return this.copy(finally = finallyBlock) + } + + override fun CgExpression.isInstance(value: CgExpression): CgIsInstance { + TODO("Not yet implemented") + } + + // TODO MINOR: check whether js has inner blocks + override fun innerBlock(init: () -> Unit): CgInnerBlock = + CgInnerBlock(block(init)).also { + currentBlock += it + } + + override fun comment(text: String): CgComment = + CgSingleLineComment(text).also { + currentBlock += it + } + + override fun comment(): CgComment = + CgSingleLineComment("").also { + currentBlock += it + } + + override fun multilineComment(lines: List): CgComment = + CgMultilineComment(lines).also { + currentBlock += it + } + + override fun lambda(type: ClassId, vararg parameters: CgVariable, body: () -> Unit): CgAnonymousFunction { + return withNameScope { + for (parameter in parameters) { + declareParameter(parameter.type, parameter.name) + } + val paramDeclarations = parameters.map { CgParameterDeclaration(it) } + CgAnonymousFunction(type, paramDeclarations, block(body)) + } + } + + override fun annotation(classId: ClassId, argument: Any?): CgAnnotation { + val annotation = CgSingleArgAnnotation(classId, argument.resolve()) + addAnnotation(annotation) + return annotation + } + + override fun annotation(classId: ClassId, namedArguments: List>): CgAnnotation { + val annotation = CgMultipleArgsAnnotation( + classId, + namedArguments.mapTo(mutableListOf()) { (name, value) -> CgNamedAnnotationArgument(name, value) } + ) + addAnnotation(annotation) + return annotation + } + + override fun annotation( + classId: ClassId, + buildArguments: MutableList>.() -> Unit + ): CgAnnotation { + val arguments = mutableListOf>() + .apply(buildArguments) + .map { (name, value) -> CgNamedAnnotationArgument(name, value) } + val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList()) + addAnnotation(annotation) + return annotation + } + + override fun returnStatement(expression: () -> CgExpression) { + currentBlock += CgReturnStatement(expression()) + } + + override fun throwStatement(exception: () -> CgExpression): CgThrowStatement = + CgThrowStatement(exception()).also { currentBlock += it } + + override fun emptyLine() { + currentBlock += CgEmptyLine() + } + + override fun emptyLineIfNeeded() { + val lastStatement = currentBlock.lastOrNull() ?: return + if (lastStatement is CgEmptyLine) return + emptyLine() + } + + override fun declareVariable(type: ClassId, name: String): CgVariable = + CgVariable(name, type).also { + updateVariableScope(it) + } + + // TODO SEVERE: think about these 2 functions + override fun guardExpression(baseType: ClassId, expression: CgExpression): ExpressionWithType = + ExpressionWithType(baseType, expression) + + override fun wrapTypeIfRequired(baseType: ClassId): ClassId = baseType +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt new file mode 100644 index 0000000000..57c619225d --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt @@ -0,0 +1,36 @@ +package framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.constructor.tree.CgVariableConstructor +import org.utbot.framework.codegen.model.tree.CgLiteral +import org.utbot.framework.codegen.model.tree.CgValue +import org.utbot.framework.codegen.model.util.nullLiteral +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtReferenceModel +import framework.api.js.JsPrimitiveModel + +class JsCgVariableConstructor(ctx: CgContext) : CgVariableConstructor(ctx) { + + private val nameGenerator = CgTestClassConstructor.CgComponents.getNameGeneratorBy(ctx) + + override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { + val baseName = name ?: nameGenerator.nameFrom(model.classId) + return if (model is UtReferenceModel) valueByModelId.getOrPut(model.id) { + when (model) { + is UtCompositeModel -> TODO() + is UtAssembleModel -> constructAssemble(model, baseName) + is UtArrayModel -> TODO() + else -> TODO() + } + } else valueByModel.getOrPut(model) { + when (model) { + is JsPrimitiveModel -> CgLiteral(model.classId, model.value) + else -> nullLiteral() + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt new file mode 100644 index 0000000000..b40057d896 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt @@ -0,0 +1,58 @@ +package framework.codegen.model.constructor.tree + +import framework.codegen.Mocha +import framework.codegen.jsAssertEquals +import framework.codegen.jsAssertThrows +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.tree.CgAnnotation +import org.utbot.framework.codegen.model.tree.CgValue +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.plugin.api.ClassId + +class MochaManager(context: CgContext) : TestFrameworkManager(context) { + override val isExpectedExceptionExecutionBreaking: Boolean = true + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Mocha) { "According to settings, Mocha.js was expected, but got: $testFramework" } + val lambda = statementConstructor.lambda(exception) { block() } + +assertions[jsAssertThrows](lambda, "Error", exception.name) + } + + override fun createDataProviderAnnotations(dataProviderMethodName: String): MutableList { + TODO("Not yet implemented") + } + + override fun createArgList(length: Int): CgVariable { + TODO("Not yet implemented") + } + + override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?): Set { + TODO("Not yet implemented") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) { + TODO("Not yet implemented") + } + + override val dataProviderMethodsHolder: TestClassContext + get() = TODO("Not yet implemented") + override val annotationForNestedClasses: CgAnnotation + get() = TODO("Not yet implemented") + override val annotationForOuterClasses: CgAnnotation + get() = TODO("Not yet implemented") + + override fun assertEquals(expected: CgValue, actual: CgValue) { + +assertions[jsAssertEquals](expected, actual) + } + + override fun disableTestMethod(reason: String) { + + } + +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt new file mode 100644 index 0000000000..4b33d306c2 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -0,0 +1,16 @@ +package framework.codegen.model.constructor.util + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet + +internal operator fun PersistentList.plus(element: T): PersistentList = + this.add(element) + +internal operator fun PersistentList.plus(other: PersistentList): PersistentList = + this.addAll(other) + +internal operator fun PersistentSet.plus(element: T): PersistentSet = + this.add(element) + +internal operator fun PersistentSet.plus(other: PersistentSet): PersistentSet = + this.addAll(other) diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt new file mode 100644 index 0000000000..00e8d335a4 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt @@ -0,0 +1,372 @@ +package framework.codegen.model.constructor.visitor + +import org.apache.commons.text.StringEscapeUtils +import org.utbot.framework.codegen.RegularImport +import org.utbot.framework.codegen.StaticImport +import org.utbot.framework.codegen.isLanguageKeyword +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.util.CgPrinterImpl +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeParameters +import org.utbot.framework.plugin.api.util.isStatic +import settings.JsTestGenerationSettings.fileUnderTestAliases + +internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : + CgAbstractRenderer(context, printer) { + + override val statementEnding: String = "" + + override val logicalAnd: String + get() = "&&" + + override val logicalOr: String + get() = "||" + + override val langPackage: String = "js" + + override val ClassId.methodsAreAccessibleAsTopLevel: Boolean + get() = false + + override fun visit(element: CgErrorWrapper) { + element.expression.accept(this) + print("alert(\"${element.message}\")") + } + + override fun visit(element: CgInnerBlock) { + println("{") + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + } + println("}") + } + + override fun visit(element: CgParameterDeclaration) { + if (element.isVararg) { + print("...") + } + print(element.name.escapeNamePossibleKeyword()) + } + + override fun visit(element: CgLiteral) { + val value = with(element.value) { + when (this) { + is Double -> toStringConstant() + is String -> "\"" + escapeCharacters() + "\"" + else -> "$this" + } + } + print(value) + } + + private fun Double.toStringConstant() = when { + isNaN() -> "Number.NaN" + this == Double.POSITIVE_INFINITY -> "Number.POSITIVE_INFINITY" + this == Double.NEGATIVE_INFINITY -> "Number.NEGATIVE_INFINITY" + else -> "$this" + } + + override fun visit(element: CgStaticsRegion) { + if (element.content.isEmpty()) return + + print(regionStart) + element.header?.let { print(" $it") } + println() + + withIndent { + for (item in element.content) { + println() + item.accept(this) + } + } + + println(regionEnd) + } + + + override fun visit(element: CgClass) { + element.body.accept(this) + } + + override fun visit(element: CgFieldAccess) { + element.caller.accept(this) + renderAccess(element.caller) + print(element.fieldId.name) + } + + override fun visit(element: CgArrayElementAccess) { + element.array.accept(this) + print("[") + element.index.accept(this) + print("]") + } + + override fun visit(element: CgArrayAnnotationArgument) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgAnonymousFunction) { + print("function (") + element.parameters.renderSeparated(true) + println(") {") + // cannot use visit(element.body) here because { was already printed + withIndent { + for (statement in element.body) { + statement.accept(this) + } + } + print("}") + } + + override fun visit(element: CgEqualTo) { + element.left.accept(this) + print(" == ") + element.right.accept(this) + } + + // TODO SEVERE + override fun visit(element: CgTypeCast) { + element.expression.accept(this) +// throw Exception("TypeCast not yet implemented") + } + + override fun visit(element: CgSpread) { + print("...") + element.array.accept(this) + } + + override fun visit(element: CgNotNullAssertion) { + throw UnsupportedOperationException("JavaScript does not support not null assertions") + } + + override fun visit(element: CgAllocateArray) { + print("new Array(${element.size})") + } + + override fun visit(element: CgAllocateInitializedArray) { + print("[") + element.initializer.accept(this) + print("]") + } + + // TODO SEVERE: I am unsure about this + override fun visit(element: CgArrayInitializer) { + val elementType = element.elementType + val elementsInLine = arrayElementsInLine(elementType) + + element.values.renderElements(elementsInLine) + } + + override fun visit(element: CgClassFile) { + element.imports.filterIsInstance().forEach { + renderRegularImport(it) + } + println() + element.declaredClass.accept(this) + } + + override fun visit(element: CgSwitchCaseLabel) { + if (element.label != null) { + print("case ") + element.label!!.accept(this) + } else { + print("default") + } + println(": ") + visit(element.statements, printNextLine = true) + } + + @Suppress("DuplicatedCode") + override fun visit(element: CgSwitchCase) { + print("switch (") + element.value.accept(this) + println(") {") + withIndent { + for (caseLabel in element.labels) { + caseLabel.accept(this) + } + element.defaultLabel?.accept(this) + } + println("}") + } + + override fun visit(element: CgGetLength) { + element.variable.accept(this) + print(".size") + } + + override fun visit(element: CgGetJavaClass) { + throw UnsupportedOperationException("No Java classes in JavaScript") + } + + override fun visit(element: CgGetKotlinClass) { + throw UnsupportedOperationException("No Kotlin classes in JavaScript") + } + + override fun visit(element: CgConstructorCall) { + print("new $fileUnderTestAliases.${element.executableId.classId.name}") + print("(") + element.arguments.renderSeparated() + print(")") + } + + override fun renderRegularImport(regularImport: RegularImport) { + println("const ${regularImport.packageName} = require(\"${regularImport.className}\")") + } + + override fun renderStaticImport(staticImport: StaticImport) { + throw Exception("Not implemented yet") + } + + override fun renderMethodSignature(element: CgTestMethod) { + println("it(\"${element.name}\", function ()") + } + + override fun renderMethodSignature(element: CgErrorTestMethod) { + println("it(\"${element.name}\", function ()") + + } + + override fun visit(element: CgMethod) { + super.visit(element) + if (element is CgTestMethod || element is CgErrorTestMethod) { + println(")") + } + } + + override fun visit(element: CgErrorTestMethod) { + renderMethodSignature(element) + visit(element as CgMethod) + } + + override fun visit(element: CgThrowStatement) { + // TODO: Should we render throw statement right here? + } + + override fun visit(element: CgClassBody) { + // render regions for test methods + for ((i, region) in (element.methodRegions + element.nestedClassRegions).withIndex()) { + if (i != 0) println() + + region.accept(this) + } + + if (element.staticDeclarationRegions.isEmpty()) { + return + } + } + + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgNamedAnnotationArgument) { + + } + + override fun visit(element: CgMultipleArgsAnnotation) { + + } + + override fun visit(element: CgMethodCall) { + val caller = element.caller + if (caller != null) { + caller.accept(this) + renderAccess(caller) + } else { + val method = element.executableId + if (method is BuiltinMethodId) { + + } else if (method.isStatic) { + val line = if (method.classId.toString() == "undefined") "" else "${method.classId}." + print("$fileUnderTestAliases.$line") + } else { + print("$fileUnderTestAliases.") + } + } + print(element.executableId.name.escapeNamePossibleKeyword()) + renderTypeParameters(element.typeParameters) + if (element.type.name == "error") { + print("(") + element.arguments[0].accept(this@CgJsRenderer) + print(", ") + print("Error, ") + element.arguments[2].accept(this@CgJsRenderer) + print(")") + } else { + renderExecutableCallArguments(element) + } + } + + //TODO MINOR: check + override fun renderForLoopVarControl(element: CgForLoop) { + print("for (") + with(element.initialization) { + print("let ") + visit(variable) + print(" = ") + initializer?.accept(this@CgJsRenderer) + print("; ") + visit(element.condition) + print("; ") + print(element.update) + } + } + + override fun renderDeclarationLeftPart(element: CgDeclaration) { + if (element.isMutable) print("var ") else print("let ") + visit(element.variable) + } + + override fun toStringConstantImpl(byte: Byte) = "$byte" + + override fun toStringConstantImpl(short: Short) = "$short" + + override fun toStringConstantImpl(int: Int) = "$int" + + override fun toStringConstantImpl(long: Long) = "$long" + + override fun toStringConstantImpl(float: Float) = "$float" + + override fun renderAccess(caller: CgExpression) { + print(".") + } + + override fun renderTypeParameters(typeParameters: TypeParameters) { + //TODO MINOR: check + } + + override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { + print("(") + executableCall.arguments.renderSeparated() + print(")") + } + + //TODO SEVERE: check + override fun renderExceptionCatchVariable(exception: CgVariable) { + print("${exception.name.escapeNamePossibleKeyword()}: ${exception.type}") + } + + override fun escapeNamePossibleKeywordImpl(s: String): String = + if (isLanguageKeyword(s, context.cgLanguageAssistant)) "`$s`" else s + + override fun renderClassVisibility(classId: ClassId) { + TODO("Not yet implemented") + } + + override fun renderClassModality(aClass: CgClass) { + TODO("Not yet implemented") + } + + //TODO MINOR: check + override fun String.escapeCharacters(): String = + StringEscapeUtils.escapeJava(this) + .replace("$", "\\$") + .replace("\\f", "\\u000C") + .replace("\\xxx", "\\\u0058\u0058\u0058") +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt b/utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt new file mode 100644 index 0000000000..284d57aac7 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt @@ -0,0 +1,32 @@ +package fuzzer + +import fuzzer.providers.JsConstantsModelProvider +import fuzzer.providers.JsMultipleTypesModelProvider +import fuzzer.providers.JsObjectModelProvider +import fuzzer.providers.JsPrimitivesModelProvider +import fuzzer.providers.JsStringModelProvider +import fuzzer.providers.JsUndefinedModelProvider +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.fuzz + +object JsFuzzer { + + fun jsFuzzing( + modelProvider: (ModelProvider) -> ModelProvider = { it }, + methodUnderTestDescription: FuzzedMethodDescription + ): Sequence> { + val modelProviderWithFallback = modelProvider( + ModelProvider.of( + JsConstantsModelProvider, + JsUndefinedModelProvider, + JsStringModelProvider, + JsMultipleTypesModelProvider, + JsPrimitivesModelProvider, + JsObjectModelProvider, + ) + ) + return fuzz(methodUnderTestDescription, modelProviderWithFallback) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsConstantsModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsConstantsModelProvider.kt new file mode 100644 index 0000000000..a3ec9fb598 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsConstantsModelProvider.kt @@ -0,0 +1,61 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isJsPrimitive +import framework.api.js.util.jsUndefinedClassId +import org.utbot.fuzzer.FuzzedContext +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider + +object JsConstantsModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.concreteValues + .asSequence() + .filter { (classId, _) -> + (classId as JsClassId).isJsPrimitive + } + .forEach { (_, value, op) -> + sequenceOf( + JsPrimitiveModel(value).fuzzed { summary = "%var% = $value" }, + modifyValue(value, op) + ) + .filterNotNull() + .forEach { m -> + description.parametersMap.getOrElse(m.model.classId) { + description.parametersMap.getOrElse(jsUndefinedClassId) { emptyList() } + }.forEach { index -> + yield(FuzzedParameter(index, m)) + } + } + } + } + + @Suppress("DuplicatedCode") + internal fun modifyValue(value: Any, op: FuzzedContext): FuzzedValue? { + if (op !is FuzzedContext.Comparison) return null + val multiplier = if (op == FuzzedContext.Comparison.LT || op == FuzzedContext.Comparison.GE) -1 else 1 + return when (value) { + is Boolean -> value.not() + is Byte -> value + multiplier.toByte() + is Char -> (value.code + multiplier).toChar() + is Short -> value + multiplier.toShort() + is Int -> value + multiplier + is Long -> value + multiplier.toLong() + is Float -> value + multiplier.toDouble() + is Double -> value + multiplier.toDouble() + else -> null + }?.let { + JsPrimitiveModel(it).fuzzed { + summary = "%var% ${ + (if (op == FuzzedContext.Comparison.EQ || op == FuzzedContext.Comparison.LE || op == FuzzedContext.Comparison.GE) { + op.reverse() + } else op).sign + } $value" + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsMultipleTypesModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsMultipleTypesModelProvider.kt new file mode 100644 index 0000000000..791c1456e2 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsMultipleTypesModelProvider.kt @@ -0,0 +1,74 @@ +package fuzzer.providers + +import fuzzer.providers.JsPrimitivesModelProvider.matchClassId +import fuzzer.providers.JsPrimitivesModelProvider.primitivesForString +import fuzzer.providers.JsStringModelProvider.mutate +import fuzzer.providers.JsStringModelProvider.random +import framework.api.js.JsClassId +import framework.api.js.JsMultipleClassId +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isJsPrimitive +import framework.api.js.util.jsStringClassId +import framework.api.js.util.toJsClassId +import org.utbot.fuzzer.* + +object JsMultipleTypesModelProvider : ModelProvider { + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + val parametersFiltered = description.parametersMap.filter { (classId, _) -> + classId is JsMultipleClassId + } + parametersFiltered.forEach { (jsMultipleClassId, indices) -> + val types = (jsMultipleClassId as JsMultipleClassId).types + types.forEach { classId -> + when { + classId.isJsPrimitive -> { + val concreteValuesFiltered = description.concreteValues.filter { (localClassId, _) -> + (localClassId as JsClassId).isJsPrimitive + } + concreteValuesFiltered.forEach { (_, value, op) -> + sequenceOf( + JsPrimitiveModel(value).fuzzed { summary = "%var% = $value" }, + JsConstantsModelProvider.modifyValue(value, op as FuzzedContext.Comparison) + ).filterNotNull() + .forEach { m -> + indices.forEach { index -> + yield(FuzzedParameter(index, m)) + } + } + } + matchClassId(classId).forEach { value -> + indices.forEach { index -> yield(FuzzedParameter(index, value)) } + } + } + + classId == jsStringClassId -> { + val concreteValuesFiltered = description.concreteValues + .asSequence() + .filter { (classId, _) -> classId.toJsClassId() == jsStringClassId } + concreteValuesFiltered.forEach { (_, value, op) -> + listOf(value, mutate(random, value as? String, op)) + .asSequence() + .filterNotNull() + .map { JsPrimitiveModel(it) } + .forEach { m -> + indices.forEach { index -> + yield( + FuzzedParameter( + index, + m.fuzzed { summary = "%var% = string" } + ) + ) + } + } + } + primitivesForString().forEach { value -> + indices.forEach { index -> yield(FuzzedParameter(index, value)) } + } + } + + else -> throw IllegalStateException("Not yet implemented!") + } + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt new file mode 100644 index 0000000000..aab8245e57 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt @@ -0,0 +1,90 @@ +package fuzzer.providers + +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import framework.api.js.JsClassId +import framework.api.js.JsConstructorId +import framework.api.js.util.isJsBasic +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.fuzzer.TooManyCombinationsException +import org.utbot.fuzzer.fuzz +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.IntSupplier + +object JsObjectModelProvider : ModelProvider { + + class SimpleIdGenerator : IntSupplier { + private val id = AtomicInteger() + override fun getAsInt() = id.incrementAndGet() + } + + val idGenerator = SimpleIdGenerator() + + private val primitiveModelProviders = ModelProvider.of( + JsConstantsModelProvider, + JsUndefinedModelProvider, + JsStringModelProvider, + JsMultipleTypesModelProvider, + JsPrimitivesModelProvider, + ) + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + val fuzzedValues = with(description) { + parameters.asSequence() + .filterNot { (it as JsClassId).isJsBasic } + .map { classId -> + val constructor = (classId as JsClassId).allConstructors.first() as JsConstructorId + constructor + }.associateWith { constructor -> + fuzzParameters(constructor, primitiveModelProviders) + }.flatMap { (constructor, fuzzedParams) -> + fuzzedParams.map { params -> + assemble(idGenerator.asInt, constructor, params) + } + } + } + fuzzedValues.forEach { fuzzedValue -> + description.parametersMap[fuzzedValue.model.classId]?.forEach { index -> + yieldValue(index, fuzzedValue) + } + } + } + + private fun assemble(id: Int, constructor: ConstructorId, values: List): FuzzedValue { + val instantiationCall = UtExecutableCallModel(null, constructor, values.map { it.model }) + val model = UtAssembleModel( + id, + constructor.classId, + "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), + instantiationCall = instantiationCall, + ) .fuzzed { + summary = + "%var% = ${constructor.classId.simpleName}(${constructor.parameters.joinToString { it.simpleName }})" + } + return model + } + + private fun FuzzedMethodDescription.fuzzParameters( + constructorId: ConstructorId, + vararg modelProviders: ModelProvider + ): Sequence> { + val fuzzedMethod = FuzzedMethodDescription( + executableId = constructorId, + concreteValues = this.concreteValues + ).apply { + this.packageName = this@fuzzParameters.packageName + } + return try { + fuzz(fuzzedMethod, *modelProviders) + } catch (t: TooManyCombinationsException) { + emptySequence() + } + } + + +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsPrimitivesModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsPrimitivesModelProvider.kt new file mode 100644 index 0000000000..777af5ae44 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsPrimitivesModelProvider.kt @@ -0,0 +1,70 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.jsBooleanClassId +import framework.api.js.util.jsDoubleClassId +import framework.api.js.util.jsNumberClassId +import framework.api.js.util.jsStringClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue + +object JsPrimitivesModelProvider : ModelProvider { + + // TODO SEVERE: research overflows in js. For now these nums are low not to go beyond Long (will be fixed) + internal const val MAX_INT = 1024 + internal const val MIN_INT = -1024 + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.parametersMap.forEach { (classId, parameterIndices) -> + val primitives = matchClassId(classId as JsClassId) + primitives.forEach { model -> + parameterIndices.forEach { index -> + yieldValue(index, model) + } + } + } + } + + internal fun matchClassId(classId: JsClassId): List { + val fuzzedValues = when (classId) { + jsBooleanClassId -> listOf( + JsPrimitiveModel(false).fuzzed { summary = "%var% = false" }, + JsPrimitiveModel(true).fuzzed { summary = "%var% = true" } + ) + + jsNumberClassId -> listOf( + JsPrimitiveModel(0).fuzzed { summary = "%var% = 0" }, + JsPrimitiveModel(1).fuzzed { summary = "%var% > 0" }, + JsPrimitiveModel((-1)).fuzzed { summary = "%var% < 0" }, + JsPrimitiveModel(MIN_INT).fuzzed { summary = "%var% = Number.MIN_SAFE_VALUE" }, + JsPrimitiveModel(MAX_INT).fuzzed { summary = "%var% = Number.MAX_SAFE_VALUE" }, + ) + + jsDoubleClassId -> listOf( + JsPrimitiveModel(0.0).fuzzed { summary = "%var% = 0.0" }, + JsPrimitiveModel(1.1).fuzzed { summary = "%var% > 0.0" }, + JsPrimitiveModel(-1.1).fuzzed { summary = "%var% < 0.0" }, + JsPrimitiveModel(MIN_INT.toDouble()).fuzzed { summary = "%var% = Number.MIN_SAFE_VALUE" }, + JsPrimitiveModel(MAX_INT.toDouble()).fuzzed { summary = "%var% = Number.MAX_SAFE_VALUE" }, +// TODO SEVERE: Think about such values as they are present in JavaScript. +// UtPrimitiveModel(Double.NEGATIVE_INFINITY).fuzzed { summary = "%var% = Double.NEGATIVE_INFINITY" }, +// UtPrimitiveModel(Double.POSITIVE_INFINITY).fuzzed { summary = "%var% = Double.POSITIVE_INFINITY" }, +// JsPrimitiveModel(Double.NaN).fuzzed { summary = "%var% = Double.NaN" }, + ) + + jsStringClassId -> primitivesForString() + else -> listOf() + } + return fuzzedValues + } + + internal fun primitivesForString() = listOf( + JsPrimitiveModel("").fuzzed { summary = "%var% = empty string" }, + JsPrimitiveModel(" ").fuzzed { summary = "%var% = blank string" }, + JsPrimitiveModel("string").fuzzed { summary = "%var% != empty string" }, + ) +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsStringModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsStringModelProvider.kt new file mode 100644 index 0000000000..80ad9708c0 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsStringModelProvider.kt @@ -0,0 +1,53 @@ +package fuzzer.providers + +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.jsStringClassId +import framework.api.js.util.toJsClassId +import org.utbot.fuzzer.FuzzedContext +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.ModelProvider +import kotlin.random.Random + +object JsStringModelProvider : ModelProvider { + + internal val random = Random(72923L) + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.concreteValues + .asSequence() + .filter { (classId, _) -> classId.toJsClassId() == jsStringClassId } + .forEach { (_, value, op) -> + listOf(value, mutate(random, value as? String, op)) + .asSequence() + .filterNotNull() + .map { JsPrimitiveModel(it) }.forEach { model -> + description.parametersMap.keys.indices.forEach { index -> + yield(FuzzedParameter(index, model.fuzzed { summary = "%var% = string" })) + } + } + } + } + + fun mutate(random: Random, value: String?, op: FuzzedContext): String? { + if (value.isNullOrEmpty() || op != FuzzedContext.Unknown) return null + val indexOfMutation = random.nextInt(value.length) + return value.replaceRange( + indexOfMutation, + indexOfMutation + 1, + SingleCharacterSequence(value[indexOfMutation] - random.nextInt(1, 128)) + ) + } + + private class SingleCharacterSequence(private val character: Char) : CharSequence { + override val length: Int + get() = 1 + + override fun get(index: Int): Char = character + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + throw UnsupportedOperationException() + } + + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsUndefinedModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsUndefinedModelProvider.kt new file mode 100644 index 0000000000..4b6ffbf2c5 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsUndefinedModelProvider.kt @@ -0,0 +1,39 @@ +package fuzzer.providers + +import fuzzer.providers.JsPrimitivesModelProvider.MAX_INT +import fuzzer.providers.JsPrimitivesModelProvider.MIN_INT +import framework.api.js.JsPrimitiveModel +import framework.api.js.util.jsUndefinedClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider + +object JsUndefinedModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + val parameters = description.parametersMap.getOrDefault(jsUndefinedClassId, emptyList()) + val primitives: List = generateValues() + primitives.forEach { model -> + parameters.forEach { index -> + yield(FuzzedParameter(index, model)) + } + } + } + + private fun generateValues() = + listOf( + JsPrimitiveModel(false).fuzzed { summary = "%var% = false" }, + JsPrimitiveModel(true).fuzzed { summary = "%var% = false" }, + + JsPrimitiveModel(0).fuzzed { summary = "%var% = 0" }, + JsPrimitiveModel(-1).fuzzed { summary = "%var% < 0" }, + JsPrimitiveModel(1).fuzzed { summary = "%var% > 0" }, + JsPrimitiveModel(MAX_INT).fuzzed { summary = "%var% = Number.MAX_SAFE_VALUE" }, + JsPrimitiveModel(MIN_INT).fuzzed { summary = "%var% = Number.MIN_SAFE_VALUE" }, + + JsPrimitiveModel(0.0).fuzzed { summary = "%var% = 0.0" }, + JsPrimitiveModel(-1.0).fuzzed { summary = "%var% < 0.0" }, + JsPrimitiveModel(1.0).fuzzed { summary = "%var% > 0.0" }, + ) +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt new file mode 100644 index 0000000000..b6e385b5d8 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt @@ -0,0 +1,26 @@ +package parser + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.visitor.NodeVisitor + +class JsClassAstVisitor( + private val target: String? +) : NodeVisitor(LexicalContext()) { + + lateinit var targetClassNode: ClassNode + lateinit var atLeastSomeClassNode: ClassNode + var classNodesCount = 0 + + override fun enterClassNode(classNode: ClassNode?): Boolean { + classNode?.let { + classNodesCount++ + atLeastSomeClassNode = it + if (it.ident.name.toString() == target) { + targetClassNode = it + return false + } + } + return true + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt new file mode 100644 index 0000000000..a7dc3a7b27 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt @@ -0,0 +1,32 @@ +package parser + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.visitor.NodeVisitor + +class JsFunctionAstVisitor( + private val target: String, + private val className: String? +) : NodeVisitor(LexicalContext()) { + + private var lastVisitedClassName: String = "" + lateinit var targetFunctionNode: FunctionNode + + override fun enterClassNode(classNode: ClassNode?): Boolean { + classNode?.let { + lastVisitedClassName = it.ident.name.toString() + } + return true + } + + override fun enterFunctionNode(functionNode: FunctionNode?): Boolean { + functionNode?.let { + if (it.name.toString() == target && (className ?: "") == lastVisitedClassName) { + targetFunctionNode = it + return false + } + } + return true + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt new file mode 100644 index 0000000000..320c69407a --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt @@ -0,0 +1,76 @@ +package parser + +import com.oracle.js.parser.ir.BinaryNode +import com.oracle.js.parser.ir.CaseNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.LiteralNode +import com.oracle.js.parser.ir.Node +import com.oracle.js.parser.ir.visitor.NodeVisitor +import com.oracle.truffle.api.strings.TruffleString +import framework.api.js.util.jsBooleanClassId +import framework.api.js.util.jsNumberClassId +import framework.api.js.util.jsStringClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedContext + +class JsFuzzerAstVisitor : NodeVisitor(LexicalContext()) { + private var lastFuzzedOpGlobal: FuzzedContext = FuzzedContext.Unknown + + val fuzzedConcreteValues = mutableSetOf() + override fun enterCaseNode(caseNode: CaseNode?): Boolean { + caseNode?.test?.let { + validateNode(it) + } + return true + } + + override fun enterBinaryNode(binaryNode: BinaryNode?): Boolean { + binaryNode?.let { binNode -> + val compOp = """>=|<=|>|<|==|!=""".toRegex() + val curOp = compOp.find(binNode.toString())?.value + val currentFuzzedOp = FuzzedContext.Comparison.values().find { curOp == it.sign } ?: FuzzedContext.Unknown + lastFuzzedOpGlobal = currentFuzzedOp + validateNode(binNode.lhs) + lastFuzzedOpGlobal = if (lastFuzzedOpGlobal is FuzzedContext.Comparison) (lastFuzzedOpGlobal as FuzzedContext.Comparison).reverse() else FuzzedContext.Unknown + validateNode(binNode.rhs) + } + return true + } + + private fun validateNode(literalNode: Node) { + if (literalNode !is LiteralNode<*>) return + when (literalNode.value) { + is TruffleString -> { + fuzzedConcreteValues.add( + FuzzedConcreteValue( + jsStringClassId, + literalNode.value.toString(), + lastFuzzedOpGlobal + ) + ) + } + + is Boolean -> { + fuzzedConcreteValues.add( + FuzzedConcreteValue( + jsBooleanClassId, + literalNode.value, + lastFuzzedOpGlobal + ) + ) + } + + is Int -> { + fuzzedConcreteValues.add(FuzzedConcreteValue(jsNumberClassId, literalNode.value, lastFuzzedOpGlobal)) + } + + is Long -> { + fuzzedConcreteValues.add(FuzzedConcreteValue(jsNumberClassId, literalNode.value, lastFuzzedOpGlobal)) + } + + is Double -> { + fuzzedConcreteValues.add(FuzzedConcreteValue(jsNumberClassId, literalNode.value, lastFuzzedOpGlobal)) + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsParserUtils.kt b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt new file mode 100644 index 0000000000..a5e5607ae7 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt @@ -0,0 +1,20 @@ +package parser + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode + +object JsParserUtils { + + // TODO SEVERE: function only works in the same file scope. Add search in exports. + fun searchForClassDecl(className: String?, parsedFile: FunctionNode, strict: Boolean = false): ClassNode? { + val visitor = JsClassAstVisitor(className) + parsedFile.accept(visitor) + return try { + visitor.targetClassNode + } catch (e: Exception) { + if (!strict && visitor.classNodesCount == 1) { + visitor.atLeastSomeClassNode + } else null + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt new file mode 100644 index 0000000000..e77db964fa --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt @@ -0,0 +1,22 @@ +package parser + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.visitor.NodeVisitor + +class JsToplevelFunctionAstVisitor : NodeVisitor(LexicalContext()) { + + val extractedMethods = mutableListOf() + + override fun enterClassNode(classNode: ClassNode?): Boolean { + return false + } + + override fun enterFunctionNode(functionNode: FunctionNode?): Boolean { + functionNode?.let { + extractedMethods += it + } + return false + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/BasicCoverageService.kt b/utbot-js/src/main/kotlin/service/BasicCoverageService.kt new file mode 100644 index 0000000000..e7df5631a2 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/BasicCoverageService.kt @@ -0,0 +1,143 @@ +package service + +import java.io.File +import java.util.Collections +import org.apache.commons.io.FileUtils +import org.json.JSONException +import org.json.JSONObject +import org.utbot.framework.plugin.api.TimeoutException +import settings.JsTestGenerationSettings.tempFileName +import utils.JsCmdExec + +// TODO: 1. Make searching for file coverage in coverage report more specific, not just by file name. +class BasicCoverageService( + private val context: ServiceContext, + private val scriptTexts: List, + private val testCaseIndices: IntRange, +) : ICoverageService { + + private val errors = mutableListOf() + private var baseCoverage = emptyList() + private val utbotDirPath = "${context.projectPath}/${context.utbotDir}" + private val _resultList = mutableListOf>() + val resultList: List + get() = _resultList + .sortedBy { (index, _) -> index } + .map { (_, obj) -> obj } + + init { + generateTempFiles() + baseCoverage = getBaseCoverage() + generateCoverageReportForAllFiles() + } + + override fun getCoveredLines(): List> { + try { + val res = testCaseIndices.map { index -> + if (index in errors) emptySet() else { + val fileCoverage = + getCoveragePerFile(context.filePathToInference.substringAfterLast("/"), index).toSet() + val resFile = File("$utbotDirPath/$tempFileName$index.json") + val rawResult = resFile.readText() + resFile.delete() + val json = JSONObject(rawResult) + _resultList.add(index to json.get("result").toString()) + fileCoverage + } + } + return res + } finally { + removeTempFiles() + } + } + + private fun getCoveragePerFile(fileName: String, index: Int): List { + if (index in errors) return emptyList() + val jsonText = with(context) { + val file = + File("$utbotDirPath/coverage$index/coverage-final.json") + file.readText() + } + val json = JSONObject(jsonText) + try { + val neededKey = json.keySet().find { it.contains(fileName) } + json.getJSONObject(neededKey) + val coveredStatements = json + .getJSONObject(neededKey) + .getJSONObject("s") + val result = coveredStatements.keySet().flatMap { + val count = coveredStatements.getInt(it) + Collections.nCopies(count, it.toInt()) + }.toMutableList() + baseCoverage.forEach { + result.remove(it) + } + return result + } catch (e: JSONException) { + return emptyList() + } + } + + private fun getBaseCoverage(): List { + generateCoverageReport(context.filePathToInference, 0) + return getCoveragePerFile(context.filePathToInference.substringAfterLast("/"), 0) + } + + private fun removeTempFiles() { + for (index in testCaseIndices) { + File("$utbotDirPath/$tempFileName$index.js").delete() + FileUtils.deleteDirectory(File("$utbotDirPath/coverage$index")) + FileUtils.deleteDirectory(File("$utbotDirPath/cache$index")) + } + } + + private fun generateCoverageReportForAllFiles() { + testCaseIndices.toList().parallelStream().forEach { parallelIndex -> + generateCoverageReport("$utbotDirPath/$tempFileName$parallelIndex.js", parallelIndex) + } + } + + private fun generateCoverageReport(filePath: String, index: Int) { + try { + with(context) { + val (_, error) = + JsCmdExec.runCommand( + cmd = arrayOf( + settings.pathToNYC, + "--report-dir=$utbotDirPath/coverage$index", + "--reporter=json", + "--temp-dir=$utbotDirPath/cache$index", + "node", + filePath + ), + shouldWait = true, + dir = context.projectPath, + timeout = settings.timeout, + ) + val errText = error.readText() + if (errText.isNotEmpty()) { + println(errText) + } + } + } catch (e: TimeoutException) { + errors += index + _resultList.add(index to "Error:Timeout") + } + } + + private fun generateTempFiles() { + scriptTexts.forEachIndexed { index, scriptText -> + val tempScriptPath = "$utbotDirPath/$tempFileName$index.js" + createTempScript( + path = tempScriptPath, + scriptText = scriptText + ) + } + } + + private fun createTempScript(path: String, scriptText: String) { + val file = File(path) + file.writeText(scriptText) + file.createNewFile() + } +} diff --git a/utbot-js/src/main/kotlin/service/CoverageMode.kt b/utbot-js/src/main/kotlin/service/CoverageMode.kt new file mode 100644 index 0000000000..a1f0c0d9cc --- /dev/null +++ b/utbot-js/src/main/kotlin/service/CoverageMode.kt @@ -0,0 +1,6 @@ +package service + +enum class CoverageMode { + FAST, + BASIC +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/CoverageServiceProvider.kt b/utbot-js/src/main/kotlin/service/CoverageServiceProvider.kt new file mode 100644 index 0000000000..2c632e757e --- /dev/null +++ b/utbot-js/src/main/kotlin/service/CoverageServiceProvider.kt @@ -0,0 +1,199 @@ +package service + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.truffle.api.strings.TruffleString +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtModel +import framework.api.js.JsMethodId +import framework.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.fuzzer.FuzzedValue +import settings.JsTestGenerationSettings +import settings.JsTestGenerationSettings.tempFileName +import utils.PathResolver + +// TODO: Add "error" field in result json to not collide with "result" field upon error. +class CoverageServiceProvider(private val context: ServiceContext) { + + private val importFileUnderTest = "instr/${context.filePathToInference.substringAfterLast("/")}" + + private val imports = "const ${JsTestGenerationSettings.fileUnderTestAliases} = require(\"./$importFileUnderTest\")\n" + + "const fs = require(\"fs\")\n\n" + + fun get( + mode: CoverageMode, + fuzzedValues: List>, + execId: JsMethodId, + classNode: ClassNode? + ): Pair>, List> { + return when (mode) { + CoverageMode.FAST -> runFastCoverageAnalysis( + context, + fuzzedValues, + execId, + classNode + ) + + CoverageMode.BASIC -> runBasicCoverageAnalysis( + context, + fuzzedValues, + execId, + classNode + ) + } + } + + private fun runBasicCoverageAnalysis( + context: ServiceContext, + fuzzedValues: List>, + execId: JsMethodId, + classNode: ClassNode?, + ): Pair>, List> { + val tempScriptTexts = fuzzedValues.indices.map { + "const ${JsTestGenerationSettings.fileUnderTestAliases} = require(\"./${PathResolver.getRelativePath("${context.projectPath}/${context.utbotDir}", context.filePathToInference)}\")\n" + "const fs = require(\"fs\")\n\n" + makeStringForRunJs( + fuzzedValue = fuzzedValues[it], + method = execId, + containingClass = classNode?.ident?.name, + index = it, + resFilePath = "${context.projectPath}/${context.utbotDir}/$tempFileName", + mode = CoverageMode.BASIC + ) + } + val coverageService = BasicCoverageService( + context = context, + scriptTexts = tempScriptTexts, + testCaseIndices = fuzzedValues.indices, + ) + return coverageService.getCoveredLines() to coverageService.resultList + } + + private fun runFastCoverageAnalysis( + context: ServiceContext, + fuzzedValues: List>, + execId: JsMethodId, + classNode: ClassNode?, + ): Pair>, List> { + val covFunName = FastCoverageService.instrument(context) + val tempScriptTexts = fuzzedValues.indices.map { + makeStringForRunJs( + fuzzedValue = fuzzedValues[it], + method = execId, + containingClass = classNode?.ident?.name, + covFunName = covFunName, + index = it, + resFilePath = "${context.projectPath}/${context.utbotDir}/$tempFileName", + mode = CoverageMode.FAST + ) + } + val baseCoverageScriptText = makeScriptForBaseCoverage(covFunName,"${context.projectPath}/${context.utbotDir}/${tempFileName}Base.json") + val coverageService = FastCoverageService( + context = context, + scriptTexts = splitTempScriptsIfNeeded(tempScriptTexts), + testCaseIndices = fuzzedValues.indices, + baseCoverageScriptText = baseCoverageScriptText, + ) + return coverageService.getCoveredLines() to coverageService.resultList + } + + // TODO: do not hardcode 1000 constant - move to settings object. + private fun splitTempScriptsIfNeeded(tempScripts: List): List { + when { + // No need to run parallel execution, so only 1 element in the list + tempScripts.size < 1000 -> { + return listOf( + imports + tempScripts.joinToString("\n\n") + ) + } + else -> { + return tempScripts + .withIndex() + .groupBy { it.index / 1000 } + .map { entry -> + imports + entry.value.joinToString("\n\n") { it.value } + } + } + } + } + + private fun makeScriptForBaseCoverage(covFunName: String, resFilePath: String): String { + return """ +$imports + +let json = {} +json.s = ${JsTestGenerationSettings.fileUnderTestAliases}.$covFunName().s +fs.writeFileSync("$resFilePath", JSON.stringify(json)) + """ + } + + private fun makeStringForRunJs( + fuzzedValue: List, + method: JsMethodId, + containingClass: TruffleString?, + covFunName: String = "", + index: Int, + resFilePath: String, + mode: CoverageMode, + ): String { + val callString = makeCallFunctionString(fuzzedValue, method, containingClass) + return """ +let json$index = {} + +let res$index +try { + res$index = $callString +} catch(e) { + res$index = "Error:" + e.message +} +${ +"json$index.result = res$index\n" + +if (mode == CoverageMode.FAST ) "json$index.index = $index\n" + +"json$index.s = ${JsTestGenerationSettings.fileUnderTestAliases}.$covFunName().s\n" else "" +} +fs.writeFileSync("$resFilePath$index.json", JSON.stringify(json$index)) + """ + } + + private fun makeCallFunctionString( + fuzzedValue: List, + method: JsMethodId, + containingClass: TruffleString? + ): String { + val initClass = containingClass?.let { + if (!method.isStatic) { + "new ${JsTestGenerationSettings.fileUnderTestAliases}.${it}()." + } else "${JsTestGenerationSettings.fileUnderTestAliases}.$it." + } ?: "${JsTestGenerationSettings.fileUnderTestAliases}." + var callString = "$initClass${method.name}" + callString += fuzzedValue.joinToString( + prefix = "(", + postfix = ")", + ) { value -> value.model.toCallString() } + return callString + } + + private fun Any.quoteWrapIfNecessary(): String = + when (this) { + is String -> "\"$this\"" + else -> "$this" + } + + private fun UtAssembleModel.toParamString(): String = + with(this) { + val callConstructorString = "new ${JsTestGenerationSettings.fileUnderTestAliases}.${classId.name}" + val paramsString = instantiationCall.params.joinToString( + prefix = "(", + postfix = ")", + ) { + (it as JsPrimitiveModel).value.quoteWrapIfNecessary() + } + return callConstructorString + paramsString + } + + private fun UtModel.toCallString(): String = + when (this) { + is UtAssembleModel -> this.toParamString() + else -> { + (this as JsPrimitiveModel).value.quoteWrapIfNecessary() + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/FastCoverageService.kt b/utbot-js/src/main/kotlin/service/FastCoverageService.kt new file mode 100644 index 0000000000..6ea777589a --- /dev/null +++ b/utbot-js/src/main/kotlin/service/FastCoverageService.kt @@ -0,0 +1,158 @@ +package service + +import java.io.File +import java.util.Collections +import org.apache.commons.io.FileUtils +import org.json.JSONException +import org.json.JSONObject +import settings.JsTestGenerationSettings.tempFileName +import utils.JsCmdExec + +class FastCoverageService( + private val context: ServiceContext, + private val scriptTexts: List, + private val testCaseIndices: IntRange, + private val baseCoverageScriptText: String, +): ICoverageService { + + private val utbotDirPath = "${context.projectPath}/${context.utbotDir}" + private val coverageList = mutableListOf>() + private val _resultList = mutableListOf>() + val resultList: List + get() = _resultList + .sortedBy { (index, _) -> index } + .map { (_, obj) -> obj } + private var baseCoverage: List + + companion object { + fun instrument(context: ServiceContext): String { + val destination = "${context.projectPath}/${context.utbotDir}/instr" + val fileName = context.filePathToInference.substringAfterLast("/") + with(context) { + JsCmdExec.runCommand( + cmd = arrayOf(settings.pathToNYC, "instrument", fileName, destination), + dir = context.filePathToInference.substringBeforeLast("/"), + shouldWait = true, + timeout = settings.timeout, + ) + } + + val instrumentedFilePath = "$destination/${context.filePathToInference.substringAfterLast("/")}" + val instrumentedFileText = File(instrumentedFilePath).readText() + val covFunRegex = Regex("function (cov_.*)\\(\\).*") + val covFunName = covFunRegex.find(instrumentedFileText.takeWhile { it != '{' })?.groups?.get(1)?.value ?: throw IllegalStateException("") + val fixedFileText = "$instrumentedFileText\nexports.$covFunName = $covFunName" + File(instrumentedFilePath).writeText(fixedFileText) + + return covFunName + } + } + + init { + generateTempFiles() + baseCoverage = getBaseCoverage() + generateCoverageReport() + } + + private fun generateTempFiles() { + scriptTexts.forEachIndexed { index, scriptText -> + val tempScriptPath = "$utbotDirPath/$tempFileName$index.js" + createTempScript( + path = tempScriptPath, + scriptText = scriptText + ) + } + createTempScript( + path = "$utbotDirPath/${tempFileName}Base.js", + scriptText = baseCoverageScriptText + ) + } + + private fun getBaseCoverage(): List { + with(context) { + JsCmdExec.runCommand( + cmd = arrayOf(settings.pathToNode, "$utbotDirPath/${tempFileName}Base.js"), + dir = context.projectPath, + shouldWait = true, + timeout = settings.timeout, + ) + } + return JSONObject(File("$utbotDirPath/${tempFileName}Base.json").readText()) + .getJSONObject("s").let { + it.keySet().flatMap { key -> + val count = it.getInt(key) + Collections.nCopies(count, key.toInt()) + } + } + } + + override fun getCoveredLines(): List> { + try { + return coverageList.sortedBy { (index, _) -> index } + .map { (_, obj) -> + val dirtyCoverage = obj + .let { + it.keySet().flatMap {key -> + val count = it.getInt(key) + Collections.nCopies(count, key.toInt()) + }.toMutableList() + } + baseCoverage.forEach { + dirtyCoverage.remove(it) + } + dirtyCoverage.toSet() + } + } catch (e: JSONException) { + throw Exception("Could not get coverage of test cases!") + } finally { + removeTempFiles() + } + } + + private fun removeTempFiles() { + FileUtils.deleteDirectory(File("$utbotDirPath/instr")) + File("$utbotDirPath/${tempFileName}Base.js").delete() + File("$utbotDirPath/${tempFileName}Base.json").delete() + for (index in testCaseIndices) { + File("$utbotDirPath/$tempFileName$index.json").delete() + } + for (index in scriptTexts.indices) { + File("$utbotDirPath/$tempFileName$index.js").delete() + } + } + + + private fun generateCoverageReport() { + scriptTexts.indices.toList().parallelStream().forEach { parallelIndex -> + with(context) { + val (_, error) = JsCmdExec.runCommand( + cmd = arrayOf(settings.pathToNode, "$utbotDirPath/$tempFileName$parallelIndex.js"), + dir = context.projectPath, + shouldWait = true, + timeout = settings.timeout, + ) + for (i in parallelIndex * 1000..minOf(parallelIndex * 1000 + 999, testCaseIndices.last)) { + val resFile = File("$utbotDirPath/$tempFileName$i.json") + val rawResult = resFile.readText() + resFile.delete() + val json = JSONObject(rawResult) + val index = json.getInt("index") + if (index != i) println("ERROR: index $index != i $i") + coverageList.add(index to json.getJSONObject("s")) + _resultList.add(index to json.get("result").toString()) + } + val errText = error.readText() + if (errText.isNotEmpty()) { + println(errText) + } + } + } + + } + + private fun createTempScript(path: String, scriptText: String) { + val file = File(path) + file.writeText(scriptText) + file.createNewFile() + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/ICoverageService.kt b/utbot-js/src/main/kotlin/service/ICoverageService.kt new file mode 100644 index 0000000000..3f0c01d747 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/ICoverageService.kt @@ -0,0 +1,6 @@ +package service + +interface ICoverageService { + + fun getCoveredLines(): List> +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/ServiceContext.kt b/utbot-js/src/main/kotlin/service/ServiceContext.kt new file mode 100644 index 0000000000..4264120de2 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/ServiceContext.kt @@ -0,0 +1,12 @@ +package service + +import com.oracle.js.parser.ir.FunctionNode +import settings.JsDynamicSettings + +data class ServiceContext( + val utbotDir: String, + val projectPath: String, + val filePathToInference: String, + val parsedFile: FunctionNode, + val settings: JsDynamicSettings, +) \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/TernService.kt b/utbot-js/src/main/kotlin/service/TernService.kt new file mode 100644 index 0000000000..2c3c1a9560 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/TernService.kt @@ -0,0 +1,205 @@ +package service + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import framework.api.js.JsClassId +import framework.api.js.JsMultipleClassId +import framework.api.js.util.jsUndefinedClassId +import java.io.File +import java.util.Locale +import org.json.JSONException +import org.json.JSONObject +import parser.JsParserUtils +import utils.JsCmdExec +import utils.MethodTypes +import utils.constructClass + +/* + NOTE: this approach is quite bad, but we failed to implement alternatives. + TODO: 1. MINOR: Find a better solution after the first stable version. + 2. SEVERE: Load all necessary .js files in Tern.js since functions can be exported and used in other files. + */ + +/** + * Installs and sets up scripts for running Tern.js type guesser. + */ +class TernService(val context: ServiceContext) { + + + private fun ternScriptCode() = """ +const tern = require("tern/lib/tern") +const condense = require("tern/lib/condense.js") +const util = require("tern/test/util.js") +const fs = require("fs") +const path = require("path") + +var condenseDir = ""; + +function runTest(options) { + + var server = new tern.Server({ + projectDir: util.resolve(condenseDir), + defs: [util.ecmascript], + plugins: options.plugins, + getFile: function(name) { + return fs.readFileSync(path.resolve(condenseDir, name), "utf8"); + } + }); + options.load.forEach(function(file) { + server.addFile(file) + }); + server.flush(function() { + var origins = options.include || options.load; + var condensed = condense.condense(origins, null, {sortOutput: true}); + var out = JSON.stringify(condensed, null, 2); + console.log(out) + }); +} + +function test(options) { + if (typeof options == "string") options = {load: [options]}; + runTest(options); +} + +test("${context.filePathToInference}") + """ + + init { + with(context) { + setupTernEnv("$projectPath/$utbotDir") + installDeps("$projectPath/$utbotDir") + runTypeInferencer() + } + } + + private lateinit var json: JSONObject + + private fun installDeps(path: String) { + JsCmdExec.runCommand( + dir = path, + cmd = arrayOf(context.settings.pathToNPM, "i", "tern", "-l") + ) + } + + private fun setupTernEnv(path: String) { + File(path).mkdirs() + val ternScriptFile = File("$path/ternScript.js") + ternScriptFile.writeText(ternScriptCode()) + } + + private fun runTypeInferencer() { + with(context) { + val (reader, _) = JsCmdExec.runCommand( + dir = "$projectPath/$utbotDir/", + shouldWait = true, + timeout = 20, + cmd = arrayOf(settings.pathToNode, "${projectPath}/$utbotDir/ternScript.js"), + ) + val text = reader.readText().replaceAfterLast("}", "") + json = try { + JSONObject(text) + } catch (_: Throwable) { + JSONObject() + } + } + } + + fun processConstructor(classNode: ClassNode): List { + return try { + val classJson = json.getJSONObject(classNode.ident.name.toString()) + val constructorFunc = classJson.getString("!type") + .filterNot { setOf(' ', '+', '!').contains(it) } + extractParameters(constructorFunc) + } catch (e: JSONException) { + (classNode.constructor.value as FunctionNode).parameters.map { jsUndefinedClassId } + } + } + + private fun extractParameters(line: String): List { + val parametersRegex = Regex("fn[(](.+)[)]") + return parametersRegex.find(line)?.groups?.get(1)?.let { matchResult -> + val value = matchResult.value + val paramList = value.split(',') + paramList.map { param -> + val paramReg = Regex(":(.*)") + try { + makeClassId( + paramReg.find(param)?.groups?.get(1)?.value + ?: throw IllegalStateException() + ) + } catch (t: Throwable) { + jsUndefinedClassId + } + } + } ?: emptyList() + } + + private fun extractReturnType(line: String): JsClassId { + val returnTypeRegex = Regex("->(.*)") + return returnTypeRegex.find(line)?.groups?.get(1)?.let { matchResult -> + val value = matchResult.value + try { + makeClassId(value) + } catch (t: Throwable) { + jsUndefinedClassId + } + } ?: jsUndefinedClassId + } + + fun processMethod(className: String?, funcNode: FunctionNode, isToplevel: Boolean = false): MethodTypes { + // Js doesn't support nested classes, so if the function is not top-level, then we can check for only one parent class. + try { + var scope = className?.let { + if (!isToplevel) json.getJSONObject(it) else json + } ?: json + try { + scope.getJSONObject(funcNode.name.toString()) + } catch (e: JSONException) { + scope = scope.getJSONObject("prototype") + } + val methodJson = scope.getJSONObject(funcNode.name.toString()) + val typesString = methodJson.getString("!type") + .filterNot { setOf(' ', '+', '!').contains(it) } + val parametersList = lazy { extractParameters(typesString) } + val returnType = lazy { extractReturnType(typesString) } + + return MethodTypes(parametersList, returnType) + } catch (e: Exception) { + return MethodTypes( + lazy { funcNode.parameters.map { jsUndefinedClassId } }, + lazy { jsUndefinedClassId } + ) + } + } + + //TODO MINOR: move to appropriate place (JsIdUtil or JsClassId constructor) + private fun makeClassId(name: String): JsClassId { + val classId = when { + // TODO SEVERE: I don't know why Tern sometimes says that type is "0" + name == "?" || name.toIntOrNull() != null -> jsUndefinedClassId + Regex("\\[(.*)]").matches(name) -> { + val arrType = Regex("\\[(.*)]").find(name)?.groups?.get(1)?.value ?: throw IllegalStateException() + JsClassId( + jsName = "array", + elementClassId = makeClassId(arrType) + ) + } + + name.contains('|') -> JsMultipleClassId(name.lowercase(Locale.getDefault())) + else -> JsClassId(name.lowercase(Locale.getDefault())) + } + + return try { + val classNode = JsParserUtils.searchForClassDecl( + className = name, + parsedFile = context.parsedFile, + strict = true, + ) + classNode?.let { + JsClassId(name).constructClass(this, it) + } ?: classId + } catch (e: Exception) { + classId + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt b/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt new file mode 100644 index 0000000000..81d1ac02a0 --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt @@ -0,0 +1,11 @@ +package settings + +import service.CoverageMode + +data class JsDynamicSettings( + val pathToNode: String = "node", + val pathToNYC: String = "nyc", + val pathToNPM: String = "npm", + val timeout: Long = 15L, + val coverageMode: CoverageMode = CoverageMode.FAST +) diff --git a/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt b/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt new file mode 100644 index 0000000000..2c30195953 --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt @@ -0,0 +1,8 @@ +package settings + +object JsExportsSettings { + + // Anchors for exports in user's code. Used in regexes to modify this section on demand. + const val startComment = "// Start of exports generated by UTBot" + const val endComment = "// End of exports generated by UTBot" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt b/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt new file mode 100644 index 0000000000..88b3c286f1 --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt @@ -0,0 +1,16 @@ +package settings + +object JsTestGenerationSettings { + + // Used for toplevel functions in IDEA plugin. + const val dummyClassName = "toplevelHack" + + // Default timeout for Node.js to try run a single testcase. + const val defaultTimeout = 15L + + // Name of file under test when importing it. + const val fileUnderTestAliases = "fileUnderTest" + + // Name of temporary files created. + const val tempFileName = "temp" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt new file mode 100644 index 0000000000..7cbfd41add --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt @@ -0,0 +1,74 @@ +package utils + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import framework.api.js.JsClassId +import framework.api.js.JsConstructorId +import framework.api.js.JsMethodId +import framework.api.js.util.jsUndefinedClassId +import service.TernService + +fun JsClassId.constructClass( + ternService: TernService, + classNode: ClassNode? = null, + functions: List = emptyList() +): JsClassId { + val className = classNode?.ident?.name?.toString() + val methods = constructMethods(classNode, ternService, className, functions) + + val constructor = classNode?.let { + JsConstructorId( + JsClassId(name), + ternService.processConstructor(it), + ) + } + val newClassId = JsClassId( + jsName = name, + methods = methods, + constructor = constructor, + classPackagePath = ternService.context.projectPath, + classFilePath = ternService.context.filePathToInference, + ) + methods.forEach { + it.classId = newClassId + } + constructor?.classId = newClassId + return newClassId +} + +private fun JsClassId.constructMethods( + classNode: ClassNode?, + ternService: TernService, + className: String?, + functions: List +): Sequence { + with(this) { + val methods = classNode?.classElements?.map { + val funcNode = it.value as FunctionNode + val types = ternService.processMethod(className, funcNode) + JsMethodId( + classId = JsClassId(name), + name = funcNode.name.toString(), + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = emptyList(), + staticModifier = it.isStatic, + lazyReturnType = types.returnType, + lazyParameters = types.parameters, + ) + }?.asSequence() ?: + // used for toplevel functions + functions.map { funcNode -> + val types = ternService.processMethod(className, funcNode, true) + JsMethodId( + classId = JsClassId(name), + name = funcNode.name.toString(), + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = emptyList(), + staticModifier = true, + lazyReturnType = types.returnType, + lazyParameters = types.parameters, + ) + }.asSequence() + return methods + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/JsCmdExec.kt b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt new file mode 100644 index 0000000000..6641ddb987 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt @@ -0,0 +1,33 @@ +package utils + +import java.io.BufferedReader +import java.io.File +import java.util.concurrent.TimeUnit +import org.utbot.framework.plugin.api.TimeoutException +import settings.JsTestGenerationSettings.defaultTimeout + +object JsCmdExec { + + fun runCommand( + dir: String? = null, + shouldWait: Boolean = false, + timeout: Long = defaultTimeout, + vararg cmd: String, + ): Pair { + val builder = ProcessBuilder(*OsProvider.getProviderByOs().getCmdPrefix(), *cmd) + dir?.let { + builder.directory(File(it)) + } + val process = builder.start() + if (shouldWait) { + if (!process.waitFor(timeout, TimeUnit.SECONDS)) { + process.descendants().forEach { + it.destroy() + } + process.destroy() + throw TimeoutException("") + } + } + return process.inputStream.bufferedReader() to process.errorStream.bufferedReader() + } +} diff --git a/utbot-js/src/main/kotlin/utils/JsOsUtils.kt b/utbot-js/src/main/kotlin/utils/JsOsUtils.kt new file mode 100644 index 0000000000..a21be883ae --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsOsUtils.kt @@ -0,0 +1,30 @@ +package utils + +import java.util.Locale + +abstract class OsProvider { + + abstract fun getCmdPrefix(): Array + abstract fun getAbstractivePathTool(): String + + companion object { + + fun getProviderByOs(): OsProvider { + val osData = System.getProperty("os.name").lowercase(Locale.getDefault()) + return when { + osData.contains("windows") -> WindowsProvider() + else -> LinuxProvider() + } + } + } +} + +class WindowsProvider : OsProvider() { + override fun getCmdPrefix() = arrayOf("cmd.exe", "/c") + override fun getAbstractivePathTool() = "where" +} + +class LinuxProvider : OsProvider() { + override fun getCmdPrefix() = emptyArray() + override fun getAbstractivePathTool() = "which" +} diff --git a/utbot-js/src/main/kotlin/utils/MethodTypes.kt b/utbot-js/src/main/kotlin/utils/MethodTypes.kt new file mode 100644 index 0000000000..342acdf508 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/MethodTypes.kt @@ -0,0 +1,8 @@ +package utils + +import framework.api.js.JsClassId + +data class MethodTypes( + val parameters: Lazy>, + val returnType: Lazy, +) \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/PathResolver.kt b/utbot-js/src/main/kotlin/utils/PathResolver.kt new file mode 100644 index 0000000000..8e1730db97 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/PathResolver.kt @@ -0,0 +1,12 @@ +package utils + +import java.nio.file.Paths + +object PathResolver { + + fun getRelativePath(to: String, from: String): String { + val toPath = Paths.get(to) + val fromPath = Paths.get(from) + return toPath.relativize(fromPath).toString().replace("\\", "/") + } +} diff --git a/utbot-js/src/main/kotlin/utils/ValueUtil.kt b/utbot-js/src/main/kotlin/utils/ValueUtil.kt new file mode 100644 index 0000000000..80842e9b04 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/ValueUtil.kt @@ -0,0 +1,45 @@ +package utils + +import org.json.JSONException +import org.json.JSONObject +import framework.api.js.JsClassId +import framework.api.js.util.jsBooleanClassId +import framework.api.js.util.jsErrorClassId +import framework.api.js.util.jsNumberClassId +import framework.api.js.util.jsStringClassId +import framework.api.js.util.jsUndefinedClassId + +fun String.toJsAny(returnType: JsClassId): Pair { + return when { + this == "true" || this == "false" -> toBoolean() to jsBooleanClassId + this == "null" || this == "undefined" -> null to jsUndefinedClassId + Regex("^.*Error:.*").matches(this) -> this.replace("Error:", "") to jsErrorClassId + Regex("\".*\"").matches(this) -> this.replace("\"", "") to jsStringClassId + else -> { + if (contains('.')) { + (toDoubleOrNull() ?: toBigDecimal()) to jsNumberClassId + } else { + val value = toByteOrNull() ?: toShortOrNull() ?: toIntOrNull() ?: toLongOrNull() + ?: toBigIntegerOrNull() ?: toDoubleOrNull() + if (value != null) value to jsNumberClassId else { + val obj = makeObject(this) + if (obj != null) obj to returnType else throw IllegalStateException() + } + } + } + } +} + +private fun makeObject(objString: String): Map? { + return try { + val trimmed = objString.substringAfter(" ") + val json = JSONObject(trimmed) + val resMap = mutableMapOf() + json.keySet().forEach { + resMap[it] = json.get(it).toString().toJsAny(jsUndefinedClassId).first as Any + } + resMap + } catch (e: JSONException) { + null + } +} \ No newline at end of file diff --git a/utbot-python/README.md b/utbot-python/README.md new file mode 100644 index 0000000000..e5c0f6529b --- /dev/null +++ b/utbot-python/README.md @@ -0,0 +1,48 @@ +# UTBot for Python + +UTBot is the tool for automated unit test generation. You can read more about this project [on the official website](https://www.utbot.org/). + +This is the support of UTBot for Python. + +UTBot tries to maximize the code coverage while minimizing the number of tests. For now, we use only the fuzzing technique for Python. + +# Get started + +There are two ways to use UTBot: as an IntelliJ IDEA plugin or through a command line interface. + +You can download both archives [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/2956160534). + +## Python requirements + +UTBot Python has been tested on Python 3.8 and 3.9. Some syntax from Python 3.10 is not supported. + +Usually nothing has to be done manually, but if you have any troubles with requirements, refer to [requirements section](docs/CLI.md#requirements) in CLI documentation. + +## IntelliJ IDEA plugin + +IntelliJ IDEA version should be 2022.1. + +1. Make sure you already have the Python plugin installed. + +2. Download the archive with the plugin and install it following [this instruction](https://www.jetbrains.com/help/idea/managing-plugins.html#install_plugin_from_disk). + +3. Configure the Python interpreter for your project and make sure that IDEA resolves all imports. + +4. After indexing has finished, move the cursor to a function, press ALT+SHIFT+U (or ALT+U, ALT+T in Ubuntu), and generate tests. + +### Dependency + +Package `com.intellij.modules.python` in `/utbot-intellij/resources/plugin.xml` is necessary dependecy for now, it is needed to use Python Psi tree. + + +## Command line interface + +You can find documentation on CLI usage [here](docs/CLI.md). + +# Contribute + +Read more in [UTBot Java Readme](../README.md#contribute-to-utbot-java). + +# Support + +Read more in [UTBot Java Readme](../README.md#find-support). diff --git a/utbot-python/build.gradle.kts b/utbot-python/build.gradle.kts new file mode 100644 index 0000000000..5bde292b12 --- /dev/null +++ b/utbot-python/build.gradle.kts @@ -0,0 +1,40 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + test { + useJUnitPlatform() + } +} + +dependencies { + api(project(":utbot-fuzzers")) + api(project(":utbot-framework")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(group = "org.apache.commons", name = "commons-lang3", version = "3.12.0") + implementation(group = "io.github.danielnaczo", name = "python3parser", version = "1.0.4") + implementation(group = "commons-io", name = "commons-io", version = "2.11.0") + implementation("com.beust:klaxon:5.5") + implementation("com.squareup.moshi:moshi:1.11.0") + implementation("com.squareup.moshi:moshi-kotlin:1.11.0") + implementation("com.squareup.moshi:moshi-adapters:1.11.0") + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation("org.functionaljava:functionaljava:5.0") + implementation("org.functionaljava:functionaljava-quickcheck:5.0") + implementation("org.functionaljava:functionaljava-java-core:5.0") + implementation(group = "org.apache.commons", name = "commons-text", version = apacheCommonsTextVersion) +} \ No newline at end of file diff --git a/utbot-python/docs/CLI.md b/utbot-python/docs/CLI.md new file mode 100644 index 0000000000..bdc5c5e0b0 --- /dev/null +++ b/utbot-python/docs/CLI.md @@ -0,0 +1,101 @@ +## Build + +.jar file can be built in Github Actions with script `publish-plugin-and-cli-from-branch`. + +## Requirements + + - Required Java version: 11. + + - Prefered Python version: 3.8 or 3.9. + + 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 + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_python dir/file_with_sources.py -p -o generated_tests.py -s dir + +This will generate tests for top-level functions from `file_with_sources.py`. + +Run generated tests: + + java -jar utbot-cli.jar run_python generated_tests.py -p + +### `generate_python` options + +- `-s, --sys-path ,` + + (required) Directories to add to `sys.path`. One of directories must contain the file with the methods under test. + + `sys.path` is a list of strings that specifies the search path for modules. It must include paths for all user modules that are used in imports. + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `-o, --output ` + + (required) File for generated tests. + +- `--coverage ` + + File to write coverage report. + +- `-c, --class ` + + Specify top-level (ordinary, not nested) class under test. Without this option tests will be generated for top-level functions. + +- `-m, --methods ,` + + Specify methods under test. + +- `--install-requirements` + + Install Python requirements if missing. + +- `--do-not-minimize` + + Turn off minimization of the number of generated tests. + +- `--do-not-check-requirements` + + 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). + +- `--timeout-for-run INT` + + Specify the maximum time in milliseconds to spend on one function run (2000 by default). + +- `--test-framework [pytest|Unittest]` + + Test framework to be used. + +### `run_python` options + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `--test-framework [pytest|Unittest]` + + Test framework of tests to run. + +- `-o, --output ` + + Specify file for report. + +## Problems + +- Unittest can not run tests from parent directories diff --git a/utbot-python/docs/docs.md b/utbot-python/docs/docs.md new file mode 100644 index 0000000000..96e0bce318 --- /dev/null +++ b/utbot-python/docs/docs.md @@ -0,0 +1,54 @@ +# UtBot-Python +__Task__: implement utbot for Python using fuzzing to generate tests. + +Subtasks: +* Get list of functions to be tested +* Generate input parameters for this functions +* Compute return values for this parameters +* Render tests + +## Getting list of functions + +We get list of functions to be tested from Intellij IDEA plugin. Other information we get from source code. + +Information about functions: +* Name +* List of parameters +* Source code +* Declaration file +* Type annotations for parameters and return type (optional) + +## Input parameters generation + +### Problem + +If we do not have type annotation, we have to find suitable types for this parameter. + +### Solution +Gather information about Python built-in types (by 'built-in types' we mean types that are implemented in C): + +* Name +* Methods: name + parameters (+ annotations) +* How to generate instances of this type (default, random, using constants from code) + +We can use CPython code and tests for it to gather this. + +For user class we need to initialize its fields recursively. Possible problems: getting types of fields, dynamic addition of new fields. + +To find suitable types for parameter we can look for them only in given and imported files. + +To narrow down the search of suitable types we can gather constraints for function parameters. For that we can analyze AST to see which attributes of parameter are used. + +## Run function with generated parameters + +After generating parameters for fuzzing we pass them on into the function under test and run it in a separate process. This approach is called concrete execution. + +To run the function we need to generate code that imports and calls it and saves result. + +## Get return value + +We write serialized return value in file. To serialize values of the most used built-in types we can use json module. For other types we will have to do it manually. + +## Test generation + +First we build AST of test code and then render it. diff --git a/utbot-python/samples/.gitignore b/utbot-python/samples/.gitignore new file mode 100644 index 0000000000..5f40fc7fee --- /dev/null +++ b/utbot-python/samples/.gitignore @@ -0,0 +1,6 @@ +.tmp/ +utbot_tests/ +utbot-cli.jar +__pycache__/ +.venv/ +venv/ diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__arithmetic.py b/utbot-python/samples/cli_utbot_tests/generated_tests__arithmetic.py new file mode 100644 index 0000000000..fe6ecb2585 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__arithmetic.py @@ -0,0 +1,40 @@ +import sys +sys.path.append('samples') +import builtins +import arithmetic +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable arithmetic.calculate_function_value + # region + def test_calculate_function_value(self): + actual = arithmetic.calculate_function_value(1, 101) + + self.assertEqual(11886.327847992769, actual) + + def test_calculate_function_value1(self): + actual = arithmetic.calculate_function_value(4294967296, 101) + + self.assertEqual(65535.99845886229, actual) + + def test_calculate_function_value2(self): + actual = arithmetic.calculate_function_value(float('nan'), 4294967296) + + self.assertTrue(isinstance(actual, builtins.float)) + + def test_calculate_function_value_throws_t(self): + arithmetic.calculate_function_value(0, 101) + + # raises builtins.ZeroDivisionError + + def test_calculate_function_value_throws_t1(self): + arithmetic.calculate_function_value(101, 101) + + # raises builtins.ValueError + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__deep_equals.py b/utbot-python/samples/cli_utbot_tests/generated_tests__deep_equals.py new file mode 100644 index 0000000000..3f04ba5a47 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__deep_equals.py @@ -0,0 +1,87 @@ +import sys +sys.path.append('samples') +import builtins +import deep_equals +import copyreg +import types +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable deep_equals.comparable_list + # region + def test_comparable_list(self): + actual = deep_equals.comparable_list(4294967296) + + comparable_class = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class.x = 0 + comparable_class1 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class1.x = 1 + comparable_class2 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class2.x = 2 + comparable_class3 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class3.x = 3 + comparable_class4 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class4.x = 4 + comparable_class5 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class5.x = 5 + comparable_class6 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class6.x = 6 + comparable_class7 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class7.x = 7 + comparable_class8 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class8.x = 8 + comparable_class9 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class9.x = 9 + + self.assertEqual([comparable_class, comparable_class1, comparable_class2, comparable_class3, comparable_class4, comparable_class5, comparable_class6, comparable_class7, comparable_class8, comparable_class9], actual) + + # endregion + + # endregion + + # region Test suites for executable deep_equals.incomparable_list + # region + def test_incomparable_list(self): + actual = deep_equals.incomparable_list(4294967296) + + incomparable_class = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class.x = 0 + incomparable_class1 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class1.x = 1 + incomparable_class2 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class2.x = 2 + incomparable_class3 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class3.x = 3 + incomparable_class4 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class4.x = 4 + incomparable_class5 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class5.x = 5 + incomparable_class6 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class6.x = 6 + incomparable_class7 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class7.x = 7 + incomparable_class8 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class8.x = 8 + incomparable_class9 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class9.x = 9 + expected_list = [incomparable_class, incomparable_class1, incomparable_class2, incomparable_class3, incomparable_class4, incomparable_class5, incomparable_class6, incomparable_class7, incomparable_class8, incomparable_class9] + expected_length = len(expected_list) + actual_length = len(actual) + + self.assertEqual(expected_length, actual_length) + + index = None + for index in range(0, expected_length, 1): + expected_element = expected_list[index] + actual_element = actual[index] + actual_x = actual_element.x + expected_x = expected_element.x + + self.assertEqual(expected_x, actual_x) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__deque.py b/utbot-python/samples/cli_utbot_tests/generated_tests__deque.py new file mode 100644 index 0000000000..fa4f36dee3 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__deque.py @@ -0,0 +1,35 @@ +import sys +sys.path.append('samples') +import builtins +import deque +import collections +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable deque.generate_people_deque + # region + def test_generate_people_deque(self): + actual = deque.generate_people_deque(4294967297) + + deque1 = collections.deque() + deque1.append('Alex') + deque1.append('Bob') + deque1.append('Cate') + deque1.append('Daisy') + deque1.append('Ed') + + self.assertEqual(deque1, actual) + + def test_generate_people_deque1(self): + actual = deque.generate_people_deque(0) + + deque1 = collections.deque() + + self.assertEqual(deque1, actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py b/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py new file mode 100644 index 0000000000..aba188aa11 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py @@ -0,0 +1,44 @@ +import sys +sys.path.append('samples') +import builtins +import types +import dicts +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable dicts.keys + + # region + + def test_keys(self): + word = dicts.Word({str(-123456789): str(), str(1.5 + 3.5j): str(), str(b'\x80'): str(), str(): str(1e+300 * 1e+300), str('unicode remains unicode'): str(), }) + + actual = word.keys() + + self.assertEqual(['-123456789', '(1.5+3.5j)', "b'\\x80'", '', 'unicode remains unicode'], actual) + # endregion + + # endregion + + # region Test suites for executable dicts.translate + + # region + + def test_translate(self): + dictionary = dicts.Dictionary([str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), str(1e+300 * 1e+300)], []) + + actual = dictionary.translate(str(id), str(1.5 + 3.5j)) + + self.assertEqual(None, actual) + + def test_translate_throws_t(self): + dictionary = dicts.Dictionary([], [{str(): str(), str(1e+300 * 1e+300): str(1e+300 * 1e+300), str(b'\x80'): str(), str(1.5 + 3.5j): str(), }, {str(-123456789): str(), str(id): str(), str(): str(), str(-1234567890): str(), }, {str(1.5 + 3.5j): str(), str(1e+300 * 1e+300): str(), str(-1234567890): str(), str(): str(1e+300 * 1e+300), }]) + + dictionary.translate(str('unicode remains unicode'), str(1.5 + 3.5j)) + + # raises builtins.KeyError + # endregion + + # endregion + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_with_eq.py b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_with_eq.py new file mode 100644 index 0000000000..f6eed2d5ab --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_with_eq.py @@ -0,0 +1,39 @@ +import sys +sys.path.append('samples') +import dummy_with_eq +import builtins +import copyreg +import types +import unittest + + +class TestDummy(unittest.TestCase): + # region Test suites for executable dummy_with_eq.propagate + # region + def test_propagate(self): + dummy = dummy_with_eq.Dummy(1) + + actual = dummy.propagate() + + dummy1 = copyreg._reconstructor(dummy_with_eq.Dummy, builtins.object, None) + dummy1.field = 1 + expected_list = [dummy1, dummy1] + expected_length = len(expected_list) + actual_length = len(actual) + + self.assertEqual(expected_length, actual_length) + + index = None + for index in range(0, expected_length, 1): + expected_element = expected_list[index] + actual_element = actual[index] + actual_field = actual_element.field + expected_field = expected_element.field + + self.assertEqual(expected_field, actual_field) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_without_eq.py b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_without_eq.py new file mode 100644 index 0000000000..3a38c1f6b7 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_without_eq.py @@ -0,0 +1,36 @@ +import sys +sys.path.append('samples') +import dummy_without_eq +import builtins +import copyreg +import types +import unittest + + +class TestDummy(unittest.TestCase): + # region Test suites for executable dummy_without_eq.propagate + # region + def test_propagate(self): + dummy = dummy_without_eq.Dummy() + + actual = dummy.propagate() + + dummy1 = copyreg._reconstructor(dummy_without_eq.Dummy, builtins.object, None) + expected_list = [dummy1, dummy1] + expected_length = len(expected_list) + actual_length = len(actual) + + self.assertEqual(expected_length, actual_length) + + index = None + for index in range(0, expected_length, 1): + expected_element = expected_list[index] + actual_element = actual[index] + + self.assertTrue(isinstance(actual_element, dummy_without_eq.Dummy)) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__graph.py b/utbot-python/samples/cli_utbot_tests/generated_tests__graph.py new file mode 100644 index 0000000000..c383980fb6 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__graph.py @@ -0,0 +1,36 @@ +import sys +sys.path.append('samples') +import unittest +import builtins +import graph +import copyreg +import types + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable graph.bfs + # region + def test_bfs(self): + actual = graph.bfs([graph.Node(str(1e+300 * 1e+300), []), graph.Node(str(id), []), graph.Node(str('unicode remains unicode'), [])]) + + node = copyreg._reconstructor(graph.Node, builtins.object, None) + node.name = 'unicode remains unicode' + node.children = [] + node1 = copyreg._reconstructor(graph.Node, builtins.object, None) + node1.name = '' + node1.children = [] + node2 = copyreg._reconstructor(graph.Node, builtins.object, None) + node2.name = 'inf' + node2.children = [] + self.assertEqual([node, node1, node2], actual) + + def test_bfs1(self): + actual = graph.bfs([]) + + self.assertEqual([], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__list_of_datetime.py b/utbot-python/samples/cli_utbot_tests/generated_tests__list_of_datetime.py new file mode 100644 index 0000000000..4339e26c0e --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__list_of_datetime.py @@ -0,0 +1,32 @@ +import sys +sys.path.append('samples') +import builtins +import list_of_datetime +import types +import datetime +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable list_of_datetime.get_data_labels + # region + def test_get_data_labels(self): + actual = list_of_datetime.get_data_labels({}) + + self.assertEqual(None, actual) + + def test_get_data_labels1(self): + actual = list_of_datetime.get_data_labels([datetime.time(0), datetime.time(microsecond=40), datetime.time(18, 45, 3, 1234), datetime.time(12, 0)]) + + self.assertEqual(['00:00', '00:00', '18:45', '12:00'], actual) + + def test_get_data_labels2(self): + actual = list_of_datetime.get_data_labels([datetime.time(microsecond=40), datetime.time()]) + + self.assertEqual(['1900-01-01', '1900-01-01'], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__lists.py b/utbot-python/samples/cli_utbot_tests/generated_tests__lists.py new file mode 100644 index 0000000000..38d5289f30 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__lists.py @@ -0,0 +1,21 @@ +import sys +sys.path.append('samples') +import builtins +import lists +import datetime +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable lists.find_articles_with_author + # region + def test_find_articles_with_author(self): + actual = lists.find_articles_with_author([lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str('unicode remains unicode'), datetime.datetime(2015, 4, 5, 1, 45)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str('unicode remains unicode'), datetime.datetime(2011, 1, 1)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(), datetime.datetime(1, 2, 3, 4, 5, 6, 7)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), datetime.datetime(1, 2, 3, 4, 5, 6, 7)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), datetime.datetime(2014, 11, 2, 1, 30)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), datetime.datetime(1, 2, 3, 4, 5, 6, 7))], str('unicode remains unicode')) + + self.assertEqual([], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__longest_subsequence.py b/utbot-python/samples/cli_utbot_tests/generated_tests__longest_subsequence.py new file mode 100644 index 0000000000..510171cbbc --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__longest_subsequence.py @@ -0,0 +1,25 @@ +import sys +sys.path.append('samples') +import builtins +import longest_subsequence +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable longest_subsequence.longest_subsequence + # region + def test_longest_subsequence(self): + actual = longest_subsequence.longest_subsequence([1, 83]) + + self.assertEqual([1, 83], actual) + + def test_longest_subsequence1(self): + actual = longest_subsequence.longest_subsequence([2, -1, 4294967296]) + + self.assertEqual([-1, 4294967296], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__matrix.py b/utbot-python/samples/cli_utbot_tests/generated_tests__matrix.py new file mode 100644 index 0000000000..db4d57758e --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__matrix.py @@ -0,0 +1,41 @@ +import sys +sys.path.append('samples') +import matrix +import builtins +import types +import copyreg +import unittest + + +class TestMatrix(unittest.TestCase): + # region Test suites for executable matrix.__add__ + + # region + + def test__add__(self): + matrix1 = matrix.Matrix([[float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(10 ** 23), float('1.4'), float(-1), float(-1), float('nan'), float('nan'), float(1970)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(314), float(-1), float('nan'), float(1970), 7.3, float(-1), float(-1)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float('nan')]]) + self1 = matrix.Matrix([[float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(10 ** 23), float('1.4'), float(-1), float(-1), float('nan'), float('nan'), float(1970)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(314), float(-1), float('nan'), float(1970), 7.3, float(-1), float(-1)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float('nan')]]) + + actual = matrix1.__add__(self1) + + matrix2 = copyreg._reconstructor(matrix.Matrix, builtins.object, None) + matrix2.dim = (6, 7) + matrix2.elements = [[float('nan'), 0.0, 3940.0, 14.6, float('nan'), 0, 0], [2e+23, 2.8, -2.0, -2.0, float('nan'), float('nan'), 3940.0], [float('nan'), 0.0, 3940.0, 14.6, float('nan'), 0, 0], [628.0, -2.0, float('nan'), 3940.0, 14.6, -2.0, -2.0], [float('nan'), 0.0, 3940.0, 14.6, float('nan'), 0, 0], [float('nan'), 0, 0, 0, 0, 0, 0]] + actual_dim = actual.dim + expected_dim = matrix2.dim + + self.assertEqual(expected_dim, actual_dim) + actual_elements = actual.elements + expected_elements = matrix2.elements + expected_list = expected_elements + expected_length = len(expected_list) + actual_length = len(actual_elements) + + self.assertEqual(expected_length, actual_length) + + self.assertTrue(isinstance(actual_elements, builtins.list)) + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__primitive_types.py b/utbot-python/samples/cli_utbot_tests/generated_tests__primitive_types.py new file mode 100644 index 0000000000..16f8f8423f --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__primitive_types.py @@ -0,0 +1,40 @@ +import sys +sys.path.append('samples') +import builtins +import primitive_types +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable primitive_types.pretty_print + # region + def test_pretty_print(self): + actual = primitive_types.pretty_print(object()) + + self.assertEqual('I do not have any variants', actual) + + def test_pretty_print1(self): + actual = primitive_types.pretty_print(str(b'\x80')) + + self.assertEqual("It is string.\nValue <>", actual) + + def test_pretty_print2(self): + actual = primitive_types.pretty_print((1 << 100)) + + self.assertEqual('It is integer.\nValue 1267650600228229401496703205376', actual) + + def test_pretty_print3(self): + actual = primitive_types.pretty_print(complex(float('inf'), float('inf'))) + + self.assertEqual('It is complex.\nValue (inf + infi)', actual) + + def test_pretty_print4(self): + actual = primitive_types.pretty_print([]) + + self.assertEqual('It is list.\nValue []', actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__quick_sort.py b/utbot-python/samples/cli_utbot_tests/generated_tests__quick_sort.py new file mode 100644 index 0000000000..ea0cf1a849 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__quick_sort.py @@ -0,0 +1,30 @@ +import sys +sys.path.append('samples') +import builtins +import quick_sort +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable quick_sort.quick_sort + # region + def test_quick_sort(self): + actual = quick_sort.quick_sort([4294967297, 83, (1 << 100), 4294967297, (1 << 100), 0, -3]) + + self.assertEqual([-3, 0, 83, 4294967297, 4294967297, 1267650600228229401496703205376, 1267650600228229401496703205376], actual) + + def test_quick_sort1(self): + actual = quick_sort.quick_sort([83, 123]) + + self.assertEqual([83, 123], actual) + + def test_quick_sort2(self): + actual = quick_sort.quick_sort([]) + + self.assertEqual([], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__test_coverage.py b/utbot-python/samples/cli_utbot_tests/generated_tests__test_coverage.py new file mode 100644 index 0000000000..02bcda08b6 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__test_coverage.py @@ -0,0 +1,35 @@ +import sys +sys.path.append('samples') +import builtins +import test_coverage +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable test_coverage.hard_function + # region + def test_hard_function(self): + actual = test_coverage.hard_function(83) + + self.assertEqual(2, actual) + + def test_hard_function1(self): + actual = test_coverage.hard_function(0) + + self.assertEqual(1, actual) + + def test_hard_function2(self): + actual = test_coverage.hard_function(4294967296) + + self.assertEqual(3, actual) + + def test_hard_function3(self): + actual = test_coverage.hard_function(float('nan')) + + self.assertEqual(4, actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__type_inference.py b/utbot-python/samples/cli_utbot_tests/generated_tests__type_inference.py new file mode 100644 index 0000000000..450f3f0488 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__type_inference.py @@ -0,0 +1,20 @@ +import sys +sys.path.append('samples') +import builtins +import type_inference +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable type_inference.type_inference + # region + def test_type_inference_by_fuzzer(self): + actual = type_inference.type_inference(0, str(), str(b'\x80'), [], {}) + + self.assertEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, ''], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__using_collections.py b/utbot-python/samples/cli_utbot_tests/generated_tests__using_collections.py new file mode 100644 index 0000000000..f9fe1c7d4a --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__using_collections.py @@ -0,0 +1,23 @@ +import sys +sys.path.append('samples') +import builtins +import using_collections +import collections +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable using_collections.generate_collections + # region + def test_generate_collections(self): + actual = using_collections.generate_collections({}) + + counter = collections.Counter({0: 100, }) + + self.assertEqual([{0: 100, }, counter, [(0, 100)]], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/easy_samples/.gitignore b/utbot-python/samples/easy_samples/.gitignore new file mode 100644 index 0000000000..aef5dc69ef --- /dev/null +++ b/utbot-python/samples/easy_samples/.gitignore @@ -0,0 +1,3 @@ +utbot_tests +.tmp +.pytest_cache \ No newline at end of file diff --git a/utbot-python/samples/easy_samples/corner_cases.py b/utbot-python/samples/easy_samples/corner_cases.py new file mode 100644 index 0000000000..2805e84108 --- /dev/null +++ b/utbot-python/samples/easy_samples/corner_cases.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +import sample_classes as s + + +@dataclass +class Inner: + x: int + + +class A: + def __init__(self, x: Inner): + self.x = x + + def f(cls, self, x: Inner): + self.x += 1 + return cls.x.x, self, x diff --git a/utbot-python/samples/easy_samples/deep_equals.py b/utbot-python/samples/easy_samples/deep_equals.py new file mode 100644 index 0000000000..e6acdb7b6c --- /dev/null +++ b/utbot-python/samples/easy_samples/deep_equals.py @@ -0,0 +1,74 @@ + + +class ComparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class BadClass: + def __init__(self, x): + self.x = x + + +def return_bad_class(x: int): + return BadClass(x) + + +def return_comparable_class(x: int): + return ComparableClass(x) + + +def primitive_list(x: int): + return [x] * 10 + + +def primitive_set(x: int): + return set(x+i for i in range(5)) + + +def primitive_dict(x: str, y: int): + return {x: y} + + +def comparable_list(length: int): + return [ComparableClass(x) for x in range(min(length, 10))] + + +def bad_list(length: int): + return [BadClass(x) for x in range(min(length, 10))] + + +class Node: + def __init__(self, name: str): + self.name = name + self.children = [] + + def __str__(self): + return f'' + + def __eq__(self, other): + if isinstance(other, Node): + return self.name == other.name + else: + return False + + +def cycle(x: str): + a = Node(x + '_a') + b = Node(x + '_b') + a.children.append(b) + b.children.append(a) + return a + + +def cycle2(x: str): + a = Node(x + '_a') + b = Node(x + '_b') + c = Node(x + '_c') + a.children.append(b) + b.children.append(c) + c.children.append(a) + return a diff --git a/utbot-python/samples/easy_samples/empty_file.py b/utbot-python/samples/easy_samples/empty_file.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/easy_samples/fully_annotated.py b/utbot-python/samples/easy_samples/fully_annotated.py new file mode 100644 index 0000000000..15854099d1 --- /dev/null +++ b/utbot-python/samples/easy_samples/fully_annotated.py @@ -0,0 +1,78 @@ +import heapq +from datetime import datetime +from typing import Any, List, Union, NoReturn + +""" +Default functions suite: fully annotated. +""" + + +def id_(x: Any) -> Any: + return x + + +def compare_with_5(x: int) -> bool: + return x > 5 + + +def add(x: int, y: int) -> int: + return x + y + + +def add_with_unused_param(x: int, y: int, unused: int) -> int: + return x + y + + +def append_exclamation_mark(s: str) -> str: + return s + "!" + + +def append_two_ints_to_typing_list(l: List[int]) -> List[int]: + return l + [1, -1] + + +def append_two_ints_to_builtin_list(l: list) -> list: + return l + [1, -1] + + +def append_ints_and_chars(l: List[Union[int, str]]) -> List[Union[int, str]]: + return l + [1, -1] + list("ab") + + +def format_data_labels(dates: List[datetime]) -> List[str]: + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] + + +class ClassWithIntField: + def __init__(self, int_field_value): + self.int_field = int_field_value + + +class ClassWithAnnotatedIntField: + def __init__(self, int_field: int): + self.int_field = int_field + + +def inc_int_field(c: ClassWithIntField) -> int: + c.int_field += 1 + return c.int_field + + +def call_heapify(ints: List[int]) -> List[int]: + heapq.heapify(ints) + return ints + + +def concatenate_args(*args: str) -> str: + return "+".join(args) + + +def concatenate_args_and_kwargs(*args: str, **kwargs: str) -> str: + return "+".join(args) + ";" + "?".join(kwargs.values()) + + +def raise_exception(exc: Exception) -> NoReturn: + raise exc diff --git a/utbot-python/samples/easy_samples/general.py b/utbot-python/samples/easy_samples/general.py new file mode 100644 index 0000000000..16c492eeb8 --- /dev/null +++ b/utbot-python/samples/easy_samples/general.py @@ -0,0 +1,193 @@ +import collections +import heapq +import typing +from socket import socket +from typing import * +from dataclasses import dataclass +import logging +import datetime + + +class Dummy: + def propagate(self): + return [self, self] + + + +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): + ans *= i + return ans + + +def empty_(): + pass + + +def conditions(x): + if x % 100 == 0: + return 1 + elif x + 100 < 400: + return 2 + else: + if x == complex(1, 2): + return x.real + elif len(str(x)) > 3: + return 3 + else: + return 4 + + +def test_call(x): + return repr_test(x) + + +def zero_division(x): + return x / x + + +def repr_test(x): + x *= 100 + return [1, x + 1, collections.UserList([1, 2, 3]), collections.Counter("flkafksdf"), collections.OrderedDict({1: 2, 4: "jflas"})] + + +def str_test(x): + x += '1"23' + x += "flskjd'jfslk" + if len(x.split('.')) == 1: + return '1"23' + else: + return """100''500""" + + +def return_socket(x: int): + return socket() + + +def empty(): + return 1 + + +def id_(x): + return x + + +def f(x, y, z, a, c, d, e, g, h, i): + if y % 2 == 0: + x = 1 + y + z += "aba" + a += [2] + list("str") + i + A = c < "abc" + B = "abc" == d + e = {1, 2, 3} + C = g == {1: 2} + h += int("777") + return x + y + + +def g(x: List[int], y: List): + y[0] += 1 + return x, y + + +def i(x: Dict[int, int]): + return x[0] + + +def j(x: Set[int]): + return x + + +def h(x): + if x < 123: + return 1 + return 2 + + +def a(x): + x.description += 1 + return x.description + + +def sqrt(x): + return x.sqrt() + + +@dataclass +class InventoryItem: + name: str + + +def inv(x): + return x.name + "aba" # interesting case with io.BytesIO + + +def b(x, y): + y = len(x) + return bytes(x, 'utf-8') + + +def c(x): + return heapq.heapify(x) + + +def d(x: Optional[int]): + return x + + +def k(x: typing.Any): + if x == complex(1): + return x + + +def constants(x): + if x == 1e5: + return "one" + elif (x > 1e4 - 2) and (x < 1e4): + return "two" + else: + return "three" + + +# interesting case with sets +def get_data_labels(dates): + if len(dates) == 0: + return None + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] + + +# bad function +def m(x): + x = frozenset() + return len(x + 1) + + +# very bad function +def n(x, y): + y = (-x) + 1 + x *= 10 + # z = -x + print(x) + x = len([1]) + if y == len([1]): + y += print() + return x.description + + +def list_of_list(x: List[List[InventoryItem]]): + return x diff --git a/utbot-python/samples/easy_samples/sample_classes.py b/utbot-python/samples/easy_samples/sample_classes.py new file mode 100644 index 0000000000..772d5b0740 --- /dev/null +++ b/utbot-python/samples/easy_samples/sample_classes.py @@ -0,0 +1,21 @@ +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 diff --git a/utbot-python/samples/generate_test_samples.sh b/utbot-python/samples/generate_test_samples.sh new file mode 100644 index 0000000000..73c765a72b --- /dev/null +++ b/utbot-python/samples/generate_test_samples.sh @@ -0,0 +1,24 @@ +# Usage: +# ./generate_test_samples.sh + +python_path=$1 +java_path=$2 + +$java_path -jar utbot-cli.jar generate_python samples/arithmetic.py -p $python_path -o cli_utbot_tests/generated_tests__arithmetic.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/deep_equals.py -p $python_path -o cli_utbot_tests/generated_tests__deep_equals.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dicts.py -p $python_path -o cli_utbot_tests/generated_tests__dicts.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Dictionary -m translate --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/deque.py -p $python_path -o cli_utbot_tests/generated_tests__deque.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_with_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_with_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Dummy -m propogate --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_without_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_without_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Dummy -m propogate --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/lists.py -p $python_path -o cli_utbot_tests/generated_tests__lists.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/list_of_datetime.py -p $python_path -o cli_utbot_tests/generated_tests__list_of_datetime.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/longest_subsequence.py -p $python_path -o cli_utbot_tests/generated_tests__longest_subsequence.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/matrix.py -p $python_path -o cli_utbot_tests/generated_tests__matrix.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Matrix -m __add__,__mul__,__matmul__ --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/primitive_types.py -p $python_path -o cli_utbot_tests/generated_tests__primitive_types.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/quick_sort.py -p $python_path -o cli_utbot_tests/generated_tests__quick_sort.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/test_coverage.py -p $python_path -o cli_utbot_tests/generated_tests__test_coverage.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/type_inference.py -p $python_path -o cli_utbot_tests/generated_tests__type_inference.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/using_collections.py -p $python_path -o cli_utbot_tests/generated_tests__using_collections.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_without_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_without_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 -c Dummy -m propagate --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_with_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_with_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 -c Dummy -m propagate --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/list_of_datetime.py -p $python_path -o cli_utbot_tests/generated_tests__list_of_datetime.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements diff --git a/utbot-python/samples/run_test_samples.sh b/utbot-python/samples/run_test_samples.sh new file mode 100755 index 0000000000..bbbf1bb171 --- /dev/null +++ b/utbot-python/samples/run_test_samples.sh @@ -0,0 +1,24 @@ +# Usage: +# ./run_test_samples.sh + +python_path=$1 +java_path=$2 + +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__arithmetic.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__deep_equals.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dicts.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__deque.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_with_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_without_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__lists.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__list_of_datetime.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__longest_subsequence.py -p $python_path +$java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__matrix.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__primitive_types.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__quick_sort.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__test_coverage.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__type_inference.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__using_collections.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_with_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_without_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__list_of_datetime.py -p $python_path diff --git a/utbot-python/samples/samples.md b/utbot-python/samples/samples.md new file mode 100644 index 0000000000..b3cf2794e8 --- /dev/null +++ b/utbot-python/samples/samples.md @@ -0,0 +1,27 @@ +## Соответствие файлов и сгенерированных тестов + +Примеры в `/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/samples/arithmetic.py b/utbot-python/samples/samples/arithmetic.py new file mode 100644 index 0000000000..6f26aa94f4 --- /dev/null +++ b/utbot-python/samples/samples/arithmetic.py @@ -0,0 +1,17 @@ +import math + + +def calculate_function_value(x, y): + """ + Calculate value `f` + | sqrt(x - 2y) , x > 100 + f(x, y) = | (3x^2 - 2xy + y^2) / sin(x) , -100 < x <= 100 + | (0.01 * x) ^ log2(y) , x < -100 + """ + + if x > 100: + return math.sqrt(x - 2 * y) + elif -100 < x <= 100: + return (3*x**2 - 2*x*y + y**2) / math.sin(x) + else: + return (0.01 * x) ** math.log2(y) diff --git a/utbot-python/samples/samples/deep_equals.py b/utbot-python/samples/samples/deep_equals.py new file mode 100644 index 0000000000..f34cb42c5b --- /dev/null +++ b/utbot-python/samples/samples/deep_equals.py @@ -0,0 +1,22 @@ +class ComparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class IncomparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return id(self) == id(other) + + +def comparable_list(length: int): + return [ComparableClass(x) for x in range(min(length, 10))] + + +def incomparable_list(length: int): + return [IncomparableClass(x) for x in range(min(length, 10))] diff --git a/utbot-python/samples/samples/deque.py b/utbot-python/samples/samples/deque.py new file mode 100644 index 0000000000..87ea0e958e --- /dev/null +++ b/utbot-python/samples/samples/deque.py @@ -0,0 +1,8 @@ +from collections import deque + + +def generate_people_deque(people_count: int): + names = ['Alex', 'Bob', 'Cate', 'Daisy', 'Ed'] + if people_count > 5: + people_count = 5 + return deque(sorted(names[:people_count])) \ No newline at end of file diff --git a/utbot-python/samples/samples/dicts.py b/utbot-python/samples/samples/dicts.py new file mode 100644 index 0000000000..6c63404587 --- /dev/null +++ b/utbot-python/samples/samples/dicts.py @@ -0,0 +1,29 @@ +from typing import List, Dict, Optional + + +class Word: + def __init__(self, translations: Dict[str, str]): + self.translations = translations + + def keys(self): + return list(self.translations.keys()) + + +class Dictionary: + def __init__( + self, + languages: List[str], + words: List[Dict[str, str]], + ): + self.languages = languages + self.words = [Word(translations) for translations in words] + + def translate(self, word: str, language: Optional[str]): + if language is not None: + for word_ in self.words: + if word_.translations[language] == word: + return word_ + else: + for word_ in self.words: + if word in word_.translations.values(): + return word_ diff --git a/utbot-python/samples/samples/dummy_with_eq.py b/utbot-python/samples/samples/dummy_with_eq.py new file mode 100644 index 0000000000..05ef7b822e --- /dev/null +++ b/utbot-python/samples/samples/dummy_with_eq.py @@ -0,0 +1,9 @@ +class Dummy: + def __init__(self, value: int): + self.field = value + + def __eq__(self, other): + return self.field == other.field + + def propagate(self): + return [self, self] diff --git a/utbot-python/samples/samples/dummy_without_eq.py b/utbot-python/samples/samples/dummy_without_eq.py new file mode 100644 index 0000000000..8df59b68d2 --- /dev/null +++ b/utbot-python/samples/samples/dummy_without_eq.py @@ -0,0 +1,3 @@ +class Dummy: + def propagate(self): + return [self, self] diff --git a/utbot-python/samples/samples/graph.py b/utbot-python/samples/samples/graph.py new file mode 100644 index 0000000000..9ef845a919 --- /dev/null +++ b/utbot-python/samples/samples/graph.py @@ -0,0 +1,42 @@ +from __future__ import annotations +from collections import deque +from typing import List + + +class Node: + def __init__(self, name: str, children: List[Node]): + self.name = name + self.children = children + + def __repr__(self): + return f'' + + def __eq__(self, other): + if isinstance(other, Node): + return self.name == other.name + else: + return False + + +def bfs(nodes): + if len(nodes) == 0: + return [] + + visited = [] + queue = deque(nodes) + while len(queue) > 0: + node = queue.pop() + if node not in visited: + visited.append(node) + for child in node.children: + queue.append(child) + return visited + + +if __name__ == '__main__': + a = Node('a', []) + b = Node('b', []) + c = Node('c', []) + a.children.append(b) + b.children.append(c) + print(bfs([a, b, c])) diff --git a/utbot-python/samples/samples/list_of_datetime.py b/utbot-python/samples/samples/list_of_datetime.py new file mode 100644 index 0000000000..b72f31d95c --- /dev/null +++ b/utbot-python/samples/samples/list_of_datetime.py @@ -0,0 +1,11 @@ +import datetime + + +def get_data_labels(dates): + if not dates: + dates.append(datetime.time(hour=23, minute=59)) + return None + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] diff --git a/utbot-python/samples/samples/lists.py b/utbot-python/samples/samples/lists.py new file mode 100644 index 0000000000..daa1472ac5 --- /dev/null +++ b/utbot-python/samples/samples/lists.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +import datetime +from typing import List + + +@dataclass +class Article: + title: str + author: str + content: str + created_at: datetime.datetime + + +def find_articles_with_author(articles: List[Article], author: str) -> List[Article]: + return [ + article for article in articles + if article.author == author + ] + + +if __name__ == '__main__': + print(find_articles_with_author([ + Article('a', 'a1', 'jfls', datetime.datetime.today()), + Article('b', 'a2', 'fjls', datetime.datetime.now()) + ], 'a1')) diff --git a/utbot-python/samples/samples/longest_subsequence.py b/utbot-python/samples/samples/longest_subsequence.py new file mode 100644 index 0000000000..1200f8fb5a --- /dev/null +++ b/utbot-python/samples/samples/longest_subsequence.py @@ -0,0 +1,27 @@ +from typing import List + + +def longest_subsequence(array: List[int]) -> List[int]: + array_length = len(array) + if array_length <= 1: + return array + pivot = array[0] + is_found = False + i = 1 + longest_subseq: List[int] = [] + while not is_found and i < array_length: + if array[i] < pivot: + is_found = True + temp_array = [element for element in array[i:] if element >= array[i]] + temp_array = longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + longest_subseq = temp_array + else: + i += 1 + + temp_array = [element for element in array[1:] if element >= pivot] + temp_array = [pivot] + longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + return temp_array + else: + return longest_subseq diff --git a/utbot-python/samples/samples/matrix.py b/utbot-python/samples/samples/matrix.py new file mode 100644 index 0000000000..1e3e58ab7f --- /dev/null +++ b/utbot-python/samples/samples/matrix.py @@ -0,0 +1,69 @@ +from __future__ import annotations +from itertools import product +from typing import List + + +class MatrixException(Exception): + def __init__(self, description): + self.description = description + + +class Matrix: + def __init__(self, elements: List[List[float]]): + self.dim = ( + len(elements), + max(len(elements[i]) for i in range(len(elements))) + if len(elements) > 0 else 0 + ) + self.elements = [ + row + [0] * (self.dim[1] - len(row)) + for row in elements + ] + + def __repr__(self): + return str(self.elements) + + def __eq__(self, other): + if isinstance(other, Matrix): + return self.elements == other.elements + + def __add__(self, other: Matrix): + if self.dim == other.dim: + return Matrix([ + [ + elem + other_elem for elem, other_elem in + zip(self.elements[i], other.elements[i]) + ] + for i in range(self.dim[0]) + ]) + + def __mul__(self, other): + if isinstance(other, (int, float, complex)): + return Matrix([ + [ + elem * other for elem in + self.elements[i] + ] + for i in range(self.dim[0]) + ]) + else: + raise MatrixException("Wrong Type") + + def __matmul__(self, other): + if isinstance(other, Matrix): + if self.dim[1] == other.dim[0]: + result = [[0 for _ in range(self.dim[0])] * other.dim[1]] + for i, j in product(range(self.dim[0]), range(other.dim[1])): + result[i][j] = sum( + self.elements[i][k] * other.elements[k][j] + for k in range(self.dim[1]) + ) + return Matrix(result) + else: + raise MatrixException("Wrong Type") + + +if __name__ == '__main__': + a = Matrix([[1., 2.]]) + b = Matrix([[3.], [4.]]) + print(a @ b) diff --git a/utbot-python/samples/samples/primitive_types.py b/utbot-python/samples/samples/primitive_types.py new file mode 100644 index 0000000000..0f95078654 --- /dev/null +++ b/utbot-python/samples/samples/primitive_types.py @@ -0,0 +1,11 @@ +def pretty_print(x): + if isinstance(x, int): + return 'It is integer.\n' + 'Value ' + str(x) + elif isinstance(x, str): + return 'It is string.\n' + 'Value <<' + x + '>>' + elif isinstance(x, complex): + return 'It is complex.\n' + 'Value (' + str(x.real) + ' + ' + str(x.real) + 'i)' + elif isinstance(x, list): + return 'It is list.\n' + f'Value {x}' + else: + return 'I do not have any variants' diff --git a/utbot-python/samples/samples/quick_sort.py b/utbot-python/samples/samples/quick_sort.py new file mode 100644 index 0000000000..a58f6d3bea --- /dev/null +++ b/utbot-python/samples/samples/quick_sort.py @@ -0,0 +1,32 @@ +import random +from typing import List + + +def quick_sort(array: List[int]): + def partition(A, left_index, right_index): + pivot = A[left_index] + i = left_index + 1 + for j in range(left_index + 1, right_index): + if A[j] < pivot: + A[j], A[i] = A[i], A[j] + i += 1 + A[left_index], A[i - 1] = A[i - 1], A[left_index] + return i - 1 + + def quick_sort_random(A, left, right): + if left < right: + pivot = random.randint(left, right - 1) + A[pivot], A[left] = ( + A[left], + A[pivot], + ) # switches the pivot with the left most bound + pivot_index = partition(A, left, right) + quick_sort_random( + A, left, pivot_index + ) # recursive quicksort to the left of the pivot point + quick_sort_random( + A, pivot_index + 1, right + ) # recursive quicksort to the right of the pivot point + quick_sort_random(array, 0, len(array)) + return array + diff --git a/utbot-python/samples/samples/test_coverage.py b/utbot-python/samples/samples/test_coverage.py new file mode 100644 index 0000000000..9a2740abfb --- /dev/null +++ b/utbot-python/samples/samples/test_coverage.py @@ -0,0 +1,12 @@ +def hard_function(x): + if x % 100 == 0: + return 1 + elif x + 100 < 400: + return 2 + else: + if x == complex(1, 2): + return x + elif len(str(x)) > 3: + return 3 + else: + return 4 diff --git a/utbot-python/samples/samples/type_inference.py b/utbot-python/samples/samples/type_inference.py new file mode 100644 index 0000000000..5cc524df1e --- /dev/null +++ b/utbot-python/samples/samples/type_inference.py @@ -0,0 +1,16 @@ +def type_inference(number, string, string_sep, list_of_number, dict_str_to_list): + new_string = '_' + string + '_' * number + new_string = new_string.capitalize() + string_sep + new_string[::-1] + + if len(list_of_number) < len(new_string): + list_of_number += [0] * (len(new_string) - len(list_of_number)) + + dict_str_to_list[string] = [] + for key in dict_str_to_list.keys(): + list_of_number.append(key) + + return list_of_number + + +if __name__ == '__main__': + print(type_inference(5, 'fjsl', '|', [1, 2, 3], {'fjls': [1, 2]})) \ No newline at end of file diff --git a/utbot-python/samples/samples/type_inference_2.py b/utbot-python/samples/samples/type_inference_2.py new file mode 100644 index 0000000000..876c2bbada --- /dev/null +++ b/utbot-python/samples/samples/type_inference_2.py @@ -0,0 +1,8 @@ +def g(x): + + def f(y): + if y in [0, 100, 200, 500]: + return y // 100 + + if f(x) > 10: + return x ** 2 \ No newline at end of file diff --git a/utbot-python/samples/samples/using_collections.py b/utbot-python/samples/samples/using_collections.py new file mode 100644 index 0000000000..debdc31bef --- /dev/null +++ b/utbot-python/samples/samples/using_collections.py @@ -0,0 +1,15 @@ +import collections + + +def generate_collections(collection): + collection[0] = 100 + elements = list(collection.items()) + return [ + collection, + collections.Counter(collection), + elements + ] + + +if __name__ == '__main__': + print(generate_collections({1: 2})) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt new file mode 100644 index 0000000000..8df65cf71b --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt @@ -0,0 +1,211 @@ +package org.utbot.python + +import mu.KotlinLogging +import org.utbot.framework.plugin.api.* +import org.utbot.fuzzer.* +import org.utbot.python.code.AnnotationProcessor.getModulesFromAnnotation +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonTreeModel +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.providers.defaultPythonModelProvider +import org.utbot.python.utils.camelToSnakeCase +import org.utbot.summary.fuzzer.names.MethodBasedNameSuggester +import org.utbot.summary.fuzzer.names.ModelBasedNameSuggester +import org.utbot.summary.fuzzer.names.TestSuggestedInfo +import java.lang.Long.max + +private val logger = KotlinLogging.logger {} +const val CHUNK_SIZE = 15 +const val TIMEOUT: Long = 10 + +class PythonEngine( + private val methodUnderTest: PythonMethod, + private val directoriesForSysPath: Set, + private val moduleToImport: String, + private val pythonPath: String, + private val fuzzedConcreteValues: List, + private val selectedTypeMap: Map, + private val timeoutForRun: Long, + private val initialCoveredLines: Set +) { + + private data class JobResult( + val evalResult: PythonEvaluationResult, + val values: List, + val thisObject: UtModel?, + val modelList: List + ) + + private fun suggestExecutionName( + methodUnderTestDescription: FuzzedMethodDescription, + jobResult: JobResult, + executionResult: UtExecutionResult + ): TestSuggestedInfo? { + val nameSuggester = sequenceOf( + ModelBasedNameSuggester(), + MethodBasedNameSuggester() + ) + + val testMethodName = try { + nameSuggester + .flatMap { it.suggest(methodUnderTestDescription, jobResult.values, executionResult) } + .firstOrNull() + } catch (t: Throwable) { + null + } + return testMethodName + } + + private fun createEvaluationInputIterator( + methodUnderTestDescription: FuzzedMethodDescription + ): Sequence { + val additionalModules = selectedTypeMap.values.flatMap { + getModulesFromAnnotation(it) + }.toSet() + + val evaluationInputIterator = fuzz(methodUnderTestDescription, defaultPythonModelProvider).map { values -> + val parameterValues = values.map { it.model } + val (thisObject, modelList) = + if (methodUnderTest.containingPythonClassId == null) + Pair(null, parameterValues) + else + Pair(parameterValues[0], parameterValues.drop(1)) + + EvaluationInput( + methodUnderTest, + parameterValues, + directoriesForSysPath, + moduleToImport, + pythonPath, + timeoutForRun, + thisObject, + modelList, + values, + additionalModules + ) + } + + return evaluationInputIterator + } + + private fun handleSuccessResult( + types: List, + evaluationResult: PythonEvaluationSuccess, + methodUnderTestDescription: FuzzedMethodDescription, + jobResult: JobResult + ): UtResult { + val (resultJSON, isException, coverage) = evaluationResult + + val prohibitedExceptions = listOf( + "builtins.AttributeError", + "builtins.TypeError" + ) + + if (isException && (resultJSON.type.name in prohibitedExceptions)) { // wrong type (sometimes mypy fails) + val errorMessage = "Evaluation with prohibited exception. Substituted types: ${ + types.joinToString { it.name } + }. Exception type: ${resultJSON.type.name}" + + logger.info(errorMessage) + + return UtError(errorMessage, Throwable()) + } + + val executionResult = + if (isException) + UtExplicitlyThrownException(Throwable(resultJSON.output.type.toString()), false) + else { + val outputType = resultJSON.type + val resultAsModel = PythonTreeModel( + resultJSON.output, + outputType + ) + UtExecutionSuccess(resultAsModel) + } + + val testMethodName = suggestExecutionName(methodUnderTestDescription, jobResult, executionResult) + + return UtFuzzedExecution( + stateBefore = EnvironmentModels(jobResult.thisObject, jobResult.modelList, emptyMap()), + stateAfter = EnvironmentModels(jobResult.thisObject, jobResult.modelList, emptyMap()), + result = executionResult, + coverage = coverage, + testMethodName = testMethodName?.testName?.camelToSnakeCase(), + displayName = testMethodName?.displayName, + ) + } + + fun fuzzing(): Sequence = sequence { + val types = methodUnderTest.arguments.map { + selectedTypeMap[it.name] ?: pythonAnyClassId + } + + val methodUnderTestDescription = FuzzedMethodDescription( + methodUnderTest.name, + pythonAnyClassId, + types, + fuzzedConcreteValues + ).apply { + compilableName = methodUnderTest.name // what's the difference with ordinary name? + parameterNameMap = { index -> methodUnderTest.arguments.getOrNull(index)?.name } + } + + val evaluationInputIterator = createEvaluationInputIterator(methodUnderTestDescription) + + val coveredLines = initialCoveredLines.toMutableSet() + evaluationInputIterator.chunked(CHUNK_SIZE).forEach { chunk -> + val coveredBefore = coveredLines.size + + // TODO: maybe reuse processes for next chunk? + val processes = chunk.map { evaluationInput -> + startEvaluationProcess(evaluationInput) + } + val startedTime = System.currentTimeMillis() + + val results = (processes zip chunk).map { (process, evaluationInput) -> + val wait = max(TIMEOUT, timeoutForRun - (System.currentTimeMillis() - startedTime)) + val evalResult = getEvaluationResult(evaluationInput, process, wait) + JobResult( + evalResult, + evaluationInput.values, + evaluationInput.thisObject, + evaluationInput.modelList + ) + } + + results.forEach { jobResult -> + when(val evaluationResult = jobResult.evalResult) { + is PythonEvaluationError -> { + if (evaluationResult.status != 0) { + yield(UtError( + "Error evaluation: ${evaluationResult.status}, ${evaluationResult.message}", + Throwable(evaluationResult.stackTrace.joinToString("\n")) + )) + } else { + yield( + UtError( + evaluationResult.message, + Throwable(evaluationResult.stackTrace.joinToString("\n")) + ) + ) + } + } + is PythonEvaluationSuccess -> { + evaluationResult.coverage.coveredInstructions.forEach { coveredLines.add(it.lineNumber) } + yield( + handleSuccessResult(types, evaluationResult, methodUnderTestDescription, jobResult) + ) + } + is PythonEvaluationTimeout -> { + yield(UtError(evaluationResult.message, Throwable())) + } + } + } + + val coveredAfter = coveredLines.size + + if (coveredAfter == coveredBefore) + return@sequence + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt new file mode 100644 index 0000000000..675718fa9c --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt @@ -0,0 +1,156 @@ +package org.utbot.python + +import com.beust.klaxon.Klaxon +import org.utbot.framework.plugin.api.Coverage +import org.utbot.framework.plugin.api.Instruction +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzer.FuzzedValue +import org.utbot.python.code.KlaxonPythonTreeParser +import org.utbot.python.code.PythonCodeGenerator +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.getResult +import org.utbot.python.utils.startProcess +import java.io.File + + +sealed class PythonEvaluationResult + +class PythonEvaluationError( + val status: Int, + val message: String, + val stackTrace: List +) : PythonEvaluationResult() + +class PythonEvaluationTimeout( + val message: String = "Timeout" +) : PythonEvaluationResult() + +class PythonEvaluationSuccess( + private val output: OutputData, + private val isException: Boolean, + val coverage: Coverage +) : PythonEvaluationResult() { + operator fun component1() = output + operator fun component2() = isException + operator fun component3() = coverage +} + +data class OutputData(val output: PythonTree.PythonTreeNode, val type: PythonClassId) + +data class EvaluationInput( + val method: PythonMethod, + val methodArguments: List, + val directoriesForSysPath: Set, + val moduleToImport: String, + val pythonPath: String, + val timeoutForRun: Long, + val thisObject: UtModel?, + val modelList: List, + val values: List, + val additionalModulesToImport: Set = emptySet() +) + +data class EvaluationProcess( + val process: Process, + val fileWithCode: File, + val fileForOutput: File +) + +fun startEvaluationProcess(input: EvaluationInput): EvaluationProcess { + val fileForOutput = TemporaryFileManager.assignTemporaryFile( + tag = "out_" + input.method.name + ".py", + addToCleaner = false + ) + val runCode = PythonCodeGenerator.generateRunFunctionCode( + input.method, + input.methodArguments, + input.directoriesForSysPath, + input.moduleToImport, + input.additionalModulesToImport, + fileForOutput.path.replace("\\", "\\\\") + ) + val fileWithCode = TemporaryFileManager.createTemporaryFile( + runCode, + tag = "run_" + input.method.name + ".py", + addToCleaner = false + ) + return EvaluationProcess( + startProcess(listOf(input.pythonPath, fileWithCode.path)), + fileWithCode, + fileForOutput + ) +} + +fun calculateCoverage(statements: List, missedStatements: List, input: EvaluationInput): Coverage { + val covered = statements.filter { it !in missedStatements } + return Coverage( + coveredInstructions=covered.map { + Instruction( + input.method.containingPythonClassId?.name ?: pythonAnyClassId.name, + input.method.methodSignature(), + it, + it.toLong() + ) + }, + instructionsCount = null, + missedInstructions = missedStatements.map { + Instruction( + input.method.containingPythonClassId?.name ?: pythonAnyClassId.name, + input.method.methodSignature(), + it, + it.toLong() + ) + } + ) +} + +fun getEvaluationResult(input: EvaluationInput, process: EvaluationProcess, timeout: Long): PythonEvaluationResult { + val result = getResult(process.process, timeout = timeout) + process.fileWithCode.delete() + + if (result.terminatedByTimeout) + return PythonEvaluationTimeout() + + if (result.exitValue != 0) + return PythonEvaluationError( + result.exitValue, + result.stdout, + result.stderr.split("\n") + ) + + val output = process.fileForOutput.readText().split(System.lineSeparator()) + process.fileForOutput.delete() + + if (output.size != 4) + return PythonEvaluationError( + 0, + "Incorrect format of output", + emptyList() + ) + + val status = output[0] + + if (status != PythonCodeGenerator.successStatus && status != PythonCodeGenerator.failStatus) + return PythonEvaluationError( + 0, + "Incorrect format of output", + emptyList() + ) + + val isSuccess = status == PythonCodeGenerator.successStatus + + val pythonTree = KlaxonPythonTreeParser(output[1]).parseJsonToPythonTree() + val stmts = Klaxon().parseArray(output[2])!! + val missed = Klaxon().parseArray(output[3])!! + + val coverage = calculateCoverage(stmts, missed, input) + + return PythonEvaluationSuccess( + OutputData(pythonTree, pythonTree.type), + !isSuccess, + coverage + ) +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt new file mode 100644 index 0000000000..1b64c4d8cf --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt @@ -0,0 +1,175 @@ +package org.utbot.python + +import mu.KotlinLogging +import org.utbot.framework.minimization.minimizeExecutions +import org.utbot.framework.plugin.api.UtError +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.python.code.ArgInfoCollector +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.typing.AnnotationFinder.findAnnotations +import org.utbot.python.typing.MypyAnnotations +import org.utbot.python.utils.AnnotationNormalizer.annotationFromProjectToClassId +import java.nio.file.Path + +private val logger = KotlinLogging.logger {} + +object PythonTestCaseGenerator { + private var withMinimization: Boolean = true + private var pythonRunRoot: Path? = null + private lateinit var directoriesForSysPath: Set + private lateinit var curModule: String + private lateinit var pythonPath: String + private lateinit var fileOfMethod: String + private lateinit var isCancelled: () -> Boolean + private var timeoutForRun: Long = 0 + + fun init( + directoriesForSysPath: Set, + moduleToImport: String, + pythonPath: String, + fileOfMethod: String, + timeoutForRun: Long, + withMinimization: Boolean = true, + pythonRunRoot: Path? = null, + isCancelled: () -> Boolean + ) { + this.directoriesForSysPath = directoriesForSysPath + this.curModule = moduleToImport + this.pythonPath = pythonPath + this.fileOfMethod = fileOfMethod + this.withMinimization = withMinimization + this.isCancelled = isCancelled + this.timeoutForRun = timeoutForRun + this.pythonRunRoot = pythonRunRoot + } + + private val storageForMypyMessages: MutableList = mutableListOf() + + fun generate(method: PythonMethod): PythonTestSet { + storageForMypyMessages.clear() + + val initialArgumentTypes = method.arguments.map { + annotationFromProjectToClassId( + it.annotation, + pythonPath, + curModule, + fileOfMethod, + directoriesForSysPath + ) + }.toMutableList() + + // TODO: consider static and class methods + if (method.containingPythonClassId != null) { + initialArgumentTypes[0] = NormalizedPythonAnnotation(method.containingPythonClassId!!.name) + } + + logger.debug("Collecting hints about arguments") + val argInfoCollector = ArgInfoCollector(method, initialArgumentTypes) + logger.debug("Collected.") + val annotationSequence = getAnnotations(method, initialArgumentTypes, argInfoCollector, isCancelled) + + val executions = mutableListOf() + val errors = mutableListOf() + var missingLines: Set? = null + val coveredLines = mutableSetOf() + var generated = 0 + + run breaking@{ + annotationSequence.forEach { annotations -> + if (isCancelled()) + return@breaking + + logger.debug( + "Found annotations: ${ + annotations.map { "${it.key}: ${it.value}" }.joinToString(" ") + }" + ) + + val engine = PythonEngine( + method, + directoriesForSysPath, + curModule, + pythonPath, + argInfoCollector.getConstants(), + annotations, + timeoutForRun, + coveredLines + ) + + engine.fuzzing().forEach { + if (isCancelled()) + return@breaking + generated += 1 + when (it) { + is UtExecution -> { + logger.debug("Added execution") + executions += it + missingLines = updateCoverage(it, coveredLines, missingLines) + } + + is UtError -> { + logger.debug("Failed evaluation. Reason: ${it.description}") + errors += it + } + } + if (withMinimization && missingLines?.isEmpty() == true && generated % CHUNK_SIZE == 0) + return@breaking + } + } + } + + val (successfulExecutions, failedExecutions) = executions.partition { it.result is UtExecutionSuccess } + + return PythonTestSet( + method, + if (withMinimization) + minimizeExecutions(successfulExecutions) + minimizeExecutions(failedExecutions) + else + executions, + errors, + storageForMypyMessages + ) + } + + // returns new missingLines + private fun updateCoverage( + execution: UtExecution, + coveredLines: MutableSet, + missingLines: Set? + ): Set { + execution.coverage?.coveredInstructions?.map { instr -> coveredLines.add(instr.lineNumber) } + val curMissing = + execution.coverage + ?.missedInstructions + ?.map { x -> x.lineNumber }?.toSet() + ?: emptySet() + return if (missingLines == null) curMissing else missingLines intersect curMissing + } + + private fun getAnnotations( + method: PythonMethod, + initialArgumentTypes: List, + argInfoCollector: ArgInfoCollector, + isCancelled: () -> Boolean + ): Sequence> { + + val existingAnnotations = mutableMapOf() + initialArgumentTypes.forEachIndexed { index, classId -> + if (classId != pythonAnyClassId) + existingAnnotations[method.arguments[index].name] = classId + } + + return findAnnotations( + argInfoCollector, + method, + existingAnnotations, + curModule, + directoriesForSysPath, + pythonPath, + isCancelled, + storageForMypyMessages + ) + } +} \ 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 new file mode 100644 index 0000000000..f5df5ac24a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt @@ -0,0 +1,288 @@ +package org.utbot.python + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.utbot.framework.codegen.PythonSysPathImport +import org.utbot.framework.codegen.PythonSystemImport +import org.utbot.framework.codegen.PythonUserImport +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonMethodId +import org.utbot.python.framework.api.python.PythonModel +import org.utbot.python.framework.api.python.RawPythonAnnotation +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.typing.MypyAnnotations +import org.utbot.python.typing.PythonTypesStorage +import org.utbot.python.typing.StubFileFinder +import org.utbot.python.utils.Cleaner +import org.utbot.python.utils.RequirementsUtils.requirementsAreInstalled +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.getLineOfFunction +import java.io.File +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, + visitOnlySpecifiedSource: Boolean = false, + withMinimization: Boolean = true, + isCanceled: () -> Boolean = { false }, + checkingRequirementsAction: () -> 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 { + TemporaryFileManager.setup() + + if (!doNotCheckRequirements) { + checkingRequirementsAction() + if (!requirementsAreInstalled(pythonPath)) { + val result = requirementsAreNotInstalledAction() + if (result == MissingRequirementsActionResult.NOT_INSTALLED) + return + } + } + + startedLoadingPythonTypesAction() + PythonTypesStorage.pythonPath = pythonPath + + val onlySpecifiedFile = if (!visitOnlySpecifiedSource) null else File(pythonFilePath) + PythonTypesStorage.refreshProjectClassesAndModulesLists(directoriesForSysPath, onlySpecifiedFile) + StubFileFinder + + startedTestGenerationAction() + val startTime = System.currentTimeMillis() + + val testCaseGenerator = PythonTestCaseGenerator.apply { + init( + directoriesForSysPath, + currentPythonModule, + pythonPath, + pythonFilePath, + timeoutForRun, + withMinimization, + pythonRunRoot = pythonRunRoot + ) { isCanceled() || (System.currentTimeMillis() - startTime) > timeout } + } + + val tests = pythonMethods.map { method -> + testCaseGenerator.generate(method) + } + + val (notEmptyTests, emptyTestSets) = tests.partition { it.executions.isNotEmpty() } + + if (isCanceled()) + return + + if (emptyTestSets.isNotEmpty()) { + notGeneratedTestsAction(emptyTestSets.map { it.method.name }) + } + + if (notEmptyTests.isEmpty()) + return + + val classId = + if (containingClassName == null) + PythonClassId("$currentPythonModule.TopLevelFunctions") + else + PythonClassId("$currentPythonModule.$containingClassName") + + val methodIds = notEmptyTests.associate { + it.method to PythonMethodId( + classId, + it.method.name, + RawPythonAnnotation(it.method.returnAnnotation ?: pythonNoneClassId.name), + it.method.arguments.map { argument -> + argument.annotation?.let { annotation -> + RawPythonAnnotation(annotation) + } ?: pythonAnyClassId + } + ) + } + + val paramNames = notEmptyTests.associate { testSet -> + methodIds[testSet.method] as ExecutableId to testSet.method.arguments.map { it.name } + }.toMutableMap() + + + val importParamModules = notEmptyTests.flatMap { testSet -> + testSet.executions.flatMap { execution -> + execution.stateBefore.parameters.flatMap { utModel -> + (utModel as PythonModel).let { + it.allContainingClassIds.map { classId -> + PythonUserImport(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(classId.moduleName) + } + } + } + } else null + }.flatten() + } + val testRootModules = notEmptyTests.mapNotNull { testSet -> + methodIds[testSet.method]?.rootModuleName?.let { PythonUserImport(it) } + } + val sysImport = PythonSystemImport("sys") + val sysPathImports = relativizePaths(pythonRunRoot, directoriesForSysPath).map { PythonSysPathImport(it) } + + val testFrameworkModule = + testFramework.testSuperClass?.let { PythonUserImport((it as PythonClassId).rootModuleName) } + + val allImports = ( + importParamModules + importResultModules + testRootModules + sysPathImports + listOf( + testFrameworkModule, + sysImport + ) + ).filterNotNull().toSet() + + val context = UtContext(this::class.java.classLoader) + withUtContext(context) { + val codegen = PythonCodeGenerator( + classId, + paramNames = paramNames, + testFramework = testFramework, + testClassPackageName = "", + ) + val testCode = codegen.pythonGenerateAsStringWithTestReport( + notEmptyTests.map { testSet -> + CgMethodTestSet( + methodIds[testSet.method] as ExecutableId, + testSet.executions + ) + }, + 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() + } + } + + 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() + } + } + + data class InstructionSet( + val start: Int, + val end: Int + ) + + data class CoverageInfo( + val covered: List, + val notCovered: List + ) + + private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + private val jsonAdapter = moshi.adapter(CoverageInfo::class.java) + + private fun getInstructionSetList(instructions: Collection): List = + instructions.sorted().fold(emptyList()) { acc, lineNumber -> + if (acc.isEmpty()) + return@fold listOf(InstructionSet(lineNumber, lineNumber)) + val elem = acc.last() + if (elem.end + 1 == lineNumber) + acc.dropLast(1) + listOf(InstructionSet(elem.start, lineNumber)) + else + acc + listOf(InstructionSet(lineNumber, lineNumber)) + } + + private fun getCoverageInfo(testSets: List): String { + val covered = mutableSetOf() + val missed = mutableSetOf>() + testSets.forEach { testSet -> + testSet.executions.forEach inner@{ execution -> + val coverage = execution.coverage ?: return@inner + coverage.coveredInstructions.forEach { covered.add(it.lineNumber) } + missed.add(coverage.missedInstructions.map { it.lineNumber }.toSet()) + } + } + val coveredInstructionSets = getInstructionSetList(covered) + val missedInstructionSets = + if (missed.isEmpty()) + emptyList() + else + getInstructionSetList(missed.reduce { a, b -> a intersect b }) + + 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 new file mode 100644 index 0000000000..401f055699 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt @@ -0,0 +1,31 @@ +package org.utbot.python + +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.FunctionDef +import org.utbot.framework.plugin.api.UtError +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.typing.MypyAnnotations + +data class PythonArgument(val name: String, val annotation: String?) + +interface PythonMethod { + val name: String + val returnAnnotation: String? + val arguments: List + val moduleFilename: String + fun asString(): String + fun ast(): FunctionDef + val containingPythonClassId: PythonClassId? + fun methodSignature(): String = "$name(" + arguments.joinToString(", ") { + "${it.name}: ${it.annotation ?: pythonAnyClassId.name}" + } + ")" +} + +data class PythonTestSet( + val method: PythonMethod, + val executions: List, + val errors: List, + val mypyReport: List, + val classId: PythonClassId? = null, +) \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/ArgInfoCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/ArgInfoCollector.kt new file mode 100644 index 0000000000..9a5d096de1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/ArgInfoCollector.kt @@ -0,0 +1,564 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.AST +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.model.expr.atoms.Num +import io.github.danielnaczo.python3parser.model.expr.atoms.Str +import io.github.danielnaczo.python3parser.model.expr.comprehensions.Comprehension +import io.github.danielnaczo.python3parser.model.expr.datastructures.Dict +import io.github.danielnaczo.python3parser.model.expr.datastructures.ListExpr +import io.github.danielnaczo.python3parser.model.expr.operators.Operator +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.* +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.boolops.Or +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.* +import io.github.danielnaczo.python3parser.model.expr.operators.unaryops.* +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.forStmts.For +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.Delete +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.Assign +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.AugAssign +import io.github.danielnaczo.python3parser.visitors.modifier.ModifierVisitor +import org.apache.commons.lang3.math.NumberUtils +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedContext +import org.utbot.python.PythonMethod +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonBoolModel +import org.utbot.python.framework.api.python.PythonListModel +import org.utbot.python.framework.api.python.PythonDictModel +import org.utbot.python.framework.api.python.PythonSetModel +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonFloatClassId +import org.utbot.python.framework.api.python.util.pythonIntClassId +import org.utbot.python.framework.api.python.util.pythonStrClassId +import org.utbot.python.typing.PythonTypesStorage +import java.math.BigDecimal +import java.math.BigInteger + +class ArgInfoCollector(val method: PythonMethod, private val argumentTypes: List) { + sealed class Hint + + class Type(val type: PythonClassId) : Hint() + data class Method(val name: String) : Hint() + data class FunctionArg(val name: String, val index: Int) : Hint() + data class FunctionRet(val name: String) : Hint() + data class Field(val name: String) : Hint() + data class Function(val name: String) : Hint() + + data class ArgInfoStorage( + val types: MutableSet = mutableSetOf(), + val methods: MutableSet = mutableSetOf(), + val functionArgs: MutableSet = mutableSetOf(), + val fields: MutableSet = mutableSetOf(), + val functionRets: MutableSet = mutableSetOf() + ) { + fun toList(): List { + return listOf( + types, + methods, + functionArgs, + fields, + functionRets + ).flatten() + } + } + + data class GeneralStorage( + val types: MutableList = mutableListOf(), + val functions: MutableSet = mutableSetOf(), + val fields: MutableSet = mutableSetOf(), + val methods: MutableSet = mutableSetOf() + ) { + fun toList(): List { + return listOf( + types, + functions, + fields, + methods + ).flatten() + } + } + + private val paramNames = method.arguments.mapIndexedNotNull { index, param -> + if (argumentTypes[index] == pythonAnyClassId) param.name else null + } + + private val collectedValues = mutableMapOf() + + private val visitor = MatchVisitor(paramNames, mutableSetOf(), GeneralStorage()) + + init { + visitor.visitFunctionDef(method.ast(), collectedValues) + } + + fun getConstants(): List = visitor.constStorage.toList() + + fun getAllGeneralHints(): List = visitor.generalStorage.toList() + + fun getAllArgHints(): Map> { + return paramNames.associateWith { argName -> (collectedValues[argName]?.toList() ?: emptyList()) } + } + + private class MatchVisitor( + private val paramNames: List, + val constStorage: MutableSet, + val generalStorage: GeneralStorage + ) : ModifierVisitor>() { + + private fun namePat(): Pattern<(String) -> A, A, N> { + val names: List A, A, N>> = paramNames.map { paramName -> + map0(refl(name(equal(paramName))), paramName) + } + return names.fold(reject()) { acc, elem -> or(acc, elem) } + } + + private fun typedExpr(atom: Pattern): Pattern = + opExpr(refl(atom), refl(name(drop()))) + + private fun typedExpressionPat(): Pattern<(Type) -> A, A, Expression> { + // map must preserve order + val typeMap = linkedMapOf>( + "builtins.int" to refl(num(int())), + "builtins.float" to refl(num(drop())), + "builtins.str" to refl(str(drop())), + "builtins.bool" to or(refl(true_()), refl(false_())), + "types.NoneType" to refl(none()), + "builtins.dict" to refl(dict(drop(), drop())), + "builtins.list" to refl(list(drop())), + "builtins.set" to refl(set(drop())), + "builtins.tuple" to refl(tuple(drop())) + ) + PythonTypesStorage.builtinTypes.forEach { typeNameWithoutPrefix -> + val typeNameWithPrefix = "builtins.$typeNameWithoutPrefix" + if (typeMap.containsKey(typeNameWithPrefix)) + typeMap[typeNameWithPrefix] = or( + typeMap[typeNameWithPrefix]!!, + refl(functionCallWithoutPrefix(name(equal(typeNameWithoutPrefix)), drop())) + ) + else + typeMap[typeNameWithPrefix] = + refl(functionCallWithoutPrefix(name(equal(typeNameWithoutPrefix)), drop())) + } + return typeMap.entries.fold(reject()) { acc, entry -> + or(acc, map0(typedExpr(entry.value), Type(PythonClassId(entry.key)))) + } + } + + private fun addToStorage( + paramName: String, + collection: MutableMap, + add: (ArgInfoStorage) -> Unit + ) { + val argInfoStorage = collection[paramName] ?: ArgInfoStorage() + add(argInfoStorage) + collection[paramName] = argInfoStorage + } + + private fun parseAndPutType( + collection: MutableMap, + pat: Pattern<(String) -> (Type) -> Pair?, Pair?, N>, + ast: N + ) { + parse(pat, onError = null, ast) { paramName -> { storage -> Pair(paramName, storage) } }?.let { + addToStorage(it.first, collection) { storage -> storage.types.add(it.second) } + } + } + + private fun parseAndPutFunctionRet( + collection: MutableMap, + pat: Pattern<(String) -> (FunctionRet) -> ResFuncRet, ResFuncRet, N>, + ast: N + ) { + parse(pat, onError = null, ast) { paramName -> { storage -> Pair(paramName, storage) } }?.let { + addToStorage(it.first, collection) { storage -> storage.functionRets.add(it.second) } + } + } + + private fun collectFunctionArg(atom: Atom, param: MutableMap) { + val argNamePatterns: List (Int) -> ResFuncArg, ResFuncArg, List>> = + paramNames.map { paramName -> + map0(anyIndexed(refl(name(equal(paramName)))), paramName) + } + val argPat: Pattern<(String) -> (Int) -> ResFuncArg, ResFuncArg, List> = + argNamePatterns.fold(reject()) { acc, elem -> or(acc, elem) } + val pat = functionCallWithPrefix( + fprefix = drop(), + fid = apply(), + farguments = arguments( + fargs = argPat, + drop(), drop(), drop() + ) + ) + parse(pat, onError = null, atom) { funcName -> + { paramName -> + { index -> + Pair(paramName, FunctionArg(funcName, index)) + } + } + }?.let { + addToStorage(it.first, param) { storage -> storage.functionArgs.add(it.second) } + } + } + + private fun collectField(atom: Atom, param: MutableMap) { + val pat = classField( + fname = namePat<(String) -> ResField, Name>(), + fattributeId = apply() + ) + parse(pat, onError = null, atom) { paramName -> + { attributeId -> + Pair(paramName, Field(attributeId)) + } + }?.let { + addToStorage(it.first, param) { storage -> storage.fields.add(it.second) } + } + } + + private fun subscriptPat(): Pattern<(String) -> A, A, Atom> = + atom( + fatomElement = namePat(), + ftrailers = first(refl(index(drop()))) + ) + + private fun collectAtomMethod(atom: Atom, param: MutableMap) { + val methodPat = classMethod( + fname = namePat<(String) -> ResMethod, Name>(), + fattributeId = apply(), + farguments = drop() + ) + val getPat = swap(map0(subscriptPat(), "__getitem__")) + val pat = or(methodPat, getPat) + parse(pat, onError = null, atom) { paramName -> + { attributeId -> + Pair(paramName, Method(attributeId)) + } + }?.let { + addToStorage(it.first, param) { storage -> storage.methods.add(it.second) } + } + } + + private val magicMethodOfFunctionCall: Map = + mapOf( + "len" to "__len__", + "str" to "__str__", + "repr" to "__repr__", + "bytes" to "__bytes__", + "format" to "__format__", + "hash" to "__hash__", + "bool" to "__bool__", + "dir" to "__dir__" + ) + + private fun collectMagicMethodsFromCalls(atom: Atom, param: MutableMap) { + val callNamePat: Pattern<(String) -> (String) -> ResMethod, (String) -> ResMethod, Name> = + magicMethodOfFunctionCall.entries.fold(reject()) { acc, entry -> + or(acc, map0(name(equal(entry.key)), entry.value)) + } + val pat = functionCallWithoutPrefix( + fname = callNamePat, + farguments = arguments( + fargs = any(namePat()), + drop(), drop(), drop() + ) + ) + parse(pat, onError = null, atom) { methodName -> + { paramName -> + Pair(paramName, Method(methodName)) + } + }?.let { + addToStorage(it.first, param) { storage -> storage.methods.add(it.second) } + } + } + + private fun collectFunction(atom: Atom) { + parse( + functionCallWithPrefix( + fid = apply(), + fprefix = drop(), + farguments = drop() + ), + onError = null, + atom + ) { it }?.let { generalStorage.functions.add(Function(it)) } + } + + private fun collectGeneralMethod(atom: Atom) { + parse( + methodFromAtom( + fattributeId = apply(), + farguments = drop() + ), + onError = null, + atom + ) { it }?.let { generalStorage.methods.add(Method(it)) } + } + + private fun collectGeneralFields(atom: Atom) { + parse( + attributesFromAtom(fattributes = apply()), + onError = null, + atom + ) { it }?.let { attributes -> + attributes.forEach { generalStorage.fields.add(Field(it)) } + } + } + + override fun visitAtom(atom: Atom, param: MutableMap): AST { + collectFunctionArg(atom, param) + collectField(atom, param) + collectAtomMethod(atom, param) + collectMagicMethodsFromCalls(atom, param) + collectFunction(atom) + collectGeneralMethod(atom) + collectGeneralFields(atom) + return super.visitAtom(atom, param) + } + + private fun collectTypes(assign: Assign, param: MutableMap) { + val pat: Pattern<(String) -> (Type) -> ResAssign, List, Assign> = assignAll( + ftargets = allMatches(namePat()), fvalue = typedExpressionPat() + ) + parse( + pat, + onError = emptyList(), + assign + ) { paramName -> { typeStorage -> Pair(paramName, typeStorage) } }.map { + addToStorage(it.first, param) { storage -> storage.types.add(it.second) } + } + } + + private fun collectSetItem(assign: Assign, param: MutableMap) { + val setItemPat: Pattern<(String) -> String, List, Assign> = assignAll( + ftargets = allMatches(refl(subscriptPat())), + fvalue = drop() + ) + val setItemParams = parse(setItemPat, onError = emptyList(), assign) { it } + setItemParams.map { paramName -> + addToStorage(paramName, param) { storage -> + storage.methods.add(Method("__setitem__")) + } + } + } + + private fun funcCallNamePat(): Pattern<(FunctionRet) -> A, A, N> = + map1( + refl( + functionCallWithPrefix( + fprefix = drop(), + fid = apply(), + farguments = drop() + ) + ) + ) { x -> FunctionRet(x) } + + private fun collectFuncRet(assign: Assign, param: MutableMap) { + val pat: Pattern<(String) -> (FunctionRet) -> ResFuncRet, List, Assign> = assignAll( + ftargets = allMatches(namePat()), + fvalue = funcCallNamePat() + ) + val functionRets = parse(pat, onError = emptyList(), assign) { paramName -> + { functionStorage -> Pair(paramName, functionStorage) } + } + functionRets.forEach { + if (it != null) + addToStorage(it.first, param) { storage -> storage.functionRets.add(it.second) } + } + } + + override fun visitAssign(ast: Assign, param: MutableMap): AST { + collectTypes(ast, param) + collectSetItem(ast, param) + collectFuncRet(ast, param) + return super.visitAssign(ast, param) + } + + private fun getOpMagicMethod(op: Operator?) = + when (op) { + is Gt -> "__gt__" + is GtE -> "__ge__" + is Lt -> "__lt__" + is LtE -> "__le__" + is Eq -> "__eq__" + is NotEq -> "__ne__" + is In -> "__contains__" + is FloorDiv -> "__floordiv__" + is Invert -> "__invert__" + is LShift -> "__lshift__" + is Mod -> "__mod__" + is Mult -> "__mul__" + is USub -> "__neg__" + is Or -> "__or__" + is UAdd -> "__pos__" + is Pow -> "__pow__" + is RShift -> "__rshift__" + is Sub -> "__sub__" + is Add -> "__add__" + is Div -> "__truediv__" + is BitXor -> "__xor__" + is Not -> "__not__" + else -> null + } + + override fun visitAugAssign(ast: AugAssign, param: MutableMap): AST { + parseAndPutType(param, augAssign(ftarget = namePat(), fvalue = typedExpressionPat(), fop = drop()), ast) + parseAndPutFunctionRet(param, augAssign(ftarget = namePat(), fvalue = funcCallNamePat(), fop = drop()), ast) + saveToAttributeStorage(ast.target, getOpMagicMethod(ast.op), param) + saveToAttributeStorage(ast.value, getOpMagicMethod(ast.op), param) + return super.visitAugAssign(ast, param) + } + + private fun getOp(ast: BinOp): FuzzedContext = + when (ast) { + is Eq -> FuzzedContext.Comparison.EQ + is NotEq -> FuzzedContext.Comparison.NE + is Gt -> FuzzedContext.Comparison.GT + is GtE -> FuzzedContext.Comparison.GE + is Lt -> FuzzedContext.Comparison.LT + is LtE -> FuzzedContext.Comparison.LE + else -> FuzzedContext.Unknown + } + + private fun getNumFuzzedValue(num: String, op: FuzzedContext = FuzzedContext.Unknown): FuzzedConcreteValue? = + try { + when (val x = NumberUtils.createNumber(num)) { + is Int -> FuzzedConcreteValue(pythonIntClassId, x.toBigInteger(), op) + is Long -> FuzzedConcreteValue(pythonIntClassId, x.toBigInteger(), op) + is BigInteger -> FuzzedConcreteValue(pythonIntClassId, x, op) + else -> FuzzedConcreteValue(pythonFloatClassId, BigDecimal(num), op) + } + } catch (e: NumberFormatException) { + null + } + + private fun constPat(op: FuzzedContext): Pattern<(FuzzedConcreteValue?) -> A, A, N> { + val pats = listOf A, A, N>>( + map1(refl(num(apply()))) { x -> getNumFuzzedValue(x, op) }, + map1(refl(str(apply()))) { x -> + FuzzedConcreteValue(pythonStrClassId, x, op) + }, + map0( + refl(true_()), + FuzzedConcreteValue(PythonBoolModel.classId, true, op) + ), + map0( + refl(false_()), + FuzzedConcreteValue(PythonBoolModel.classId, false, op) + ) + ) + return pats.reduce { acc, elem -> or(acc, elem) } + } + + override fun visitNum(num: Num, param: MutableMap): AST { + val value = getNumFuzzedValue(num.n) + if (value != null && constStorage.find { it.value == value.value } == null) { + constStorage.add(value) + (value.classId as? PythonClassId)?.let { generalStorage.types.add(Type(it)) } + } + return super.visitNum(num, param) + } + + override fun visitStr(str: Str, param: MutableMap?): AST { + if (str.s.isEmpty() || str.s[0] != 'f') + constStorage.add(FuzzedConcreteValue(pythonStrClassId, str.s)) + generalStorage.types.add(Type(pythonStrClassId)) + return super.visitStr(str, param) + } + + override fun visitBinOp(ast: BinOp, param: MutableMap): AST { + parseAndPutType( + param, + or( + binOp(fleft = namePat(), fright = typedExpressionPat()), + swap(binOp(fleft = typedExpressionPat(), fright = namePat())) + ), + ast + ) + parseAndPutFunctionRet( + param, + or( + binOp(fleft = namePat(), fright = funcCallNamePat()), + swap(binOp(fleft = funcCallNamePat(), fright = namePat())) + ), + ast + ) + val op = getOp(ast) + if (op != FuzzedContext.Unknown) + parse( + binOp(fleft = refl(name(drop())), fright = constPat(op)), + onError = null, + ast + ) { it }?.let { constStorage.add(it) } + + saveToAttributeStorage(ast.left, getOpMagicMethod(ast), param) + saveToAttributeStorage(ast.right, getOpMagicMethod(ast), param) + return super.visitBinOp(ast, param) + } + + fun saveToAttributeStorage(name: AST?, methodName: String?, param: MutableMap) { + if (methodName == null) + return + paramNames.forEach { + if (name is Name && name.id.name == it) { + addToStorage(it, param) { storage -> + storage.methods.add(Method(methodName)) + } + } + } + } + + override fun visitUnaryOp(unaryOp: UnaryOp, param: MutableMap): AST { + saveToAttributeStorage(unaryOp.expression, getOpMagicMethod(unaryOp), param) + return super.visitUnaryOp(unaryOp, param) + } + + override fun visitDelete(delete: Delete, param: MutableMap): AST { + saveToAttributeStorage(delete.expression, "__delitem__", param) + return super.visitDelete(delete, param) + } + + override fun visitListExpr(listExpr: ListExpr, param: MutableMap): AST { + generalStorage.types.add(Type(PythonListModel.classId)) + return super.visitListExpr(listExpr, param) + } + + override fun visitSet( + set: io.github.danielnaczo.python3parser.model.expr.datastructures.Set, + param: MutableMap + ): AST { + generalStorage.types.add(Type(PythonSetModel.classId)) + return super.visitSet(set, param) + } + + override fun visitDict(dict: Dict, param: MutableMap): AST { + generalStorage.types.add(Type(PythonDictModel.classId)) + return super.visitDict(dict, param) + } + + override fun visitComprehension( + comprehension: Comprehension, + param: MutableMap + ): AST { + generalStorage.methods.add(Method("__iter__")) + parse(namePat(), onError = null, comprehension.iter) { it }?.let { paramName -> + addToStorage(paramName, param) { storage -> storage.methods.add(Method("__iter__")) } + } + return super.visitComprehension(comprehension, param) + } + + override fun visitFor(forElement: For, param: MutableMap): AST { + generalStorage.methods.add(Method("__iter__")) + parse(namePat(), onError = null, forElement.iter) { it }?.let { paramName -> + addToStorage(paramName, param) { storage -> storage.methods.add(Method("__iter__")) } + } + return super.visitFor(forElement, param) + } + } +} + +typealias ResFuncArg = Pair? +typealias ResField = Pair? +typealias ResMethod = Pair? +typealias ResAssign = Pair +typealias ResFuncRet = Pair? \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/ClassInfoCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/ClassInfoCollector.kt new file mode 100644 index 0000000000..fdd73fa5fc --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/ClassInfoCollector.kt @@ -0,0 +1,65 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.AST +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.visitors.modifier.ModifierVisitor +import org.utbot.python.PythonMethod + +class ClassInfoCollector(pyClass: PythonClass) { + + class Storage { + val fields = mutableSetOf() + val methods = mutableSetOf() + } + + val storage = Storage() + + init { + pyClass.methods.forEach { method -> + if (isProperty(method)) + storage.fields.add(method.name) + else + storage.methods.add(method.name) + + val selfName = getSelfName(method) + if (selfName != null) { + val visitor = Visitor(selfName) + visitor.visitFunctionDef(method.ast(), storage) + } + } + pyClass.topLevelFields.forEach { annAssign -> + (annAssign.target as? Name)?.let { storage.fields.add(it.id.name) } + } + } + + companion object { + fun getSelfName(method: PythonMethod): String? { + val params = method.arguments + if (params.isEmpty() || method.ast().decorators.any { + listOf( + "staticmethod", + "classmethod" + ).contains(it.name.name) + }) return null + return params[0].name + } + + fun isProperty(method: PythonMethod): Boolean { + return method.ast().decorators.any { it.name.name == "property" } + } + } + + private class Visitor(val selfName: String) : ModifierVisitor() { + override fun visitAtom(atom: Atom, param: Storage): AST { + parse( + classField(fname = name(equal(selfName)), fattributeId = apply()), + onError = null, + atom + ) { it }?.let { fieldName -> + param.fields.add(fieldName) + } + return super.visitAtom(atom, param) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..48b46893da --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt @@ -0,0 +1,450 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.Identifier +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.model.expr.atoms.Str +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.Attribute +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Arguments +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Keyword +import io.github.danielnaczo.python3parser.model.expr.datastructures.ListExpr +import io.github.danielnaczo.python3parser.model.expr.datastructures.Tuple +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.Add +import io.github.danielnaczo.python3parser.model.mods.Module +import io.github.danielnaczo.python3parser.model.stmts.Body +import io.github.danielnaczo.python3parser.model.stmts.Statement +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.FunctionDef +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.parameters.Parameter +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.parameters.Parameters +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.tryExceptStmts.ExceptHandler +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.tryExceptStmts.Try +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.withStmts.With +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.withStmts.WithItem +import io.github.danielnaczo.python3parser.model.stmts.importStmts.Alias +import io.github.danielnaczo.python3parser.model.stmts.importStmts.Import +import io.github.danielnaczo.python3parser.model.stmts.importStmts.ImportFrom +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.Assign +import io.github.danielnaczo.python3parser.visitors.prettyprint.IndentationPrettyPrint +import io.github.danielnaczo.python3parser.visitors.prettyprint.ModulePrettyPrintVisitor +import org.utbot.framework.plugin.api.UtModel +import org.utbot.python.PythonMethod +import org.utbot.python.code.AnnotationProcessor.getModulesFromAnnotation +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.util.pythonAnyClassId + + +object PythonCodeGenerator { + private val pythonTreeSerializerCode = PythonCodeGenerator::class.java.getResource("/python_tree_serializer.py") + ?.readText(Charsets.UTF_8) + ?: error("Didn't find preprocessed_values.json") + + private fun toString(module: Module): String { + val modulePrettyPrintVisitor = ModulePrettyPrintVisitor() + return modulePrettyPrintVisitor.visitModule(module, IndentationPrettyPrint(0)) + } + + private fun createArguments( + args: List = emptyList(), + keywords: List = emptyList(), + starredArgs: List = emptyList(), + doubleStarredArgs: List = emptyList() + ): Arguments { + return Arguments(args, keywords, starredArgs, doubleStarredArgs) + } + + private fun generateImportFunctionCode( + functionPath: String, + directoriesForSysPath: Set, + additionalModules: Set = emptySet(), + ): List { + val systemImport = Import( + listOf( + Alias("sys"), + Alias("typing"), + Alias("json"), + Alias("inspect"), + Alias("builtins"), + ) + ) + val systemCalls = directoriesForSysPath.map { path -> + Atom( + Name("sys.path.append"), + listOf( + createArguments( + listOf(Str(path)) + ) + ) + ) + } + + val additionalImport = additionalModules.map { Import(listOf(Alias(it))) } + val import = ImportFrom(functionPath, listOf(Alias("*"))) + return listOf(systemImport) + systemCalls + additionalImport + listOf(import) + } + + private fun generateFunctionCallForTopLevelFunction(method: PythonMethod): Atom { + val keywords = method.arguments.map { + Keyword(Name(it.name), Name(it.name)) + } + return Atom( + Name(method.name), + listOf( + createArguments(emptyList(), keywords) + ) + ) + } + + private fun generateMethodCall(method: PythonMethod): Atom { + assert(method.containingPythonClassId != null) + val keywords = method.arguments.drop(1).map { + Keyword(Name(it.name), Name(it.name)) + } + return Atom( + Name(method.arguments[0].name), + listOf( + Attribute(Identifier(method.name)), + createArguments(emptyList(), keywords) + ) + ) + } + + const val successStatus = "success" + const val failStatus = "fail" + + fun generateRunFunctionCode( + method: PythonMethod, + methodArguments: List, + directoriesForSysPath: Set, + moduleToImport: String, + additionalModules: Set = emptySet(), + fileForOutputName: String + ): String { + + val importStatements = generateImportFunctionCode( + moduleToImport, + directoriesForSysPath, + additionalModules + setOf("coverage") + ) + + val testFunctionName = "__run_${method.name}" + val testFunction = FunctionDef(testFunctionName) + + val parameters = methodArguments.zip(method.arguments).map { (model, argument) -> + Assign( + listOf(Name(argument.name)), + Name(model.toString()) + ) + } + + val resultName = Name("__result") + val startName = Name("__start") + val endName = Name("__end") + val sourcesName = Name("__sources") + val stmtsName = Name("__stmts") + val stmtsFilteredName = Name("__stmts_filtered") + val stmtsFilteredWithDefName = Name("__stmts_filtered_with_def") + val missedName = Name("__missed") + val missedFilteredName = Name("__missed_filtered") + val coverageName = Name("__cov") + val fullpathName = Name("__fullpath") + val statusName = Name("__status") + val exceptionName = Name("__exception") + val serialisedName = Name("__serialized") + val fileName = Name("__out_file") + + val fullpath = Assign( + listOf(fullpathName), + Str(method.moduleFilename) + ) + + val functionCall = + if (method.containingPythonClassId == null) + generateFunctionCallForTopLevelFunction(method) + else + generateMethodCall(method) + + val fullFunctionName = Name( + ( + listOf((functionCall.atomElement as Name).id.name) + functionCall.trailers.mapNotNull { + if (it is Attribute) { + it.attr.name + } else { + null + } + }).joinToString(".") + ) + + val coverage = Assign( + listOf(coverageName), + Name("coverage.Coverage(data_suffix=True)") + ) + val startCoverage = Atom( + coverageName, + listOf(Attribute(Identifier("start")), createArguments()) + ) + + val resultSuccess = Assign( + listOf(resultName), + functionCall + ) + + val statusSuccess = Assign( + listOf(statusName), + Str("\"" + successStatus + "\"") + ) + + val resultError = Assign( + listOf(resultName), + exceptionName + ) + + val statusError = Assign( + listOf(statusName), + Str("\"" + failStatus + "\"") + ) + + val stopCoverage = Atom( + coverageName, + listOf(Attribute(Identifier("stop")), createArguments()) + ) + val sourcesAndStart = Assign( + listOf(Tuple(listOf(sourcesName, startName))), + Atom( + Name("inspect.getsourcelines"), + listOf(createArguments(listOf(fullFunctionName))) + ) + ) + val end = Assign( + listOf(endName), + Add( + startName, + Atom(Name("len"), listOf(createArguments(listOf(sourcesName)))) + ) + ) + val covAnalysis = Assign( + listOf( + Tuple( + listOf( + Name("_"), + stmtsName, + Name("_"), + missedName, + Name("_") + ) + ) + ), + Atom( + coverageName, + listOf( + Attribute(Identifier("analysis2")), + createArguments(listOf(fullpathName)) + ) + ) + ) + val clean = Atom( + coverageName, + listOf(Attribute(Identifier("erase")), createArguments()) + ) + val stmtsFiltered = Assign( + listOf(stmtsFilteredName), + Atom( + Name(getLinesName), + listOf(createArguments(listOf(startName, endName, stmtsName))) + ) + ) + val stmtsFilteredWithDef = Assign( + listOf(stmtsFilteredWithDefName), + Add( + ListExpr(listOf(startName)), + stmtsFilteredName + ) + ) + val missedFiltered = Assign( + listOf(missedFilteredName), + Atom( + Name(getLinesName), + listOf(createArguments(listOf(startName, endName, missedName))) + ) + ) + + val serialize = Assign( + listOf(serialisedName), + Atom( + Name("_PythonTreeSerializer().dumps"), + listOf(createArguments(listOf(resultName))) + ) + ) + + val jsonDumps = Atom( + Name("json"), + listOf( + Attribute(Identifier("dumps")), + createArguments(listOf(serialisedName)) + ) + ) + + val listToJoin = ListExpr( + listOf( + Atom(Name("str"), listOf(createArguments(listOf(statusName)))), + Atom(Name("str"), listOf(createArguments(listOf(jsonDumps)))), + Atom( + Name("str"), + listOf(createArguments(listOf(stmtsFilteredWithDefName))) + ), + Atom( + Name("str"), + listOf(createArguments(listOf(missedFilteredName))) + ) + ) + ) + + val joinedList = + Atom( + Name("\"\\n\""), + listOf( + Attribute(Identifier("join")), + createArguments( + listOf( + listToJoin + ) + ) + ) + ) + + val printStmt = With( + listOf( + WithItem(Name("open(\"$fileForOutputName\", \"w\")"), fileName) + ), + Atom( + fileName, + listOf( + Attribute(Identifier("write")), + createArguments( + listOf( + joinedList + ) + ) + ) + ) + ) + + val tryBody = Body( + listOf( + resultSuccess, + statusSuccess + ) + ) + val suppressedBlock = With( + listOf( + WithItem( + Atom( + Name(getStdoutSuppressName), + listOf(createArguments()) + ) + ) + ), + tryBody + ) + val failBody = Body( + listOf( + resultError, + statusError + ) + ) + val tryHandler = ExceptHandler("Exception", exceptionName.id.name) + val tryBlock = Try(suppressedBlock, listOf(tryHandler), listOf(failBody)) + + (parameters + listOf( + fullpath, + coverage, + startCoverage + )).forEach { testFunction.addStatement(it) } + + testFunction.addStatement(tryBlock) + + listOf( + stopCoverage, + sourcesAndStart, + end, + covAnalysis, + clean, + stmtsFiltered, + stmtsFilteredWithDef, + missedFiltered, + serialize, + printStmt + ).forEach { testFunction.addStatement(it) } + + val runFunction = Atom( + Name(testFunctionName), + listOf(createArguments()) + ) + + return listOf( + getStdoutSuppress, + pythonTreeSerializerCode, + getLines, + toString( + Module( + importStatements + listOf(testFunction, runFunction) + ) + ) + ).joinToString("\n\n") + } + + fun generateMypyCheckCode( + method: PythonMethod, + methodAnnotations: Map, + directoriesForSysPath: Set, + moduleToImport: String + ): String { + val importStatements = generateImportFunctionCode( + moduleToImport, + directoriesForSysPath, + methodAnnotations.values.flatMap { annotation -> + getModulesFromAnnotation(annotation) + }.toSet(), + ) + + val parameters = Parameters( + method.arguments.map { argument -> + Parameter("${argument.name}: ${methodAnnotations[argument.name] ?: pythonAnyClassId.name}") + }, + ) + + val testFunctionName = "__mypy_check_${method.name}" + val testFunction = FunctionDef( + testFunctionName, + parameters, + method.ast().body + ) + + return toString( + Module( + importStatements + listOf(testFunction) + ) + ) + } + + private const val getLinesName: String = "__get_lines" + private val getLines: String = """ + def ${this.getLinesName}(start, end, lines): + return list(filter(lambda x: start < x < end, lines)) + """.trimIndent() + + private const val getStdoutSuppressName: String = "__suppress_stdout" + private val getStdoutSuppress: String = """ + import os + from contextlib import contextmanager + @contextmanager + def ${this.getStdoutSuppressName}(): + with open(os.devnull, "w") as devnull: + old_stdout = sys.stdout + sys.stdout = devnull + try: + yield + finally: + sys.stdout = old_stdout + """.trimIndent() +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/KlaxonPythonTreeParser.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/KlaxonPythonTreeParser.kt new file mode 100644 index 0000000000..f8453fe8e1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/KlaxonPythonTreeParser.kt @@ -0,0 +1,112 @@ +package org.utbot.python.code + +import com.beust.klaxon.JsonArray +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree + +class KlaxonPythonTreeParser( + jsonString: String +) { + private val jsonObject = parseJsonString(jsonString) + + private val rawMemory = jsonObject.obj("memory")!!.map { + it.key.toLong() to it.value as JsonObject + }.toMap() + + private val memory = emptyMap().toMutableMap() + + fun parseJsonToPythonTree(): PythonTree.PythonTreeNode { + return parseToPythonTree(jsonObject.obj("json")!!) + } + + private fun parseJsonString(jsonString: String): JsonObject { + val parser: Parser = Parser.default() + val stringBuilder: StringBuilder = StringBuilder(jsonString) + return parser.parse(stringBuilder) as JsonObject + } + + private fun findInMemory(id: Long): PythonTree.PythonTreeNode { + return if (memory.containsKey(id)) + memory[id]!! + else { + return parseReduce(rawMemory[id]!!) + } + } + + private fun parseToPythonTree(json: JsonObject): PythonTree.PythonTreeNode { + val type = json.string("type")!! + val strategy = json.string("strategy")!! + val comparable = json.boolean("comparable")!! + + val result = if (strategy == "repr") { + var repr = json.string("value")!! + if (type == "builtins.complex") { + repr = "complex('$repr')" + } else if (repr == "nan") { + repr = "float('$repr')" + } else if (repr == "inf") { + repr = "float('$repr')" + } else if (repr == "-inf") { + repr = "float('$repr')" + } + PythonTree.PrimitiveNode(PythonClassId(type), repr) + } else { + when (type) { + "builtins.list" -> parsePythonList(json.array("value")!!) + "builtins.set" -> parsePythonSet(json.array("value")!!) + "builtins.tuple" -> parsePythonTuple(json.array("value")!!) + "builtins.dict" -> parsePythonDict(json.array("value")!!) + else -> findInMemory(json.long("value")!!) + } + } + result.comparable = comparable + return result + } + + private fun parseReduce(value: JsonObject): PythonTree.PythonTreeNode { + val id = value.long("id")!! + val initObject = PythonTree.ReduceNode( + id, + PythonClassId(value.string("type")!!), + PythonClassId(value.string("constructor")!!), + parsePythonList(value.array("args")!!).items, + ) + memory[id] = initObject + initObject.state = parsePythonDict(value.array("state")!!).items.map { + (it.key as PythonTree.PrimitiveNode).repr to it.value + }.toMap() + initObject.listitems = parsePythonList(value.array("listitems")!!).items + initObject.dictitems = parsePythonDict(value.array("dictitems")!!).items + return initObject + } + + private fun parsePythonList(items: JsonArray): PythonTree.ListNode { + return PythonTree.ListNode(items.map { parseToPythonTree(it) }) + } + + private fun parsePythonSet(items: JsonArray): PythonTree.SetNode { + return PythonTree.SetNode(items.map { parseToPythonTree(it) }.toSet()) + } + + private fun parsePythonTuple(items: JsonArray): PythonTree.TupleNode { + return PythonTree.TupleNode(items.map { parseToPythonTree(it) }) + } + + private fun parsePythonDict(items: JsonArray>): PythonTree.DictNode { + return PythonTree.DictNode(items.associate { + val key = it[0] + val value = it[1] as JsonObject + ( + if (key is String) + PythonTree.PrimitiveNode( + PythonClassId("builtins.str"), + key + ) + else + parseToPythonTree(key as JsonObject) + ) to parseToPythonTree(value) + }) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/PythonASTParser.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonASTParser.kt new file mode 100644 index 0000000000..070c131d78 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonASTParser.kt @@ -0,0 +1,421 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.* +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.Attribute +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Arguments +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Keyword +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.subscripts.Index +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.subscripts.Subscript +import io.github.danielnaczo.python3parser.model.expr.datastructures.Dict +import io.github.danielnaczo.python3parser.model.expr.datastructures.ListExpr +import io.github.danielnaczo.python3parser.model.expr.datastructures.Set +import io.github.danielnaczo.python3parser.model.expr.datastructures.Tuple +import io.github.danielnaczo.python3parser.model.expr.operators.Operator +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.BinOp +import io.github.danielnaczo.python3parser.model.expr.operators.unaryops.UnaryOp +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.Assign +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.AugAssign +import org.apache.commons.lang3.math.NumberUtils +import java.math.BigInteger + +sealed class Result +class Match(val value: T) : Result() +class Error : Result() + +open class Pattern( + val go: (N, A) -> Result +) + +fun parse(pat: Pattern, onError: B, node: N, x: A): B { + return when (val result = pat.go(node, x)) { + is Match -> result.value + is Error -> onError + } +} + +inline fun refl(pat: Pattern): Pattern = + Pattern { node, x -> + when (node) { + is N -> pat.go(node, x) + else -> Error() + } + } + +fun drop(): Pattern = + Pattern { _, x -> Match(x) } + +fun apply(): Pattern<(N) -> A, A, N> = + Pattern { node, x -> Match(x(node)) } + +fun name(fid: Pattern): Pattern = + Pattern { node, x -> + fid.go(node.id.name, x) + } + +fun int(): Pattern = + Pattern { node, x -> + when (NumberUtils.createNumber(node)) { + is Int -> Match(x) + is Long -> Match(x) + is BigInteger -> Match(x) + else -> Error() + } + } + +fun num(fnum: Pattern): Pattern = Pattern { node, x -> fnum.go(node.n, x) } +fun str(fstr: Pattern): Pattern = Pattern { node, x -> fstr.go(node.s, x) } +fun true_(): Pattern = Pattern { _, x -> Match(x) } +fun false_(): Pattern = Pattern { _, x -> Match(x) } +fun none(): Pattern = Pattern { _, x -> Match(x) } + +fun equal(value: N): Pattern = + Pattern { node, x -> + if (node != value) + Error() + else + Match(x) + } + +fun go(y: Pattern, node: N, x: Result) = + when (x) { + is Match -> y.go(node, x.value) + is Error -> Error() + } + +fun toMatch(x: Result>): Match> = + when (x) { + is Error -> Match(emptyList()) + is Match -> x + } + +fun multiGo(y: Pattern, node: N, x: Result>): Result> { + val res = mutableListOf() + for (z in (toMatch(x)).value) { + when (val w = y.go(node, z)) { + is Error -> Unit + is Match -> res.add(w.value) + } + } + return Match(res) +} + +fun assign( + ftargets: Pattern>, + fvalue: Pattern +): Pattern = + Pattern { node, x -> + if (!node.value.isPresent) + return@Pattern Error() + val x1 = ftargets.go(node.targets, x) + go(fvalue, node.value.get(), x1) + } + +fun assignAll( + ftargets: Pattern, List>, + fvalue: Pattern +): Pattern, Assign> = + Pattern { node, x -> + if (!node.value.isPresent) + return@Pattern Error() + val x1 = ftargets.go(node.targets, x) + multiGo(fvalue, node.value.get(), x1) + } + +fun dict( + fkeys: Pattern>, + fvalues: Pattern> +): Pattern = + Pattern { node, x -> + val x1 = fkeys.go(node.keys, x) + go(fvalues, node.values, x1) + } + +fun list( + felts: Pattern> +): Pattern = + Pattern { node, x -> + felts.go(node.elts, x) + } + +fun set( + felts: Pattern> +): Pattern = + Pattern { node, x -> + felts.go(node.elts, x) + } + +fun tuple( + felts: Pattern> +): Pattern = + Pattern { node, x -> + felts.go(node.elts, x) + } + +fun augAssign( + ftarget: Pattern, + fop: Pattern, + fvalue: Pattern +): Pattern = + Pattern { node, x -> + val x1 = ftarget.go(node.target, x) + val x2 = go(fop, node.op, x1) + go(fvalue, node.value, x2) + } + +fun binOp( + fleft: Pattern, + fright: Pattern +): Pattern = + Pattern { node, x -> + if (node.left == null || node.right == null) + return@Pattern Error() + val x1 = fleft.go(node.left, x) + go(fright, node.right, x1) + } + +fun arguments( + fargs: Pattern>, + fkeywords: Pattern>, + fstarredArgs: Pattern>, + fdoubleStarredArgs: Pattern> +): Pattern = + Pattern { node, x -> + val x1 = fargs.go(node.args ?: emptyList(), x) + val x2 = go(fkeywords, node.keywords ?: emptyList(), x1) + val x3 = go(fstarredArgs, node.starredArgs ?: emptyList(), x2) + go(fdoubleStarredArgs, node.doubleStarredArgs ?: emptyList(), x3) + } + +fun atom( + fatomElement: Pattern, + ftrailers: Pattern> +): Pattern = + Pattern { node, x -> + val x1 = fatomElement.go(node.atomElement, x) + go(ftrailers, node.trailers, x1) + } + +fun list1( + felem1: Pattern +): Pattern> = + Pattern { node, x -> + if (node.size != 1) + return@Pattern Error() + felem1.go(node[0], x) + } + +fun list2( + felem1: Pattern, + felem2: Pattern +): Pattern> = + Pattern { node, x -> + if (node.size != 2) + return@Pattern Error() + val x1 = felem1.go(node[0], x) + go(felem2, node[1], x1) + } + +fun first( + felem: Pattern +): Pattern> = + Pattern { node, x -> + if (node.isEmpty()) + return@Pattern Error() + felem.go(node[0], x) + } + +fun index( + felem: Pattern +): Pattern = + Pattern { node, x -> + if (node.slice !is Index) + return@Pattern Error() + felem.go((node.slice as Index).value, x) + } + +fun attribute( + fattributeId: Pattern +): Pattern = + Pattern { node, x -> fattributeId.go(node.attr.name, x) } + +fun classField( + fname: Pattern, + fattributeId: Pattern +): Pattern = + atom(refl(fname), list1(refl(attribute(fattributeId)))) + +fun attributesFromAtom( + fattributes: Pattern> +): Pattern = + Pattern { node, x -> + val res = mutableListOf() + for (elem in node.trailers) { + if (elem is Attribute) + res.add(elem.attr.name) + } + fattributes.go(res, x) + } + +fun classMethod( + fname: Pattern, + fattributeId: Pattern, + farguments: Pattern +): Pattern = + atom(refl(fname), list2(refl(attribute(fattributeId)), refl(farguments))) + +fun methodFromAtom( + fattributeId: Pattern, + farguments: Pattern +): Pattern = + Pattern { node, x -> + if (node.trailers.size < 2 || node.trailers.last() !is Arguments) + return@Pattern Error() + val methodName = node.trailers[node.trailers.size - 2] + if (methodName !is Attribute) + return@Pattern Error() + val x1 = fattributeId.go(methodName.attr.name, x) + go(farguments, node.trailers.last() as Arguments, x1) + } + +fun nameWithPrefixFromAtom( + fname: Pattern +): Pattern = + Pattern { node, x -> + if (node.atomElement !is Name) + return@Pattern Error() + var res = (node.atomElement as Name).id.name + for (elem in node.trailers) { + if (elem !is Attribute) + break + res += "." + elem.attr.name + } + fname.go(res, x) + } + +fun functionCallWithoutPrefix( + fname: Pattern, + farguments: Pattern +): Pattern = + atom(refl(fname), list1(refl(farguments))) + +fun functionCallWithPrefix( + fprefix: Pattern>, + fid: Pattern, + farguments: Pattern +): Pattern = + Pattern { node, x -> + if (node.trailers.size == 0) + return@Pattern Error() + if (node.trailers.size == 1) { + val x1 = fprefix.go(emptyList(), x) + return@Pattern go(functionCallWithoutPrefix(name(fid), farguments), node, x1) + } + val prefix = listOf(node.atomElement) + node.trailers.dropLast(2) + val x1 = fprefix.go(prefix, x) + val x2 = go(refl(attribute(fid)), node.trailers[node.trailers.size - 2], x1) + go(refl(farguments), node.trailers.last(), x2) + } + +fun goWithMatches( + pat: (N, A) -> Result>, + node: N, + x: Result> +): Result> = + when (x) { + is Error -> Error() + is Match -> { + when (val x1 = pat(node, x.value.first)) { + is Error -> Error() + is Match -> Match(Pair(x1.value.first, x1.value.second + x.value.second)) + } + } + } + +fun opExpr( + fatomMatch: Pattern, + fatomExtra: Pattern +): Pattern { + fun innerGo(node: Expression, x: A): Result> { + val y = fatomMatch.go(node, x) + if (y is Match) + return Match(Pair(y.value, 1)) + + val z = fatomExtra.go(node, x) + if (z is Match) + return Match(Pair(z.value, 0)) + + return when (node) { + is BinOp -> { + val x1 = innerGo(node.left, x) + goWithMatches(::innerGo, node.right, x1) + } + + is UnaryOp -> innerGo(node.expression, x) + else -> Error() + } + } + return Pattern { node, x -> + when (val x1 = innerGo(node, x)) { + is Error -> Error() + is Match -> if (x1.value.second == 0) Error() else Match(x1.value.first) + } + } +} + +fun any(felem: Pattern): Pattern> = + Pattern { node, x -> + for (elem in node) { + val x1 = felem.go(elem, x) + if (x1 is Match) + return@Pattern x1 + } + return@Pattern Error() + } + +fun allMatches(felem: Pattern): Pattern, List> = + Pattern { node, x -> + val res = mutableListOf() + for (elem in node) { + val x1 = felem.go(elem, x) + if (x1 is Match) + res.add(x1.value) + } + Match(res) + } + +fun anyIndexed(felem: Pattern): Pattern<(Int) -> A, B, List> = + Pattern { node, x -> + node.forEachIndexed { index, elem -> + val x1 = felem.go(elem, x(index)) + if (x1 is Match) + return@Pattern x1 + } + return@Pattern Error() + } + +fun or(pat1: Pattern, pat2: Pattern): Pattern = + Pattern { node, x -> + val x1 = pat1.go(node, x) + when (x1) { + is Match -> x1 + is Error -> pat2.go(node, x) + } + } + +fun reject(): Pattern = Pattern { _, _ -> Error() } + +fun map0(pat: Pattern, value: C): Pattern<(C) -> A, B, N> = + Pattern { node, x: (C) -> A -> + pat.go(node, x(value)) + } + +fun map1(pat: Pattern<(A) -> B, C, N>, f: (A) -> D): Pattern<(D) -> B, C, N> = + Pattern { node, x: (D) -> B -> + pat.go(node) { y -> x(f(y)) } + } + +fun swap(pat: Pattern<(A) -> (B) -> C, D, N>): Pattern<(B) -> (A) -> C, D, N> = + Pattern { node, f: (B) -> (A) -> C -> + pat.go(node) { x -> { y -> f(y)(x) } } + } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt new file mode 100644 index 0000000000..8f36dd60f3 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt @@ -0,0 +1,208 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.Python3Lexer +import io.github.danielnaczo.python3parser.Python3Parser +import io.github.danielnaczo.python3parser.model.AST +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.model.mods.Module +import io.github.danielnaczo.python3parser.model.stmts.Body +import io.github.danielnaczo.python3parser.model.stmts.Statement +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.ClassDef +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.FunctionDef +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.parameters.Parameter +import io.github.danielnaczo.python3parser.model.stmts.importStmts.Import +import io.github.danielnaczo.python3parser.model.stmts.importStmts.ImportFrom +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.AnnAssign +import io.github.danielnaczo.python3parser.visitors.ast.ModuleVisitor +import io.github.danielnaczo.python3parser.visitors.modifier.ModifierVisitor +import io.github.danielnaczo.python3parser.visitors.prettyprint.IndentationPrettyPrint +import io.github.danielnaczo.python3parser.visitors.prettyprint.ModulePrettyPrintVisitor +import mu.KotlinLogging +import org.antlr.v4.runtime.CharStreams.fromString +import org.antlr.v4.runtime.CommonTokenStream +import org.utbot.python.PythonArgument +import org.utbot.python.PythonMethod +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.utils.moduleOfType +import java.util.* + +private val logger = KotlinLogging.logger {} + +class PythonCode( + private val body: Module, + private val filename: String, + private val pythonModule: String? = null +) { + fun getToplevelFunctions(): List = + body.functionDefs.mapNotNull { functionDef -> + PythonMethodBody(functionDef, filename) + } + + fun getToplevelClasses(): List = + body.classDefs?.mapNotNull { classDef -> + PythonClass(classDef, filename, pythonModule) + } ?: emptyList() + + fun getToplevelModules(): List = + body.statements?.flatMap { statement -> + when (statement) { + is Import -> statement.names.map { it.name.name } + is ImportFrom -> { + try { + listOf(statement.module.get().name) + } catch (e: NoSuchElementException) { + emptyList() + } + } + + else -> emptyList() + } + }?.toSet()?.map { + PythonModule(it) + } ?: emptyList() + + companion object { + fun getFromString(code: String, filename: String, pythonModule: String? = null): PythonCode? { + logger.debug("Parsing file $filename") + return try { + val ast = textToModule(code) + if (ast.statements == null) null else PythonCode(ast, filename, pythonModule) + } catch (_: Exception) { + null + } + } + } +} + +data class PythonModule( + val name: String, +) + +class PythonClass( + private val ast: ClassDef, + val filename: String? = null, + private val pythonModule: String? = null +) { + val name: String + get() = ast.name.name + + val methods: List + get() = ast.functionDefs?.map { + PythonMethodBody(it, filename ?: "", pythonClassId) + } ?: emptyList() + + val pythonClassId: PythonClassId? + get() = pythonModule?.let { PythonClassId("$it.$name") } + + val initSignature: List? + get() { + val ordinary = ast.functionDefs + ?.find { it.name.name == "__init__" } + ?.let { PythonMethodBody(it, filename ?: "") } + + if (ordinary != null) { + return ordinary.arguments.drop(1) // drop 'self' parameter + } + + val isDataclass = ast.decorators.any { it.name.name == "dataclass" } + + if (isDataclass) { + return topLevelFields.map { + PythonArgument( + (it.target as? Name)?.id?.name ?: return null, + astToString(it.annotation).trim() + ) + } + } + + val noOneArgument = ast.decorators.isEmpty() && (ast.arguments == null || !ast.arguments.isPresent) + if (noOneArgument) { + return emptyList() + } + + return null + } + + val topLevelFields: List + get() = (ast.body as? Body)?.statements?.mapNotNull { it as? AnnAssign } ?: emptyList() +} + +class PythonMethodBody( + private val ast: FunctionDef, + override val moduleFilename: String = "", + override val containingPythonClassId: PythonClassId? = null +) : PythonMethod { + override val name: String + get() = ast.name.name + + override val returnAnnotation: String? + get() = annotationToString(ast.returns) + + // TODO: consider cases of default and keyword arguments + private val getParams: List = + if (ast.parameters.isPresent) + ast.parameters.get().params ?: emptyList() + else + emptyList() + + override val arguments: List + get() = getParams.map { param -> + PythonArgument( + param.parameterName.name, + annotationToString(param.annotation) + ) + } + + override fun asString(): String { + return astToString(ast) + } + + override fun ast(): FunctionDef { + return ast + } + + companion object { + fun annotationToString(annotation: Optional): String? = + if (annotation.isPresent) astToString(annotation.get()).trim() else null + } +} + +fun astToString(stmt: Statement): String { + val modulePrettyPrintVisitor = ModulePrettyPrintVisitor() + return modulePrettyPrintVisitor.visitModule(Module(listOf(stmt)), IndentationPrettyPrint(0)) +} + +fun textToModule(code: String): Module { + val lexer = Python3Lexer(fromString(code + "\n")) + val tokens = CommonTokenStream(lexer) + val parser = Python3Parser(tokens) + val moduleVisitor = ModuleVisitor() + return moduleVisitor.visit(parser.file_input()) as Module +} + +object AnnotationProcessor { + fun getModulesFromAnnotation(annotation: NormalizedPythonAnnotation): Set { + val annotationAST = textToModule(annotation.name) + val visitor = Visitor() + val result = mutableSetOf() + visitor.visitModule(annotationAST, result) + return result + } + + private class Visitor : ModifierVisitor>() { + override fun visitAtom(atom: Atom, param: MutableSet): AST { + parse( + nameWithPrefixFromAtom(apply()), + onError = null, + atom + ) { it }?.let { typeName -> + moduleOfType(typeName)?.let { param.add(it) } + } + + return super.visitAtom(atom, param) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..ba396b1ca2 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonApi.kt @@ -0,0 +1,166 @@ +package org.utbot.python.framework.api.python + +import org.utbot.common.withToStringThreadLocalReentrancyGuard +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.python.framework.api.python.util.moduleOfType + +/** + * PythonClassId represents Python type. + * NormalizedPythonAnnotation represents annotation after normalization. + * + * Example of PythonClassId, but not NormalizedPythonAnnotation: + * builtins.list (normalized annotation is typing.List[typing.Any]) + */ + +const val pythonBuiltinsModuleName = "builtins" + +class PythonClassId( + name: String // includes module (like "_ast.Assign") +) : ClassId(name) { + override fun toString(): String = name + val rootModuleName: String = this.toString().split(".")[0] + override val simpleName: String = name.split(".").last() + val moduleName: String + get() { + return moduleOfType(name) ?: pythonBuiltinsModuleName + } + override val packageName = moduleName + override val canonicalName = name +} + +open class RawPythonAnnotation( + annotation: String +): ClassId(annotation) + +class NormalizedPythonAnnotation( + annotation: String +) : RawPythonAnnotation(annotation) + +class PythonMethodId( + override val classId: PythonClassId, // may be a fake class for top-level functions + override val name: String, + override val returnType: RawPythonAnnotation, + override val parameters: List, +) : MethodId(classId, name, returnType, parameters) { + val moduleName: String = classId.moduleName + val rootModuleName: String = this.toString().split(".")[0] + override fun toString(): String = if (moduleName.isNotEmpty()) "$moduleName.$name" else name +} + +sealed class PythonModel(classId: PythonClassId): UtModel(classId) { + open val allContainingClassIds: Set = setOf(classId) +} + +class PythonTreeModel( + val tree: PythonTree.PythonTreeNode, + classId: PythonClassId, +): PythonModel(classId) { + override val allContainingClassIds: Set + get() { + val children = tree.children.map { PythonTreeModel(it, it.type) } + return super.allContainingClassIds + children.flatMap { it.allContainingClassIds } + } +} + +class PythonDefaultModel( + val repr: String, + classId: PythonClassId +): PythonModel(classId) { + override fun toString() = repr +} + +class PythonPrimitiveModel( + val value: Any, + classId: PythonClassId +): PythonModel(classId) { + override fun toString() = "$value" +} + +class PythonBoolModel(val value: Boolean): PythonModel(classId) { + override fun toString() = + if (value) "True" else "False" + companion object { + val classId = PythonClassId("builtins.bool") + } +} + +class PythonInitObjectModel( + val type: String, + val initValues: List +): PythonModel(PythonClassId(type)) { + override fun toString(): String { + val params = initValues.joinToString(separator = ", ") { it.toString() } + return "$type($params)" + } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + initValues.flatMap { it.allContainingClassIds } +} + +class PythonListModel( + val length: Int = 0, + val stores: List +) : PythonModel(classId) { + override fun toString() = + (0 until length).joinToString(", ", "[", "]") { stores[it].toString() } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + stores.flatMap { it.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.list") + } +} + +class PythonTupleModel( + val length: Int = 0, + val stores: List +) : PythonModel(classId) { + override fun toString() = + (0 until length).joinToString(", ", "(", ")") { stores[it].toString() } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + stores.flatMap { it.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.tuple") + } +} + +class PythonDictModel( + val length: Int = 0, + val stores: Map +) : PythonModel(classId) { + override fun toString() = withToStringThreadLocalReentrancyGuard { + stores.entries.joinToString(", ", "{", "}") { "${it.key}: ${it.value}" } + } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + + stores.entries.flatMap { it.key.allContainingClassIds + it.value.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.dict") + } +} + +class PythonSetModel( + val length: Int = 0, + val stores: Set +) : PythonModel(classId) { + override fun toString() = withToStringThreadLocalReentrancyGuard { + if (stores.isEmpty()) + "set()" + else + stores.joinToString(", ", "{", "}") { it.toString() } + } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + stores.flatMap { it.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.set") + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonTree.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonTree.kt new file mode 100644 index 0000000000..3041f22324 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/PythonTree.kt @@ -0,0 +1,133 @@ +package org.utbot.python.framework.api.python + +object PythonTree { + open class PythonTreeNode( + val type: PythonClassId, + var comparable: Boolean = true + ) { + open val children: List = emptyList() + + open fun typeEquals(other: Any?): Boolean { + return if (other is PythonTreeNode) + type == other.type && comparable && other.comparable + else + false + } + } + + class PrimitiveNode( + type: PythonClassId, + val repr: String, + ) : PythonTreeNode(type) + + class ListNode( + val items: List + ) : PythonTreeNode(PythonClassId("builtins.list")) { + override val children: List + get() = items + + override fun typeEquals(other: Any?): Boolean { + return if (other is ListNode) + items.zip(other.items).all { + it.first.typeEquals(it.second) + } + else false + } + } + + class DictNode( + val items: Map + ) : PythonTreeNode(PythonClassId("builtins.dict")) { + override val children: List + get() = items.values + items.keys + + override fun typeEquals(other: Any?): Boolean { + return if (other is DictNode) { + items.keys.size == other.items.keys.size && items.keys.all { + items[it]?.typeEquals(other.items[it]) ?: false + } + + } else false + } + } + + class SetNode( + val items: Set + ) : PythonTreeNode(PythonClassId("builtins.set")) { + override val children: List + get() = items.toList() + + override fun typeEquals(other: Any?): Boolean { + return if (other is SetNode) { + items.size == other.items.size && ( + items.isEmpty() || items.all { + items.first().typeEquals(it) + } && other.items.all { + items.first().typeEquals(it) + }) + } else { + false + } + } + } + + class TupleNode( + val items: List + ) : PythonTreeNode(PythonClassId("builtins.tuple")) { + override val children: List + get() = items + + override fun typeEquals(other: Any?): Boolean { + return if (other is TupleNode) { + items.size == other.items.size && items.zip(other.items).all { + it.first.typeEquals(it.second) + } + } else { + false + } + } + } + + class ReduceNode( + val id: Long, + type: PythonClassId, + val constructor: PythonClassId, + val args: List, + var state: Map, + var listitems: List, + var dictitems: Map, + ) : PythonTreeNode(type) { + constructor( + id: Long, + type: PythonClassId, + constructor: PythonClassId, + args: List, + ) : this(id, type, constructor, args, emptyMap(), emptyList(), emptyMap()) + + override val children: List + get() = args + state.values + listitems + dictitems.values + dictitems.keys + PythonTreeNode(constructor) + + override fun typeEquals(other: Any?): Boolean { + return if (other is ReduceNode) { + type == other.type && state.all { (key, value) -> + other.state.containsKey(key) && value.typeEquals(other.state[key]) + } && listitems.withIndex().all { (index, item) -> + other.listitems.size > index && item.typeEquals(other.listitems[index]) + } && dictitems.all { (key, value) -> + other.dictitems.containsKey(key) && value.typeEquals(other.dictitems[key]) + } + } else false + } + } + + fun allElementsHaveSameStructure(elements: Collection): Boolean { + return if (elements.isEmpty()) { + true + } else { + val firstElement = elements.first() + elements.drop(1).all { + it.typeEquals(firstElement) + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonIdUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonIdUtils.kt new file mode 100644 index 0000000000..f5d94675c8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonIdUtils.kt @@ -0,0 +1,22 @@ +package org.utbot.python.framework.api.python.util + +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonBoolModel +import org.utbot.python.framework.api.python.PythonListModel +import org.utbot.python.framework.api.python.PythonTupleModel +import org.utbot.python.framework.api.python.PythonDictModel +import org.utbot.python.framework.api.python.PythonSetModel + +// none annotation can be used in code only since Python 3.10 +val pythonNoneClassId = PythonClassId("types.NoneType") +val pythonAnyClassId = NormalizedPythonAnnotation("typing.Any") +val pythonIntClassId = PythonClassId("builtins.int") +val pythonFloatClassId = PythonClassId("builtins.float") +val pythonStrClassId = PythonClassId("builtins.str") +val pythonBoolClassId = PythonBoolModel.classId +val pythonRangeClassId = PythonClassId("builtins.range") +val pythonListClassId = PythonListModel.classId +val pythonTupleClassId = PythonTupleModel.classId +val pythonDictClassId = PythonDictModel.classId +val pythonSetClassId = PythonSetModel.classId \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonUtils.kt new file mode 100644 index 0000000000..ea0ca0b6fe --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/api/python/util/PythonUtils.kt @@ -0,0 +1,17 @@ +package org.utbot.python.framework.api.python.util + + +fun moduleOfType(typeName: String): String? { + val lastIndex = typeName.lastIndexOf('.') + return if (lastIndex == -1) null else typeName.substring(0, lastIndex) +} + +fun String.toSnakeCase(): String { + val splitSymbols = "_" + return this.mapIndexed { index: Int, c: Char -> + if (c.isLowerCase() || c.isDigit() || splitSymbols.contains(c)) c + else if (c.isUpperCase()) { + (if (index > 0) "_" else "") + c.lowercase() + } else c + }.joinToString("") +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCgLanguageAssistant.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCgLanguageAssistant.kt new file mode 100644 index 0000000000..6f245175f0 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCgLanguageAssistant.kt @@ -0,0 +1,53 @@ +package org.utbot.python.framework.codegen + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.CgLanguageAssistant +import org.utbot.framework.plugin.api.ClassId +import org.utbot.python.framework.codegen.model.constructor.name.PythonCgNameGenerator +import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgCallableAccessManagerImpl +import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgMethodConstructor +import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgStatementConstructorImpl +import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgVariableConstructor +import org.utbot.python.framework.codegen.model.constructor.visitor.CgPythonRenderer + +object PythonCgLanguageAssistant : CgLanguageAssistant() { + + override val extension: String + get() = ".py" + + override val languageKeywords: Set = setOf( + "True", "False", "None", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", + "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", + "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", "list", "int", "str", "float", "bool", + "bytes", "frozenset", "dict", "set", "tuple", "abs", "aiter", "all", "any", "anext", "ascii", "bool", + "breakpoint", "bytearray", "callable", "chr", "classmethod", "compile", "complex", "delattr", "dir", "divmod", + "enumerate", "eval", "exec", "filter", "format", "getattr", "globals", "hasattr", "hash", "help", "hex", "id", + "input", "isinstance", "issubclass", "iter", "len", "list", "locals", "map", "max", "memoryview", "min", + "next", "object", "oct", "open", "ord", "pow", "print", "property", "range", "repr", "reversed", "round", + "set", "setattr", "slice", "sorted", "staticmethod", "sum", "super", "type", "vars", "zip", "self" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + val simpleName = testClassCustomName ?: "Test${classUnderTest.simpleName}" + return Pair(simpleName, simpleName) + } + + override fun getNameGeneratorBy(context: CgContext) = PythonCgNameGenerator(context) + override fun getCallableAccessManagerBy(context: CgContext) = PythonCgCallableAccessManagerImpl(context) + override fun getStatementConstructorBy(context: CgContext) = PythonCgStatementConstructorImpl(context) + override fun getVariableConstructorBy(context: CgContext) = PythonCgVariableConstructor(context) + override fun getMethodConstructorBy(context: CgContext) = PythonCgMethodConstructor(context) + override fun getLanguageTestFrameworkManager() = PythonTestFrameworkManager() + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = + CgPythonRenderer(context, printer) + + var memoryObjects: MutableMap = emptyMap().toMutableMap() +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonTestFrameworkManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonTestFrameworkManager.kt new file mode 100644 index 0000000000..d649b56ffe --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonTestFrameworkManager.kt @@ -0,0 +1,22 @@ +package org.utbot.python.framework.codegen + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.plugin.api.LanguageTestFrameworkManager +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.framework.codegen.model.constructor.tree.PytestManager +import org.utbot.python.framework.codegen.model.constructor.tree.UnittestManager + +class PythonTestFrameworkManager : LanguageTestFrameworkManager() { + + override fun managerByFramework(context: CgContext) = when (context.testFramework) { + is Unittest -> UnittestManager(context) + is Pytest -> PytestManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Unittest + + override val testFrameworks = listOf(Unittest, Pytest) + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..33aec599b1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt @@ -0,0 +1,73 @@ +package org.utbot.python.framework.codegen.model + +import org.utbot.framework.codegen.* +import org.utbot.framework.codegen.model.CodeGenerator +import org.utbot.framework.codegen.model.CodeGeneratorResult + +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.python.framework.codegen.PythonCgLanguageAssistant +import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgTestClassConstructor + +class PythonCodeGenerator( + classUnderTest: ClassId, + paramNames: MutableMap> = mutableMapOf(), + testFramework: TestFramework = TestFramework.defaultItem, + mockFramework: MockFramework = MockFramework.defaultItem, + staticsMocking: StaticsMocking = StaticsMocking.defaultItem, + forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, + generateWarningsForStaticMocking: Boolean = true, + parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem, + runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + enableTestsTimeout: Boolean = true, + testClassPackageName: String = classUnderTest.packageName +) : CodeGenerator( + classUnderTest=classUnderTest, + paramNames=paramNames, + generateUtilClassFile = true, + testFramework=testFramework, + mockFramework=mockFramework, + staticsMocking=staticsMocking, + forceStaticMocking=forceStaticMocking, + generateWarningsForStaticMocking=generateWarningsForStaticMocking, + parameterizedTestSource=parameterizedTestSource, + runtimeExceptionTestsBehaviour=runtimeExceptionTestsBehaviour, + hangingTestsTimeout=hangingTestsTimeout, + enableTestsTimeout=enableTestsTimeout, + testClassPackageName=testClassPackageName +) { + override var context: CgContext = CgContext( + classUnderTest = classUnderTest, + paramNames = paramNames, + testFramework = testFramework, + mockFramework = mockFramework, + cgLanguageAssistant = PythonCgLanguageAssistant, + parametrizedTestSource = parameterizedTestSource, + staticsMocking = staticsMocking, + forceStaticMocking = forceStaticMocking, + generateWarningsForStaticMocking = generateWarningsForStaticMocking, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + hangingTestsTimeout = hangingTestsTimeout, + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName + ) + + fun pythonGenerateAsStringWithTestReport( + cgTestSets: List, + importModules: Set, + testClassCustomName: String? = null, + ): CodeGeneratorResult = withCustomContext(testClassCustomName) { + context.withTestClassFileScope { + val testClassModel = TestClassModel(classUnderTest, cgTestSets) + context.collectedImports.addAll(importModules) + val testClassFile = PythonCgTestClassConstructor(context).construct(testClassModel) + CodeGeneratorResult(renderClassFile(testClassFile), testClassFile.testsGenerationReport) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt new file mode 100644 index 0000000000..4dba586516 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt @@ -0,0 +1,85 @@ +package org.utbot.python.framework.codegen.model + +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.methodId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId + +object Pytest : TestFramework(displayName = "pytest", id = "pytest") { + override val mainPackage: String = "pytest" + override val assertionsClass: ClassId = pythonNoneClassId + override val arraysAssertionsClass: ClassId = assertionsClass + override val testAnnotation: String + get() = TODO("Not yet implemented") + override val testAnnotationId: ClassId = BuiltinClassId( + canonicalName = "pytest", + simpleName = "Tests" + ) + override val testAnnotationFqn: String = "" + + override val parameterizedTestAnnotation: String = "" + override val parameterizedTestAnnotationId: ClassId = pythonAnyClassId + override val parameterizedTestAnnotationFqn: String = "" + override val methodSourceAnnotation: String = "" + override val methodSourceAnnotationId: ClassId = pythonAnyClassId + override val methodSourceAnnotationFqn: String = "" + override val nestedClassesShouldBeStatic: Boolean = false + override val argListClassId: ClassId = pythonAnyClassId + + @OptIn(ExperimentalStdlibApi::class) + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List = buildList { + add(executionInvoke) + addAll(additionalArguments) + add(mainPackage) + } +} + +object Unittest : TestFramework(displayName = "Unittest", id = "Unittest") { + override val testSuperClass: ClassId = PythonClassId("unittest.TestCase") + override val mainPackage: String = "unittest" + override val assertionsClass: ClassId = PythonClassId("self") + override val arraysAssertionsClass: ClassId = assertionsClass + override val testAnnotation: String = "" + override val testAnnotationId: ClassId = BuiltinClassId( + canonicalName = "Unittest", + simpleName = "Tests" + ) + override val testAnnotationFqn: String = "unittest" + + override val parameterizedTestAnnotation: String = "" + override val parameterizedTestAnnotationId: ClassId = pythonAnyClassId + override val parameterizedTestAnnotationFqn: String = "" + override val methodSourceAnnotation: String = "" + override val methodSourceAnnotationId: ClassId = pythonAnyClassId + override val methodSourceAnnotationFqn: String = "" + override val nestedClassesShouldBeStatic: Boolean = false + override val argListClassId: ClassId = pythonAnyClassId + + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List { + throw UnsupportedOperationException() + } + + override val assertEquals by lazy { assertionId("assertEqual", objectClassId, objectClassId) } + + override fun assertionId(name: String, vararg params: ClassId): MethodId = + methodId(assertionsClass, name, voidClassId, *params) +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt new file mode 100644 index 0000000000..e40c76b78e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt @@ -0,0 +1,88 @@ +package org.utbot.framework.codegen + +sealed class PythonImport(order: Int) : Import(order) { + var importName: String = "" + var moduleName: String? = null + + constructor(order: Int, importName: String, moduleName: String? = null) : this(order) { + this.importName = importName + this.moduleName = moduleName + } + + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + val rootModuleName: String + get() = qualifiedName.split(".")[0] + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} + +data class PythonSysPathImport(val sysPath: String) : PythonImport(2) { + override val qualifiedName: String + get() = sysPath + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonSysPathImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + return sysPath.hashCode() + } +} + +data class PythonUserImport(val importName_: String, val moduleName_: String? = null) : + PythonImport(3, importName_, moduleName_) { + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonUserImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} + +data class PythonSystemImport(val importName_: String, val moduleName_: String? = null) : + PythonImport(1, importName_, moduleName_) { + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonSystemImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt new file mode 100644 index 0000000000..94692fb9b4 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt @@ -0,0 +1,107 @@ +package org.utbot.python.framework.codegen.model.constructor.name + +import org.utbot.framework.codegen.PythonImport +import org.utbot.framework.codegen.isLanguageKeyword +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.name.CgNameGenerator +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.util.toSnakeCase + +internal fun infiniteInts(): Sequence = + generateSequence(1) { it + 1 } + +class PythonCgNameGenerator(val context: CgContext) + : CgNameGenerator, CgContextOwner by context { + + private fun nextIndexedVarName(base: String): String = + infiniteInts() + .map { "$base$it" } + .first { it !in existingVariableNames } + + private fun nextIndexedMethodName(base: String, skipOne: Boolean = false): String = + infiniteInts() + .map { if (skipOne && it == 1) base else "$base$it" } + .first { it !in existingMethodNames } + + private fun createNameFromKeyword(baseName: String): String = + nextIndexedVarName(baseName) + + private fun createExecutableName(executableId: ExecutableId): String { + return when (executableId) { + is ConstructorId -> executableId.classId.prettifiedName + is MethodId -> executableId.name + } + } + + override fun nameFrom(id: ClassId): String = + when (id) { + is NormalizedPythonAnnotation -> "var" + else -> id.simpleName.toSnakeCase() + } + + override fun variableName(base: String, isMock: Boolean, isStatic: Boolean): String { + val baseName = when { + isMock -> base + "_mock" + isStatic -> base + "_static" + else -> base + } + return when { + baseName in existingVariableNames -> nextIndexedVarName(baseName) + isLanguageKeyword(baseName, context.cgLanguageAssistant) -> createNameFromKeyword(baseName) + else -> baseName + }.also { + existingVariableNames = existingVariableNames.add(it) + } + } + + override fun variableName(type: ClassId, base: String?, isMock: Boolean): String { + val baseName = base?.toSnakeCase() ?: nameFrom(type) + val importedModuleNames = collectedImports.mapNotNull { + if (it is PythonImport) it.rootModuleName else null + } + return when { + baseName in existingVariableNames -> nextIndexedVarName(baseName) + baseName in importedModuleNames -> nextIndexedVarName(baseName) + isLanguageKeyword(baseName, context.cgLanguageAssistant) -> createNameFromKeyword(baseName) + else -> baseName + }.also { + existingVariableNames = existingVariableNames.add(it) + } + } + + override fun testMethodNameFor(executableId: ExecutableId, customName: String?): String { + val executableName = createExecutableName(executableId) + + val name = if (customName != null && customName !in existingMethodNames) { + customName + } else { + val base = customName ?: "test_${executableName.toSnakeCase()}" + nextIndexedMethodName(base) + } + existingMethodNames += name + return name + } + + override fun parameterizedTestMethodName(dataProviderMethodName: String): String { + TODO("Not yet implemented") + } + + override fun dataProviderMethodNameFor(executableId: ExecutableId): String { + TODO("Not yet implemented") + } + + override fun errorMethodNameFor(executableId: ExecutableId): String { + val executableName = createExecutableName(executableId) + val newName = when (val base = "test_${executableName.toSnakeCase()}_errors") { + !in existingMethodNames -> base + else -> nextIndexedMethodName(base) + } + existingMethodNames += newName + return newName + } +} 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 new file mode 100644 index 0000000000..69ed440e93 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt @@ -0,0 +1,62 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.tree.CgIncompleteMethodCall +import org.utbot.framework.codegen.model.constructor.util.importIfNeeded +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.util.exceptions +import org.utbot.python.framework.api.python.PythonMethodId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded + +class PythonCgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessManager, + CgContextOwner by context { + + override fun CgExpression?.get(methodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(methodId, this) + + override fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(staticMethodId, CgThisInstance(pythonAnyClassId)) + + override fun CgExpression.get(fieldId: FieldId): CgExpression { + TODO("Not yet implemented") + } + + override fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess { + TODO("Not yet implemented") + } + + override fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { + val resolvedArgs = args.resolve() + val constructorCall = CgConstructorCall(this, resolvedArgs) + newConstructorCall(this) + return constructorCall + } + + override fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { + val resolvedArgs = args.resolve() + val methodCall = CgMethodCall(caller, method, resolvedArgs) + if (method is PythonMethodId) + newMethodCall(method) + return methodCall + } + + private fun newMethodCall(methodId: MethodId) { + importIfNeeded(methodId as PythonMethodId) + } + + private fun newConstructorCall(constructorId: ConstructorId) { + importIfNeeded(constructorId.classId) + for (exception in constructorId.exceptions) { + addExceptionIfNeeded(exception) + } + } + +} 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 new file mode 100644 index 0000000000..4b78a4cc14 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt @@ -0,0 +1,358 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.fields.StateModificationInfo +import org.utbot.framework.plugin.api.* +import org.utbot.python.framework.api.python.* +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.tree.* + +class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(context) { + override fun assertEquality(expected: CgValue, actual: CgVariable) { + pythonDeepEquals(expected, actual) + } + + private fun generatePythonTestComments(execution: UtExecution) { + when (execution.result) { + is UtExplicitlyThrownException -> + (execution.result as UtExplicitlyThrownException).exception.message?.let { + emptyLineIfNeeded() + comment("raises $it") + } + + else -> { + // nothing + } + } + } + + override fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = + withTestMethodScope(execution) { + val testMethodName = nameGenerator.testMethodNameFor(executableId, execution.testMethodName) + // TODO: remove this line when SAT-1273 is completed + execution.displayName = execution.displayName?.let { "${executableId.name}: $it" } + testMethod(testMethodName, execution.displayName) { + val statics = currentExecution!!.stateBefore.statics + rememberInitialStaticFields(statics) + (context.cgLanguageAssistant as PythonCgLanguageAssistant).memoryObjects.clear() + + val modificationInfo = StateModificationInfo() + val fieldStateManager = context.cgLanguageAssistant.getCgFieldStateManager(context) + // TODO: move such methods to another class and leave only 2 public methods: remember initial and final states + val mainBody = { + substituteStaticFields(statics) + setupInstrumentation() + // build this instance + thisInstance = execution.stateBefore.thisInstance?.let { + variableConstructor.getOrCreateVariable(it) + } + // build arguments + for ((index, param) in execution.stateBefore.parameters.withIndex()) { + val name = paramNames[executableId]?.get(index) + methodArguments += variableConstructor.getOrCreateVariable(param, name) + } + fieldStateManager.rememberInitialEnvironmentState(modificationInfo) + recordActualResult() + generateResultAssertions() + fieldStateManager.rememberFinalEnvironmentState(modificationInfo) + generateFieldStateAssertions() + if (executableId is PythonMethodId) + generatePythonTestComments(execution) + } + + if (statics.isNotEmpty()) { + +tryBlock { + mainBody() + }.finally { + recoverStaticFields() + } + } else { + mainBody() + } + } + } + + private fun pythonBuildObject(objectNode: PythonTree.PythonTreeNode): CgValue { + return when (objectNode) { + is PythonTree.PrimitiveNode -> { + CgLiteral(objectNode.type, objectNode.repr) + } + + is PythonTree.ListNode -> { + CgPythonList( + objectNode.items.map { pythonBuildObject(it) } + ) + } + + is PythonTree.TupleNode -> { + CgPythonTuple( + objectNode.items.map { pythonBuildObject(it) } + ) + } + + is PythonTree.SetNode -> { + CgPythonSet( + objectNode.items.map { pythonBuildObject(it) }.toSet() + ) + } + + is PythonTree.DictNode -> { + CgPythonDict( + objectNode.items.map { (key, value) -> + pythonBuildObject(key) to pythonBuildObject(value) + }.toMap() + ) + } + + is PythonTree.ReduceNode -> { + val id = objectNode.id + if ((context.cgLanguageAssistant as PythonCgLanguageAssistant).memoryObjects.containsKey(id)) { + return (context.cgLanguageAssistant as PythonCgLanguageAssistant).memoryObjects[id]!! + } + + val initArgs = objectNode.args.map { + pythonBuildObject(it) + } + val constructor = ConstructorId( + objectNode.constructor, + initArgs.map { it.type } + ) + + val obj = newVar(objectNode.type) { + CgConstructorCall( + constructor, + initArgs + ) + } + (context.cgLanguageAssistant as PythonCgLanguageAssistant).memoryObjects[id] = obj + + val state = objectNode.state.map { (key, value) -> + key to pythonBuildObject(value) + }.toMap() + val listitems = objectNode.listitems.map { + pythonBuildObject(it) + } + val dictitems = objectNode.dictitems.map { (key, value) -> + pythonBuildObject(key) to pythonBuildObject(value) + } + + state.forEach { (key, value) -> + val fieldAccess = CgFieldAccess(obj, FieldId(objectNode.type, key)) + fieldAccess `=` value + } + listitems.forEach { + +CgMethodCall( + obj, + PythonMethodId( + obj.type as PythonClassId, + "append", + NormalizedPythonAnnotation(pythonNoneClassId.name), + listOf(RawPythonAnnotation(it.type.name)) + ), + listOf(it) + ) + } + dictitems.forEach { (key, value) -> + val index = CgPythonIndex( + value.type as PythonClassId, + obj, + key + ) + index `=` value + } + + return obj + } + + else -> { + throw UnsupportedOperationException() + } + } + } + + private fun pythonDeepEquals(expected: CgValue, actual: CgVariable) { + require(expected is CgPythonTree) { + "Expected value have to be CgPythonTree but `${expected::class}` found" + } + val expectedValue = pythonBuildObject(expected.tree) + pythonDeepTreeEquals(expected.tree, expectedValue, actual) + } + + private fun pythonLenAssertConstructor(expected: CgVariable, actual: CgVariable): CgVariable { + val expectedValue = newVar(pythonIntClassId, "expected_length") { + CgGetLength(expected) + } + val actualValue = newVar(pythonIntClassId, "actual_length") { + CgGetLength(actual) + } + emptyLineIfNeeded() + testFrameworkManager.assertEquals(expectedValue, actualValue) + return expectedValue + } + + private fun assertIsInstance(expected: CgValue, actual: CgVariable) { + when (testFrameworkManager) { + is PytestManager -> + (testFrameworkManager as PytestManager).assertIsinstance(listOf(expected.type), actual) + is UnittestManager -> + (testFrameworkManager as UnittestManager).assertIsinstance(listOf(expected.type), actual) + else -> testFrameworkManager.assertEquals(expected, actual) + } + } + + private fun pythonAssertElementsByKey( + expectedNode: PythonTree.PythonTreeNode, + expected: CgVariable, + actual: CgVariable, + iterator: CgReferenceExpression, + keyName: String = "index", + ) { + val elements = when (expectedNode) { + is PythonTree.ListNode -> expectedNode.items + is PythonTree.TupleNode -> expectedNode.items + is PythonTree.DictNode -> expectedNode.items.values + else -> throw UnsupportedOperationException() + } + if (elements.isNotEmpty()) { + val elementsHaveSameStructure = PythonTree.allElementsHaveSameStructure(elements) + val firstChild = + elements.first() // TODO: We can use only structure => we should use another element if the first is empty + + emptyLine() + if (elementsHaveSameStructure) { + val index = newVar(pythonNoneClassId, keyName) { + CgLiteral(pythonNoneClassId, "None") + } + forEachLoop { + innerBlock { + condition = index + iterable = iterator + val indexExpected = newVar(firstChild.type, "expected_element") { + CgPythonIndex( + pythonIntClassId, + expected, + index + ) + } + val indexActual = newVar(firstChild.type, "actual_element") { + CgPythonIndex( + pythonIntClassId, + actual, + index + ) + } + pythonDeepTreeEquals(firstChild, indexExpected, indexActual) + statements = currentBlock + } + } + } else { + emptyLineIfNeeded() + assertIsInstance(expected, actual) + } + } + } + + private fun pythonAssertBuiltinsCollection( + expectedNode: PythonTree.PythonTreeNode, + expected: CgValue, + actual: CgVariable, + expectedName: String, + elementName: String = "index", + ) { + val expectedCollection = newVar(expected.type, expectedName) { expected } + + val length = pythonLenAssertConstructor(expectedCollection, actual) + + val iterator = if (expectedNode is PythonTree.DictNode) expected else CgPythonRange(length) + pythonAssertElementsByKey(expectedNode, expectedCollection, actual, iterator, elementName) + } + + private fun pythonDeepTreeEquals( + expectedNode: PythonTree.PythonTreeNode, + expected: CgValue, + actual: CgVariable + ) { + if (expectedNode.comparable) { + emptyLineIfNeeded() + testFrameworkManager.assertEquals( + expected, + actual, + ) + return + } + when (expectedNode) { + is PythonTree.PrimitiveNode -> { + emptyLineIfNeeded() + assertIsInstance(expected, actual) + } + + is PythonTree.ListNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_list" + ) + } + + is PythonTree.TupleNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_tuple" + ) + } + + is PythonTree.SetNode -> { + emptyLineIfNeeded() + testFrameworkManager.assertEquals( + expected, actual + ) + } + + is PythonTree.DictNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_dict", + "key" + ) + } + + is PythonTree.ReduceNode -> { + if (expectedNode.state.isNotEmpty()) { + expectedNode.state.forEach { (field, value) -> + val fieldActual = newVar(value.type, "actual_$field") { + CgFieldAccess( + actual, FieldId( + value.type, + field + ) + ) + } + val fieldExpected = newVar(value.type, "expected_$field") { + CgFieldAccess( + expected, FieldId( + value.type, + field + ) + ) + } + pythonDeepTreeEquals(value, fieldExpected, fieldActual) + } + } else { + emptyLineIfNeeded() + assertIsInstance(expected, actual) + } + } + + else -> {} + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..0767de10eb --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt @@ -0,0 +1,252 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import fj.data.Either +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getCallableAccessManagerBy +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor +import org.utbot.framework.codegen.model.constructor.util.ExpressionWithType +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.buildExceptionHandler +import org.utbot.framework.codegen.model.util.isAccessibleFrom +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.ClassId +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.codegen.model.constructor.util.plus +import java.util.* + +class PythonCgStatementConstructorImpl(context: CgContext) : + CgStatementConstructor, + CgContextOwner by context, + CgCallableAccessManager by getCallableAccessManagerBy(context) { + + private val nameGenerator = getNameGeneratorBy(context) + + override fun newVar( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutable: Boolean, + init: () -> CgExpression + ): CgVariable { + val declarationOrVar: Either = + createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType, + model, + baseName, + isMock, + isMutable, + init + ) + + return declarationOrVar.either( + { declaration -> + currentBlock += declaration + + declaration.variable + }, + { variable -> variable } + ) + } + + override fun createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutableVar: Boolean, + init: () -> CgExpression + ): Either { + val baseExpr = init() + + val name = nameGenerator.variableName(baseType, baseName) + val (type, expr) = guardExpression(baseType, baseExpr) + + val declaration = buildDeclaration { + variableType = type + variableName = name + initializer = expr + } + updateVariableScope(declaration.variable, model) + return Either.left(declaration) + } + + override fun CgExpression.`=`(value: Any?) { + currentBlock += buildAssignment { + lValue = this@`=` + rValue = value.resolve() + } + } + + override fun CgExpression.and(other: CgExpression): CgLogicalAnd = + CgLogicalAnd(this, other) + + override fun CgExpression.or(other: CgExpression): CgLogicalOr = + CgLogicalOr(this, other) + + override fun ifStatement( + condition: CgExpression, + trueBranch: () -> Unit, + falseBranch: (() -> Unit)? + ): CgIfStatement { + val trueBranchBlock = block(trueBranch) + val falseBranchBlock = falseBranch?.let { block(it) } + return CgIfStatement(condition, trueBranchBlock, falseBranchBlock).also { + currentBlock += it + } + } + + override fun forLoop(init: CgForLoopBuilder.() -> Unit) { + currentBlock += buildForLoop(init) + } + + override fun whileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun doWhileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildDoWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun forEachLoop(init: CgForEachLoopBuilder.() -> Unit) = withNameScope { + currentBlock += buildCgForEachLoop(init) + } + + override fun getClassOf(classId: ClassId): CgExpression { + TODO("Not yet implemented") + } + + override fun createFieldVariable(fieldId: FieldId): CgVariable { + TODO("Not yet implemented") + } + + override fun createExecutableVariable(executableId: ExecutableId, arguments: List): CgVariable { + TODO("Not yet implemented") + } + + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) + + override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = + buildTryCatch { + statements = block(init) + this.resources = resources + } + + override fun CgTryCatch.catch(exception: ClassId, init: (CgVariable) -> Unit): CgTryCatch { + val newHandler = buildExceptionHandler { + val e = declareVariable(exception, nameGenerator.variableName(exception.simpleName.replaceFirstChar { + it.lowercase( + Locale.getDefault() + ) + })) + this.exception = e + this.statements = block { init(e) } + } + return this.copy(handlers = handlers + newHandler) + } + + override fun CgTryCatch.finally(init: () -> Unit): CgTryCatch { + val finallyBlock = block(init) + return this.copy(finally = finallyBlock) + } + + override fun CgExpression.isInstance(value: CgExpression): CgIsInstance = TODO("Not yet implemented") + + override fun innerBlock(init: () -> Unit): CgInnerBlock = + CgInnerBlock(block(init)).also { + currentBlock += it + } + + override fun comment(text: String): CgComment = + CgSingleLineComment(text).also { + currentBlock += it + } + + override fun comment(): CgComment = + CgSingleLineComment("").also { + currentBlock += it + } + + override fun multilineComment(lines: List): CgComment = + CgMultilineComment(lines).also { + currentBlock += it + } + + override fun lambda(type: ClassId, vararg parameters: CgVariable, body: () -> Unit): CgAnonymousFunction { + return withNameScope { + for (parameter in parameters) { + declareParameter(parameter.type, parameter.name) + } + val paramDeclarations = parameters.map { CgParameterDeclaration(it) } + CgAnonymousFunction(type, paramDeclarations, block(body)) + } + } + + override fun annotation(classId: ClassId, argument: Any?): CgAnnotation { + val annotation = CgSingleArgAnnotation(classId, argument.resolve()) + addAnnotation(annotation) + return annotation + } + + override fun annotation(classId: ClassId, namedArguments: List>): CgAnnotation { + val annotation = CgMultipleArgsAnnotation( + classId, + namedArguments.mapTo(mutableListOf()) { (name, value) -> CgNamedAnnotationArgument(name, value) } + ) + addAnnotation(annotation) + return annotation + } + + override fun annotation( + classId: ClassId, + buildArguments: MutableList>.() -> Unit + ): CgAnnotation { + val arguments = mutableListOf>() + .apply(buildArguments) + .map { (name, value) -> CgNamedAnnotationArgument(name, value) } + val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList()) + addAnnotation(annotation) + return annotation + } + + override fun returnStatement(expression: () -> CgExpression) { + currentBlock += CgReturnStatement(expression()) + } + + override fun throwStatement(exception: () -> CgExpression): CgThrowStatement = + CgThrowStatement(exception()).also { currentBlock += it } + + override fun emptyLine() { + currentBlock += CgEmptyLine() + } + + override fun emptyLineIfNeeded() { + val lastStatement = currentBlock.lastOrNull() ?: return + if (lastStatement is CgEmptyLine) return + emptyLine() + } + + override fun declareVariable(type: ClassId, name: String): CgVariable = + CgVariable(name, type).also { + updateVariableScope(it) + } + + override fun guardExpression(baseType: ClassId, expression: CgExpression): ExpressionWithType { + return ExpressionWithType(baseType, expression) + } + + override fun wrapTypeIfRequired(baseType: ClassId): ClassId = + if (baseType.isAccessibleFrom(testClassPackageName)) baseType else objectClassId +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt new file mode 100644 index 0000000000..07eab68d30 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt @@ -0,0 +1,20 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.tree.CgTestClassFile +import org.utbot.framework.codegen.model.tree.buildTestClassFile + +internal class PythonCgTestClassConstructor(context: CgContext) : CgTestClassConstructor(context) { + override fun construct(testClassModel: TestClassModel): CgTestClassFile { + return buildTestClassFile { + this.declaredClass = withTestClassScope { + with(currentTestClassContext) { testClassSuperclass = testFramework.testSuperClass } + constructTestClass(testClassModel) + } + imports.addAll(context.collectedImports) + testsGenerationReport = this@PythonCgTestClassConstructor.testsGenerationReport + } + } +} 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 new file mode 100644 index 0000000000..0a5b04163c --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt @@ -0,0 +1,50 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.constructor.tree.CgVariableConstructor +import org.utbot.framework.codegen.model.tree.CgConstructorCall +import org.utbot.framework.codegen.model.tree.CgLiteral +import org.utbot.framework.codegen.model.tree.CgValue +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.python.framework.api.python.* +import org.utbot.python.framework.codegen.model.tree.* + +class PythonCgVariableConstructor(context_: CgContext) : CgVariableConstructor(context_) { + private val nameGenerator = CgTestClassConstructor.CgComponents.getNameGeneratorBy(context) + + override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { + val baseName = name ?: nameGenerator.nameFrom(model.classId) + return valueByModel.getOrPut(model) { + when (model) { + is PythonBoolModel -> CgLiteral(model.classId, model.value) + is PythonPrimitiveModel -> CgLiteral(model.classId, model.value) + is PythonTreeModel -> CgPythonTree(model.classId, model.tree) + is PythonInitObjectModel -> constructInitObjectModel(model, baseName) + is PythonDictModel -> CgPythonDict(model.stores.map { + getOrCreateVariable(it.key) to getOrCreateVariable( + it.value + ) + }.toMap()) + + is PythonListModel -> CgPythonList(model.stores.map { getOrCreateVariable(it) }) + is PythonSetModel -> CgPythonSet(model.stores.map { getOrCreateVariable(it) }.toSet()) + is PythonTupleModel -> CgPythonTuple(model.stores.map { getOrCreateVariable(it) }) + is PythonDefaultModel -> CgPythonRepr(model.classId, model.repr) + is PythonModel -> error("Unexpected PythonModel: ${model::class}") + else -> super.getOrCreateVariable(model, name) + } + } + } + + private fun constructInitObjectModel(model: PythonInitObjectModel, baseName: String): CgVariable { + return newVar(model.classId, baseName) { + CgConstructorCall( + ConstructorId(model.classId, model.initValues.map { it.classId }), + model.initValues.map { getOrCreateVariable(it) } + ) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..9ee31649ea --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt @@ -0,0 +1,143 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonBoolClassId +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.framework.codegen.model.tree.CgPythonAssertEquals +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall +import org.utbot.python.framework.codegen.model.tree.CgPythonTuple + +internal class PytestManager(context: CgContext) : TestFrameworkManager(context) { + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Pytest) { "According to settings, Pytest was expected, but got: $testFramework" } + block() + } + + override val isExpectedExceptionExecutionBreaking: Boolean = true + + override fun createDataProviderAnnotations(dataProviderMethodName: String): MutableList { + TODO("Not yet implemented") + } + + override fun createArgList(length: Int): CgVariable { + TODO("Not yet implemented") + } + + override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?): Set { + TODO("Not yet implemented") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) = Unit + + override fun disableTestMethod(reason: String) { + } + + override val dataProviderMethodsHolder: TestClassContext get() = TODO() + override val annotationForNestedClasses: CgAnnotation + get() = TODO("Not yet implemented") + override val annotationForOuterClasses: CgAnnotation + get() = TODO("Not yet implemented") + + override fun assertEquals(expected: CgValue, actual: CgValue) { + +CgPythonAssertEquals( + CgEqualTo(actual, expected) + ) + } + + fun assertIsinstance(types: List, actual: CgVariable) { + +CgPythonAssertEquals( + CgPythonFunctionCall( + pythonBoolClassId, + "isinstance", + listOf( + actual, + if (types.size == 1) + CgLiteral(pythonAnyClassId, types[0].name) + else + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.name) }) + ), + ), + ) + } +} + +internal class UnittestManager(context: CgContext) : TestFrameworkManager(context) { + override val isExpectedExceptionExecutionBreaking: Boolean = true + + override val dataProviderMethodsHolder: TestClassContext + get() = TODO() + override val annotationForNestedClasses: CgAnnotation + get() = TODO("Not yet implemented") + override val annotationForOuterClasses: CgAnnotation + get() = TODO("Not yet implemented") + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Unittest) { "According to settings, Unittest was expected, but got: $testFramework" } + block() + } + + override fun createDataProviderAnnotations(dataProviderMethodName: String): MutableList { + TODO("Not yet implemented") + } + + override fun createArgList(length: Int): CgVariable { + TODO("Not yet implemented") + } + + override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?): Set { + TODO("Not yet implemented") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) = Unit + + override fun disableTestMethod(reason: String) { + require(testFramework is Unittest) { "According to settings, Unittest was expected, but got: $testFramework" } + + collectedMethodAnnotations += CgMultipleArgsAnnotation( + skipAnnotationClassId, + mutableListOf( + CgNamedAnnotationArgument( + name = "value", + value = reason.resolve() + ) + ) + ) + } + + private val skipAnnotationClassId = BuiltinClassId( + canonicalName = "unittest.skip", + simpleName = "skip" + ) + + fun assertIsinstance(types: List, actual: CgVariable) { + +assertions[assertTrue]( + CgPythonFunctionCall( + pythonBoolClassId, + "isinstance", + listOf( + actual, + if (types.size == 1) + CgLiteral(pythonAnyClassId, types[0].name) + else + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.name) }) + ), + ), + ) + } +} 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 new file mode 100644 index 0000000000..222850ac89 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -0,0 +1,29 @@ +package org.utbot.python.framework.codegen.model.constructor.util + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet +import org.utbot.framework.codegen.PythonUserImport +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonMethodId + +internal fun CgContextOwner.importIfNeeded(method: PythonMethodId) { + collectedImports += PythonUserImport(method.moduleName) +} + +internal fun CgContextOwner.importIfNeeded(pyClass: PythonClassId) { + collectedImports += PythonUserImport(pyClass.moduleName) +} + +internal operator fun PersistentList.plus(element: T): PersistentList = + this.add(element) + +internal operator fun PersistentList.plus(other: PersistentList): PersistentList = + this.addAll(other) + +internal operator fun PersistentSet.plus(element: T): PersistentSet = + this.add(element) + +internal operator fun PersistentSet.plus(other: PersistentSet): PersistentSet = + this.addAll(other) + 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 new file mode 100644 index 0000000000..e3f1cd36f3 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt @@ -0,0 +1,496 @@ +package org.utbot.python.framework.codegen.model.constructor.visitor + +import org.apache.commons.text.StringEscapeUtils +import org.utbot.common.WorkaroundReason +import org.utbot.common.workaround +import org.utbot.framework.codegen.PythonImport +import org.utbot.framework.codegen.PythonSysPathImport +import org.utbot.framework.codegen.RegularImport +import org.utbot.framework.codegen.StaticImport +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.util.CgPrinterImpl +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.TypeParameters +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.tree.* + +internal class CgPythonRenderer( + context: CgRendererContext, + printer: CgPrinter = CgPrinterImpl() +) : + CgAbstractRenderer(context, printer), + CgPythonVisitor { + + override val regionStart: String = "# region" + override val regionEnd: String = "# endregion" + + override val statementEnding: String = "" + + override val logicalAnd: String + get() = "and" + + override val logicalOr: String + get() = "or" + + override val langPackage: String = "python" + + override val ClassId.methodsAreAccessibleAsTopLevel: Boolean + get() = false + + override fun visit(element: CgClassFile) { + renderClassFileImports(element) + + println() + println() + + element.declaredClass.accept(this) + } + + override fun visit(element: CgClass) { + print("class ") + print(element.simpleName) + if (element.superclass != null) { + print("(${element.superclass!!.asString()})") + } + println(":") + withIndent { element.body.accept(this) } + println("") + } + + override fun visit(element: CgCommentedAnnotation) { + print("#") + element.annotation.accept(this) + } + + override fun visit(element: CgSingleArgAnnotation) { + print("") + } + + override fun visit(element: CgMultipleArgsAnnotation) { + print("") + } + + override fun visit(element: CgSingleLineComment) { + println("# ${element.comment}") + } + + override fun visit(element: CgAbstractMultilineComment) { + visit(element as CgElement) + } + + override fun visit(element: CgTripleSlashMultilineComment) { + element.lines.forEach { line -> + println("# $line") + } + } + + override fun visit(element: CgMultilineComment) { + val lines = element.lines + if (lines.isEmpty()) return + + if (lines.size == 1) { + print("# ${lines.first()}") + return + } + + // print lines saving indentation + print("\"\"\"") + println(lines.first()) + lines.subList(1, lines.lastIndex).forEach { println(it) } + print(lines.last()) + println("\"\"\"") + } + + override fun visit(element: CgDocumentationComment) { + if (element.lines.all { it.isEmpty() }) return + + println("\"\"\"") + for (line in element.lines) line.accept(this) + println("\"\"\"") + } + + override fun visit(element: CgErrorWrapper) { + element.expression.accept(this) + } + + override fun visit(element: CgClassBody) { + // render regions for test methods + for ((i, region) in (element.methodRegions + element.nestedClassRegions).withIndex()) { + if (i != 0) println() + + region.accept(this) + } + + if (element.staticDeclarationRegions.isEmpty()) { + return + } + } + + override fun visit(element: CgTryCatch) { + println("try") + // TODO introduce CgBlock + visit(element.statements) + for ((exception, statements) in element.handlers) { + print("except") + renderExceptionCatchVariable(exception) + println("") + // TODO introduce CgBlock + visit(statements, printNextLine = element.finally == null) + } + element.finally?.let { + print("finally") + // TODO introduce CgBlock + visit(element.finally!!, printNextLine = true) + } + } + + override fun visit(element: CgArrayAnnotationArgument) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgAnonymousFunction) { + print("lambda ") + element.parameters.renderSeparated() + print(": ") + + visit(element.body) + } + + override fun visit(element: CgEqualTo) { + element.left.accept(this) + print(" == ") + element.right.accept(this) + } + + override fun visit(element: CgTypeCast) { + TODO("Not yet implemented") + } + + override fun visit(element: CgNotNullAssertion) { + element.expression.accept(this) + } + + override fun visit(element: CgAllocateArray) { + print("[None] * ${element.size}") + } + + override fun visit(element: CgAllocateInitializedArray) { + print(" [") + element.initializer.accept(this) + print(" ]") + } + + override fun visit(element: CgArrayInitializer) { + val elementType = element.elementType + val elementsInLine = arrayElementsInLine(elementType) + + print("[") + element.values.renderElements(elementsInLine) + print("]") + } + + override fun visit(element: CgSwitchCaseLabel) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgSwitchCase) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgParameterDeclaration) { + print(element.name.escapeNamePossibleKeyword()) + if (element.type.name != "") + print(": ") + print(element.type.name) + } + + override fun visit(element: CgGetLength) { + print("len(") + element.variable.accept(this) + print(")") + } + + override fun visit(element: CgGetJavaClass) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgGetKotlinClass) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgConstructorCall) { + print(element.executableId.classId.name) + renderExecutableCallArguments(element) + } + + override fun renderRegularImport(regularImport: RegularImport) { + val escapedImport = getEscapedImportRendering(regularImport) + println("import $escapedImport") + } + + override fun renderStaticImport(staticImport: StaticImport) { + throw UnsupportedOperationException() + } + + override fun renderClassFileImports(element: CgClassFile) { + element.imports + .filterIsInstance() + .sortedBy { it.order } + .forEach { renderPythonImport(it) } + } + + private fun renderPythonImport(pythonImport: PythonImport) { + if (pythonImport is PythonSysPathImport) { + println("sys.path.append('${pythonImport.sysPath}')") + } else if (pythonImport.moduleName == null) { + println("import ${pythonImport.importName}") + } else { + println("from ${pythonImport.moduleName} import ${pythonImport.importName}") + } + } + + override fun renderMethodSignature(element: CgTestMethod) { + print("def ") + print(element.name) + + print("(") + val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine + val selfParameter = CgThisInstance(pythonAnyClassId) + (listOf(selfParameter) + element.parameters).renderSeparated(newLinesNeeded) + print(")") + } + + override fun renderMethodSignature(element: CgErrorTestMethod) { + print("def ") + print(element.name) + print("(") + val selfParameter = CgThisInstance(pythonAnyClassId) + listOf(selfParameter).renderSeparated() + print(")") + } + + override fun visit(element: CgErrorTestMethod) { + renderMethodDocumentation(element) + renderMethodSignature(element) + visit(element as CgMethod) + println("pass") + } + + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { + val returnType = element.returnType.canonicalName + println("def ${element.name}() -> $returnType: pass") + } + + override fun visit(element: CgInnerBlock) { + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + } + } + + override fun renderForLoopVarControl(element: CgForLoop) { + print("for ") + visit(element.condition) + print(" in ") + element.initialization.accept(this@CgPythonRenderer) + println(":") + } + + override fun renderDeclarationLeftPart(element: CgDeclaration) { + visit(element.variable) + } + + override fun toStringConstantImpl(byte: Byte): String { + return "b'$byte'" + } + + override fun toStringConstantImpl(short: Short): String { + return "$short" + } + + override fun toStringConstantImpl(int: Int): String { + return "$int" + } + + override fun toStringConstantImpl(long: Long): String { + return "$long" + } + + override fun toStringConstantImpl(float: Float): String { + return "$float" + } + + override fun renderAccess(caller: CgExpression) { + print(".") + } + + override fun renderTypeParameters(typeParameters: TypeParameters) { + if (typeParameters.parameters.isNotEmpty()) { + print("[") + if (typeParameters is WildcardTypeParameter) { + print("typing.Any") + } else { + print(typeParameters.parameters.joinToString { it.name }) + } + print("]") + } + } + + override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { + print("(") + executableCall.arguments.renderSeparated() + print(")") + } + + override fun renderExceptionCatchVariable(exception: CgVariable) { + print(exception.name.escapeNamePossibleKeyword()) + } + + override fun escapeNamePossibleKeywordImpl(s: String): String = s + override fun renderClassVisibility(classId: ClassId) { + throw UnsupportedOperationException() + } + + override fun renderClassModality(aClass: CgClass) { + throw UnsupportedOperationException() + } + + override fun visit(block: List, printNextLine: Boolean) { + println(":") + + val isBlockTooLarge = workaround(WorkaroundReason.LONG_CODE_FRAGMENTS) { block.size > 120 } + + withIndent { + if (isBlockTooLarge) { + print("\"\"\"") + println(" This block of code is ${block.size} lines long and could lead to compilation error") + } + + for (statement in block) { + statement.accept(this) + } + + if (isBlockTooLarge) println("\"\"\"") + } + + if (printNextLine) println() + } + + override fun visit(element: CgThisInstance) { + print("self") + } + + override fun visit(element: CgMethod) { + visit(element.statements, printNextLine = false) + } + + override fun visit(element: CgMethodCall) { + if (element.caller == null) { + val module = (element.executableId.classId as PythonClassId).moduleName + if (module != pythonBuiltinsModuleName) { + print("$module.") + } + } else { + element.caller!!.accept(this) + print(".") + } + print(element.executableId.name) + + renderTypeParameters(element.typeParameters) + renderExecutableCallArguments(element) + } + + override fun visit(element: CgPythonRepr) { + print(element.content) + } + + override fun visit(element: CgPythonIndex) { + visit(element.obj) + print("[") + element.index.accept(this) + print("]") + } + + override fun visit(element: CgPythonFunctionCall) { + print(element.name) + print("(") + val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine + element.parameters.renderSeparated(newLinesNeeded) + print(")") + } + + override fun visit(element: CgPythonAssertEquals) { + print("${element.keyword} ") + element.expression.accept(this) + println() + } + + override fun visit(element: CgPythonRange) { + print("range(") + listOf(element.start, element.stop, element.step).renderSeparated() + print(")") + } + + override fun visit(element: CgPythonList) { + print("[") + element.elements.renderSeparated() + print("]") + } + + override fun visit(element: CgPythonTuple) { + print("(") + element.elements.renderSeparated() + if (element.elements.size == 1) { + print(",") + } + print(")") + } + + override fun visit(element: CgPythonSet) { + if (element.elements.isEmpty()) + print("set()") + else { + print("{") + element.elements.toList().renderSeparated() + print("}") + } + } + + override fun visit(element: CgPythonDict) { + print("{") + element.elements.map { (key, value) -> + key.accept(this) + print(": ") + value.accept(this) + print(", ") + } + print("}") + } + + override fun visit(element: CgForEachLoop) { + print("for ") + element.condition.accept(this) + print(" in ") + element.iterable.accept(this) + println(":") + withIndent { element.statements.forEach { it.accept(this) } } + } + + override fun visit(element: CgLiteral) { + print(element.value.toString()) + } + + override fun String.escapeCharacters(): String = + StringEscapeUtils + .escapeJava(this) + .replace("'", "\\'") + .replace("\\f", "\\u000C") + .replace("\\xxx", "\\\u0058\u0058\u0058") +} \ No newline at end of file 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 new file mode 100644 index 0000000000..ffc02d4f96 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt @@ -0,0 +1,18 @@ +package org.utbot.python.framework.codegen.model.constructor.visitor + +import org.utbot.framework.codegen.model.visitor.CgVisitor +import org.utbot.python.framework.codegen.model.tree.* + +interface CgPythonVisitor : CgVisitor { + + fun visit(element: CgPythonRepr): R + fun visit(element: CgPythonIndex): R + fun visit(element: CgPythonAssertEquals): R + fun visit(element: CgPythonFunctionCall): R + fun visit(element: CgPythonRange): R + fun visit(element: CgPythonDict): R + fun visit(element: CgPythonTuple): R + fun visit(element: CgPythonList): R + fun visit(element: CgPythonSet): R + +} 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 new file mode 100644 index 0000000000..faafb23e9e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt @@ -0,0 +1,102 @@ +package org.utbot.python.framework.codegen.model.tree + +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.visitor.CgVisitor +import org.utbot.framework.plugin.api.ClassId +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonTree +import org.utbot.python.framework.api.python.util.* +import org.utbot.python.framework.codegen.model.constructor.visitor.CgPythonVisitor + +interface CgPythonElement : CgElement { + override fun accept(visitor: CgVisitor): R = visitor.run { + if (visitor is CgPythonVisitor) { + when (val element = this@CgPythonElement) { + is CgPythonRepr -> visitor.visit(element) + is CgPythonIndex -> visitor.visit(element) + is CgPythonAssertEquals -> visitor.visit(element) + is CgPythonFunctionCall -> visitor.visit(element) + is CgPythonRange -> visitor.visit(element) + is CgPythonList -> visitor.visit(element) + is CgPythonSet -> visitor.visit(element) + is CgPythonDict -> visitor.visit(element) + is CgPythonTuple -> visitor.visit(element) + else -> throw IllegalArgumentException("Can not visit element of type ${element::class}") + } + } else { + super.accept(visitor) + } + } +} + +class CgPythonTree( + override val type: ClassId, + val tree: PythonTree.PythonTreeNode +) : CgValue, CgPythonElement + +class CgPythonRepr( + override val type: ClassId, + val content: String +) : CgValue, CgPythonElement + +class CgPythonAssertEquals( + val expression: CgExpression, + val keyword: String = "assert", +) : CgStatement, CgPythonElement + +class CgPythonFunctionCall( + override val type: PythonClassId, + val name: String, + val parameters: List, +) : CgExpression, CgPythonElement + +class CgPythonIndex( + override val type: PythonClassId, + val obj: CgVariable, + val index: CgExpression, +) : CgValue, CgPythonElement + +class CgPythonRange( + val start: CgValue, + val stop: CgValue, + val step: CgValue, +) : CgValue, CgPythonElement { + override val type: PythonClassId + get() = pythonRangeClassId + + constructor(stop: Int) : this( + CgLiteral(pythonIntClassId, 0), + CgLiteral(pythonIntClassId, stop), + CgLiteral(pythonIntClassId, 1), + ) + + constructor(stop: CgValue) : this( + CgLiteral(pythonIntClassId, 0), + stop, + CgLiteral(pythonIntClassId, 1), + ) +} + +class CgPythonList( + val elements: List +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonListClassId +} + +class CgPythonTuple( + val elements: List +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonTupleClassId +} + +class CgPythonSet( + val elements: Set +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonSetClassId +} + +class CgPythonDict( + val elements: Map +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonDictClassId +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/ConstantModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/ConstantModelProvider.kt new file mode 100644 index 0000000000..1307fe7994 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/ConstantModelProvider.kt @@ -0,0 +1,49 @@ +package org.utbot.python.providers + +import org.utbot.fuzzer.FuzzedContext +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.PythonPrimitiveModel +import java.math.BigDecimal +import java.math.BigInteger + +class ConstantModelProvider(recursionDepth: Int) : PythonModelProvider(recursionDepth) { + + override fun generate(description: PythonFuzzedMethodDescription) = sequence { + description.concreteValues + .asSequence() + .flatMap { (classId, value, op) -> + val model = (classId as? PythonClassId)?.let { PythonPrimitiveModel(value, it) } + sequenceOf( + model?.fuzzed { summary = "%var% = $value" }, + modifyValue(model, op) + ) + } + .filterNotNull() + .forEach { value -> + description.parametersMap.getOrElse(value.model.classId) { emptyList() }.forEach { index -> + yieldValue(index, value) + } + } + } + + private fun modifyValue(model: PythonPrimitiveModel?, op: FuzzedContext): FuzzedValue? { + if (op !is FuzzedContext.Comparison || model == null) return null + val multiplier = if (op == FuzzedContext.Comparison.LT || op == FuzzedContext.Comparison.GE) -1 else 1 + + return when (val value = model.value) { + is BigInteger -> value + multiplier.toBigInteger() + is BigDecimal -> value + multiplier.toBigDecimal() + else -> null + }?.let { + PythonPrimitiveModel(it, model.classId as PythonClassId).fuzzed { + summary = "%var% ${ + (if (op == FuzzedContext.Comparison.EQ || op == FuzzedContext.Comparison.LE || op == FuzzedContext.Comparison.GE) { + op.reverse() + } else op).sign + } ${model.value}" + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/DefaultValuesModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/DefaultValuesModelProvider.kt new file mode 100644 index 0000000000..c3d31f60e7 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/DefaultValuesModelProvider.kt @@ -0,0 +1,28 @@ +package org.utbot.python.providers + +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.python.framework.api.python.PythonDefaultModel +import org.utbot.python.typing.PythonTypesStorage + +const val MAX_DEEP = 10 + +class DefaultValuesModelProvider(recursionDepth: Int) : PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription) = sequence { + val generated = Array(description.parameters.size) { 0 } + description.parametersMap.forEach { (classId, parameterIndices) -> + val pythonClassIdInfo = PythonTypesStorage.findPythonClassIdInfoByName(classId.name) ?: return@forEach + pythonClassIdInfo.preprocessedInstances?.forEach { instance -> + parameterIndices.forEach { index -> + generated[index] += 1 + if (generated[index] < MAX_DEEP) + yield( + FuzzedParameter( + index, + PythonDefaultModel(instance, pythonClassIdInfo.pythonClassId).fuzzed() + ) + ) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/GeneralPythonModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/GeneralPythonModelProvider.kt new file mode 100644 index 0000000000..24e3c0bca8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/GeneralPythonModelProvider.kt @@ -0,0 +1,54 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.ModelProvider +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.util.pythonAnyClassId + +val defaultPythonModelProvider = getDefaultPythonModelProvider(recursionDepth = 4) + +fun getDefaultPythonModelProvider(recursionDepth: Int): ModelProvider = + ModelProvider.of( + ConstantModelProvider(recursionDepth), + DefaultValuesModelProvider(recursionDepth), + GenericModelProvider(recursionDepth), + UnionModelProvider(recursionDepth), + OptionalModelProvider(recursionDepth), + InitModelProvider(recursionDepth) + ) + +abstract class PythonModelProvider(protected val recursionDepth: Int) : ModelProvider { + override fun generate(description: FuzzedMethodDescription): Sequence = + generate( + PythonFuzzedMethodDescription( + description.name, + description.returnType, + description.parameters.map { (it as? NormalizedPythonAnnotation) ?: pythonAnyClassId }, + description.concreteValues + ) + ) + + abstract fun generate(description: PythonFuzzedMethodDescription): Sequence +} + +class PythonFuzzedMethodDescription( + name: String, + returnType: ClassId, + parameters: List, + concreteValues: Collection = emptyList() +) : FuzzedMethodDescription(name, returnType, parameters, concreteValues) + +fun substituteTypesByIndex( + description: PythonFuzzedMethodDescription, + newTypes: List +): PythonFuzzedMethodDescription { + return PythonFuzzedMethodDescription( + description.name, + description.returnType, + newTypes, + description.concreteValues + ) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/GenericModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/GenericModelProvider.kt new file mode 100644 index 0000000000..f47a8676de --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/GenericModelProvider.kt @@ -0,0 +1,107 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.fuzz +import org.utbot.python.framework.api.python.* +import org.utbot.python.framework.api.python.util.pythonNoneClassId +import org.utbot.python.typing.DictAnnotation +import org.utbot.python.typing.ListAnnotation +import org.utbot.python.typing.SetAnnotation +import org.utbot.python.typing.parseGeneric +import java.lang.Integer.min +import kotlin.random.Random + +const val MAX_CONTAINER_SIZE = 7 + +class GenericModelProvider(recursionDepth: Int) : PythonModelProvider(recursionDepth) { + private val maxGenNum = 10 + + override fun generate(description: PythonFuzzedMethodDescription): Sequence = sequence { + fun fuzzGeneric( + parameters: List, + index: Int, + modelConstructor: (List>) -> T? + ) = sequence inner@{ + if (recursionDepth <= 0) + return@inner + + val syntheticGenericType = FuzzedMethodDescription( + "${description.name}", + pythonNoneClassId, + parameters, + description.concreteValues + ) + + fuzz(syntheticGenericType, getDefaultPythonModelProvider(recursionDepth - 1)) + .randomChunked() + .mapNotNull(modelConstructor) + .forEach { + yield(FuzzedParameter(index, it.fuzzed())) + } + } + + fun genList(listAnnotation: ListAnnotation, index: Int): Sequence { + return fuzzGeneric(listOf(listAnnotation.elemAnnotation), index) { list -> + PythonListModel( + list.size, + list.flatten().mapNotNull { it.model as? PythonModel } + ) + } + } + + fun genDict(dictAnnotation: DictAnnotation, index: Int): Sequence { + return fuzzGeneric(listOf(dictAnnotation.keyAnnotation, dictAnnotation.valueAnnotation), index) { list -> + if (list.any { it.any { value -> value.model !is PythonModel } }) + return@fuzzGeneric null + PythonDictModel( + list.size, + list.associate { pair -> + (pair[0].model as PythonModel) to (pair[1].model as PythonModel) + } + ) + } + } + + fun genSet(setAnnotation: SetAnnotation, index: Int): Sequence { + return fuzzGeneric(listOf(setAnnotation.elemAnnotation), index) { list -> + PythonSetModel( + list.size, + list.flatten().mapNotNull { it.model as? PythonModel }.toSet(), + ) + } + } + + description.parametersMap.forEach { (classId, parameterIndices) -> + val parsedAnnotation = parseGeneric(classId as NormalizedPythonAnnotation) ?: return@forEach + parameterIndices.forEach { index -> + val generatedModels = when (parsedAnnotation) { + is ListAnnotation -> genList(parsedAnnotation, index) + is DictAnnotation -> genDict(parsedAnnotation, index) + is SetAnnotation -> genSet(parsedAnnotation, index) + } + yieldAll( + generatedModels.take(maxGenNum).distinctBy { fuzzedParameter -> + fuzzedParameter.value.model.toString() + } + ) + } + } + } +} + +fun Sequence>.randomChunked(): Sequence>> { + val seq = this + val itemsToGenerateFrom = seq.take(MAX_CONTAINER_SIZE * 2).toList() + return sequenceOf(emptyList>()) + generateSequence { + if (itemsToGenerateFrom.isEmpty()) + return@generateSequence null + val size = Random.nextInt(1, min(MAX_CONTAINER_SIZE, itemsToGenerateFrom.size) + 1) + (0 until size).map { + val index = Random.nextInt(0, itemsToGenerateFrom.size) + itemsToGenerateFrom[index] + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/InitModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/InitModelProvider.kt new file mode 100644 index 0000000000..4e3abb75cb --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/InitModelProvider.kt @@ -0,0 +1,43 @@ +package org.utbot.python.providers + +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.fuzz +import org.utbot.python.framework.api.python.PythonInitObjectModel +import org.utbot.python.framework.api.python.PythonModel +import org.utbot.python.typing.PythonTypesStorage + +class InitModelProvider(recursionDepth: Int) : PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription) = sequence { + if (recursionDepth <= 0) + return@sequence + + description.parametersMap.forEach { (classId, parameterIndices) -> + val type = PythonTypesStorage.findPythonClassIdInfoByName(classId.name) ?: return@forEach + val initSignature = type.initSignature ?: return@forEach + + val models: Sequence = + if (initSignature.isEmpty()) + sequenceOf(PythonInitObjectModel(classId.name, emptyList())) + else { + val constructor = FuzzedMethodDescription( + type.pythonClassId.name, + classId, + initSignature, + description.concreteValues + ) + + val modelProvider = getDefaultPythonModelProvider(recursionDepth - 1) + fuzz(constructor, modelProvider).map { initValues -> + PythonInitObjectModel(classId.name, initValues.mapNotNull { it.model as? PythonModel }) + } + } + + parameterIndices.forEach { index -> + models.forEach { model -> + yield(FuzzedParameter(index, model.fuzzed())) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/OptionalModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/OptionalModelProvider.kt new file mode 100644 index 0000000000..de32148ceb --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/OptionalModelProvider.kt @@ -0,0 +1,35 @@ +package org.utbot.python.providers + +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.framework.api.python.util.pythonNoneClassId + +class OptionalModelProvider(recursionDepth: Int) : PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription): Sequence { + var result = emptySequence() + description.parametersMap.forEach { (classId, parameterIndices) -> + val regex = Regex("typing.Optional\\[(.*)]") + val annotation = classId.name + val match = regex.matchEntire(annotation) ?: return@forEach + parameterIndices.forEach { index -> + val descriptionWithNoneType = substituteTypesByIndex( + description, + (0 until description.parameters.size).map { + if (it == index) NormalizedPythonAnnotation(pythonNoneClassId.name) else pythonAnyClassId + } + ) + val modelProvider = getDefaultPythonModelProvider(recursionDepth) + result += modelProvider.generate(descriptionWithNoneType) + val descriptionWithNonNoneType = substituteTypesByIndex( + description, + (0 until description.parameters.size).map { + if (it == index) NormalizedPythonAnnotation(match.groupValues[1]) else pythonAnyClassId + } + ) + result += modelProvider.generate(descriptionWithNonNoneType) + } + } + return result + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/UnionModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/UnionModelProvider.kt new file mode 100644 index 0000000000..dc964a100e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/UnionModelProvider.kt @@ -0,0 +1,28 @@ +package org.utbot.python.providers + +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.util.pythonAnyClassId + +class UnionModelProvider(recursionDepth: Int) : PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription): Sequence { + var result = emptySequence() + description.parametersMap.forEach { (classId, parameterIndices) -> + val regex = Regex("typing.Union\\[(.*), *(.*)]") + val annotation = classId.name + val match = regex.matchEntire(annotation) ?: return@forEach + parameterIndices.forEach { index -> + for (newAnnotation in listOf(match.groupValues[1], match.groupValues[2])) { + val newDescription = substituteTypesByIndex( + description, + (0 until description.parameters.size).map { + if (it == index) NormalizedPythonAnnotation(newAnnotation) else pythonAnyClassId + } + ) + result += getDefaultPythonModelProvider(recursionDepth).generate(newDescription) + } + } + } + return result + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/FindAnnotations.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/FindAnnotations.kt new file mode 100644 index 0000000000..8eca0ddbf8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/FindAnnotations.kt @@ -0,0 +1,210 @@ +package org.utbot.python.typing + +import mu.KotlinLogging +import org.utbot.python.PythonMethod +import org.utbot.python.code.ArgInfoCollector +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import org.utbot.python.utils.AnnotationNormalizer +import org.utbot.python.utils.PriorityCartesianProduct + +private val logger = KotlinLogging.logger {} + +object AnnotationFinder { + + private const val MAX_CANDIDATES_FOR_PARAM = 100 + + private const val EPS = 1e-6 + + fun findAnnotations( + argInfoCollector: ArgInfoCollector, + methodUnderTest: PythonMethod, + existingAnnotations: Map, + moduleToImport: String, + directoriesForSysPath: Set, + pythonPath: String, + isCancelled: () -> Boolean, + storageForMypyMessages: MutableList + ): Sequence> { + + logger.debug("Finding candidates...") + val annotationsToCheck = findTypeCandidates(argInfoCollector, existingAnnotations) + logger.debug("Found") + + return MypyAnnotations.getCheckedByMypyAnnotations( + methodUnderTest, + annotationsToCheck, + moduleToImport, + directoriesForSysPath, + pythonPath, + isCancelled, + storageForMypyMessages + ) + } + + private fun increaseValue( + map: MutableMap, + key: NormalizedPythonAnnotation, + by: Double + ) { + if (key == pythonAnyClassId) + return + map[key] = (map[key] ?: 0.0) + by + } + + private fun getInitCandidateMap(): MutableMap { + val candidates = mutableMapOf() // key: type, value: priority + PythonTypesStorage.builtinTypes.associateByTo( + destination = candidates, + { AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(PythonClassId("builtins.$it")) }, + { 0.0 } + ) + return candidates + } + + private fun candidatesMapToRating(candidates: Map) = + candidates.toList().sortedByDescending { it.second }.map { it.first } + + private fun increaseForProjectClasses(candidates: MutableMap) { + candidates.keys.forEach { typeName -> + if (PythonTypesStorage.isClassFromProject(typeName)) + increaseValue(candidates, typeName, EPS) + } + } + + private fun increaseForGenerics(candidates: MutableMap) { + candidates.keys.forEach { typeName -> + if (isGeneric(typeName)) + increaseValue(candidates, typeName, EPS) + } + } + + private fun calcAdd(foundCandidates: Int): Double = + if (foundCandidates == 0) 0.0 else 1.0 / foundCandidates + + private fun getFirstLevelCandidates( + hints: List? + ): List { + val candidates = getInitCandidateMap() + hints?.forEach { hint -> + var isIter = false + val foundCandidates: Set = + when (hint) { + is ArgInfoCollector.Type -> + setOf(AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(hint.type)) + + is ArgInfoCollector.Method -> { + if (hint.name == "__iter__") + isIter = true + PythonTypesStorage.findTypeWithMethod(hint.name) + } + + is ArgInfoCollector.Field -> PythonTypesStorage.findTypeWithField(hint.name) + is ArgInfoCollector.FunctionArg -> { + PythonTypesStorage.findTypeByFunctionWithArgumentPosition( + hint.name, + argumentPosition = hint.index + ) + } + + is ArgInfoCollector.FunctionRet -> PythonTypesStorage.findTypeByFunctionReturnValue(hint.name) + else -> emptySet() + } + val add = calcAdd(foundCandidates.size) + foundCandidates.forEach { increaseValue(candidates, it, add) } + if (isIter) + increaseForGenerics(candidates) + } + increaseForProjectClasses(candidates) + return candidatesMapToRating(candidates).take(MAX_CANDIDATES_FOR_PARAM) + } + + private fun getGeneralTypeRating( + argInfoCollector: ArgInfoCollector + ): List { + val candidates = getInitCandidateMap() + argInfoCollector.getAllGeneralHints().map { hint -> + val foundCandidates: Set = + when (hint) { + is ArgInfoCollector.Type -> + setOf(AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(hint.type)) + + is ArgInfoCollector.Function -> + listOf( + PythonTypesStorage.findTypeByFunctionReturnValue(hint.name), + PythonTypesStorage.findTypeByFunctionWithArgumentPosition(hint.name) + ).flatten().toSet() + + is ArgInfoCollector.Method -> PythonTypesStorage.findTypeWithMethod(hint.name) + is ArgInfoCollector.Field -> PythonTypesStorage.findTypeWithField(hint.name) + else -> emptySet() + } + val add = calcAdd(foundCandidates.size) + foundCandidates.forEach { increaseValue(candidates, it, add) } + } + increaseForProjectClasses(candidates) + return candidatesMapToRating(candidates).take(MAX_CANDIDATES_FOR_PARAM / 2) + } + + private fun getArgCandidates( + generalTypeRating: List, + argStorages: List = emptyList(), + userAnnotation: NormalizedPythonAnnotation? = null + ): List { + val root = + if (userAnnotation != null) + sequenceOf(userAnnotation).iterator() + else + getFirstLevelCandidates(argStorages).asSequence().iterator() + + val bfsQueue = ArrayDeque(listOf(root)) + val result = mutableListOf() + while (result.size < MAX_CANDIDATES_FOR_PARAM && bfsQueue.isNotEmpty()) { + val curIter = bfsQueue.removeFirst() + if (!curIter.hasNext()) + continue + + val value = curIter.next() + result.add(value) + bfsQueue.addLast(curIter) + + val asGeneric = parseGeneric(value) + if (asGeneric == null || !asGeneric.args.any { it == pythonAnyClassId }) + continue + + val argCandidates = asGeneric.args.map { + if (it == pythonAnyClassId) + generalTypeRating + else + listOf(it) + } + val toAnnotation = + when (asGeneric) { + is ListAnnotation -> ListAnnotation::pack + is DictAnnotation -> DictAnnotation::pack + is SetAnnotation -> SetAnnotation::pack + } + val nextGenericCandidates = PriorityCartesianProduct(argCandidates).getSequence().map { + NormalizedPythonAnnotation(toAnnotation(it).toString()) + } + bfsQueue.addFirst(nextGenericCandidates.iterator()) + } + return result + } + + private fun findTypeCandidates( + argInfoCollector: ArgInfoCollector, + existingAnnotations: Map + ): Map> { + val storageMap = argInfoCollector.getAllArgHints() + val generalTypeRating = getGeneralTypeRating(argInfoCollector) + val userAnnotations = existingAnnotations.entries.associate { + it.key to getArgCandidates(generalTypeRating, userAnnotation = it.value) + } + val annotationCombinations = storageMap.entries.associate { (name, storages) -> + name to getArgCandidates(generalTypeRating, storages) + } + return userAnnotations + annotationCombinations + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/GenericAnnotations.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/GenericAnnotations.kt new file mode 100644 index 0000000000..bd90c09231 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/GenericAnnotations.kt @@ -0,0 +1,89 @@ +package org.utbot.python.typing + +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation + +fun parseGeneric(annotation: NormalizedPythonAnnotation): GenericAnnotation? = + ListAnnotation.parse(annotation) + ?: DictAnnotation.parse(annotation) + ?: SetAnnotation.parse(annotation) + + +fun isGeneric(annotation: NormalizedPythonAnnotation): Boolean = parseGeneric(annotation) != null + +sealed class GenericAnnotation { + abstract val args: List + + companion object { + fun getFromMatch(match: MatchResult, index: Int): NormalizedPythonAnnotation { + return NormalizedPythonAnnotation(match.groupValues[index]) + } + } +} + +class ListAnnotation( + val elemAnnotation: NormalizedPythonAnnotation +) : GenericAnnotation() { + + override val args: List + get() = listOf(elemAnnotation) + + override fun toString(): String = "typing.List[$elemAnnotation]" + + companion object { + private val regex = Regex("typing.List\\[(.*)]") + + fun parse(annotation: NormalizedPythonAnnotation): ListAnnotation? { + val res = regex.matchEntire(annotation.name) + return res?.let { + ListAnnotation(getFromMatch(it, 1)) + } + } + + fun pack(args: List) = ListAnnotation(args[0]) + } +} + +class DictAnnotation( + val keyAnnotation: NormalizedPythonAnnotation, + val valueAnnotation: NormalizedPythonAnnotation +) : GenericAnnotation() { + + override val args: List + get() = listOf(keyAnnotation, valueAnnotation) + + override fun toString(): String = "typing.Dict[$keyAnnotation, $valueAnnotation]" + + companion object { + private val regex = Regex("typing.Dict\\[(.*), *(.*)]") + + fun parse(annotation: NormalizedPythonAnnotation): DictAnnotation? { + val res = regex.matchEntire(annotation.name) + return res?.let { + DictAnnotation(getFromMatch(it, 1), getFromMatch(it, 2)) + } + } + + fun pack(args: List) = DictAnnotation(args[0], args[1]) + } +} + +class SetAnnotation( + val elemAnnotation: NormalizedPythonAnnotation +) : GenericAnnotation() { + + override val args: List + get() = listOf(elemAnnotation) + + override fun toString(): String = "typing.Set[$elemAnnotation]" + + companion object { + private val regex = Regex("typing.Set\\[(.*)]") + + fun parse(annotation: NormalizedPythonAnnotation): SetAnnotation? { + val res = regex.matchEntire(annotation.name) + return res?.let { SetAnnotation(getFromMatch(it, 1)) } + } + + fun pack(args: List) = SetAnnotation(args[0]) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..50ea2ba43e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt @@ -0,0 +1,151 @@ +package org.utbot.python.typing + +import mu.KotlinLogging +import org.utbot.python.PythonMethod +import org.utbot.python.code.PythonCodeGenerator.generateMypyCheckCode +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.utils.* +import java.io.File + +private val logger = KotlinLogging.logger {} + +object MypyAnnotations { + const val TEMPORARY_MYPY_FILE = "" + + private const val configFilename = "mypy.ini" + + data class MypyReportLine( + val line: Int, + val type: String, + val message: String, + val file: String + ) + + fun getCheckedByMypyAnnotations( + method: PythonMethod, + functionArgAnnotations: Map>, + moduleToImport: String, + directoriesForSysPath: Set, + pythonPath: String, + isCancelled: () -> Boolean, + storageForMypyMessages: MutableList? = null + ) = sequence { + val fileWithCode = TemporaryFileManager.assignTemporaryFile(tag = "mypy.py") + val codeWithoutAnnotations = generateMypyCheckCode( + method, + emptyMap(), + directoriesForSysPath, + moduleToImport + ) + + TemporaryFileManager.writeToAssignedFile(fileWithCode, codeWithoutAnnotations) + val configFile = setConfigFile(directoriesForSysPath) + Cleaner.addFunction { stopMypy(pythonPath) } + + logger.debug("First mypy run") + val defaultOutputAsString = mypyCheck(pythonPath, fileWithCode, configFile) + val defaultErrorsAndNotes = getErrorsAndNotes(defaultOutputAsString, codeWithoutAnnotations, fileWithCode) + + if (storageForMypyMessages != null) { + defaultErrorsAndNotes.forEach { storageForMypyMessages.add(it) } + } + + val defaultErrorNum = getErrorNumber(defaultErrorsAndNotes) + + val candidates = functionArgAnnotations.entries.map { (key, value) -> + value.map { + Pair(key, it) + } + } + if (candidates.any { it.isEmpty() }) { + return@sequence + } + + PriorityCartesianProduct(candidates).getSequence().forEach { generatedAnnotations -> + if (isCancelled()) { + return@sequence + } + + logger.debug("Checking annotations: ${ + generatedAnnotations.joinToString { "${it.first}: ${it.second}" } + }") + + val annotationMap = generatedAnnotations.toMap() + val codeWithAnnotations = generateMypyCheckCode( + method, + annotationMap, + directoriesForSysPath, + moduleToImport + ) + TemporaryFileManager.writeToAssignedFile(fileWithCode, codeWithAnnotations) + + val mypyOutputAsString = mypyCheck(pythonPath, fileWithCode, configFile) + val mypyOutput = getErrorsAndNotes(mypyOutputAsString, codeWithAnnotations, fileWithCode) + val errorNum = getErrorNumber(mypyOutput) + + if (errorNum <= defaultErrorNum) { + yield(annotationMap.mapValues { entry -> + entry.value + }) + } + } + } + + private fun setConfigFile(directoriesForSysPath: Set): File { + val file = TemporaryFileManager.assignTemporaryFile(configFilename) + val configContent = """ + [mypy] + mypy_path = ${directoriesForSysPath.joinToString(separator = ":")} + namespace_packages = True + explicit_package_bases = True + show_absolute_path = True + """.trimIndent() + TemporaryFileManager.writeToAssignedFile(file, configContent) + return file + } + + private fun stopMypy(pythonPath: String): Int { + val result = runCommand( + listOf( + pythonPath, + "-m", + "mypy.dmypy", + "stop" + ) + ) + return result.exitValue + } + + private fun mypyCheck(pythonPath: String, fileWithCode: File, configFile: File): String { + val result = runCommand( + listOf( + pythonPath, + "-m", + "mypy.dmypy", + "run", + "--", + fileWithCode.path, + "--config-file", + configFile.path + ) + ) + return result.stdout + } + + private fun getErrorNumber(mypyReport: List) = + mypyReport.count { it.type == "error" && it.file == TEMPORARY_MYPY_FILE } + + private fun getErrorsAndNotes(mypyOutput: String, mypyCode: String, fileWithCode: File): List { + val regex = Regex("(?m)^([^\n]*):([0-9]*): (error|note): ([^\n]*)\n") + return regex.findAll(mypyOutput).toList().map { match -> + val file = match.groupValues[1] + MypyReportLine( + match.groupValues[2].toInt() - getLineOfFunction(mypyCode)!!, + match.groupValues[3], + match.groupValues[4], + if (file == fileWithCode.path) TEMPORARY_MYPY_FILE else file + ) + } + } +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/PythonTypeCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/PythonTypeCollector.kt new file mode 100644 index 0000000000..e726d8fbc4 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/PythonTypeCollector.kt @@ -0,0 +1,232 @@ +package org.utbot.python.typing + +import com.beust.klaxon.Klaxon +import mu.KotlinLogging +import org.apache.commons.io.FileUtils +import org.apache.commons.io.IOUtils +import org.apache.commons.io.filefilter.FileFilterUtils +import org.apache.commons.io.filefilter.NameFileFilter +import org.utbot.python.code.ClassInfoCollector +import org.utbot.python.code.PythonClass +import org.utbot.python.code.PythonCode +import org.utbot.python.code.PythonModule +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.utils.AnnotationNormalizer +import org.utbot.python.utils.AnnotationNormalizer.annotationFromProjectToClassId +import org.utbot.python.utils.checkIfFileLiesInPath +import org.utbot.python.utils.getModuleNameWithoutCheck +import java.io.File +import java.io.FileInputStream +import java.nio.charset.StandardCharsets + +private val logger = KotlinLogging.logger {} + +class PythonClassIdInfo( + val pythonClassId: PythonClassId, + val initSignature: List?, + val preprocessedInstances: List?, + val methods: Set, + val fields: Set +) + +object PythonTypesStorage { + private const val PYTHON_NOT_SPECIFIED = "PythonPath in PythonTypeCollector not specified" + private var projectClasses: List = emptyList() + private var projectModules: List = emptyList() + var pythonPath: String? = null + + fun findTypeWithMethod( + methodName: String + ): Set { + val fromStubs = StubFileFinder.findTypeWithMethod(methodName).map { + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it) + } + val fromProject = projectClasses.mapNotNull { + if (it.info.methods.contains(methodName)) + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it.name) + else + null + } + return (fromStubs union fromProject).toSet() + } + + fun findTypeWithField( + fieldName: String + ): Set { + val fromStubs = StubFileFinder.findTypeWithField(fieldName).map { + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it) + } + val fromProject = projectClasses.mapNotNull { + if (it.info.fields.contains(fieldName)) + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it.name) + else + null + } + return (fromStubs union fromProject).toSet() + } + + fun findTypeByFunctionWithArgumentPosition( + functionName: String, + argumentName: String? = null, + argumentPosition: Int? = null, + ): Set = + StubFileFinder.findAnnotationByFunctionWithArgumentPosition(functionName, argumentName, argumentPosition) + + fun findTypeByFunctionReturnValue(functionName: String): Set = + StubFileFinder.findAnnotationByFunctionReturnValue(functionName).toSet() + + fun isClassFromProject(typeName: NormalizedPythonAnnotation): Boolean { + return projectClasses.any { it.name.name == typeName.name } + } + + fun findPythonClassIdInfoByName(classIdName: String): PythonClassIdInfo? { + val fromStub = StubFileFinder.nameToClassMap[classIdName] + val result = + if (fromStub != null) { + val fromPreprocessed = TypesFromJSONStorage.typeNameMap[classIdName] + val classId = PythonClassId(fromStub.className) + return PythonClassIdInfo( + classId, + fromStub.methods.find { it.name == "__init__" } + ?.args + ?.drop(1) // drop 'self' parameter + ?.map { NormalizedPythonAnnotation(it.annotation) }, + fromPreprocessed?.instances, + fromStub.methods.map { it.name }.toSet(), + fromStub.fields.map { it.name }.toSet() + ) + } else { + projectClasses.find { it.name.name == classIdName }?.let { projectClass -> + PythonClassIdInfo( + projectClass.name, + projectClass.initAnnotation, + null, + projectClass.info.methods, + projectClass.info.fields + ) + } + } + + return result + } + + val builtinTypes: List + get() = TypesFromJSONStorage.preprocessedTypes.mapNotNull { + if (it.name.startsWith("builtins.")) it.name.removePrefix("builtins.") else null + } + + data class ProjectClass( + val pythonClass: PythonClass, + val info: ClassInfoCollector.Storage, + val initAnnotation: List?, + val name: PythonClassId + ) + + private fun getPythonFiles(directory: File): Collection = + FileUtils.listFiles( + directory, + /* fileFilter = */ FileFilterUtils.and( + FileFilterUtils.suffixFileFilter(".py"), + FileFilterUtils.notFileFilter( + FileFilterUtils.prefixFileFilter("test") + ), + ), + /* dirFilter = */ FileFilterUtils.and( + FileFilterUtils.notFileFilter( + NameFileFilter("test") + ), + FileFilterUtils.notFileFilter( + FileFilterUtils.suffixFileFilter("venv") + ), + FileFilterUtils.notFileFilter( + FileFilterUtils.prefixFileFilter(".") + ) + ) + ) + + fun refreshProjectClassesAndModulesLists( + directoriesForSysPath: Set, + onlyFromSpecifiedFile: File? = null + ) { + val projectClassesSet = mutableSetOf() + val projectModulesSet = mutableSetOf(PythonModule("builtins")) + + val filesToVisit = directoriesForSysPath.flatMap { path -> + if (onlyFromSpecifiedFile != null && !checkIfFileLiesInPath(path, onlyFromSpecifiedFile.path)) + return@flatMap emptyList() + + val pathFile = File(path) + if (onlyFromSpecifiedFile != null) + return@flatMap listOf( + Pair(getModuleNameWithoutCheck(pathFile, onlyFromSpecifiedFile), onlyFromSpecifiedFile) + ) + + getPythonFiles(pathFile).map { Pair(getModuleNameWithoutCheck(pathFile, it), it) } + }.distinctBy { it.second } + + filesToVisit.forEach { (module, file) -> + val content = IOUtils.toString(FileInputStream(file), StandardCharsets.UTF_8) + val code = PythonCode.getFromString(content, file.path) ?: return@forEach + projectClassesSet += code.getToplevelClasses().map { pyClass -> + val collector = ClassInfoCollector(pyClass) + val initSignature = pyClass.initSignature + ?.map { + annotationFromProjectToClassId( + it.annotation, + pythonPath ?: error(PYTHON_NOT_SPECIFIED), + module, + pyClass.filename!!, + directoriesForSysPath + ) + } + val fullClassName = module + "." + pyClass.name + ProjectClass(pyClass, collector.storage, initSignature, PythonClassId(fullClassName)) + } + projectModulesSet += code.getToplevelModules() + } + projectClasses = projectClassesSet.toList() + + val newModules = projectModulesSet - projectModules.toSet() + + logger.debug("Updating info from stub files") + + updateStubFiles(newModules.map { it.name }.toList()) + projectModules = projectModulesSet.toList() + } + + private fun updateStubFiles(newModules: List) { + if (newModules.isNotEmpty()) { + val jsonData = StubFileReader.getStubInfo( + newModules, + pythonPath ?: error(PYTHON_NOT_SPECIFIED), + ) + StubFileFinder.updateStubs(jsonData) + } + } + + + private data class PreprocessedValueFromJSON( + val name: String, + val instances: List + ) + + private object TypesFromJSONStorage { + val preprocessedTypes: List + + init { + val typesAsString = PythonTypesStorage::class.java.getResource("/preprocessed_values.json") + ?.readText(Charsets.UTF_8) + ?: error("Didn't find preprocessed_values.json") + preprocessedTypes = Klaxon().parseArray(typesAsString) ?: emptyList() + } + + val typeNameMap: Map by lazy { + val result = mutableMapOf() + preprocessedTypes.forEach { type -> + result[type.name] = type + } + result + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileFinder.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileFinder.kt new file mode 100644 index 0000000000..3e44fb3580 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileFinder.kt @@ -0,0 +1,126 @@ +package org.utbot.python.typing + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonClassId + +object StubFileFinder { + private val methodToTypeMap: MutableMap> = mutableMapOf() + private val functionToTypeMap: MutableMap> = mutableMapOf() + private val fieldToTypeMap: MutableMap> = mutableMapOf() + val nameToClassMap: MutableMap = mutableMapOf() + + private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + private val jsonAdapter = moshi.adapter(StubFileStructures.JsonData::class.java) + + private fun parseJson(json: String): StubFileStructures.JsonData? { + return if (json.isNotEmpty()) { + jsonAdapter.fromJson(json) + } else { + null + } + } + + fun updateStubs( + json: String + ) { + val jsonData = parseJson(json) + if (jsonData != null) { + jsonData.normalizeAnnotations() + updateMethods(jsonData.methodAnnotations) + updateFields(jsonData.fieldAnnotations) + updateFunctions(jsonData.functionAnnotations) + updateClasses(jsonData.classAnnotations) + } + } + + private fun updateMethods(newMethods: List) { + newMethods.forEach { function -> + if (!methodToTypeMap.containsKey(function.name)) + methodToTypeMap[function.name] = function.definitions.toMutableSet() + else + methodToTypeMap[function.name]?.addAll(function.definitions) + } + } + + private fun updateFunctions(newFunctions: List) { + newFunctions.forEach { function -> + if (!functionToTypeMap.containsKey(function.name)) + functionToTypeMap[function.name] = function.definitions.toMutableSet() + else + functionToTypeMap[function.name]?.addAll(function.definitions) + } + } + + private fun updateFields(newFields: List) { + newFields.forEach { field -> + if (!fieldToTypeMap.containsKey(field.name)) + fieldToTypeMap[field.name] = field.definitions.toMutableSet() + else + fieldToTypeMap[field.name]?.addAll(field.definitions) + } + } + + private fun updateClasses(newClasses: List) { + newClasses.forEach { pyClass -> + nameToClassMap[pyClass.className] = pyClass + } + } + + fun findTypeWithMethod( + methodName: String + ): Set { + return (methodToTypeMap[methodName] ?: emptyList()).mapNotNull { methodInfo -> + methodInfo.className?.let { PythonClassId(it) } + }.toSet() + } + + fun findTypeWithField( + fieldName: String + ): Set { + return (fieldToTypeMap[fieldName] ?: emptyList()).map { + PythonClassId(it.className) + }.toSet() + } + + fun findAnnotationByFunctionWithArgumentPosition( + functionName: String, + argumentName: String? = null, + argumentPosition: Int? = null, + ): Set { + val functionInfos = functionToTypeMap[functionName] ?: emptyList() + val types = mutableSetOf() + if (argumentName != null) { + functionInfos.forEach { functionInfo -> + (functionInfo.args + functionInfo.kwonlyargs).forEach { + if (it.arg == argumentName && it.annotation != "") + types.add(NormalizedPythonAnnotation(it.annotation)) + } + } + } else if (argumentPosition != null) { + functionInfos.forEach { functionInfo -> + val checkCountArgs = functionInfo.args.size > argumentPosition + val ann = functionInfo.args.getOrNull(argumentPosition)?.annotation ?: "" + if (checkCountArgs && ann != "") { + types.add(NormalizedPythonAnnotation(ann)) + } + } + } else { + functionInfos.forEach { functionInfo -> + functionInfo.args.forEach { + if (it.annotation != "") + types.add(NormalizedPythonAnnotation(it.annotation)) + } + } + } + return types + } + + fun findAnnotationByFunctionReturnValue(functionName: String): Set { + return functionToTypeMap[functionName]?.map { + NormalizedPythonAnnotation(it.returns) + }?.toSet() ?: emptySet() + } +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileReader.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileReader.kt new file mode 100644 index 0000000000..fc51984c05 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileReader.kt @@ -0,0 +1,25 @@ +package org.utbot.python.typing + +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.runCommand + +object StubFileReader { + private const val scriptPath = "/typeshed_stub.py" + + fun getStubInfo( + modules: List, + pythonPath: String, + ): String { + val scriptContent = + StubFileFinder::class.java.getResource(scriptPath)?.readText() ?: error("Didn't find $scriptPath") + val scriptFile = TemporaryFileManager.createTemporaryFile(scriptContent, tag = "stub_file_reader") + + val command = + listOf( + pythonPath, + scriptFile.absolutePath, + ) + modules + val result = runCommand(command) + return result.stdout + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileStructures.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileStructures.kt new file mode 100644 index 0000000000..c4be21938e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileStructures.kt @@ -0,0 +1,112 @@ +package org.utbot.python.typing + +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.utils.AnnotationNormalizer + +object StubFileStructures { + + data class JsonData( + val classAnnotations: List, + val fieldAnnotations: List, + val functionAnnotations: List, + val methodAnnotations: List, + ) { + fun normalizeAnnotations() { + classAnnotations.forEach { clazz -> + clazz.normalizeAnnotations() + } + fieldAnnotations.forEach { field -> + field.definitions.forEach { def -> + def.normalizeAnnotations() + } + } + functionAnnotations.forEach { function -> + function.definitions.forEach { def -> + def.normalizeAnnotations() + } + } + methodAnnotations.forEach { method -> + method.definitions.forEach { def -> + def.normalizeAnnotations() + } + } + } + } + + data class FieldIndex( + val name: String, + val definitions: List + ) + + data class FunctionIndex( + val name: String, + val definitions: List + ) + + data class MethodIndex( + val name: String, + val definitions: List + ) + + data class FieldInfo( + var annotation: String, // must be NormalizedAnnotation + val className: String, // must be PythonClassId + val name: String, + ) { + fun normalizeAnnotations() { + this.annotation = getNormalAnnotation(this.annotation) + } + } + + data class ClassInfo( + val className: String, // must be PythonClassId + val fields: List, + val methods: List, + ) { + fun normalizeAnnotations() { + this.fields.forEach { field -> + field.normalizeAnnotations() + } + this.methods.forEach { method -> + method.normalizeAnnotations() + } + } + } + + data class FunctionInfo( + val className: String?, // must be PythonClassId? + val args: List = emptyList(), + val kwonlyargs: List = emptyList(), + val name: String, + var returns: String, // must be NormalizedAnnotation + ) { + val defName: String + get() = name.split('.').last() + + val module: String + get() = name.split('.').dropLast(1).joinToString(".") + + fun normalizeAnnotations() { + this.args.forEach { arg -> + arg.normalizeAnnotations() + } + this.kwonlyargs.forEach { kwarg -> + kwarg.normalizeAnnotations() + } + this.returns = getNormalAnnotation(this.returns) + } + } + + data class ArgInfo( + val arg: String, + var annotation: String, // must be NormalizedAnnotation + ) { + fun normalizeAnnotations() { + this.annotation = getNormalAnnotation(this.annotation) + } + } + + fun getNormalAnnotation(annotation: String): String { + return AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(PythonClassId(annotation)).name + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/AnnotationNormalizer.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/AnnotationNormalizer.kt new file mode 100644 index 0000000000..6d004223b6 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/AnnotationNormalizer.kt @@ -0,0 +1,92 @@ +package org.utbot.python.utils + +import org.utbot.python.framework.api.python.NormalizedPythonAnnotation +import org.utbot.python.framework.api.python.PythonClassId +import org.utbot.python.framework.api.python.util.pythonAnyClassId +import java.io.File + + +object AnnotationNormalizer { + private val scriptContent = AnnotationNormalizer::class.java + .getResource("/normalize_annotation_from_project.py") + ?.readText() + ?: error("Didn't find /normalize_annotation_from_project.py") + + private var normalizeAnnotationFromProjectScript_: File? = null + private val normalizeAnnotationFromProjectScript: File + get() { + val result = normalizeAnnotationFromProjectScript_ + if (result == null || !result.exists()) { + val result1 = TemporaryFileManager.createTemporaryFile(scriptContent, tag = "normalize_annotation.py") + normalizeAnnotationFromProjectScript_ = result1 + return result1 + } + return result + } + + private fun normalizeAnnotationFromProject( + annotation: String, + pythonPath: String, + curPythonModule: String, + fileOfAnnotation: String, + filesToAddToSysPath: Set + ): String { + val result = runCommand( + listOf( + pythonPath, + normalizeAnnotationFromProjectScript.path, + annotation, + curPythonModule, + fileOfAnnotation, + ) + filesToAddToSysPath, + ) + return if (result.exitValue == 0) result.stdout else annotation + } + + fun annotationFromProjectToClassId( + annotation: String?, + pythonPath: String, + curPythonModule: String, + fileOfAnnotation: String, + filesToAddToSysPath: Set + ): NormalizedPythonAnnotation = + if (annotation == null) + pythonAnyClassId + else + NormalizedPythonAnnotation( + substituteTypes( + normalizeAnnotationFromProject( + annotation, + pythonPath, + curPythonModule, + fileOfAnnotation, + filesToAddToSysPath + ) + ) + ) + + private val substitutionMapFirstStage = listOf( + "builtins.list" to "typing.List", + "builtins.dict" to "typing.Dict", + "builtins.set" to "typing.Set" + ) + + private val substitutionMapSecondStage = listOf( + Regex("typing.List *([^\\[]|$)") to "typing.List[typing.Any]", + Regex("typing.Dict *([^\\[]|$)") to "typing.Dict[typing.Any, typing.Any]", + Regex("typing.Set *([^\\[]|$)") to "typing.Set[typing.Any]" + ) + + private fun substituteTypes(annotation: String): String { + val firstStage = substitutionMapFirstStage.fold(annotation) { acc, (old, new) -> + acc.replace(old, new) + } + return substitutionMapSecondStage.fold(firstStage) { acc, (re, new) -> + acc.replace(re, new) + } + } + + fun pythonClassIdToNormalizedAnnotation(classId: PythonClassId): NormalizedPythonAnnotation { + return NormalizedPythonAnnotation(substituteTypes(classId.name)) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt new file mode 100644 index 0000000000..cc29c36562 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt @@ -0,0 +1,22 @@ +package org.utbot.python.utils + +object Cleaner { + private var clean: () -> Unit = {} + + fun addFunction(f: () -> Unit) { + val oldClean = clean + val newClean = { + f() + oldClean() + } + clean = newClean + } + + fun restart() { + clean = {} + } + + fun doCleaning() { + clean() + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt new file mode 100644 index 0000000000..5cd6ca501f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt @@ -0,0 +1,41 @@ +package org.utbot.python.utils + +import java.lang.Integer.min + +class PriorityCartesianProduct(private val lists: List>) { + + private fun generateFixedSumRepresentation( + sum: Int, + index: Int = 0, + curRepr: List = emptyList() + ): Sequence> { + val itemNumber = lists.size + var result = emptySequence>() + if (index == itemNumber && sum == 0) { + return sequenceOf(curRepr) + } else if (index < itemNumber && sum >= 0) { + for (i in 0..min(sum, lists[index].size - 1)) { + result += generateFixedSumRepresentation( + sum - i, + index + 1, + curRepr + listOf(i) + ) + } + } + return result + } + + fun getSequence(): Sequence> { + var curSum = 0 + val maxSum = lists.fold(0) { acc, elem -> acc + elem.size } + val combinations = generateSequence { + if (curSum > maxSum) + null + else + generateFixedSumRepresentation(curSum++) + } + return combinations.flatten().map { combination: List -> + combination.mapIndexed { element, value -> lists[element][value] } + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt new file mode 100644 index 0000000000..583e0531df --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt @@ -0,0 +1,42 @@ +package org.utbot.python.utils + +import java.io.BufferedReader +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit + +data class CmdResult( + val stdout: String, + val stderr: String, + val exitValue: Int, + val terminatedByTimeout: Boolean = false +) + +fun startProcess(command: List): Process = ProcessBuilder(command).start() + +fun getResult(process: Process, timeout: Long? = null): CmdResult { + if (timeout != null) { + if (!process.waitFor(timeout, TimeUnit.MILLISECONDS)) { + process.destroy() + return CmdResult("", "", 1, terminatedByTimeout = true) + } + } + + val reader = BufferedReader(InputStreamReader(process.inputStream)) + var stdout = "" + var line: String? = "" + while (line != null) { + stdout += "$line\n" + line = reader.readLine() + } + + if (timeout == null) + process.waitFor() + + val stderr = process.errorStream.readBytes().decodeToString().trimIndent() + return CmdResult(stdout.trimIndent(), stderr, process.exitValue()) +} + +fun runCommand(command: List, timeout: Long? = null): CmdResult { + val process = startProcess(command) + return getResult(process, timeout) +} \ 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 new file mode 100644 index 0000000000..f462a7e9fd --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt @@ -0,0 +1,38 @@ +package org.utbot.python.utils + +object RequirementsUtils { + val requirements: List = + RequirementsUtils::class.java.getResource("/requirements.txt") + ?.readText() + ?.split('\n') + ?.filter { it.isNotEmpty() } + ?: error("Didn't find /requirements.txt") + + private val requirementsScriptContent: String = + RequirementsUtils::class.java.getResource("/check_requirements.py") + ?.readText() + ?: error("Didn't find /check_requirements.py") + + fun requirementsAreInstalled(pythonPath: String): Boolean { + val requirementsScript = + TemporaryFileManager.createTemporaryFile(requirementsScriptContent, tag = "requirements") + val result = runCommand( + listOf( + pythonPath, + requirementsScript.path + ) + requirements + ) + return result.exitValue == 0 + } + + fun installRequirements(pythonPath: String): CmdResult { + return runCommand( + listOf( + pythonPath, + "-m", + "pip", + "install" + ) + requirements + ) + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt new file mode 100644 index 0000000000..2daa636802 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt @@ -0,0 +1,47 @@ +package org.utbot.python.utils + +import org.utbot.common.PathUtil.toPath +import java.io.File +import java.nio.file.Paths + +// numeration from zero +fun getLineNumber(content: String, pos: Int) = + content.substring(0, pos).count { it == '\n' } + +fun getLineOfFunction(code: String, functionName: String? = null): Int? { + val regex = + if (functionName != null) + """(?m)^def +$functionName\(""".toRegex() + else + """(?m)^def""".toRegex() + + val trimmedCode = code.replaceIndent() + return regex.find(trimmedCode)?.range?.first?.let { getLineNumber(trimmedCode, it) } +} + +fun String.camelToSnakeCase(): String { + val camelRegex = "(?<=[a-zA-Z])[\\dA-Z]".toRegex() + return camelRegex.replace(this) { + "_${it.value}" + }.lowercase() +} + +fun moduleOfType(typeName: String): String? { + val lastIndex = typeName.lastIndexOf('.') + return if (lastIndex == -1) null else typeName.substring(0, lastIndex) +} + +fun checkIfFileLiesInPath(path: String, fileWithClassPath: String): Boolean { + val parentPath = Paths.get(path).toAbsolutePath() + val childPath = Paths.get(fileWithClassPath).toAbsolutePath() + return childPath.startsWith(parentPath) +} + +fun getModuleNameWithoutCheck(path: File, fileWithClass: File): String = + path.toURI().relativize(fileWithClass.toURI()).path.removeSuffix(".py").toPath().joinToString(".") + +fun getModuleName(path: String, fileWithClassPath: String): String? { + if (checkIfFileLiesInPath(path, fileWithClassPath)) + return getModuleNameWithoutCheck(File(path), File(fileWithClassPath)) + return null +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt new file mode 100644 index 0000000000..e46b5bd987 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt @@ -0,0 +1,43 @@ +package org.utbot.python.utils + +import org.utbot.common.FileUtil +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.deleteExisting + +object TemporaryFileManager { + private lateinit var tmpDirectory: Path + private var nextId = 0 + + fun setup() { + tmpDirectory = FileUtil.createTempDirectory("python-test-generation-${nextId++}") + Cleaner.addFunction { tmpDirectory.deleteExisting() } + } + + fun assignTemporaryFile(fileName_: String? = null, tag: String? = null, addToCleaner: Boolean = true): File { + val fileName = fileName_ ?: ("${nextId++}_" + (tag ?: "")) + val fullpath = Paths.get(tmpDirectory.toString(), fileName) + val result = fullpath.toFile() + if (addToCleaner) + Cleaner.addFunction { result.delete() } + return result + } + + fun writeToAssignedFile(file: File, content: String) { + file.writeText(content) + file.parentFile?.mkdirs() + file.createNewFile() + } + + fun createTemporaryFile( + content: String, + fileName: String? = null, + tag: String? = null, + addToCleaner: Boolean = true + ): File { + val file = assignTemporaryFile(fileName, tag, addToCleaner) + writeToAssignedFile(file, content) + return file + } +} \ No newline at end of file diff --git a/utbot-python/src/main/resources/check_requirements.py b/utbot-python/src/main/resources/check_requirements.py new file mode 100644 index 0000000000..e1b466d627 --- /dev/null +++ b/utbot-python/src/main/resources/check_requirements.py @@ -0,0 +1,7 @@ +import pkg_resources +import sys + + +if __name__ == "__main__": + dependencies = sys.argv[1:] + pkg_resources.require(dependencies) diff --git a/utbot-python/src/main/resources/normalize_annotation_from_project.py b/utbot-python/src/main/resources/normalize_annotation_from_project.py new file mode 100644 index 0000000000..5d3e29d5f7 --- /dev/null +++ b/utbot-python/src/main/resources/normalize_annotation_from_project.py @@ -0,0 +1,50 @@ +import importlib.machinery +import inspect +import sys +import types +import mypy.fastparse + + +def main(annotation: str, cur_module: str, path: str): + def walk_mypy_type(mypy_type) -> str: + try: + source = inspect.getfile(eval(mypy_type.name)) + # in_project = source.startswith(project_root) + except: + None + + modname = eval(mypy_type.name).__module__ + simple_name = mypy_type.name.split('.')[-1] + fullname = f'{modname}.{simple_name}' + + result = fullname + if len(mypy_type.args) != 0: + arg_strs = [ + walk_mypy_type(arg) + for arg in mypy_type.args + ] + result += f"[{', '.join(arg_strs)}]" + return result + + loader = importlib.machinery.SourceFileLoader(cur_module, path) + mod = types.ModuleType(loader.name) + loader.exec_module(mod) + + for name in dir(mod): + globals()[name] = getattr(mod, name) + + mypy_type_ = mypy.fastparse.parse_type_string(annotation, annotation, -1, -1) + print(walk_mypy_type(mypy_type_), end='') + + +def get_args(): + annotation = sys.argv[1] + cur_module = sys.argv[2] + path = sys.argv[3] + for extra_path in sys.argv[4:]: + sys.path.append(extra_path) + return annotation, cur_module, path + + +if __name__ == '__main__': + main(*get_args()) diff --git a/utbot-python/src/main/resources/preprocessed_values.json b/utbot-python/src/main/resources/preprocessed_values.json new file mode 100644 index 0000000000..4174022ed9 --- /dev/null +++ b/utbot-python/src/main/resources/preprocessed_values.json @@ -0,0 +1,997 @@ +[ + { + "name": "builtins.int", + "instances": [ + "0", + "1", + "-1", + "4294967297", + "4294967296", + "(1 << 100)", + "83", + "123", + "-3", + "100", + "10", + "314", + "int('281d55i5', 21)", + "int(' 0B100 ', 0)", + "int('1_00', 3)", + "int('32244002423141', 5)", + "int('10000000000000001', 4)", + "int('4f5aff66', 19)", + "int('mb994ag', 24)", + "int('1' * 600)", + "int('40000000000', 8)", + "int(memoryview(b'1234')[1:3])", + "int('1904440554', 11)", + "int('3723ai4g', 20)", + "int(' 0X123 ', 0)", + "int('100000000000000000000000000000001', 2)", + "int(memoryview(b'123 ')[1:3])", + "int('1_2_3_4_5_6_7_8_9', 16)", + "int('a7ffda91', 17)", + "int('0x123', 0)", + "int('0B100', 2)", + "int('0O123', 8)", + "int('102002022201221111211', 3)", + "int('4f5aff67', 19)", + "int('8pfgih4', 28)", + "int(1e+100)", + "int(b'-1')", + "int('dnchbnn', 26)", + "int(' 0o123 ', 0)", + "int('0o123', 8)", + "int('1z141z5', 36)", + "int('12068657455', 9)", + "int('1_2_3_4_5_6_7', 32)", + "int('2ca5b7464', 14)", + "int('9ba461595', 12)", + "int('4q0jto5', 31)", + "int('b28jpdn', 27)", + "int('5qmcpqg', 30)", + "int(3.14)", + "int('12068657454', 9)", + "int('3aokq95', 33)", + "int('211301422354', 7)", + "int('2qhxjlj', 34)", + "int('1fj8b184', 22)", + "int('\\u2003-3\\u2002')", + "int('1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1', 2)", + "int('0b100', 0)", + "int('hek2mgm', 25)", + "int('0X123', 16)", + "int('1904440555', 11)" + ] + }, + { + "name": "builtins.bool", + "instances": [ + "True", + "False" + ] + }, + { + "name": "builtins.str", + "instances": [ + "str(1.5 + 3.5j)", + "str()", + "str(b'\\xf0\\xa3\\x91\\x96', 'utf-8')", + "str(-1234567890)", + "str(1e+300 * 1e+300)", + "str(b'\\x80')", + "str(-123456789)", + "str(id)", + "str('unicode remains unicode')", + "str(3 + 0.0j)", + "str(object=500)", + "str(3.0j)", + "str('strings are converted to unicode')", + "str(b'foo', errors='strict')", + "str(b'xn--pythn-mua.org.', 'idna')", + "str(1 + 3.0j)", + "str(OSError(1001))", + "str(bytearray(b''))", + "str(b'\\xf0\\x90\\x80\\x82', 'utf-8')", + "str(['1', '2', '3'])", + "str(True)", + "str('abcdefghijklmnopqrst')", + "str(b'\\xe2\\x82\\xac', 'utf-8')", + "str(b'x')", + "str('a = 1')", + "str(2 ** 1000)", + "str(bytearray(b'x'))", + "str(3.2 + 0.0j)", + "str(6442450944)", + "str('3')", + "str('global')", + "str(False)" + ] + }, + { + "name": "builtins.float", + "instances": [ + "float(-1)", + "0.0", + "float('nan')", + "float('1.4')", + "float('+infinity')", + "float(10 ** 23)", + "7.3", + "float(1970)", + "float(314)", + "float(1)", + "float(-2100.0)", + "float(2 ** 31)", + "float(2 ** 64)", + "float('-INFINITY')", + "float(1 - 2 ** 31)", + "-2.1", + "float(2 ** 63)", + "3.14", + "3.2e3", + "2.1", + "float(-2 ** 63)", + "2.5", + "0.5", + "float(2 ** 31 - 1)", + "float('-nan')", + "3.e3", + "float(-2 ** 31)", + "float(-7.3)", + "float(2 ** 34)", + "float(2 ** 32)", + "1.23e+300" + ] + }, + { + "name": "builtins.range", + "instances": [ + "range(127, 256)", + "range(24)", + "range(0, 2 ** 100 + 1, 2)", + "range(1, 25 + 1)", + "range(0, 3)", + "range(-10, 10)", + "range(10, -11, -1)", + "range(240)", + "range(2 ** 200, 2 ** 201, 2 ** 100)", + "range(200)", + "range(50, 400)", + "range(150)", + "range(9, -1, -2)", + "range(3 * 5 * 7 * 11)", + "range(4, 16)", + "range(0, 2 ** 100 - 1, 2)", + "range(0, 55296)", + "range(101)", + "range(5000)", + "range(65536, 1114112)", + "range((1 << 16) - 1)", + "range(1500)", + "range(1, 9)", + "range(512)", + "range(0, -20, -1)", + "range(32, 127)", + "range(52, 64)", + "range(1 << 1000)", + "range(70000)" + ] + }, + { + "name": "builtins.complex", + "instances": [ + "complex(1.0, float('inf'))", + "complex('1j')", + "complex(1.0, 10.0)", + "complex(0.0j, 3.14)", + "complex('(1+2j)')", + "complex(float('inf'), float('inf'))", + "complex('1' * 500)", + "complex(float('inf'), -1)", + "complex(0.0, float('nan'))", + "complex(10.0)", + "complex(float('nan'), 1)", + "complex(1.0, 0.0)", + "complex(0, 0)", + "complex('(1.3+2.2j)')", + "complex(3.14 + 0.0j)", + "complex(float('nan'), -1)", + "complex(0.0, -float('inf'))", + "complex(repr(-6.0j))", + "complex(real=17 + 23.0j)", + "complex('( j )')", + "complex('-1e500+1.8e308j')", + "complex(float('inf'), 0)", + "complex(0, float('nan'))", + "complex('1e500')", + "complex('-1e-500j')", + "complex(10)", + "complex(' ( +3.14-6J )')", + "complex(1, 10)", + "complex(0.0, 0.0)", + "complex(3.14 + 0.0j, 0.0j)", + "complex(1, float('inf'))", + "complex(314, 0)", + "complex('-1e-500+1e-500j')", + "complex(3.14, 0.0)", + "complex(-float('inf'), float('inf'))", + "complex('( -j)')", + "complex(10 + 0.0j)", + "complex(-0.0, 0.0)", + "complex(5.3, 9.8)", + "complex(1.0, -float('inf'))", + "complex('-1e500j')", + "complex(0.0j, 3.14j)", + "complex(0.0, -1.0)", + "complex(real=17 + 23.0j, imag=23)", + "complex(0.0, -0.0)", + "complex('-1')", + "complex(repr(6.0j))", + "complex(1e-200, 1e-200)", + "complex(314)", + "complex(repr(1 + 6.0j))", + "complex(-0.0, -1.0)", + "complex(float('inf'), 0.0)", + "complex(0, -float('inf'))", + "complex()", + "complex(0.0, 1.0)", + "complex('+1')", + "complex(' ( +3.14+j )')", + "complex('1')", + "complex('1+10j')", + "complex(-0.0, 1.0)", + "complex(0, float('inf'))", + "complex('3.14+1J')", + "complex(float('nan'), float('nan'))", + "complex(3.14)", + "complex(real=17, imag=23)", + "complex(1e+200, 1e+200)", + "complex('+J')", + "complex(0.0, 3.0)", + "complex(1.0, 10)", + "complex(-0.0, 2.0)", + "complex('J')", + "complex(' ( +3.14-J )')", + "complex(float('inf'), 1)", + "complex(repr(1 - 6.0j))", + "complex(-0.0, -0.0)", + "complex('1e-500')", + "complex(real=1 + 2.0j, imag=3 + 4.0j)", + "complex(1, 10.0)", + "complex(1, float('nan'))", + "complex(0.0, 3.14)", + "complex(0.0, 3.14j)", + "complex(1.0, -0.0)" + ] + }, + { + "name": "builtins.BaseException", + "instances": [ + "BaseException()" + ] + }, + { + "name": "types.NoneType", + "instances": [ + "None" + ] + }, + { + "name": "builtins.bytearray", + "instances": [ + "bytearray(b'a')", + "bytearray(100)", + "bytearray(b'\\x00' * 100)", + "bytearray(range(1, 10))", + "bytearray(b'\\x07\\x7f\\x7f')", + "bytearray(b'mutable')", + "bytearray([1, 2])", + "bytearray(range(256))", + "bytearray(b'hell')", + "bytearray([5, 6, 7, 8, 9])", + "bytearray(b'memoryview')", + "bytearray(b'a:b::c:::d')", + "bytearray(b'b')", + "bytearray(b'cd')", + "bytearray(b'world')", + "bytearray(b'[emoryvie]')", + "bytearray(b'x' * 5)", + "bytearray([0, 1, 254, 255])", + "bytearray(b'*$')", + "bytearray(b'abc\\xe9\\x00')", + "bytearray(b'a\\xffb')", + "bytearray(range(100))", + "bytearray(b'[abracadabra]')", + "bytearray([0, 1, 2, 100, 101, 7, 8, 9])", + "bytearray(b'Mary')", + "bytearray(b'baz')", + "bytearray(b'\\xff')", + "bytearray(128 * 1024)", + "bytearray([100, 101])", + "bytearray(b'1')", + "bytearray([1, 2, 3])", + "bytearray(b'')", + "bytearray(b'one')", + "bytearray(b'nul:\\x00')", + "bytearray(1024)", + "bytearray(10)", + "bytearray(b'abcdefgh')", + "bytearray(b'123')", + "bytearray(b'\\x80')", + "bytearray(b'01 had a 9')", + "bytearray(2)", + "bytearray(range(10))", + "bytearray(b'a\\x80b')", + "bytearray(b':a:b::c')", + "bytearray(b'\\x00' * 15 + b'\\x01')", + "bytearray(b'hash this!')", + "bytearray(b'bar')", + "bytearray(b'x' * 4)", + "bytearray([0, 1, 2, 42, 42, 42, 3, 4, 5, 6, 7, 8, 9])", + "bytearray(b'----')", + "bytearray([i for i in range(256)])", + "bytearray(b'bytearray')", + "bytearray(b'spam')", + "bytearray([10, 100, 200])", + "bytearray(b'abcdefghijk')", + "bytearray(b'msssspp')", + "bytearray(b'no error')", + "bytearray(b'YWJj\\n')", + "bytearray([1, 1, 1, 1, 1, 5, 6, 7, 8, 9])", + "bytearray(b'foobaz')", + "bytearray([0])", + "bytearray(b'little lamb---')", + "bytearray(b'abc\\xe9\\x00xxx')", + "bytearray(b'def')", + "bytearray(b'eggs\\n')", + "bytearray(b'foo')", + "bytearray(b'foobar')", + "bytearray(128)", + "bytearray(b'key')", + "bytearray(16)", + "bytearray(b'file.py')", + "bytearray(b'ab')", + "bytearray(b'this is a random bytearray object')", + "bytearray(b'x' * 8)", + "bytearray(b' world\\n\\n\\n')", + "bytearray([1, 100, 200])", + "bytearray([102, 111, 111, 111, 111])", + "bytearray(range(1, 9))", + "bytearray([126, 127, 128, 129])", + "bytearray(5)", + "bytearray([0, 1, 2, 102, 111, 111])", + "bytearray(b'\\x00python\\x00test\\x00')", + "bytearray(9)", + "bytearray(b'abcde')", + "bytearray(b'x')", + "bytearray(b'0123456789')", + "bytearray([102, 111, 111, 102, 111, 111])", + "bytearray(2 ** 16)", + "bytearray(b'python')", + "bytearray(8192)", + "bytearray(list(range(8)) + list(range(256)))", + "bytearray(b'\\xff\\x00\\x00')", + "bytearray(b'hello1')", + "bytearray(range(16))", + "bytearray(b'xyz')", + "bytearray(b'\\xaaU\\xaaU')", + "bytearray(b'Z')", + "bytearray(8)", + "bytearray([1, 1, 1, 1, 1])", + "bytearray([0, 1, 2, 3, 4])", + "bytearray(b'ghi')", + "bytearray(b'[ytearra]')", + "bytearray(b'abc')", + "bytearray(b'this is a test')", + "bytearray(b'xxx')", + "bytearray()", + "bytearray(b'abcdefghijklmnopqrstuvwxyz')", + "bytearray(b'my dog has fleas')", + "bytearray([1, 100, 3])", + "bytearray(b'g\\xfcrk')", + "bytearray(b'hello world')", + "bytearray([26, 43, 48])", + "bytearray([1, 2, 3, 4, 6, 7, 8])", + "bytearray(b'0102abcdef')", + "bytearray(1)", + "bytearray(b'--------------')", + "bytearray(20)", + "bytearray(b'hello')" + ] + }, + { + "name": "builtins.bytes", + "instances": [ + "bytes(b'ab')", + "bytes(b'def')", + "bytes(b'abc')", + "bytes([126, 128, 129])", + "bytes([126, 128])", + "bytes(b'Hello world\\n\\x80\\x81\\xfe\\xff')", + "bytes(range(255))", + "bytes(3)", + "bytes(2)" + ] + }, + { + "name": "builtins.dict", + "instances": [ + "dict()" + ] + }, + { + "name": "builtins.frozenset", + "instances": [ + "frozenset()" + ] + }, + { + "name": "builtins.list", + "instances": [ + "list()" + ] + }, + { + "name": "builtins.memoryview", + "instances": [ + "memoryview(b'a')", + "memoryview(b'1234')", + "memoryview(b'ax = 123')", + "memoryview(b'$23$')", + "memoryview(bytes(range(256)))", + "memoryview(b'hash this!')", + "memoryview(b'12.3')", + "memoryview(b'bytes')", + "memoryview(b'a:b::c:::d')", + "memoryview(b'ab')", + "memoryview(b'123')", + "memoryview(b'ac')", + "memoryview(b'YWJj\\n')", + "memoryview(b'')", + "memoryview(b'*$')", + "memoryview(b'spam\\n')", + "memoryview(b'\\xff\\x00\\x00')", + "memoryview(b' ')", + "memoryview(b'abc')", + "memoryview(b'12.3A')", + "memoryview(b':a:b::c')", + "memoryview(b'foo')", + "memoryview(b'\\x124Vx')", + "memoryview(b'[abracadabra]')", + "memoryview(b'123 ')", + "memoryview(b'spam')", + "memoryview(b'text')", + "memoryview(b'memoryview')", + "memoryview(b'123\\x00')", + "memoryview(b'12.3\\x00')", + "memoryview(b'character buffers are decoded to unicode')", + "memoryview(b'xyz')", + "memoryview(b'12.3 ')", + "memoryview(b'cd')", + "memoryview(b'baz')", + "memoryview(b'0102abcdef')", + "memoryview(b'123A')", + "memoryview(b'\\x07\\x7f\\x7f')", + "memoryview(b'file.py')", + "memoryview(b'\\x1a+0')", + "memoryview(b'12.34')" + ] + }, + { + "name": "builtins.object", + "instances": [ + "object()" + ] + }, + { + "name": "builtins.set", + "instances": [ + "set()" + ] + }, + { + "name": "builtins.tuple", + "instances": [ + "tuple()" + ] + }, + { + "name": "builtins.slice", + "instances": [ + "slice(0, 2, 1)", + "slice(2)", + "slice(1, 3)", + "slice(4)", + "slice(0, 10)", + "slice(0, 1, 1)", + "slice(0, 1, 2)", + "slice(0, 2)", + "slice(1, 1)", + "slice(0)", + "slice(0, 1, 2)", + "slice(0, 2, 1)", + "slice(3, 5, 1)", + "slice(0, 1, 1)", + "slice(0, 1, 0)", + "slice(0, 8, 1)", + "slice(0, 2, 0)", + "slice(1, 18, 2)", + "slice(1)", + "slice(0, 10, 1)", + "slice(None, 10, -1)", + "slice(None, -10)", + "slice(None, -11, -1)", + "slice(None, 9)", + "slice(100, -100, -1)", + "slice(None, 10)", + "slice(None)", + "slice(-100, 100)", + "slice(None, None, -1)", + "slice(None, -9)", + "slice(None, 9, -1)", + "slice(0.0, 10, 1)", + "slice(0, 10, 0)", + "slice(10, 20, 3)", + "slice(0, 10, 1.0)", + "slice(1, 2, 4)", + "slice(1, 2)", + "slice(None, None, -2)", + "slice(-100, 100, 2)", + "slice(0, 10.0, 1)", + "slice(3, None, -2)", + "slice(1, None, 2)", + "slice(None, -10, -1)", + "slice(None, 11)", + "slice(1, 2, 3)", + "slice(None, -12, -1)", + "slice(5)", + "slice(None, 8, -1)", + "slice(None, -11)", + "slice(None, None, 2)", + "slice(0, 10, 2)", + "slice(2, 3)", + "slice(0, 10)", + "slice(0, 1, 5)", + "slice(0, 10, 0)", + "slice(2, 10, 3)", + "slice(0, 2)", + "slice(1, 3)", + "slice(1, 2)", + "slice(2, 2)", + "slice(0, 1)", + "slice(0, 0)", + "slice(2, 1)", + "slice(2000, 1000)", + "slice(0, 1000)", + "slice(0, 3)", + "slice(1000, 1000)", + "slice(2, 4)", + "slice(1, 2)", + "slice(1, 2, 3)", + "slice(0, 1)", + "slice(0, 0)", + "slice(2, 3)", + "slice(None, 42)", + "slice(None, 24, None)", + "slice(2, 1024, 10)", + "slice(None, 42, None)", + "slice(0, 2)", + "slice(1, 2)", + "slice(0, 1)", + "slice(3, 5)", + "slice(0, 10, 0)", + "slice(0, 3)", + "slice(1, 10, 2)", + "slice(2, 2, 2)", + "slice(1, 1, 1)" + ] + }, + { + "name": "builtins.type", + "instances": [ + "type(len)", + "type('123')", + "type(1.0)", + "type({})", + "type('foo', (), {})", + "type('A', (), {'__qualname__': 'B.C'})", + "type(lambda x: x)", + "type((True).real)", + "type(list.append)", + "type('blah', (), {})", + "type(())", + "type('A', (), {})", + "type(list)", + "type({}.items())", + "type({}.values())", + "type('NewClass', (object,), {})", + "type(list.__add__)", + "type(classmethod(lambda c: None))", + "type(range(0))", + "type(None)", + "type(iter(range(0)))", + "type('C', (object,), {'__hash__': None})", + "type(complex('1' * 500))", + "type(staticmethod(lambda : None))", + "type(iter(range(1 << 1000)))", + "type('C', (), {})", + "type({}.keys())" + ] + }, + { + "name": "datetime.date", + "instances": [ + "datetime.date(1, 1, 1)", + "datetime.date(1995, 4, 12)", + "datetime.date(2011, 1, 1)", + "datetime.date(2002, 3, 4)", + "datetime.date(1993, 8, 26)", + "datetime.date(2000, 1, 2)", + "datetime.date(1970, 1, 1)" + ] + }, + { + "name": "datetime.datetime", + "instances": [ + "datetime.datetime(2011, 1, 1)", + "datetime.datetime(1970, 1, 1)", + "datetime.datetime(2015, 4, 5, 1, 45)", + "datetime.datetime(1, 1, 1)", + "datetime.datetime(2014, 11, 2, 1, 30)", + "datetime.datetime(1, 2, 3, 4, 5, 6, 7)", + "datetime.datetime(2002, 4, 7, 2)", + "datetime.datetime(1, 1, 1, fold=1)", + "datetime.datetime(2010, 1, 1)", + "datetime.datetime(2011, 1, 1, 12, 30)", + "datetime.datetime(1993, 8, 26, 22, 12, 55, 99999)", + "datetime.datetime(1, 4, 1, 2)", + "datetime.datetime(1995, 4, 12)", + "datetime.datetime(1, 10, 25, 1)", + "datetime.datetime(10, 10, 10, 10, 10, 10, 10)", + "datetime.datetime(2002, 10, 27, 1)" + ] + }, + { + "name": "datetime.time", + "instances": [ + "datetime.time(fold=1)", + "datetime.time()", + "datetime.time(0, fold=1)", + "datetime.time(22, 12, 55, 99999)", + "datetime.time(0)", + "datetime.time(microsecond=40)", + "datetime.time(18, 45, 3, 1234)", + "datetime.time(12, 0)", + "datetime.time(12, 30)" + ] + }, + { + "name": "datetime.timedelta", + "instances": [ + "datetime.timedelta(days=100, weeks=-7, hours=-24 * (100 - 49), minutes=-3, seconds=12, microseconds=(3 * 60 - 12) * 1000000)", + "datetime.timedelta(hours=24)", + "datetime.timedelta(hours=23, minutes=59)", + "datetime.timedelta(0, 4000, 1)", + "datetime.timedelta(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999)", + "datetime.timedelta(minutes=1440)", + "datetime.timedelta(microseconds=1)", + "datetime.timedelta(minutes=24)", + "datetime.timedelta(minutes=60)", + "datetime.timedelta(weeks=13)", + "datetime.timedelta(minutes=2, seconds=1, microseconds=3)", + "datetime.timedelta(26, 55, 99999)", + "datetime.timedelta(seconds=0.5)", + "datetime.timedelta(minutes=-200)", + "datetime.timedelta(seconds=30)", + "datetime.timedelta(days=-999999999)", + "datetime.timedelta(minutes=-2)", + "datetime.timedelta(minutes=2 * 1439)", + "datetime.timedelta(hours=12, minutes=32, seconds=30)", + "datetime.timedelta(hours=-5)", + "datetime.timedelta(hours=5)", + "datetime.timedelta(42)", + "datetime.timedelta(minutes=23)", + "datetime.timedelta(minutes=3)", + "datetime.timedelta(microseconds=-1)", + "datetime.timedelta(0, 0, 1000)", + "datetime.timedelta(minutes=1)", + "datetime.timedelta(days=365)", + "datetime.timedelta(minutes=0)", + "datetime.timedelta(microseconds=-81)", + "datetime.timedelta(hours=9.5)", + "datetime.timedelta(minutes=2, seconds=30)", + "datetime.timedelta(hours=-4)", + "datetime.timedelta(minutes=-300)", + "datetime.timedelta(days=1, seconds=2, microseconds=3)", + "datetime.timedelta(hours=2)", + "datetime.timedelta(0)" + ] + }, + { + "name": "_decimal.Decimal", + "instances": [ + "_decimal.Decimal('22.2')", + "_decimal.Decimal('1.234e7')", + "_decimal.Decimal('sNaN')", + "_decimal.Decimal(3)", + "_decimal.Decimal('45.34')", + "_decimal.Decimal('580')", + "_decimal.Decimal((0, (0,), 0))", + "_decimal.Decimal('3.4e200')", + "_decimal.Decimal('1e2')", + "_decimal.Decimal('2.59')", + "_decimal.Decimal((1, (0, 0, 0), 37))", + "_decimal.Decimal(10000)", + "_decimal.Decimal('10e99999')", + "_decimal.Decimal(7.5)", + "_decimal.Decimal('1.1')", + "_decimal.Decimal(10 ** (19 * 24))", + "_decimal.Decimal('-inf')", + "_decimal.Decimal(' 3.45679 ')", + "_decimal.Decimal('1.0e-20')", + "_decimal.Decimal('-0.8')", + "_decimal.Decimal('1652.9E100')", + "_decimal.Decimal('-10')", + "_decimal.Decimal('0E10')", + "_decimal.Decimal('2.54')", + "_decimal.Decimal('15.32')", + "_decimal.Decimal('-0')", + "_decimal.Decimal('0.00390625')", + "_decimal.Decimal(5)", + "_decimal.Decimal((1, (0, 0, 0), 'N'))", + "_decimal.Decimal('-3.141590000')", + "_decimal.Decimal('0')", + "_decimal.Decimal(45)", + "_decimal.Decimal('1234e9999')", + "_decimal.Decimal(2)", + "_decimal.Decimal('1.634E100')", + "_decimal.Decimal('4.125')", + "_decimal.Decimal('4.2084')", + "_decimal.Decimal('56531E100')", + "_decimal.Decimal((1, [4, 3, 4, 9, 1, 3, 5, 3, 4], -25))", + "_decimal.Decimal('9.99')", + "_decimal.Decimal('45')", + "_decimal.Decimal('100.0')", + "_decimal.Decimal('7')", + "_decimal.Decimal('1.01')", + "_decimal.Decimal('111')", + "_decimal.Decimal(2 ** 16)", + "_decimal.Decimal('0.0012885819')", + "_decimal.Decimal('1')", + "_decimal.Decimal(-12)", + "_decimal.Decimal('1.12345')", + "_decimal.Decimal('-0.5')", + "_decimal.Decimal((0, (4, 5, 3, 4), -2))", + "_decimal.Decimal('-0.4')", + "_decimal.Decimal('-33.3')", + "_decimal.Decimal(2 ** 578)", + "_decimal.Decimal('1.00000001e-20')", + "_decimal.Decimal('10001111111')", + "_decimal.Decimal([0, [0], 0])", + "_decimal.Decimal('INF')", + "_decimal.Decimal(2 ** 64 + 2 ** 32 - 1)", + "_decimal.Decimal('4.9712')", + "_decimal.Decimal('8.392')", + "_decimal.Decimal('1e-9999')", + "_decimal.Decimal('9.123')", + "_decimal.Decimal('1e99')", + "_decimal.Decimal('1.47e5')", + "_decimal.Decimal(23)", + "_decimal.Decimal('3.0')", + "_decimal.Decimal('152587890625')", + "_decimal.Decimal('3.1234')", + "_decimal.Decimal(+45)", + "_decimal.Decimal(float('nan'))", + "_decimal.Decimal('32')", + "_decimal.Decimal('snan123')", + "_decimal.Decimal(10101)", + "_decimal.Decimal('28.5')", + "_decimal.Decimal('0.00')", + "_decimal.Decimal('11.68')", + "_decimal.Decimal('99999999999999999999999999.9')", + "_decimal.Decimal('1_0_0_0')", + "_decimal.Decimal('5')", + "_decimal.Decimal('1.00000001e-100')", + "_decimal.Decimal('0.28')", + "_decimal.Decimal('NaN')", + "_decimal.Decimal((0, (), 'F'))", + "_decimal.Decimal('-NaN')", + "_decimal.Decimal('9.87654321')", + "_decimal.Decimal('10912837129')", + "_decimal.Decimal('1.00000001')", + "_decimal.Decimal('2E+1')", + "_decimal.Decimal('-1')", + "_decimal.Decimal('Nan891287828')", + "_decimal.Decimal('-11.1')", + "_decimal.Decimal((1, (), 37))", + "_decimal.Decimal('1e1')", + "_decimal.Decimal('NAN')", + "_decimal.Decimal('9.8765e-12')", + "_decimal.Decimal(99)", + "_decimal.Decimal('625')", + "_decimal.Decimal(200)", + "_decimal.Decimal(123)", + "_decimal.Decimal('1.00')", + "_decimal.Decimal('81.3971')", + "_decimal.Decimal('123456789.1')", + "_decimal.Decimal('0.372')", + "_decimal.Decimal('1.23')", + "_decimal.Decimal(1234)", + "_decimal.Decimal('-21.1')", + "_decimal.Decimal('10901935')", + "_decimal.Decimal('-0E12')", + "_decimal.Decimal('Inf')", + "_decimal.Decimal((0, (0,), 'F'))", + "_decimal.Decimal('-0.6')", + "_decimal.Decimal('9e2')", + "_decimal.Decimal('35.719')", + "_decimal.Decimal('390625')", + "_decimal.Decimal(10 ** (19 * 25))", + "_decimal.Decimal('-2.5')", + "_decimal.Decimal('3.571')", + "_decimal.Decimal('1e797')", + "_decimal.Decimal('1e-425000000')", + "_decimal.Decimal('188.83E100')", + "_decimal.Decimal('0.001')", + "_decimal.Decimal((1, (4, 3, 4, 9, 1, 3, 5, 3, 4), -25))", + "_decimal.Decimal('100E-425000010')", + "_decimal.Decimal('1e100000')", + "_decimal.Decimal('3.5e-2')", + "_decimal.Decimal('100000000000000000000000000')", + "_decimal.Decimal('-5')", + "_decimal.Decimal(11)", + "_decimal.Decimal('1.0e20')", + "_decimal.Decimal('1e4')", + "_decimal.Decimal(1001)", + "_decimal.Decimal('-4.34913534E-17')", + "_decimal.Decimal('-23.00000')", + "_decimal.Decimal('-25e55')", + "_decimal.Decimal('456789')", + "_decimal.Decimal('10')", + "_decimal.Decimal('999.9')", + "_decimal.Decimal([1, (4, 3, 4, 9, 1, 3, 5, 3, 4), -25])", + "_decimal.Decimal(0)", + "_decimal.Decimal('-0.0625')", + "_decimal.Decimal('9.99e-5')", + "_decimal.Decimal('256e7')", + "_decimal.Decimal('1.3E4 \\n')", + "_decimal.Decimal()", + "_decimal.Decimal('10.0')", + "_decimal.Decimal('3.1415926')", + "_decimal.Decimal(0.1)", + "_decimal.Decimal('0.25')", + "_decimal.Decimal('9.8182731e181273')", + "_decimal.Decimal('16.1')", + "_decimal.Decimal('nan')", + "_decimal.Decimal('-3.217160342717258261933904529E-7')", + "_decimal.Decimal('1e9999')", + "_decimal.Decimal('0.05')", + "_decimal.Decimal('25')", + "_decimal.Decimal(100)", + "_decimal.Decimal(-2)", + "_decimal.Decimal('NaN12345')", + "_decimal.Decimal('1e-99')", + "_decimal.Decimal('0.' + '9' * 30)", + "_decimal.Decimal(1221 ** 1271)", + "_decimal.Decimal('32.9714')", + "_decimal.Decimal((1, (0, 2, 7, 1), 'F'))", + "_decimal.Decimal((1, (4, 5), 0))", + "_decimal.Decimal(50)", + "_decimal.Decimal([1, [4, 3, 4, 9, 1, 3, 5, 3, 4], -25])", + "_decimal.Decimal('45e2')", + "_decimal.Decimal('2.234e2000')", + "_decimal.Decimal(1000)", + "_decimal.Decimal('7.33')", + "_decimal.Decimal('5e3')", + "_decimal.Decimal('-0.0')", + "_decimal.Decimal('1.0e-100')", + "_decimal.Decimal('5.5')", + "_decimal.Decimal('-75')", + "_decimal.Decimal('1.2')", + "_decimal.Decimal('-0.625')", + "_decimal.Decimal((0, (0, 0, 4, 0, 5, 3, 4), 'n'))", + "_decimal.Decimal('33.3')", + "_decimal.Decimal(9)", + "_decimal.Decimal(4)", + "_decimal.Decimal('1230E100')", + "_decimal.Decimal('.000e20')", + "_decimal.Decimal((0, (0, 0, 4, 0, 5, 3, 4), -2))", + "_decimal.Decimal(67)", + "_decimal.Decimal('sNAN')", + "_decimal.Decimal(float('-inf'))", + "_decimal.Decimal(123456789000)", + "_decimal.Decimal('NaN123')", + "_decimal.Decimal(True)", + "_decimal.Decimal('5e-3')", + "_decimal.Decimal('-6.1')", + "_decimal.Decimal('-25')", + "_decimal.Decimal('2')", + "_decimal.Decimal('-1.25')", + "_decimal.Decimal('0.1')", + "_decimal.Decimal('1.0')", + "_decimal.Decimal('90.697E100')", + "_decimal.Decimal(152587890625)", + "_decimal.Decimal(10 ** (9 * 24))", + "_decimal.Decimal('12.7')", + "_decimal.Decimal('66')", + "_decimal.Decimal(-100)", + "_decimal.Decimal('.1')", + "_decimal.Decimal('7.34')", + "_decimal.Decimal(float('inf'))", + "_decimal.Decimal('7.335')", + "_decimal.Decimal('1.2345')", + "_decimal.Decimal('0.2')", + "_decimal.Decimal((0, (4, 5, 3, 4), 'F'))", + "_decimal.Decimal('Infinity')", + "_decimal.Decimal('0.871831e800')", + "_decimal.Decimal('1.00000001e20')", + "_decimal.Decimal('3.1415')", + "_decimal.Decimal('-Inf')", + "_decimal.Decimal('-nan')", + "_decimal.Decimal(456)", + "_decimal.Decimal('194')", + "_decimal.Decimal('0.025')", + "_decimal.Decimal('snan')", + "_decimal.Decimal(567)", + "_decimal.Decimal('9.8765e12')", + "_decimal.Decimal('3.1')", + "_decimal.Decimal('-1.5')", + "_decimal.Decimal('inf')", + "_decimal.Decimal('1.3')", + "_decimal.Decimal('-4.5678E50')", + "_decimal.Decimal(5 ** 2659)", + "_decimal.Decimal('33e+33')", + "_decimal.Decimal('1e-100000')", + "_decimal.Decimal(float('-0.0'))", + "_decimal.Decimal('1.23456789')", + "_decimal.Decimal('1e-10')", + "_decimal.Decimal(12)", + "_decimal.Decimal('nan123')", + "_decimal.Decimal('0.0')", + "_decimal.Decimal('-0.000')", + "_decimal.Decimal('152587890625e7')", + "_decimal.Decimal('-16.1')", + "_decimal.Decimal('0.333333333333333333')", + "_decimal.Decimal(-1)", + "_decimal.Decimal('43.24')", + "_decimal.Decimal('9.9')", + "_decimal.Decimal('3.1416')", + "_decimal.Decimal('2.1')", + "_decimal.Decimal('16807')", + "_decimal.Decimal('62.4802')", + "_decimal.Decimal('-15')", + "_decimal.Decimal(500000123)", + "_decimal.Decimal('-38.3')", + "_decimal.Decimal('0.333333333333333333333333')", + "_decimal.Decimal('1e425000000')", + "_decimal.Decimal('1.5')", + "_decimal.Decimal(768)", + "_decimal.Decimal(10 ** (9 * 25))", + "_decimal.Decimal((1, (), 'n'))", + "_decimal.Decimal('11.1')", + "_decimal.Decimal('0.01')", + "_decimal.Decimal(-45)", + "_decimal.Decimal('9.99e10')", + "_decimal.Decimal('1e-3')", + "_decimal.Decimal('100000000.123')", + "_decimal.Decimal('0.5')", + "_decimal.Decimal('3')", + "_decimal.Decimal(1)", + "_decimal.Decimal(10)", + "_decimal.Decimal('20')", + "_decimal.Decimal('0.1234')", + "_decimal.Decimal('-Infinity')", + "_decimal.Decimal('.01')", + "_decimal.Decimal('23.42')", + "_decimal.Decimal(' -7.89')", + "_decimal.Decimal(False)", + "_decimal.Decimal('1_3.3e4_0')", + "_decimal.Decimal('2.234e-2000')", + "_decimal.Decimal('-1E+1')", + "_decimal.Decimal('1.50001')", + "_decimal.Decimal('8.71E+799')", + "_decimal.Decimal('20.686')" + ] + } +] diff --git a/utbot-python/src/main/resources/python_tree_serializer.py b/utbot-python/src/main/resources/python_tree_serializer.py new file mode 100644 index 0000000000..557cb56954 --- /dev/null +++ b/utbot-python/src/main/resources/python_tree_serializer.py @@ -0,0 +1,206 @@ +import copy +import pickle +import types +from itertools import zip_longest +import copyreg +import importlib + + +class _PythonTreeSerializer: + class MemoryObj: + def __init__(self, json): + self.json = json + self.deserialized_obj = None + self.comparable = False + self.is_draft = True + + def __init__(self): + self.memory = {} + + def memory_view(self): + return ' | '.join(f'{id_}: {obj.deserialized_obj}' for id_, obj in self.memory.items()) + + @staticmethod + def get_type(py_object): + if py_object is None: + return 'types.NoneType' + module = type(py_object).__module__ + return '{module}.{name}'.format( + module=module, + name=type(py_object).__name__, + ) + + @staticmethod + def get_type_name(type_): + if type_ is None: + return 'types.NoneType' + return '{module}.{name}'.format( + module=type_.__module__, + name=type_.__name__, + ) + + @staticmethod + def has_reduce(py_object) -> bool: + if getattr(py_object, '__reduce__', None) is None: + return False + else: + try: + py_object.__reduce__() + return True + except TypeError: + return False + + def save_to_memory(self, id_, py_json, deserialized_obj): + mem_obj = _PythonTreeSerializer.MemoryObj(py_json) + mem_obj.deserialized_obj = deserialized_obj + self.memory[id_] = mem_obj + return mem_obj + + def get_reduce(self, py_object): + id_ = id(py_object) + + py_object_reduce = py_object.__reduce__() + reduce_value = [ + default if obj is None else obj + for obj, default in zip_longest( + py_object_reduce, + [None, [], {}, [], []], + fillvalue=None + ) + ] + + constructor = _PythonTreeSerializer.get_type_name(reduce_value[0]) + args, deserialized_args = _PythonTreeSerializer.unzip_list([ + self.serialize(arg) + for arg in reduce_value[1] + ]) + json_obj = { + 'id': id_, + 'type': _PythonTreeSerializer.get_type(py_object), + 'constructor': constructor, + 'args': args, + 'state': [], + 'listitems': [], + 'dictitems': [], + } + deserialized_obj = reduce_value[0](*deserialized_args) + memory_obj = self.save_to_memory(id_, json_obj, deserialized_obj) + + state, deserialized_state = self.unzip_dict([ + (attr, self.serialize(value)) + for attr, value in reduce_value[2].items() + ], skip_first=True) + listitems, deserialized_listitems = self.unzip_list([ + self.serialize(item) + for item in reduce_value[3] + ]) + dictitems, deserialized_dictitems = self.unzip_dict([ + (self.serialize(key), self.serialize(value)) + for key, value in reduce_value[4] + ]) + + memory_obj.json['state'] = state + memory_obj.json['listitems'] = listitems + memory_obj.json['dictitems'] = dictitems + + for key, value in deserialized_state.items(): + setattr(deserialized_obj, key, value) + for item in deserialized_listitems: + deserialized_obj.append(item) + for key, value in deserialized_dictitems.items(): + deserialized_obj[key] = value + + memory_obj.deserialized_obj = deserialized_obj + memory_obj.is_draft = False + + return id_, deserialized_obj + + def serialize(self, py_object): + type_ = _PythonTreeSerializer.get_type(py_object) + id_ = id(py_object) + skip_comparable = False + comparable = True + + if id_ in self.memory: + value = id_ + strategy = 'memory' + skip_comparable = True + comparable = False + deserialized_obj = self.memory[id_].deserialized_obj + if not self.memory[id_].is_draft: + self.memory[id_].comparable = py_object == deserialized_obj + skip_comparable = False + elif isinstance(py_object, type): + value = _PythonTreeSerializer.get_type_name(py_object) + strategy = 'repr' + deserialized_obj = py_object + elif any(type(py_object) == t for t in (list, set, tuple)): + elements = [ + self.serialize(element) for element in py_object + ] + value, deserialized_obj = _PythonTreeSerializer.unzip_list(elements, type(py_object)) + comparable = all([element['comparable'] for element in value]) + strategy = 'generic' + elif type(py_object) == dict: + elements = [ + [self.serialize(key), self.serialize(value)] + for key, value in py_object.items() + ] + value, deserialized_obj = _PythonTreeSerializer.unzip_dict(elements) + comparable = all([element[1]['comparable'] for element in value]) + strategy = 'generic' + elif _PythonTreeSerializer.has_reduce(py_object): + value, deserialized_obj = self.get_reduce(py_object) + strategy = 'memory' + else: + value = repr(py_object) + try: + deserialized_obj = pickle.loads(pickle.dumps(py_object)) + except Exception: + deserialized_obj = py_object + skip_comparable = True + comparable = False + strategy = 'repr' + + if not skip_comparable: + try: + comparable = comparable and (py_object == deserialized_obj) + except Exception: + comparable = False + + return { + 'type': type_, + 'value': value, + 'strategy': strategy, + 'comparable': comparable, + }, deserialized_obj + + @staticmethod + def unzip_list(elements, cast_second=list): + if len(elements) == 0: + first, second = [], [] + else: + first, second = list(zip(*elements)) + return first, cast_second(second) + + @staticmethod + def unzip_dict(elements, cast_second=dict, skip_first=False): + if len(elements) == 0: + first, second = [], [] + else: + if skip_first: + first = [[element[0], element[1][0]] for element in elements] + second = [[element[0], element[1][1]] for element in elements] + else: + first = [[element[0][0], element[1][0]] for element in elements] + second = [[element[0][1], element[1][1]] for element in elements] + return first, cast_second(second) + + def dumps(self, obj): + return { + 'json': self.serialize(obj)[0], + 'memory': { + key: value.json + for key, value in self.memory.items() + } + } diff --git a/utbot-python/src/main/resources/requirements.txt b/utbot-python/src/main/resources/requirements.txt new file mode 100644 index 0000000000..60cf8c7abe --- /dev/null +++ b/utbot-python/src/main/resources/requirements.txt @@ -0,0 +1,4 @@ +mypy==0.971 +astor +typeshed-client +coverage \ No newline at end of file diff --git a/utbot-python/src/main/resources/typeshed_stub.py b/utbot-python/src/main/resources/typeshed_stub.py new file mode 100644 index 0000000000..7d77a113a2 --- /dev/null +++ b/utbot-python/src/main/resources/typeshed_stub.py @@ -0,0 +1,322 @@ +import ast +import importlib +import json +import sys +import os + +import mypy.fastparse + +from contextlib import contextmanager +from collections import defaultdict + +import astor +from typeshed_client import get_stub_names, get_search_context, OverloadedName + + +def normalize_annotation(annotation, module_of_annotation): + def walk_mypy_type(mypy_type): + try: + prefix = f'{module_of_annotation}.' if len(module_of_annotation) > 0 else '' + + if mypy_type.name[:len(prefix)] == prefix: + name = mypy_type.name[len(prefix):] + else: + name = mypy_type.name + + if eval(name) is None: + result = "types.NoneType" + else: + modname = eval(name).__module__ + result = f'{modname}.{name}' + + except Exception as e: + result = 'typing.Any' + + if hasattr(mypy_type, 'args') and len(mypy_type.args) != 0: + arg_strs = [ + walk_mypy_type(arg) + for arg in mypy_type.args + ] + result += f"[{', '.join(arg_strs)}]" + + return result + + mod = importlib.import_module(module_of_annotation) + + for name in dir(mod): + globals()[name] = getattr(mod, name) + + mypy_type_ = mypy.fastparse.parse_type_string(annotation, annotation, -1, -1) + return walk_mypy_type(mypy_type_) + + +class AstClassEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.ClassDef): + json_dump = { + 'className': o.name, + 'methods': [], + 'fields': [], + } + + def _function_statements_handler(_statement): + if isinstance(_statement, ast.FunctionDef): + method = AstFunctionDefEncoder().default(_statement) + is_property = method['is_property'] + del method['is_property'] + if is_property: + del method['args'] + del method['kwonlyargs'] + + method['annotation'] = method['returns'] + del method['returns'] + + json_dump['fields'].append(method) + else: + json_dump['methods'].append(method) + if isinstance(_statement, ast.AnnAssign): + field = AstAnnAssignEncoder().default(_statement) + json_dump['fields'].append(field) + + for statement in o.body: + _function_statements_handler(statement) + + return json_dump + return json.JSONEncoder.default(self, o) + + +class AstAnnAssignEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.AnnAssign): + json_dump = { + 'name': '...' if isinstance(o.target, type(Ellipsis)) else o.target.id, + 'annotation': transform_annotation(o.annotation), + } + return json_dump + return json.JSONEncoder.default(self, o) + + +def find_init_method(function_ast): + for statement in function_ast.body: + if isinstance(statement, ast.FunctionDef) and statement.name == '__init__': + return statement + return None + + +class AstFunctionDefEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, (ast.FunctionDef, ast.AsyncFunctionDef)): + json_dump = { + 'name': o.name, + 'returns': transform_annotation(o.returns), + 'args': [ + AstArgEncoder().default(arg) + for arg in o.args.args + ], + 'kwonlyargs': [ + AstArgEncoder().default(arg) + for arg in o.args.kwonlyargs + ], + 'is_property': function_is_property(o), + } + return json_dump + + +def function_is_property(function): + return bool(any([ + 'property' == astor.code_gen.to_source(decorator).strip() + for decorator in function.decorator_list + ])) + + +class AstArgEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.arg): + json_dump = { + 'arg': o.arg, + 'annotation': transform_annotation(o.annotation) + } + return json_dump + + +class AstConstantEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.Constant): + json_dump = '...' if isinstance(o.value, type(Ellipsis)) else o.value + + return json_dump + if isinstance(o, type(Ellipsis)): + return '...' + if o is None: + return None + + +def transform_annotation(annotation): + return '' if annotation is None else astor.code_gen.to_source(annotation).strip() + + +def recursive_normalize_annotations(json_data, module_name): + if 'annotation' in json_data: + json_data['annotation'] = normalize_annotation( + annotation=json_data['annotation'], + module_of_annotation=module_name + ) + elif 'returns' in json_data: + json_data['returns'] = normalize_annotation( + annotation=json_data['returns'], + module_of_annotation=module_name + ) + json_data['args'] = [ + recursive_normalize_annotations(arg, module_name) + for arg in json_data['args'] + ] + json_data['kwonlyargs'] = [ + recursive_normalize_annotations(arg, module_name) + for arg in json_data['kwonlyargs'] + ] + elif 'className' in json_data: + for key, value in json_data.items(): + if key in {'methods', 'fields'}: + json_data[key] = [ + recursive_normalize_annotations(elem, module_name) + for elem in value + ] + else: + for key, value in json_data.items(): + json_data[key] = [ + recursive_normalize_annotations(elem, module_name) + for elem in value + ] + + return json_data + + +class StubFileCollector: + def __init__(self, python_version): + self.methods_dataset = defaultdict(list) + self.fields_dataset = defaultdict(list) + self.functions_dataset = defaultdict(list) + self.classes_dataset = [] + self.assigns_dataset = defaultdict(list) + self.ann_assigns_dataset = defaultdict(list) + self.python_version = python_version + self.visited_modules = [] + + def create_module_table(self, module_name): + self.visited_modules.append(module_name) + + stub = get_stub_names( + module_name, + search_context=get_search_context(version=self.python_version) + ) + + def _ast_handler(ast_): + if isinstance(ast_, OverloadedName): + for definition in ast_.definitions: + _ast_handler(definition) + else: + if isinstance(ast_, ast.ClassDef): + json_data = AstClassEncoder().default(ast_) + recursive_normalize_annotations(json_data, module_name) + + if not ast_.name.startswith('_'): + class_name = f'{module_name}.{ast_.name}' + json_data['className'] = class_name + self.classes_dataset.append(json_data) + + for method in json_data['methods']: + method['className'] = class_name + self.methods_dataset[method['name']].append(method) + + for field in json_data['fields']: + field['className'] = class_name + self.fields_dataset[field['name']].append(field) + + elif isinstance(ast_, (ast.FunctionDef, ast.AsyncFunctionDef)): + json_data = AstFunctionDefEncoder().default(ast_) + recursive_normalize_annotations(json_data, module_name) + + function_name = f'{module_name}.{ast_.name}' + json_data['name'] = function_name + json_data['className'] = None + self.functions_dataset[ast_.name].append(json_data) + + else: + pass + + ast_nodes = set() + + if stub is None: + return + + for name, name_info in stub.items(): + ast_nodes.add(name_info.ast.__class__.__name__) + _ast_handler(name_info.ast) + + def save_method_annotations(self): + return json.dumps({ + 'classAnnotations': self.classes_dataset, + 'fieldAnnotations': defaultdict_to_array(self.fields_dataset), + 'functionAnnotations': defaultdict_to_array(self.functions_dataset), + 'methodAnnotations': defaultdict_to_array(self.methods_dataset), + }) + + +def defaultdict_to_array(dataset): + return [ + { + 'name': name, + 'definitions': types, + } + for name, types in dataset.items() + ] + + +def parse_submodule(module_name, collector_): + collector_.create_module_table(module_name) + try: + submodules = [ + f'{module_name}.{submodule}' if module_name != 'builtins' else submodule + for submodule in importlib.import_module(module_name).__dir__() + ] + for submodule in submodules: + if type(eval(submodule)) == 'module' and submodule not in collector_.visited_modules: + parse_submodule(submodule, collector_) + except ModuleNotFoundError: + pass + except ImportError: + pass + except NameError: + pass + except AttributeError: + pass + + +@contextmanager +def suppress_stdout(): + with open(os.devnull, "w") as devnull: + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = devnull + sys.stderr = devnull + try: + yield + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + +def main(): + python_version = sys.version_info + modules = sys.argv[1:] + with suppress_stdout(): + collector = StubFileCollector((python_version.major, python_version.minor)) + for module in modules: + parse_submodule(module, collector) + result = collector.save_method_annotations() + sys.stdout.write(result) + + +if __name__ == '__main__': + main() + sys.exit(0) diff --git a/utbot-ui-commons/build.gradle.kts b/utbot-ui-commons/build.gradle.kts new file mode 100644 index 0000000000..cdedf344a8 --- /dev/null +++ b/utbot-ui-commons/build.gradle.kts @@ -0,0 +1,57 @@ +val kotlinLoggingVersion: String by rootProject +val ideType: String by rootProject +val ideVersion: String by rootProject +val kotlinPluginVersion: String by rootProject +val semVer: String? by rootProject +val androidStudioPath: String? by rootProject + +plugins { + id("org.jetbrains.intellij") version "1.7.0" +} +project.tasks.asMap["runIde"]?.enabled = false + +intellij { + version.set(ideVersion) + type.set(ideType) + + plugins.set(listOf( + "java", + "org.jetbrains.kotlin:$kotlinPluginVersion", + "org.jetbrains.android" + )) +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + runIde { + jvmArgs("-Xmx2048m") + jvmArgs("--add-exports", "java.desktop/sun.awt.windows=ALL-UNNAMED") + androidStudioPath?.let { ideDir.set(file(it)) } + } + + patchPluginXml { + sinceBuild.set("212") + untilBuild.set("222.*") + version.set(semVer) + } +} + +dependencies { + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation(group = "org.jetbrains", name = "annotations", version = "16.0.2") + implementation(project(":utbot-api")) + implementation(project(":utbot-framework")) + implementation(group = "org.slf4j", name = "slf4j-api", version = "1.7.25") +} diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt new file mode 100644 index 0000000000..82080bd233 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt @@ -0,0 +1,46 @@ +package org.utbot.intellij.plugin.language.agnostic + +import mu.KotlinLogging +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys + +private val logger = KotlinLogging.logger {} + +abstract class LanguageAssistant { + + abstract fun update(e: AnActionEvent) + abstract fun actionPerformed(e: AnActionEvent) + + companion object { + private val languages = mutableMapOf() + + fun get(e: AnActionEvent): LanguageAssistant? { + e.getData(CommonDataKeys.PSI_FILE)?.language?.let { language -> + if (!languages.containsKey(language.id)) { + loadWithException(language)?.let { + languages.put(language.id, it.kotlin.objectInstance as LanguageAssistant) + } + } + return languages[language.id] + } + return null + } + } +} + +private fun loadWithException(language: Language): Class<*>? { + try { + return when (language.id) { + "Python" -> Class.forName("org.utbot.intellij.plugin.language.python.PythonLanguageAssistant") + "ECMAScript 6" -> Class.forName("org.utbot.intellij.plugin.language.js.JsLanguageAssistant") + "JAVA", "kotlin" -> Class.forName("org.utbot.intellij.plugin.language.JvmLanguageAssistant") + else -> error("Unknown language id: ${language.id}") + } + } catch (e: ClassNotFoundException) { + logger.info("Language ${language.id} is disabled") + } catch (e: IllegalStateException) { + logger.info("Language ${language.id} is not supported") + } + return null +} \ No newline at end of file diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt new file mode 100644 index 0000000000..bf517cdca7 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt @@ -0,0 +1,57 @@ +package org.utbot.intellij.plugin.models + +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile +import com.intellij.psi.PsiClass +import org.jetbrains.kotlin.idea.core.getPackage +import org.jetbrains.kotlin.idea.util.projectStructure.allModules +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.intellij.plugin.ui.utils.TestSourceRoot +import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle +import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots + +val PsiClass.packageName: String get() = this.containingFile.containingDirectory.getPackage()?.qualifiedName ?: "" + +open class BaseTestsModel( + val project: Project, + val srcModule: Module, + val potentialTestModules: List, + var srcClasses: Set = emptySet(), +) { + // GenerateTestsModel is supposed to be created with non-empty list of potentialTestModules. + // Otherwise, the error window is supposed to be shown earlier. + var testModule: Module = potentialTestModules.firstOrNull() ?: error("Empty list of test modules in model") + + var testSourceRoot: VirtualFile? = null + var testPackageName: String? = null + open lateinit var codegenLanguage: CodegenLanguage + + fun setSourceRootAndFindTestModule(newTestSourceRoot: VirtualFile?) { + requireNotNull(newTestSourceRoot) + testSourceRoot = newTestSourceRoot + var target = newTestSourceRoot + while (target != null && target is FakeVirtualFile) { + target = target.parent + } + if (target == null) { + error("Could not find module for $newTestSourceRoot") + } + + testModule = ModuleUtil.findModuleForFile(target, project) + ?: error("Could not find module for $newTestSourceRoot") + } + + val isMultiPackage: Boolean by lazy { + srcClasses.map { it.packageName }.distinct().size != 1 + } + + fun getAllTestSourceRoots() : MutableList { + with(if (project.isBuildWithGradle) project.allModules() else potentialTestModules) { + return this.flatMap { it.suitableTestSourceRoots().toList() }.toMutableList() + } + } + +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt similarity index 100% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt similarity index 88% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt index 24b6b0a195..48e46a1e3d 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt @@ -5,7 +5,7 @@ import javax.swing.DefaultListCellRenderer import javax.swing.JList import org.utbot.framework.plugin.api.CodeGenerationSettingItem -internal class CodeGenerationSettingItemRenderer : DefaultListCellRenderer() { +class CodeGenerationSettingItemRenderer : DefaultListCellRenderer() { override fun getListCellRendererComponent( list: JList<*>?, value: Any?, diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt similarity index 92% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt index a18c4db402..222d016167 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt @@ -20,14 +20,13 @@ import javax.swing.DefaultComboBoxModel import javax.swing.JList import org.jetbrains.kotlin.idea.util.rootManager import org.utbot.common.PathUtil -import org.utbot.intellij.plugin.generator.CodeGenerationController.getAllTestSourceRoots -import org.utbot.intellij.plugin.models.GenerateTestsModel +import org.utbot.intellij.plugin.models.BaseTestsModel import org.utbot.intellij.plugin.ui.utils.TestSourceRoot import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot import org.utbot.intellij.plugin.ui.utils.dedicatedTestSourceRootName import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle -class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : +class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) : ComponentWithBrowseButton>(ComboBox(), null) { private val SET_TEST_FOLDER = "set test folder" @@ -87,7 +86,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : } } - private fun GenerateTestsModel.getSortedTestRoots(): MutableList { + private fun BaseTestsModel.getSortedTestRoots(): MutableList { var commonModuleSourceDirectory = "" for ((i, sourceRoot) in srcModule.rootManager.sourceRoots.withIndex()) { commonModuleSourceDirectory = if (i == 0) { @@ -114,7 +113,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : ).toMutableList() } - private fun chooseTestRoot(model: GenerateTestsModel): VirtualFile? = + private fun chooseTestRoot(model: BaseTestsModel): VirtualFile? = ReadAction.compute { val desc = object:FileChooserDescriptor(false, true, false, false, false, false) { override fun isFileSelectable(file: VirtualFile?): Boolean { @@ -139,7 +138,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : childComponent.model = DefaultComboBoxModel(ArrayUtil.toObjectArray(comboItems)) } - private fun formatUrl(virtualFile: VirtualFile, model: GenerateTestsModel): String { + private fun formatUrl(virtualFile: VirtualFile, model: BaseTestsModel): String { var directoryUrl = if (virtualFile is FakeVirtualFile) { virtualFile.parent.presentableUrl + File.separatorChar + virtualFile.name } else { diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt new file mode 100644 index 0000000000..12ced23966 --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestSourceDirectoryChooser.kt @@ -0,0 +1,53 @@ +package org.utbot.intellij.plugin.ui.components + +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.ui.TextBrowseFolderListener +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.vfs.VirtualFile +import java.nio.file.Paths +import org.utbot.common.PathUtil.replaceSeparator +import org.utbot.intellij.plugin.models.BaseTestsModel + +class TestSourceDirectoryChooser( + val model: BaseTestsModel, + file: VirtualFile +) : TextFieldWithBrowseButton() { + private val projectRoot = getContentRoot(model.project, file) + + init { + val descriptor = FileChooserDescriptor( + false, + true, + false, + false, + false, + false + ) + descriptor.setRoots(projectRoot) + addBrowseFolderListener( + TextBrowseFolderListener(descriptor, model.project) + ) + text = replaceSeparator(Paths.get(projectRoot.path, defaultDirectory).toString()) + } + + fun validatePath(): ValidationInfo? { + val typedPath = Paths.get(text).toAbsolutePath() + return if (typedPath.startsWith(replaceSeparator(projectRoot.path))) { + defaultDirectory = Paths.get(projectRoot.path).relativize(typedPath).toString() + null + } else + ValidationInfo("Specified directory lies outside of the project", this) + } + + private fun getContentRoot(project: Project, file: VirtualFile): VirtualFile { + return ProjectFileIndex.getInstance(project) + .getContentRootForFile(file) ?: error("Source file lies outside of a module") + } + + companion object { + private var defaultDirectory = "utbot_tests" + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt similarity index 67% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt index 948bb9344a..05472409d9 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt @@ -8,4 +8,10 @@ fun showErrorDialogLater(project: Project, message: String, title: String) { invokeLater { Messages.showErrorDialog(project, message, title) } +} + +fun showWarningDialogLater(project: Project, message: String, title: String) { + invokeLater { + Messages.showWarningDialog(project, message, title) + } } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt similarity index 98% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt index c2621c40f0..3af0593f66 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt @@ -159,9 +159,9 @@ private fun Module.suitableTestSourceFolders(): List { private val GRADLE_SYSTEM_ID = ProjectSystemId("GRADLE") val Project.isBuildWithGradle get() = - ModuleManager.getInstance(this).modules.any { - ExternalSystemApiUtil.isExternalSystemAwareModule(GRADLE_SYSTEM_ID, it) - } + ModuleManager.getInstance(this).modules.any { + ExternalSystemApiUtil.isExternalSystemAwareModule(GRADLE_SYSTEM_ID, it) + } const val dedicatedTestSourceRootName = "utbot_tests" diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt similarity index 97% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt index 4d7d19c28f..e687656721 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt @@ -1,6 +1,5 @@ package org.utbot.intellij.plugin.ui.utils -import org.utbot.framework.plugin.api.CodegenLanguage import com.intellij.openapi.roots.SourceFolder import org.jetbrains.jps.model.java.JavaResourceRootProperties import org.jetbrains.jps.model.java.JavaResourceRootType @@ -11,6 +10,7 @@ import org.jetbrains.kotlin.config.ResourceKotlinRootType import org.jetbrains.kotlin.config.SourceKotlinRootType import org.jetbrains.kotlin.config.TestResourceKotlinRootType import org.jetbrains.kotlin.config.TestSourceKotlinRootType +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.intellij.plugin.util.IntelliJApiHelper val sourceRootTypes: Set> = setOf(JavaSourceRootType.SOURCE, SourceKotlinRootType) @@ -25,6 +25,7 @@ fun CodegenLanguage.testRootType(): JpsModuleSourceRootType JavaSourceRootType.TEST_SOURCE CodegenLanguage.KOTLIN -> TestSourceKotlinRootType + else -> TestSourceKotlinRootType } /** @@ -34,6 +35,7 @@ fun CodegenLanguage.testResourcesRootType(): JpsModuleSourceRootType JavaResourceRootType.TEST_RESOURCE CodegenLanguage.KOTLIN -> TestResourceKotlinRootType + else -> TestResourceKotlinRootType } /** diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt similarity index 100% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/util/IntelliJApiHelper.kt