diff --git a/gradle.properties b/gradle.properties index 4a172b8ed8..b2b8a29a9d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,7 @@ ideVersion=222.4167.29 pythonIde=IC,IU,PC,PY jsIde=IU,PY,WS +goIde=IU # In order to run Android Studion instead of Intellij Community, specify the path to your Android Studio installation #androidStudioPath=your_path_to_android_studio @@ -15,6 +16,9 @@ jsIde=IU,PY,WS pythonCommunityPluginVersion=222.4167.37 pythonUltimatePluginVersion=222.4167.37 +# Version numbers: https://plugins.jetbrains.com/plugin/9568-go/versions +goPluginVersion=222.4167.21 + kotlinPluginVersion=222-1.7.20-release-201-IJ4167.29 junit5Version=5.8.0-RC1 diff --git a/settings.gradle.kts b/settings.gradle.kts index 3480b63ceb..269100a835 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ val ideType: String by settings val pythonIde: String by settings val jsIde: String by settings val includeRiderInBuild: String by settings +val goIde: String by settings pluginManagement { resolutionStrategy { @@ -59,3 +60,9 @@ if (jsIde.split(",").contains(ideType)) { include("utbot-cli-js") include("utbot-intellij-js") } + +if (goIde.split(",").contains(ideType)) { + include("utbot-go") + include("utbot-cli-go") + include("utbot-intellij-go") +} diff --git a/utbot-cli-go/build.gradle b/utbot-cli-go/build.gradle new file mode 100644 index 0000000000..277f6881ce --- /dev/null +++ b/utbot-cli-go/build.gradle @@ -0,0 +1,77 @@ +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-go') + + // 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') + + implementation 'com.beust:klaxon:5.5' // to read and write JSON +} + +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.go.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.go' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot Go 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-go/src/main/kotlin/org/utbot/cli/go/Application.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt new file mode 100644 index 0000000000..2492442c23 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt @@ -0,0 +1,36 @@ +package org.utbot.cli.go + +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 +import org.utbot.cli.go.commands.GenerateGoTestsCommand +import org.utbot.cli.go.commands.RunGoTestsCommand + +class UtBotCli : CliktCommand(name = "UnitTestBot Go 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 { + UtBotCli().subcommands( + GenerateGoTestsCommand(), + RunGoTestsCommand(), + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt new file mode 100644 index 0000000000..55c97cb554 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt @@ -0,0 +1,13 @@ +package org.utbot.cli.go.commands + +import com.beust.klaxon.Json + +internal data class Position(@Json(index = 1) val line: Int, @Json(index = 2) val column: Int) + +internal data class CodeRegion(@Json(index = 1) val start: Position, @Json(index = 2) val end: Position) + +internal data class CoveredSourceFile( + @Json(index = 1) val sourceFileName: String, + @Json(index = 2) val covered: List, + @Json(index = 3) val uncovered: List +) \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt new file mode 100644 index 0000000000..b77bda075e --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt @@ -0,0 +1,106 @@ +package org.utbot.cli.go.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.long +import mu.KotlinLogging +import org.utbot.cli.go.logic.CliGoUtTestsGenerationController +import org.utbot.cli.go.util.durationInMillis +import org.utbot.cli.go.util.now +import org.utbot.cli.go.util.toAbsolutePath +import org.utbot.go.logic.GoUtTestsGenerationConfig +import java.nio.file.Files +import java.nio.file.Paths + +private val logger = KotlinLogging.logger {} + +class GenerateGoTestsCommand : + CliktCommand(name = "generateGo", help = "Generates tests for the specified Go source file") { + + private val sourceFile: String by option( + "-s", "--source", + help = "Specifies Go source file to generate tests for" + ) + .required() + .check("Must exist and ends with *.go suffix") { + it.endsWith(".go") && Files.exists(Paths.get(it)) + } + + private val selectedFunctionsNames: List by option( + "-f", "--function", + help = StringBuilder() + .append("Specifies function name to generate tests for. ") + .append("Can be used multiple times to select multiple functions at the same time.") + .toString() + ) + .multiple(required = true) + + private val goExecutablePath: String by option( + "-go", "--go-path", + help = "Specifies path to Go executable. For example, it could be [/usr/local/go/bin/go] for some systems" + ) + .required() // TODO: attempt to find it if not specified + + private val eachFunctionExecutionTimeoutMillis: Long by option( + "-et", "--each-execution-timeout", + help = StringBuilder() + .append("Specifies a timeout in milliseconds for each fuzzed function execution.") + .append("Default is ${GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS} ms") + .toString() + ) + .long() + .default(GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS) + .check("Must be positive") { it > 0 } + + private val allFunctionExecutionTimeoutMillis: Long by option( + "-at", "--all-execution-timeout", + help = StringBuilder() + .append("Specifies a timeout in milliseconds for all fuzzed function execution.") + .append("Default is ${GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS} ms") + .toString() + ) + .long() + .default(GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS) + .check("Must be positive") { it > 0 } + + private val printToStdOut: Boolean by option( + "-p", + "--print-test", + help = "Specifies whether a test should be printed out to StdOut. Is disabled by default" + ) + .flag(default = false) + + private val overwriteTestFiles: Boolean by option( + "-w", + "--overwrite", + help = "Specifies whether to overwrite the output test file if it already exists. Is disabled by default" + ) + .flag(default = false) + + override fun run() { + val sourceFileAbsolutePath = sourceFile.toAbsolutePath() + val goExecutableAbsolutePath = goExecutablePath.toAbsolutePath() + + val testsGenerationStarted = now() + logger.info { "Test file generation for [$sourceFile] - started" } + try { + CliGoUtTestsGenerationController( + printToStdOut = printToStdOut, + overwriteTestFiles = overwriteTestFiles + ).generateTests( + mapOf(sourceFileAbsolutePath to selectedFunctionsNames), + GoUtTestsGenerationConfig( + goExecutableAbsolutePath, + eachFunctionExecutionTimeoutMillis, + allFunctionExecutionTimeoutMillis + ) + ) + } catch (t: Throwable) { + logger.error { "An error has occurred while generating test for snippet $sourceFile: $t" } + throw t + } finally { + val duration = durationInMillis(testsGenerationStarted) + logger.info { "Test file generation for [$sourceFile] - completed in [$duration] (ms)" } + } + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt new file mode 100644 index 0000000000..f8a5773655 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt @@ -0,0 +1,223 @@ +package org.utbot.cli.go.commands + +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.go.util.* +import org.utbot.go.util.convertObjectToJsonString +import java.io.File + +private val logger = KotlinLogging.logger {} + +class RunGoTestsCommand : CliktCommand(name = "runGo", help = "Runs tests for the specified Go package") { + + private val packageDirectory: String by option( + "-p", "--package", + help = "Specifies Go package to run tests for" + ) + .required() + .check("Must exist and be directory") { + File(it).let { file -> file.exists() && file.isDirectory } + } + + private val goExecutablePath: String by option( + "-go", "--go-path", + help = "Specifies path to Go executable. For example, it could be [/usr/local/go/bin/go] for some systems" + ) + .required() // TODO: attempt to find it if not specified + + private val verbose: Boolean by option( + "-v", "--verbose", + help = "Specifies whether an output should be verbose. Is disabled by default" + ) + .flag(default = false) + + private val json: Boolean by option( + "-j", "--json", + help = "Specifies whether an output should be in JSON format. Is disabled by default" + ) + .flag(default = false) + + private val output: String? by option( + "-o", "--output", + help = "Specifies output file for tests run report. Prints to StdOut by default" + ) + + private enum class CoverageMode(val displayName: String) { + REGIONS_HTML("html"), PERCENTS_BY_FUNCS("func"), REGIONS_JSON("json"); + + override fun toString(): String = displayName + + val fileExtensionValidator: (String) -> Boolean + get() = when (this) { + REGIONS_HTML -> { + { it.substringAfterLast('.') == "html" } + } + + REGIONS_JSON -> { + { it.substringAfterLast('.') == "json" } + } + + PERCENTS_BY_FUNCS -> { + { true } + } + } + } + + private val coverageMode: CoverageMode? by option( + "-cov-mode", "--coverage-mode", + help = StringBuilder() + .append("Specifies whether a test coverage report should be generated and defines its mode. ") + .append("Coverage report generation is disabled by default") + .toString() + ) + .choice( + CoverageMode.REGIONS_HTML.toString() to CoverageMode.REGIONS_HTML, + CoverageMode.PERCENTS_BY_FUNCS.toString() to CoverageMode.PERCENTS_BY_FUNCS, + CoverageMode.REGIONS_JSON.toString() to CoverageMode.REGIONS_JSON, + ) + .check( + StringBuilder() + .append("Test coverage report output file must be set ") + .append("and have an extension that matches the coverage mode") + .toString() + ) { mode -> + coverageOutput?.let { mode.fileExtensionValidator(it) } ?: false + } + + private val coverageOutput: String? by option( + "-cov-out", "--coverage-output", + help = "Specifies output file for test coverage report. Required if [--coverage-mode] is set" + ) + .check("Test coverage report mode must be specified") { + coverageMode != null + } + + override fun run() { + val runningTestsStarted = now() + try { + logger.debug { "Running tests for [$packageDirectory] - started" } + + /* run tests */ + + val packageDirectoryFile = File(packageDirectory).canonicalFile + + val coverProfileFile = if (coverageMode != null) { + createFile(createCoverProfileFileName()) + } else { + null + } + + try { + val runGoTestCommand = mutableListOf( + goExecutablePath.toAbsolutePath(), + "test", + "./" + ) + if (verbose) { + runGoTestCommand.add("-v") + } + if (json) { + runGoTestCommand.add("-json") + } + if (coverageMode != null) { + runGoTestCommand.add("-coverprofile") + runGoTestCommand.add(coverProfileFile!!.canonicalPath) + } + + val outputStream = if (output == null) { + System.out + } else { + createFile(output!!).outputStream() + } + executeCommandAndRedirectStdoutOrFail(runGoTestCommand, packageDirectoryFile, outputStream) + + /* generate coverage report */ + + val coverageOutputFile = coverageOutput?.let { createFile(it) } ?: return + + when (coverageMode) { + null -> { + return + } + + CoverageMode.REGIONS_HTML, CoverageMode.PERCENTS_BY_FUNCS -> { + val runToolCoverCommand = mutableListOf( + "go", + "tool", + "cover", + "-${coverageMode!!.displayName}", + coverProfileFile!!.canonicalPath, + "-o", + coverageOutputFile.canonicalPath + ) + executeCommandAndRedirectStdoutOrFail(runToolCoverCommand, packageDirectoryFile) + } + + CoverageMode.REGIONS_JSON -> { + val coveredSourceFiles = parseCoverProfile(coverProfileFile!!) + val jsonCoverage = convertObjectToJsonString(coveredSourceFiles) + coverageOutputFile.writeText(jsonCoverage) + } + } + } finally { + coverProfileFile?.delete() + } + } catch (t: Throwable) { + logger.error { "An error has occurred while running tests for [$packageDirectory]: $t" } + throw t + } finally { + val duration = durationInMillis(runningTestsStarted) + logger.debug { "Running tests for [$packageDirectory] - completed in [$duration] (ms)" } + } + } + + private fun createCoverProfileFileName(): String { + return "ut_go_cover_profile.out" + } + + private fun parseCoverProfile(coverProfileFile: File): List { + data class CoverageRegions( + val covered: MutableList, + val uncovered: MutableList + ) + + val coverageRegionsBySourceFilesNames = mutableMapOf() + + coverProfileFile.readLines().asSequence() + .drop(1) // drop "mode" value + .forEach { fullLine -> + val (sourceFileFullName, coverageInfoLine) = fullLine.split(":", limit = 2) + val sourceFileName = sourceFileFullName.substringAfterLast("/") + val (regionString, _, countString) = coverageInfoLine.split(" ", limit = 3) + + fun parsePosition(positionString: String): Position { + val (lineNumber, columnNumber) = positionString.split(".", limit = 2).asSequence() + .map { it.toInt() } + .toList() + return Position(lineNumber, columnNumber) + } + val (startString, endString) = regionString.split(",", limit = 2) + val region = CodeRegion(parsePosition(startString), parsePosition(endString)) + + val regions = coverageRegionsBySourceFilesNames.getOrPut(sourceFileName) { + CoverageRegions( + mutableListOf(), + mutableListOf() + ) + } + // it is called "count" in docs, but in reality it is like boolean for covered / uncovered + val count = countString.toInt() + if (count == 0) { + regions.uncovered.add(region) + } else { + regions.covered.add(region) + } + } + + return coverageRegionsBySourceFilesNames.map { (sourceFileName, regions) -> + CoveredSourceFile(sourceFileName, regions.covered, regions.uncovered) + } + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..c77768bf61 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt @@ -0,0 +1,120 @@ +package org.utbot.cli.go.logic + +import mu.KotlinLogging +import org.utbot.cli.go.util.durationInMillis +import org.utbot.cli.go.util.now +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.logic.AbstractGoUtTestsGenerationController +import java.io.File +import java.time.LocalDateTime + +private val logger = KotlinLogging.logger {} + +class CliGoUtTestsGenerationController( + private val printToStdOut: Boolean, + private val overwriteTestFiles: Boolean +) : AbstractGoUtTestsGenerationController() { + + private lateinit var currentStageStarted: LocalDateTime + + override fun onSourceCodeAnalysisStart(targetFunctionsNamesBySourceFiles: Map>): Boolean { + currentStageStarted = now() + logger.debug { "Source code analysis - started" } + + return true + } + + override fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean { + val stageDuration = durationInMillis(currentStageStarted) + logger.debug { "Source code analysis - completed in [$stageDuration] (ms)" } + + return handleMissingSelectedFunctions(analysisResults) + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean { + currentStageStarted = now() + logger.debug { "Test cases generation for [${sourceFile.fileName}] - started" } + + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + val stageDuration = durationInMillis(currentStageStarted) + logger.debug { + "Test cases generation for [${sourceFile.fileName}] functions - completed in [$stageDuration] (ms)" + } + + return true + } + + override fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + currentStageStarted = now() + logger.debug { "Test cases file code generation for [${sourceFile.fileName}] - started" } + + return true + } + + override fun onTestCasesFileCodeGenerationFinished(sourceFile: GoUtFile, generatedTestsFileCode: String): Boolean { + if (printToStdOut) { + logger.info { generatedTestsFileCode } + return true + } + writeGeneratedCodeToFile(sourceFile, generatedTestsFileCode) + + val stageDuration = durationInMillis(currentStageStarted) + logger.debug { + "Test cases file code generation for [${sourceFile.fileName}] functions - completed in [$stageDuration] (ms)" + } + + return true + } + + private fun handleMissingSelectedFunctions( + analysisResults: Map + ): Boolean { + val missingSelectedFunctionsListMessage = generateMissingSelectedFunctionsListMessage(analysisResults) + val okSelectedFunctionsArePresent = + analysisResults.any { (_, analysisResult) -> analysisResult.functions.isNotEmpty() } + + if (missingSelectedFunctionsListMessage != null) { + logger.warn { "Some selected functions were skipped during source code analysis.$missingSelectedFunctionsListMessage" } + } + if (!okSelectedFunctionsArePresent) { + throw Exception("Nothing to process. No functions were provided") + } + + return true + } + + private fun writeGeneratedCodeToFile(sourceFile: GoUtFile, generatedTestsFileCode: String) { + val testsFileNameWithExtension = createTestsFileNameWithExtension(sourceFile) + val testFile = File(sourceFile.absoluteDirectoryPath).resolve(testsFileNameWithExtension) + if (testFile.exists()) { + val alreadyExistsMessage = "File [${testFile.absolutePath}] already exists" + if (overwriteTestFiles) { + logger.warn { "$alreadyExistsMessage: it will be overwritten" } + } else { + logger.warn { "$alreadyExistsMessage: skipping test generation for [${sourceFile.fileName}]" } + return + } + } + testFile.writeText(generatedTestsFileCode) + } + + private fun createTestsFileNameWithExtension(sourceFile: GoUtFile) = + sourceFile.fileNameWithoutExtension + "_go_ut_test.go" +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt new file mode 100644 index 0000000000..2c4540b090 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt @@ -0,0 +1,14 @@ +package org.utbot.cli.go.util + +import java.io.File + +fun String.toAbsolutePath(): String = File(this).canonicalPath + +fun createFile(filePath: String): File = createFile(File(filePath).canonicalFile) + +fun createFile(file: File): File { + return file.also { + it.parentFile?.mkdirs() + it.createNewFile() + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt new file mode 100644 index 0000000000..220b2f0e5f --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt @@ -0,0 +1,12 @@ +package org.utbot.cli.go.util + +import java.io.InputStream +import java.io.OutputStream + +fun copy(from: InputStream, to: OutputStream?) { + val buffer = ByteArray(10240) + var len: Int + while (from.read(buffer).also { len = it } != -1) { + to?.write(buffer, 0, len) + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt new file mode 100644 index 0000000000..3dbeebd99b --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt @@ -0,0 +1,33 @@ +package org.utbot.cli.go.util + +import java.io.File +import java.io.InputStreamReader +import java.io.OutputStream + +fun executeCommandAndRedirectStdoutOrFail( + command: List, + workingDirectory: File? = null, + redirectStdoutToStream: OutputStream? = null // if null, stdout of process is suppressed +) { + val executedProcess = runCatching { + val process = ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectErrorStream(true) + .directory(workingDirectory) + .start() + copy(process.inputStream, redirectStdoutToStream) + process.waitFor() + process + }.getOrElse { + throw RuntimeException( + "Execution of [${command.joinToString(separator = " ")}] failed with throwable: $it" + ) + } + val exitCode = executedProcess.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(executedProcess.inputStream).readText() + throw RuntimeException( + "Execution of [${command.joinToString(separator = " ")}] failed with non-zero exit code = $exitCode:\n$processOutput" + ) + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt new file mode 100644 index 0000000000..c72601574b --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt @@ -0,0 +1,8 @@ +package org.utbot.cli.go.util + +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +fun now(): LocalDateTime = LocalDateTime.now() + +fun durationInMillis(started: LocalDateTime): Long = ChronoUnit.MILLIS.between(started, now()) \ No newline at end of file diff --git a/utbot-cli-go/src/main/resources/log4j2.xml b/utbot-cli-go/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..3d6ee82bcf --- /dev/null +++ b/utbot-cli-go/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-go/src/main/resources/version.properties b/utbot-cli-go/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-go/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-core/src/main/kotlin/org/utbot/common/FileUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt index 6cc649f37f..84ce9f0f0a 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt @@ -17,6 +17,7 @@ import java.util.zip.ZipFile import kotlin.concurrent.thread import kotlin.streams.asSequence import mu.KotlinLogging +import java.util.zip.ZipEntry fun Class<*>.toClassFilePath(): String { val name = requireNotNull(name) { "Class is local or anonymous" } @@ -30,12 +31,14 @@ object FileUtil { fun extractArchive( archiveFile: Path, destPath: Path, - vararg options: CopyOption = arrayOf(StandardCopyOption.REPLACE_EXISTING) + vararg options: CopyOption = arrayOf(StandardCopyOption.REPLACE_EXISTING), + extractOnlySuchEntriesPredicate: (ZipEntry) -> Boolean = { true } ) { Files.createDirectories(destPath) ZipFile(archiveFile.toFile()).use { archive -> val entries = archive.stream().asSequence() + .filter(extractOnlySuchEntriesPredicate) .sortedBy { it.name } .toList() @@ -77,7 +80,7 @@ object FileUtil { return createTempDirectory(utBotTempDirectory, prefix) } - fun createTempFile(prefix: String, suffix: String) : Path { + fun createTempFile(prefix: String, suffix: String): Path { return Files.createTempFile(utBotTempDirectory, prefix, suffix) } @@ -175,12 +178,26 @@ object FileUtil { /** * Extracts archive to temp directory and returns path to directory. */ - fun extractArchive(archiveFile: Path): Path { + fun extractArchive(archiveFile: Path, extractOnlySuchEntriesPredicate: (ZipEntry) -> Boolean = { true }): Path { val tempDir = createTempDirectory(TEMP_DIR_NAME).toFile().apply { deleteOnExit() } - extractArchive(archiveFile, tempDir.toPath()) + extractArchive(archiveFile, tempDir.toPath(), extractOnlySuchEntriesPredicate = extractOnlySuchEntriesPredicate) return tempDir.toPath() } + /** + * Extracts specified directory (with all contents) from archive to temp directory and returns path to it. + */ + fun extractDirectoryFromArchive(archiveFile: Path, directoryName: String): Path? { + val extractedJarDirectory = extractArchive(archiveFile) { entry -> + entry.name.normalizePath().startsWith(directoryName) + } + val extractedTargetDirectoryPath = extractedJarDirectory.resolve(directoryName) + if (!extractedTargetDirectoryPath.toFile().exists()) { + return null + } + return extractedTargetDirectoryPath + } + /** * Returns the path to the class files for the given ClassLocation. */ diff --git a/utbot-go/README.md b/utbot-go/README.md new file mode 100644 index 0000000000..90645ebe2a --- /dev/null +++ b/utbot-go/README.md @@ -0,0 +1,180 @@ +# UTBot Go + +## About project + +UTBot Go _**automatically generates unit tests for Go programs**_. Generated tests: + +* provide _high code coverage_ and, as a result, its reliability; +* fixate the current behavior of the code as _regression tests_. + +The core principles of UTBot Go are _**ease of use**_ and _**maximizing code coverage**_. + +*** + +_The project is currently under development._ + +## Features + +At the moment, only the _basic fuzzing technique_ is supported: namely, the execution of functions on predefined values, +depending on the type of parameter. + +At the moment, functions are supported, the parameters of which have _any primitive types_, _arrays_ and _structs_. + +For floating point types, _correct work with infinities and NaNs_ is also supported. + +Function result types are supported the same as for parameters, but with _support for types that implement `error`_. + +In addition, UTBot Go correctly captures not only errors returned by functions, but also _`panic` cases_. + +Examples of supported functions can be found [here](go-samples/simple/samples.go). + +## Important notes + +### Where are tests generated? + +It is true that in the described API it is currently almost impossible to customize the file in which the tests are +generated. By default, test generation results in the file `[name of source file]_go_ut_test.go` _located in the same +directory and Go package_ as the source file. + +In other words, tests are generated right next to the source code. But why? + +* Go was created for convenient and fast development, therefore it has appropriate guidelines: `Testing code typically + lives in the same package as the code it tests` ([source](https://gobyexample.com/testing)). For example, this + approach provides a clear file structure and allows you to run tests as simply and quickly as possible. +* Placing tests in the same package with the source code allows you to test private functions. Yes, this is not good + practice in programming in general: but, again, it allows you to develop in Go faster by automatically checking even + the internal implementation of the public API of the package via unit testing. +* This approach avoids problems with dependencies from imported packages etc. It's always nice not to have them, if + possible. + +Of course, Go has the ability to store tests away from the source code. In the future, it is planned to support this +functionality in the UTBot Go. + +However, the word `almost` in the first sentence of this section is not redundant at all, there is _a small hack_. When +using the `generateGo` CLI command, you can set the generated tests output mode to StdOut (`-p, --print-test` flag). +Then using, for example, bash primitives, you can redirect the output to an arbitrary file. Such a solution will not +solve possible problems with dependencies, but will automatically save the result of the generation in the right place. + +### Is there any specific structure of Go source files required? + +Yes, unfortunately or fortunately, it is required. Namely, the source code file for which the tests are generated _must +be in a Go project_ consisting of a module and packages. + +But do not be afraid! Go is designed for convenient and fast development, so _it's easy to start a Go +project_. For example, the [starter tutorial](https://go.dev/doc/tutorial/getting-started) of the language just +tells how to create the simplest project in Go. For larger projects, it is recommended to read a couple of sections of +the tutorial further: [Create a Go module](https://go.dev/doc/tutorial/create-module) +and [Call your code from another module](https://go.dev/doc/tutorial/call-module-code). + +To put it simply and briefly, in the simplest case, it is enough to use one call to the `go mod init` command. For more +complex ones, `go mod tidy` and `go mod edit` may come in handy. Finally, when developing in IntelliJ IDEA, you almost +don’t have to think about setting up a project: it will set everything up by itself. + +But _why does UTBot Go need a Go project_ and not enough files in a vacuum? The answer is simple — +dependencies. Go modules are designed to conveniently support project dependencies, which are simply listed in +the `go.mod` file. Thanks to it, modern Go projects are easy to reproduce and, importantly for UTBot Go, to test. + +In the future, it is planned to add the ability to accept arbitrary code as input to UTBot Go and generate the simplest +Go project automatically. + +## Install and use easily + +### IntelliJ IDEA plugin + +_Requirements:_ + +* `IntelliJ IDEA (Ultimate Edition)`, compatible with version `2022.2`; +* installed `Go SDK` version later than `1.18`; +* installed in IntelliJ IDEA [Go plugin](https://plugins.jetbrains.com/plugin/9568-go), compatible with the IDE + version (it is for this that the `Ultimate` edition of the IDE is needed); +* properly configured Go module for source code file (i.e. for file to generate tests for): corresponding `go.mod` file + must exist; +* installed Go modules `github.com/stretchr/testify/assert` and `golang.org/x/tools@v0.4.0`: fortunately, IDEA will automatically highlight + it and offer to install the first time the tests are generated. + +Most likely, if you are already developing Go project in IntelliJ IDEA, then you have already met all the requirements. + +_To install the UTBot Go plugin in IntelliJ IDEA:_ + +* just find the latest version of [UnitTestBot](https://plugins.jetbrains.com/plugin/19445-unittestbot) in the plugin + market; +* or download zip archive with `utbot-intellij JAR` + from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/3012565900) and install it in IntelliJ IDEA as + follows from plugins section (yes, you need to select the entire downloaded zip archive, it does not need to be + unpacked). + ![](docs/images/install-intellij-plugin-from-disk.png) + +Finally, you can _start using UTBot Go_: open any `.go` file in the IDE and press `alt + u, alt + t`. After +that, a window will appear in which you can configure the test generation settings and start running it in a couple +of clicks. + + +### CLI application + +_Requirements:_ + +* installed `Java SDK` version `11` or higher; +* installed `Go SDK` version later than `1.18`; +* properly configured Go module for source code file (i.e. for file to generate tests for): corresponding `go.mod` file + must exist; +* installed `gcc` and Go modules `github.com/stretchr/testify/assert` and `golang.org/x/tools@v0.4.0` to run tests. + +_To install the UTBot Go CLI application:_ download zip archive containing `utbot-cli JAR` +from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/3012565900), then extract its content (JAR file) to a +convenient location. + +Finally, you can _start using UTBot Go_ by running the extracted JAR on the command line. Two actions are +currently supported: `generateGo` and `runGo` for generating and running tests, respectively. + +For example, to find out about all options for actions, run the commands as follows +(`utbot-cli-2022.8-beta.jar` here is the path to the extracted JAR): + +```bash +java -jar utbot-cli-2022.8-beta.jar generateGo --help +``` + +or + +```bash +java -jar utbot-cli-2022.8-beta.jar runGo --help +``` + +respectively. + +_Action `generateGo` options:_ + +* `-s, --source TEXT`, _required_: specifies Go source file to generate tests for. +* `-f, --function TEXT`, _required_: specifies function name to generate tests for. Can be used multiple times to select multiple + functions at the same time. +* `-go, --go-path TEXT`, _required_: specifies path to Go executable. For example, it could be `/usr/local/go/bin/go` + for some systems. +* `-et, --each-execution-timeout INT`: specifies a timeout in milliseconds for each fuzzed function execution. Default is + `1000` ms. +* `-at, --all-execution-timeout INT`: specifies a timeout in milliseconds for all fuzzed function execution. Default is + `60000` ms. +* `-p, --print-test`: specifies whether a test should be printed out to StdOut. Is disabled by default. +* `-w, --overwrite`: specifies whether to overwrite the output test file if it already exists. Is disabled by default. +* `-h, --help`: show help message and exit. + +_Action `runGo` options:_ + +* `-p, --package TEXT`, _required_: specifies Go package to run tests for. +* `-go, --go-path TEXT`, _required_: specifies path to Go executable. For example, it could be `/usr/local/go/bin/go` + for some systems. +* `-v, --verbose`: specifies whether an output should be verbose. Is disabled by default. +* `-j, --json`: specifies whether an output should be in JSON format. Is disabled by default. +* `-o, --output TEXT`: specifies output file for tests run report. Prints to StdOut by default. +* `-cov-mode, --coverage-mode [html|func|json]`: specifies whether a test coverage report should be generated and + defines its mode. Coverage report generation is disabled by default. Examples of different coverage reports modes can + be found [here](go-samples/simple/reports). +* `-cov-out, --coverage-output TEXT`: specifies output file for test coverage report. Required if `[--coverage-mode]` is + set. + +## Contribute to UTBot Go + +If you want to _take part in the development_ of the project or _learn more_ about how it works, check +out [DEVELOPERS_GUIDE.md](docs/DEVELOPERS_GUIDE.md). + +For the current list of tasks, check out [FUTURE_PLANS.md](docs/FUTURE_PLANS.md). + +Your help and interest is greatly appreciated! diff --git a/utbot-go/build.gradle.kts b/utbot-go/build.gradle.kts new file mode 100644 index 0000000000..9a65fd5b1e --- /dev/null +++ b/utbot-go/build.gradle.kts @@ -0,0 +1,31 @@ +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("com.beust:klaxon:5.5") + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) +} \ No newline at end of file diff --git a/utbot-go/docs/DEVELOPERS_GUIDE.md b/utbot-go/docs/DEVELOPERS_GUIDE.md new file mode 100644 index 0000000000..f52693b896 --- /dev/null +++ b/utbot-go/docs/DEVELOPERS_GUIDE.md @@ -0,0 +1,67 @@ +# UTBot Go: Developers Guide + +# How UTBot Go works in general + +```mermaid +flowchart TB + A["Targets selection and configuration (IDEA plugin or CLI)"]:::someclass --> B(Go source code analysis) + classDef someclass fill:#b810 + + B --> C(Fuzzing) + C --> D(Fuzzed function execution) + D --> E(Getting results) + E --> C + C ---> F(Test file generation) +``` + + +In the diagram above, you can see _the main stages of the UTBot Go test generation pipeline_. Let's take a look at each +in more detail! + +### Targets selection and configuration + +This block in the diagram is highlighted in a separate color, since the action is mainly performed by the user. Namely, +the user selects the target source file and functions for which tests will be generated and configure generation +parameters (for example, fuzzed function execution timeout, path to Go executable, etc.). + +If UTBot Go is built as a plugin for IntelliJ IDEA, then the user opens the target Go source file and, using a keyboard +shortcut, calls up a dialog window where it's possible to select functions and configure settings. + +If UTBot Go is build as a CLI application, then the user sets the functions and parameters using command line flags. + +### Go source code analysis + +Once the user has chosen the target functions, it is necessary to collect information about them. Namely: their +signatures and information about types (in the basic version); constants in the body of functions and more (in the +future). This is exactly what happens at the stage of code analysis. As a result, UTBot Go gets an internal +representation of the target functions, sufficient for the subsequent generation of tests. + +### Fuzzing + +Fuzzing is the first part of the test cases generation process. At this stage, values, that will be used +to test the target functions, are generated. Namely, to be passed to functions as arguments in the next steps. +Then the result of function execution is analyzed and the generation of new values continues or stops. + +### Fuzzed function execution + +In the previous step, UTBot Go generated the values that the functions need to be tested with — now the task is to +do this. Namely, execute the functions with the values generated for them. + +Essentially, the target function, the values generated for it, and the result of its execution form a test case. In +fact, that is why this stage ends the global process of generating test cases. + +### Getting results + +Saving results of fuzzed function execution and sending them to fuzzing for analysis. + +### Test code generation + +Finally, the last stage: the test cases are ready, UTBot Go needs only to generate code for them. Need to carefully consider the features of the Go language (for example, the necessity to cast +constants to the desired type). + +_That's how the world (UTBot Go) works!_ + +## How to test UTBot Go + +_**TODO:**_ Gradle `runIde` task or building CLI application JAR locally. To build CLI version the `build` on `utbot-cli-go` should be called. + diff --git a/utbot-go/docs/FUTURE_PLANS.md b/utbot-go/docs/FUTURE_PLANS.md new file mode 100644 index 0000000000..d841237f73 --- /dev/null +++ b/utbot-go/docs/FUTURE_PLANS.md @@ -0,0 +1,13 @@ +# UTBot Go: Future plans + +## Primarily + +_**TODO**_ + +## Afterwards + +_**TODO**_ + +## Maybe in the future + +_**TODO**_ diff --git a/utbot-go/docs/images/install-intellij-plugin-from-disk.png b/utbot-go/docs/images/install-intellij-plugin-from-disk.png new file mode 100644 index 0000000000..1f0ceee756 Binary files /dev/null and b/utbot-go/docs/images/install-intellij-plugin-from-disk.png differ diff --git a/utbot-go/go-samples/go.mod b/utbot-go/go-samples/go.mod new file mode 100644 index 0000000000..96da2c797d --- /dev/null +++ b/utbot-go/go-samples/go.mod @@ -0,0 +1,11 @@ +module go-samples + +go 1.19 + +require github.com/stretchr/testify v1.8.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/utbot-go/go-samples/go.sum b/utbot-go/go-samples/go.sum new file mode 100644 index 0000000000..2ec90f70f8 --- /dev/null +++ b/utbot-go/go-samples/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/utbot-go/go-samples/simple/samples.go b/utbot-go/go-samples/simple/samples.go new file mode 100644 index 0000000000..4c6499c44f --- /dev/null +++ b/utbot-go/go-samples/simple/samples.go @@ -0,0 +1,120 @@ +// Most of the algorithm examples are taken from https://github.com/TheAlgorithms/Go + +package simple + +import ( + "errors" + "math" +) + +// DivOrPanic divides x by y or panics if y is 0 +func DivOrPanic(x int, y int) int { + if y == 0 { + panic("div by 0") + } + return x / y +} + +// Extended simple extended gcd +func Extended(a, b int64) (int64, int64, int64) { + if a == 0 { + return b, 0, 1 + } + gcd, xPrime, yPrime := Extended(b%a, a) + return gcd, yPrime - (b/a)*xPrime, xPrime +} + +func ArraySum(array [5]int) int { + sum := 0 + for _, elem := range array { + sum += elem + } + return sum +} + +func GenerateArrayOfIntegers(num int) [10]int { + result := [10]int{} + for i := range result { + result[i] = num + } + return result +} + +type Point struct { + x, y float64 +} + +func DistanceBetweenTwoPoints(a, b Point) float64 { + return math.Sqrt(math.Pow(a.x-b.x, 2) + math.Pow(a.y-b.y, 2)) +} + +func GetCoordinatesOfMiddleBetweenTwoPoints(a, b Point) (float64, float64) { + return (a.x + b.x) / 2, (a.y + b.y) / 2 +} + +func GetCoordinateSumOfPoints(points [10]Point) (float64, float64) { + sumX := 0.0 + sumY := 0.0 + for _, point := range points { + sumX += point.x + sumY += point.y + } + return sumX, sumY +} + +type Circle struct { + Center Point + Radius float64 +} + +func GetAreaOfCircle(circle Circle) float64 { + return math.Pi * math.Pow(circle.Radius, 2) +} + +func IsIdentity(matrix [3][3]int) bool { + for i := 0; i < 3; i++ { + for j := 0; j < 3; j++ { + if i == j && matrix[i][j] != 1 { + return false + } + + if i != j && matrix[i][j] != 0 { + return false + } + } + } + return true +} + +var ErrNotFound = errors.New("target not found in array") + +// Binary search for target within a sorted array by repeatedly dividing the array in half and comparing the midpoint with the target. +// This function uses recursive call to itself. +// If a target is found, the index of the target is returned. Else the function return -1 and ErrNotFound. +func Binary(array [10]int, target int, lowIndex int, highIndex int) (int, error) { + if highIndex < lowIndex { + return -1, ErrNotFound + } + mid := lowIndex + (highIndex-lowIndex)/2 + if array[mid] > target { + return Binary(array, target, lowIndex, mid-1) + } else if array[mid] < target { + return Binary(array, target, mid+1, highIndex) + } else { + return mid, nil + } +} + +func StringSearch(str string) bool { + if len(str) != 3 { + return false + } + if str[0] == 'A' { + if str[1] == 'B' { + if str[2] == 'C' { + return true + } + } + } + return false +} diff --git a/utbot-go/go-samples/simple/samples_go_ut_test.go b/utbot-go/go-samples/simple/samples_go_ut_test.go new file mode 100644 index 0000000000..e8d4d5a6fd --- /dev/null +++ b/utbot-go/go-samples/simple/samples_go_ut_test.go @@ -0,0 +1,188 @@ +package simple + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDivOrPanicByUtGoFuzzer(t *testing.T) { + actualVal := DivOrPanic(9223372036854775807, -1) + + assert.Equal(t, -9223372036854775807, actualVal) +} + +func TestDivOrPanicPanicsByUtGoFuzzer(t *testing.T) { + assert.PanicsWithValue(t, "div by 0", func() { DivOrPanic(9223372036854775807, 0) }) +} + +func TestExtendedByUtGoFuzzer1(t *testing.T) { + actualVal0, actualVal1, actualVal2 := Extended(9223372036854775807, -1) + + assertMultiple := assert.New(t) + assertMultiple.Equal(int64(-1), actualVal0) + assertMultiple.Equal(int64(0), actualVal1) + assertMultiple.Equal(int64(1), actualVal2) +} + +func TestExtendedByUtGoFuzzer2(t *testing.T) { + actualVal0, actualVal1, actualVal2 := Extended(0, 9223372036854775807) + + assertMultiple := assert.New(t) + assertMultiple.Equal(int64(9223372036854775807), actualVal0) + assertMultiple.Equal(int64(0), actualVal1) + assertMultiple.Equal(int64(1), actualVal2) +} + +func TestArraySumByUtGoFuzzer(t *testing.T) { + actualVal := ArraySum([5]int{-1, 0, 0, 0, 0}) + + assert.Equal(t, -1, actualVal) +} + +func TestGenerateArrayOfIntegersByUtGoFuzzer(t *testing.T) { + actualVal := GenerateArrayOfIntegers(9223372036854775807) + + assert.Equal(t, [10]int{9223372036854775807, 9223372036854775807, 9223372036854775807, 9223372036854775807, 9223372036854775807, 9223372036854775807, 9223372036854775807, 9223372036854775807, 9223372036854775807, 9223372036854775807}, actualVal) +} + +func TestDistanceBetweenTwoPointsByUtGoFuzzer(t *testing.T) { + actualVal := DistanceBetweenTwoPoints(Point{x: 0.730967787376657, y: 0.730967787376657}, Point{x: 0.730967787376657, y: 0.730967787376657}) + + assert.Equal(t, 0.0, actualVal) +} + +func TestGetCoordinatesOfMiddleBetweenTwoPointsByUtGoFuzzer(t *testing.T) { + actualVal0, actualVal1 := GetCoordinatesOfMiddleBetweenTwoPoints(Point{x: 0.24053641567148587, y: 0.24053641567148587}, Point{x: 0.24053641567148587, y: 0.24053641567148587}) + + assertMultiple := assert.New(t) + assertMultiple.Equal(0.24053641567148587, actualVal0) + assertMultiple.Equal(0.24053641567148587, actualVal1) +} + +func TestGetCoordinateSumOfPointsByUtGoFuzzer(t *testing.T) { + actualVal0, actualVal1 := GetCoordinateSumOfPoints([10]Point{{x: 0.6374174253501083, y: 0.6374174253501083}, {}, {}, {}, {}, {}, {}, {}, {}, {}}) + + assertMultiple := assert.New(t) + assertMultiple.Equal(0.6374174253501083, actualVal0) + assertMultiple.Equal(0.6374174253501083, actualVal1) +} + +func TestGetAreaOfCircleByUtGoFuzzer(t *testing.T) { + actualVal := GetAreaOfCircle(Circle{Center: Point{x: 0.5504370051176339, y: 0.5504370051176339}, Radius: 0.5504370051176339}) + + assert.Equal(t, 0.9518425589456255, actualVal) +} + +func TestIsIdentityByUtGoFuzzer1(t *testing.T) { + actualVal := IsIdentity([3][3]int{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}) + + assert.Equal(t, false, actualVal) +} + +func TestIsIdentityByUtGoFuzzer2(t *testing.T) { + actualVal := IsIdentity([3][3]int{{1, 0, -9223372036854775808}, {-9223372036854775808, 1, -9223372036854775808}, {9223372036854775807, -1, 9223372036854775805}}) + + assert.Equal(t, false, actualVal) +} + +func TestIsIdentityByUtGoFuzzer3(t *testing.T) { + actualVal := IsIdentity([3][3]int{{1, 0, 0}, {0, 0, 0}, {0, 0, 0}}) + + assert.Equal(t, false, actualVal) +} + +func TestIsIdentityByUtGoFuzzer4(t *testing.T) { + actualVal := IsIdentity([3][3]int{{1, 288230376151711745, 513}, {1, 9223372036854775807, 9223372036854775807}, {1, 129, 32769}}) + + assert.Equal(t, false, actualVal) +} + +func TestIsIdentityByUtGoFuzzer5(t *testing.T) { + actualVal := IsIdentity([3][3]int{{1, 0, 0}, {0, 1, 0}, {0, 0, 0}}) + + assert.Equal(t, false, actualVal) +} + +func TestIsIdentityByUtGoFuzzer6(t *testing.T) { + actualVal := IsIdentity([3][3]int{{1, 0, 0}, {1, 0, 0}, {0, 0, 0}}) + + assert.Equal(t, false, actualVal) +} + +func TestIsIdentityByUtGoFuzzer7(t *testing.T) { + actualVal := IsIdentity([3][3]int{{1, 0, 0}, {0, 1, 0}, {1, 0, 0}}) + + assert.Equal(t, false, actualVal) +} + +func TestIsIdentityByUtGoFuzzer8(t *testing.T) { + actualVal := IsIdentity([3][3]int{{1, 0, 0}, {0, 1, 0}, {0, 1, 0}}) + + assert.Equal(t, false, actualVal) +} + +func TestBinaryWithNonNilErrorByUtGoFuzzer1(t *testing.T) { + actualVal, actualErr := Binary([10]int{-9223372036854775808, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 9223372036854775807, 9223372036854775807, -1) + + assertMultiple := assert.New(t) + assertMultiple.Equal(-1, actualVal) + assertMultiple.ErrorContains(actualErr, "target not found in array") +} + +func TestBinaryWithNonNilErrorByUtGoFuzzer2(t *testing.T) { + actualVal, actualErr := Binary([10]int{9223372036854775807, -9223372036854775808, 0, 0, 0, 0, 0, 0, 0, 0}, -1, 1, 1) + + assertMultiple := assert.New(t) + assertMultiple.Equal(-1, actualVal) + assertMultiple.ErrorContains(actualErr, "target not found in array") +} + +func TestBinaryWithNonNilErrorByUtGoFuzzer3(t *testing.T) { + actualVal, actualErr := Binary([10]int{9223372036854775807, 0, 0, 0, 0, 0, 0, 0, 0, 0}, -1, 0, 0) + + assertMultiple := assert.New(t) + assertMultiple.Equal(-1, actualVal) + assertMultiple.ErrorContains(actualErr, "target not found in array") +} + +func TestBinaryByUtGoFuzzer4(t *testing.T) { + actualVal, actualErr := Binary([10]int{9223372036854775807, 1, 9223372036854775807, -1, 0, 0, 0, 0, 0, 0}, 9223372036854775807, 0, 1) + + assertMultiple := assert.New(t) + assertMultiple.Equal(0, actualVal) + assertMultiple.Nil(actualErr) +} + +func TestBinaryPanicsByUtGoFuzzer(t *testing.T) { + assert.PanicsWithError(t, "runtime error: index out of range [-9223372036854775808]", func() { Binary([10]int{-1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, -1, -9223372036854775808, 9223372036854775807) }) +} + +func TestStringSearchByUtGoFuzzer1(t *testing.T) { + actualVal := StringSearch("hello") + + assert.Equal(t, false, actualVal) +} + +func TestStringSearchByUtGoFuzzer2(t *testing.T) { + actualVal := StringSearch("elo") + + assert.Equal(t, false, actualVal) +} + +func TestStringSearchByUtGoFuzzer3(t *testing.T) { + actualVal := StringSearch("Am[") + + assert.Equal(t, false, actualVal) +} + +func TestStringSearchByUtGoFuzzer4(t *testing.T) { + actualVal := StringSearch("AB3") + + assert.Equal(t, false, actualVal) +} + +func TestStringSearchByUtGoFuzzer5(t *testing.T) { + actualVal := StringSearch("ABC") + + assert.Equal(t, true, actualVal) +} diff --git a/utbot-go/go-samples/simple/supported_types.go b/utbot-go/go-samples/simple/supported_types.go new file mode 100644 index 0000000000..1826169c72 --- /dev/null +++ b/utbot-go/go-samples/simple/supported_types.go @@ -0,0 +1,174 @@ +package simple + +import ( + "errors" + "github.com/pmezard/go-difflib/difflib" + "math" +) + +func WithoutParametersAndReturnValues() { + print("Hello World") +} + +func Int(n int) int { + if n < 0 { + return -n + } + return n +} + +func Int8(n int8) int8 { + if n < 0 { + return -n + } + return n +} + +func Int16(n int16) int16 { + if n < 0 { + return -n + } + return n +} + +func Int32(n int32) int32 { + if n < 0 { + return -n + } + return n +} + +func Int64(n int64) int64 { + if n < 0 { + return -n + } + return n +} + +func Uint(n uint) uint { + return n +} + +func Uint8(n uint8) uint8 { + return n +} + +func Uint16(n uint16) uint16 { + return n +} + +func Uint32(n uint32) uint32 { + return n +} + +func Uint64(n uint64) uint64 { + return n +} + +func UintPtr(n uintptr) uintptr { + return n +} + +func Float32(n float32) float32 { + return n +} + +func Float64(n float64) float64 { + return n +} + +func Complex64(n complex64) complex64 { + return n +} + +func Complex128(n complex128) complex128 { + return n +} + +func Byte(n byte) byte { + return n +} + +func Rune(n rune) rune { + return n +} + +func String(n string) string { + return n +} + +func Bool(n bool) bool { + return n +} + +type Structure struct { + int + int8 + int16 + int32 + int64 + uint + uint8 + uint16 + uint32 + uint64 + uintptr + float32 + float64 + complex64 + complex128 + byte + rune + string + bool +} + +func Struct(s Structure) Structure { + return s +} + +func StructWithNan(s Structure) Structure { + s.float64 = math.NaN() + return s +} + +func ArrayOfInt(array [10]int) [10]int { + return array +} + +func ArrayOfUintPtr(array [10]uintptr) [10]uintptr { + return array +} + +func ArrayOfString(array [10]string) [10]string { + return array +} + +func ArrayOfStructs(array [10]Structure) [10]Structure { + return array +} + +func ArrayOfStructsWithNan(array [10]Structure) [10]Structure { + array[0].float64 = math.NaN() + return array +} + +func ArrayOfArrayOfUint(array [5][5]uint) [5][5]uint { + return array +} + +func ArrayOfArrayOfStructs(array [5][5]Structure) [5][5]Structure { + return array +} + +func returnErrorOrNil(n int) error { + if n > 0 { + return errors.New("error") + } else { + return nil + } +} + +func ExternalStruct(match difflib.Match, structure Structure) Structure { + return structure +} diff --git a/utbot-go/go-samples/simple/supported_types_go_ut_test.go b/utbot-go/go-samples/simple/supported_types_go_ut_test.go new file mode 100644 index 0000000000..8612f59590 --- /dev/null +++ b/utbot-go/go-samples/simple/supported_types_go_ut_test.go @@ -0,0 +1,228 @@ +package simple + +import ( + "github.com/pmezard/go-difflib/difflib" + "github.com/stretchr/testify/assert" + "math" + "testing" +) + +func TestWithoutParametersAndReturnValuesByUtGoFuzzer(t *testing.T) { + assert.NotPanics(t, func() { WithoutParametersAndReturnValues() }) +} + +func TestIntByUtGoFuzzer1(t *testing.T) { + actualVal := Int(9223372036854775807) + + assert.Equal(t, 9223372036854775807, actualVal) +} + +func TestIntByUtGoFuzzer2(t *testing.T) { + actualVal := Int(-9223372036854775808) + + assert.Equal(t, -9223372036854775808, actualVal) +} + +func TestInt8ByUtGoFuzzer1(t *testing.T) { + actualVal := Int8(127) + + assert.Equal(t, int8(127), actualVal) +} + +func TestInt8ByUtGoFuzzer2(t *testing.T) { + actualVal := Int8(-128) + + assert.Equal(t, int8(-128), actualVal) +} + +func TestInt16ByUtGoFuzzer1(t *testing.T) { + actualVal := Int16(32767) + + assert.Equal(t, int16(32767), actualVal) +} + +func TestInt16ByUtGoFuzzer2(t *testing.T) { + actualVal := Int16(-32768) + + assert.Equal(t, int16(-32768), actualVal) +} + +func TestInt32ByUtGoFuzzer1(t *testing.T) { + actualVal := Int32(2147483647) + + assert.Equal(t, int32(2147483647), actualVal) +} + +func TestInt32ByUtGoFuzzer2(t *testing.T) { + actualVal := Int32(-2147483648) + + assert.Equal(t, int32(-2147483648), actualVal) +} + +func TestInt64ByUtGoFuzzer1(t *testing.T) { + actualVal := Int64(9223372036854775807) + + assert.Equal(t, int64(9223372036854775807), actualVal) +} + +func TestInt64ByUtGoFuzzer2(t *testing.T) { + actualVal := Int64(-9223372036854775808) + + assert.Equal(t, int64(-9223372036854775808), actualVal) +} + +func TestUintByUtGoFuzzer(t *testing.T) { + actualVal := Uint(0) + + assert.Equal(t, uint(0), actualVal) +} + +func TestUint8ByUtGoFuzzer(t *testing.T) { + actualVal := Uint8(0) + + assert.Equal(t, uint8(0), actualVal) +} + +func TestUint16ByUtGoFuzzer(t *testing.T) { + actualVal := Uint16(0) + + assert.Equal(t, uint16(0), actualVal) +} + +func TestUint32ByUtGoFuzzer(t *testing.T) { + actualVal := Uint32(0) + + assert.Equal(t, uint32(0), actualVal) +} + +func TestUint64ByUtGoFuzzer(t *testing.T) { + actualVal := Uint64(0) + + assert.Equal(t, uint64(0), actualVal) +} + +func TestUintPtrByUtGoFuzzer(t *testing.T) { + actualVal := UintPtr(0) + + assert.Equal(t, uintptr(0), actualVal) +} + +func TestFloat32ByUtGoFuzzer(t *testing.T) { + actualVal := Float32(0.59754527) + + assert.Equal(t, float32(0.59754527), actualVal) +} + +func TestFloat64ByUtGoFuzzer(t *testing.T) { + actualVal := Float64(0.7815346320453048) + + assert.Equal(t, 0.7815346320453048, actualVal) +} + +func TestComplex64ByUtGoFuzzer(t *testing.T) { + actualVal := Complex64(complex(0.25277615, 0.25277615)) + + assert.Equal(t, complex(float32(0.25277615), float32(0.25277615)), actualVal) +} + +func TestComplex128ByUtGoFuzzer(t *testing.T) { + actualVal := Complex128(complex(0.3851891847407185, 0.3851891847407185)) + + assert.Equal(t, complex(0.3851891847407185, 0.3851891847407185), actualVal) +} + +func TestByteByUtGoFuzzer(t *testing.T) { + actualVal := Byte(0) + + assert.Equal(t, byte(0), actualVal) +} + +func TestRuneByUtGoFuzzer(t *testing.T) { + actualVal := Rune(2147483647) + + assert.Equal(t, rune(2147483647), actualVal) +} + +func TestStringByUtGoFuzzer(t *testing.T) { + actualVal := String("hello") + + assert.Equal(t, "hello", actualVal) +} + +func TestBoolByUtGoFuzzer(t *testing.T) { + actualVal := Bool(true) + + assert.Equal(t, true, actualVal) +} + +func TestStructByUtGoFuzzer(t *testing.T) { + actualVal := Struct(Structure{int: -1, int8: 1, int16: 32767, int32: -1, int64: -1, uint: 18446744073709551615, uint8: 0, uint16: 1, uint32: 0, uint64: 18446744073709551615, uintptr: 18446744073709551615, float32: 0.9848415, float64: 0.9828195255872982, complex64: complex(0.9848415, 0.9848415), complex128: complex(0.9828195255872982, 0.9828195255872982), byte: 0, rune: -1, string: "", bool: false}) + + assert.Equal(t, Structure{int: -1, int8: 1, int16: 32767, int32: -1, int64: -1, uint: 18446744073709551615, uint8: 0, uint16: 1, uint32: 0, uint64: 18446744073709551615, uintptr: 18446744073709551615, float32: 0.9848415, float64: 0.9828195255872982, complex64: complex(float32(0.9848415), float32(0.9848415)), complex128: complex(0.9828195255872982, 0.9828195255872982), byte: 0, rune: -1, string: "", bool: false}, actualVal) +} + +func TestStructWithNanByUtGoFuzzer(t *testing.T) { + actualVal := StructWithNan(Structure{int: -1, int8: 1, int16: 32767, int32: -1, int64: -1, uint: 18446744073709551615, uint8: 0, uint16: 1, uint32: 0, uint64: 18446744073709551615, uintptr: 18446744073709551615, float32: 0.02308184, float64: 0.9412491794821144, complex64: complex(0.02308184, 0.02308184), complex128: complex(0.9412491794821144, 0.9412491794821144), byte: 0, rune: -1, string: "", bool: false}) + + assert.NotEqual(t, Structure{int: -1, int8: 1, int16: 32767, int32: -1, int64: -1, uint: 18446744073709551615, uint8: 0, uint16: 1, uint32: 0, uint64: 18446744073709551615, uintptr: 18446744073709551615, float32: 0.02308184, float64: math.NaN(), complex64: complex(float32(0.02308184), float32(0.02308184)), complex128: complex(0.9412491794821144, 0.9412491794821144), byte: 0, rune: -1, string: "", bool: false}, actualVal) +} + +func TestArrayOfIntByUtGoFuzzer(t *testing.T) { + actualVal := ArrayOfInt([10]int{-1, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + + assert.Equal(t, [10]int{-1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, actualVal) +} + +func TestArrayOfUintPtrByUtGoFuzzer(t *testing.T) { + actualVal := ArrayOfUintPtr([10]uintptr{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + + assert.Equal(t, [10]uintptr{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, actualVal) +} + +func TestArrayOfStringByUtGoFuzzer(t *testing.T) { + actualVal := ArrayOfString([10]string{"hello", "", "", "", "", "", "", "", "", ""}) + + assert.Equal(t, [10]string{"hello", "", "", "", "", "", "", "", "", ""}, actualVal) +} + +func TestArrayOfStructsByUtGoFuzzer(t *testing.T) { + actualVal := ArrayOfStructs([10]Structure{{int: -1, int8: 0, int16: -1, int32: -1, int64: -9223372036854775808, uint: 1, uint8: 0, uint16: 65535, uint32: 1, uint64: 18446744073709551615, uintptr: 1, float32: 0.27495396, float64: 0.31293596519376554, complex64: complex(0.27495396, 0.27495396), complex128: complex(0.31293596519376554, 0.31293596519376554), byte: 255, rune: 2147483647, string: "", bool: false}, {}, {}, {}, {}, {}, {}, {}, {}, {}}) + + assert.Equal(t, [10]Structure{{int: -1, int8: 0, int16: -1, int32: -1, int64: -9223372036854775808, uint: 1, uint8: 0, uint16: 65535, uint32: 1, uint64: 18446744073709551615, uintptr: 1, float32: 0.27495396, float64: 0.31293596519376554, complex64: complex(float32(0.27495396), float32(0.27495396)), complex128: complex(0.31293596519376554, 0.31293596519376554), byte: 255, rune: 2147483647, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}}, actualVal) +} + +func TestArrayOfStructsWithNanByUtGoFuzzer(t *testing.T) { + actualVal := ArrayOfStructsWithNan([10]Structure{{int: -1, int8: 0, int16: -1, int32: -1, int64: -9223372036854775808, uint: 1, uint8: 0, uint16: 65535, uint32: 1, uint64: 18446744073709551615, uintptr: 1, float32: 0.3679757, float64: 0.14660165764651822, complex64: complex(0.3679757, 0.3679757), complex128: complex(0.14660165764651822, 0.14660165764651822), byte: 255, rune: 2147483647, string: "", bool: false}, {}, {}, {}, {}, {}, {}, {}, {}, {}}) + + assert.NotEqual(t, [10]Structure{{int: -1, int8: 0, int16: -1, int32: -1, int64: -9223372036854775808, uint: 1, uint8: 0, uint16: 65535, uint32: 1, uint64: 18446744073709551615, uintptr: 1, float32: 0.3679757, float64: math.NaN(), complex64: complex(float32(0.3679757), float32(0.3679757)), complex128: complex(0.14660165764651822, 0.14660165764651822), byte: 255, rune: 2147483647, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}}, actualVal) +} + +func TestArrayOfArrayOfUintByUtGoFuzzer(t *testing.T) { + actualVal := ArrayOfArrayOfUint([5][5]uint{{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}}) + + assert.Equal(t, [5][5]uint{{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}}, actualVal) +} + +func TestArrayOfArrayOfStructsByUtGoFuzzer(t *testing.T) { + actualVal := ArrayOfArrayOfStructs([5][5]Structure{{{}, {}, {}, {}, {}}, {{}, {}, {}, {}, {}}, {{}, {}, {}, {}, {}}, {{}, {}, {}, {}, {}}, {{}, {}, {}, {}, {}}}) + + assert.Equal(t, [5][5]Structure{{{int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}}, {{int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}}, {{int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}}, {{int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}}, {{int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}, {int: 0, int8: 0, int16: 0, int32: 0, int64: 0, uint: 0, uint8: 0, uint16: 0, uint32: 0, uint64: 0, uintptr: 0, float32: 0.0, float64: 0.0, complex64: complex(float32(0.0), float32(0.0)), complex128: complex(0.0, 0.0), byte: 0, rune: 0, string: "", bool: false}}}, actualVal) +} + +func TestReturnErrorOrNilWithNonNilErrorByUtGoFuzzer1(t *testing.T) { + actualErr := returnErrorOrNil(9223372036854775807) + + assert.ErrorContains(t, actualErr, "error") +} + +func TestReturnErrorOrNilByUtGoFuzzer2(t *testing.T) { + actualErr := returnErrorOrNil(0) + + assert.Nil(t, actualErr) +} + +func TestExternalStructByUtGoFuzzer(t *testing.T) { + actualVal := ExternalStruct(difflib.Match{A: 9223372036854775807, B: -1, Size: -9223372036854775808}, Structure{int: -1, int8: 1, int16: -32768, int32: 2147483647, int64: 1, uint: 1, uint8: 1, uint16: 1, uint32: 1, uint64: 18446744073709551615, uintptr: 18446744073709551615, float32: 0.009224832, float64: 0.9644868606768501, complex64: complex(0.009224832, 0.009224832), complex128: complex(0.9644868606768501, 0.9644868606768501), byte: 1, rune: 0, string: "", bool: false}) + + assert.Equal(t, Structure{int: -1, int8: 1, int16: -32768, int32: 2147483647, int64: 1, uint: 1, uint8: 1, uint16: 1, uint32: 1, uint64: 18446744073709551615, uintptr: 18446744073709551615, float32: 0.009224832, float64: 0.9644868606768501, complex64: complex(float32(0.009224832), float32(0.009224832)), complex128: complex(0.9644868606768501, 0.9644868606768501), byte: 1, rune: 0, string: "", bool: false}, actualVal) +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/GoEngine.kt b/utbot-go/src/main/kotlin/org/utbot/go/GoEngine.kt new file mode 100644 index 0000000000..e3aba5cd2d --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/GoEngine.kt @@ -0,0 +1,153 @@ +package org.utbot.go + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import mu.KotlinLogging +import org.utbot.framework.plugin.api.TimeoutException +import org.utbot.fuzzing.BaseFeedback +import org.utbot.fuzzing.Control +import org.utbot.fuzzing.utils.Trie +import org.utbot.go.api.* +import org.utbot.go.logic.EachExecutionTimeoutsMillisConfig +import org.utbot.go.util.executeCommandByNewProcessOrFailWithoutWaiting +import org.utbot.go.worker.GoWorker +import org.utbot.go.worker.GoWorkerCodeGenerationHelper +import org.utbot.go.worker.convertRawExecutionResultToExecutionResult +import java.io.File +import java.io.InputStreamReader +import java.net.ServerSocket +import java.net.SocketException +import java.net.SocketTimeoutException +import java.util.concurrent.TimeUnit + +val logger = KotlinLogging.logger {} + +class GoEngine( + private val methodUnderTest: GoUtFunction, + private val sourceFile: GoUtFile, + private val goExecutableAbsolutePath: String, + private val eachExecutionTimeoutsMillisConfig: EachExecutionTimeoutsMillisConfig, + private val timeoutExceededOrIsCanceled: () -> Boolean, + private val timeoutMillis: Long = 10000 +) { + + fun fuzzing(): Flow> = flow { + var attempts = 0 + val attemptsLimit = Int.MAX_VALUE + ServerSocket(0).use { serverSocket -> + var fileToExecute: File? = null + try { + fileToExecute = GoWorkerCodeGenerationHelper.createFileToExecute( + sourceFile, + methodUnderTest, + eachExecutionTimeoutsMillisConfig, + serverSocket.localPort + ) + val testFunctionName = GoWorkerCodeGenerationHelper.workerTestFunctionName + val command = listOf( + goExecutableAbsolutePath, "test", "-run", testFunctionName + ) + val sourceFileDir = File(sourceFile.absoluteDirectoryPath) + val processStartTime = System.currentTimeMillis() + val process = executeCommandByNewProcessOrFailWithoutWaiting(command, sourceFileDir) + val workerSocket = try { + serverSocket.soTimeout = timeoutMillis.toInt() + serverSocket.accept() + } catch (e: SocketTimeoutException) { + val processHasExited = process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS) + if (processHasExited) { + val processOutput = InputStreamReader(process.inputStream).readText() + throw TimeoutException("Timeout exceeded: Worker not connected. Process output: $processOutput") + } else { + process.destroy() + } + throw TimeoutException("Timeout exceeded: Worker not connected") + } + logger.debug { "Worker connected - completed in ${System.currentTimeMillis() - processStartTime} ms" } + try { + val worker = GoWorker(workerSocket) + if (methodUnderTest.parameters.isEmpty()) { + worker.sendFuzzedParametersValues(listOf()) + val rawExecutionResult = worker.receiveRawExecutionResult() + val executionResult = convertRawExecutionResultToExecutionResult( + methodUnderTest.getPackageName(), + rawExecutionResult, + methodUnderTest.resultTypes, + eachExecutionTimeoutsMillisConfig[methodUnderTest], + ) + val fuzzedFunction = GoUtFuzzedFunction(methodUnderTest, listOf()) + emit(fuzzedFunction to executionResult) + } else { + runGoFuzzing(methodUnderTest) { description, values -> + if (timeoutExceededOrIsCanceled()) { + return@runGoFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.STOP) + } + val fuzzedFunction = GoUtFuzzedFunction(methodUnderTest, values) + worker.sendFuzzedParametersValues(values) + val rawExecutionResult = worker.receiveRawExecutionResult() + val executionResult = convertRawExecutionResultToExecutionResult( + methodUnderTest.getPackageName(), + rawExecutionResult, + methodUnderTest.resultTypes, + eachExecutionTimeoutsMillisConfig[methodUnderTest], + ) + if (executionResult.trace.isEmpty()) { + logger.error { "Coverage is empty for [${methodUnderTest.name}] with $values}" } + if (executionResult is GoUtPanicFailure) { + logger.error { "Execution completed with panic: ${executionResult.panicValue}" } + } + return@runGoFuzzing BaseFeedback(result = Trie.emptyNode(), control = Control.PASS) + } + val trieNode = description.tracer.add(executionResult.trace.map { GoInstruction(it) }) + if (trieNode.count > 1) { + if (++attempts >= attemptsLimit) { + return@runGoFuzzing BaseFeedback( + result = Trie.emptyNode(), + control = Control.STOP + ) + } + return@runGoFuzzing BaseFeedback(result = trieNode, control = Control.CONTINUE) + } + emit(fuzzedFunction to executionResult) + BaseFeedback(result = trieNode, control = Control.CONTINUE) + } + workerSocket.close() + val processHasExited = process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS) + if (!processHasExited) { + process.destroy() + throw TimeoutException("Timeout exceeded: Worker didn't finish") + } + val exitCode = process.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(process.inputStream).readText() + throw RuntimeException( + StringBuilder() + .append("Execution of ${"function [${methodUnderTest.name}] from $sourceFile"} in child process failed with non-zero exit code = $exitCode: ") + .append("\n$processOutput") + .toString() + ) + } + } + } catch (e: SocketException) { + val processHasExited = process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS) + if (!processHasExited) { + process.destroy() + throw TimeoutException("Timeout exceeded: Worker didn't finish") + } + val exitCode = process.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(process.inputStream).readText() + throw RuntimeException( + StringBuilder() + .append("Execution of ${"function [${methodUnderTest.name}] from $sourceFile"} in child process failed with non-zero exit code = $exitCode: ") + .append("\n$processOutput") + .toString() + ) + } + } + } finally { + fileToExecute?.delete() + } + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/GoLanguage.kt b/utbot-go/src/main/kotlin/org/utbot/go/GoLanguage.kt new file mode 100644 index 0000000000..55b4378389 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/GoLanguage.kt @@ -0,0 +1,32 @@ +package org.utbot.go + +import org.utbot.fuzzing.* +import org.utbot.fuzzing.utils.Trie +import org.utbot.go.api.GoUtFunction +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import org.utbot.go.fuzzer.providers.GoArrayValueProvider +import org.utbot.go.fuzzer.providers.GoPrimitivesValueProvider +import org.utbot.go.fuzzer.providers.GoStructValueProvider + + +fun goDefaultValueProviders() = listOf( + GoPrimitivesValueProvider, GoArrayValueProvider, GoStructValueProvider +) + +class GoInstruction( + val lineNumber: Int +) + +class GoDescription( + val methodUnderTest: GoUtFunction, + val tracer: Trie +) : Description(methodUnderTest.parameters.map { it.type }.toList()) + +suspend fun runGoFuzzing( + methodUnderTest: GoUtFunction, + providers: List> = goDefaultValueProviders(), + exec: suspend (description: GoDescription, values: List) -> BaseFeedback, GoTypeId, GoUtModel> +) { + BaseFuzzing(providers, exec).fuzz(GoDescription(methodUnderTest, Trie(GoInstruction::lineNumber))) +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt new file mode 100644 index 0000000000..eaa3a620c4 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt @@ -0,0 +1,106 @@ +package org.utbot.go.api + +import org.utbot.go.framework.api.go.GoFieldId +import org.utbot.go.framework.api.go.GoTypeId + +/** + * Represents real Go primitive type. + */ +class GoPrimitiveTypeId(name: String) : GoTypeId(name) { + override val packageName: String = "" + override val canonicalName: String = simpleName + + override fun getRelativeName(packageName: String): String = simpleName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoPrimitiveTypeId) return false + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() +} + +class GoStructTypeId( + name: String, + implementsError: Boolean, + override val packageName: String, + val packagePath: String, + val fields: List, +) : GoTypeId(name, implementsError = implementsError) { + override val canonicalName: String = "$packageName.$name" + + override fun getRelativeName(packageName: String): String = if (this.packageName != packageName) { + canonicalName + } else { + simpleName + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoStructTypeId) return false + + return packagePath == other.packagePath && packageName == other.packageName && name == other.name + } + + override fun hashCode(): Int { + var result = packagePath.hashCode() + result = 31 * result + packageName.hashCode() + result = 31 * result + name.hashCode() + return result + } +} + +class GoArrayTypeId( + name: String, + elementTypeId: GoTypeId, + val length: Int +) : GoTypeId(name, elementTypeId = elementTypeId) { + override val canonicalName: String = "[$length]${elementTypeId.canonicalName}" + + override fun getRelativeName(packageName: String): String = + "[$length]${elementTypeId!!.getRelativeName(packageName)}" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoArrayTypeId) return false + + return elementTypeId == other.elementTypeId && length == other.length + } + + override fun hashCode(): Int = 31 * elementTypeId.hashCode() + length +} + +class GoInterfaceTypeId( + name: String, + implementsError: Boolean, + override val packageName: String, + val packagePath: String, +) : GoTypeId(name, implementsError = implementsError) { + override val canonicalName: String = if (packageName != "") { + "$packageName.$name" + } else { + simpleName + } + + override fun getRelativeName(packageName: String): String = if (this.packageName != packageName) { + canonicalName + } else { + simpleName + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoInterfaceTypeId) return false + + return packagePath == other.packagePath && packageName == other.packageName && name == other.name + } + + override fun hashCode(): Int { + var result = packagePath.hashCode() + result = 31 * result + packageName.hashCode() + result = 31 * result + name.hashCode() + return result + } +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt new file mode 100644 index 0000000000..a9f7a4cca2 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt @@ -0,0 +1,32 @@ +package org.utbot.go.api + +import org.utbot.go.framework.api.go.GoUtModel + +interface GoUtExecutionResult { + val trace: List +} + +interface GoUtExecutionCompleted : GoUtExecutionResult { + val models: List +} + +data class GoUtExecutionSuccess( + override val models: List, + override val trace: List +) : GoUtExecutionCompleted + +data class GoUtExecutionWithNonNilError( + override val models: List, + override val trace: List +) : GoUtExecutionCompleted + +data class GoUtPanicFailure( + val panicValue: GoUtModel, + val panicValueIsErrorMessage: Boolean, + override val trace: List +) : GoUtExecutionResult + +data class GoUtTimeoutExceeded( + val timeoutMillis: Long, + override val trace: List +) : GoUtExecutionResult \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt new file mode 100644 index 0000000000..dfcbac38ca --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt @@ -0,0 +1,37 @@ +package org.utbot.go.api + +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import java.io.File +import java.nio.file.Paths + +data class GoUtFile(val absolutePath: String, val packageName: String) { + val fileName: String get() = File(absolutePath).name + val fileNameWithoutExtension: String get() = File(absolutePath).nameWithoutExtension + val absoluteDirectoryPath: String get() = Paths.get(absolutePath).parent.toString() +} + +data class GoUtFunctionParameter(val name: String, val type: GoTypeId) + +data class GoUtFunction( + val name: String, + val modifiedName: String, + val parameters: List, + val resultTypes: List, + val modifiedFunctionForCollectingTraces: String, + val numberOfAllStatements: Int, + val sourceFile: GoUtFile +) { + fun getPackageName(): String = sourceFile.packageName +} + +data class GoUtFuzzedFunction(val function: GoUtFunction, val parametersValues: List) + +data class GoUtFuzzedFunctionTestCase( + val fuzzedFunction: GoUtFuzzedFunction, + val executionResult: GoUtExecutionResult, +) { + val function: GoUtFunction get() = fuzzedFunction.function + val parametersValues: List get() = fuzzedFunction.parametersValues +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt new file mode 100644 index 0000000000..d9337f51a8 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt @@ -0,0 +1,183 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "CanBeParameter") + +package org.utbot.go.api + +import org.utbot.go.api.util.goDefaultValueModel +import org.utbot.go.api.util.goFloat64TypeId +import org.utbot.go.api.util.goStringTypeId +import org.utbot.go.api.util.neverRequiresExplicitCast +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtFieldModel +import org.utbot.go.framework.api.go.GoUtModel + +// NEVER and DEPENDS difference is useful in code generation of assert.Equals(...). +enum class ExplicitCastMode { + REQUIRED, NEVER, DEPENDS +} + +open class GoUtPrimitiveModel( + val value: Any, + typeId: GoPrimitiveTypeId, + val explicitCastMode: ExplicitCastMode = + if (typeId.neverRequiresExplicitCast) { + ExplicitCastMode.NEVER + } else { + ExplicitCastMode.DEPENDS + }, + private val requiredImports: Set = emptySet(), +) : GoUtModel(typeId) { + override val typeId: GoPrimitiveTypeId + get() = super.typeId as GoPrimitiveTypeId + + override fun getRequiredImports(): Set = requiredImports + + override fun isComparable(): Boolean = true + + override fun toString(): String = when (explicitCastMode) { + ExplicitCastMode.REQUIRED -> toCastedValueGoCode() + ExplicitCastMode.DEPENDS, ExplicitCastMode.NEVER -> toValueGoCode() + } + + open fun toValueGoCode(): String = if (typeId == goStringTypeId) "\"$value\"" else "$value" + fun toCastedValueGoCode(): String = "$typeId(${toValueGoCode()})" +} + +abstract class GoUtCompositeModel( + typeId: GoTypeId, + val packageName: String, +) : GoUtModel(typeId) + +class GoUtStructModel( + val value: List, + typeId: GoStructTypeId, + packageName: String, +) : GoUtCompositeModel(typeId, packageName) { + override val typeId: GoStructTypeId + get() = super.typeId as GoStructTypeId + + override fun getRequiredImports(): Set { + val imports = if (typeId.packageName != packageName) { + mutableSetOf(typeId.packagePath) + } else { + mutableSetOf() + } + value.filter { packageName == typeId.packageName || it.fieldId.isExported } + .map { it.getRequiredImports() } + .forEach { imports += it } + return imports + } + + override fun isComparable(): Boolean = value.all { it.isComparable() } + + fun toStringWithoutStructName(): String = + value.filter { packageName == typeId.packageName || it.fieldId.isExported } + .joinToString(prefix = "{", postfix = "}") { "${it.fieldId.name}: ${it.model}" } + + override fun toString(): String = + "${typeId.getRelativeName(packageName)}${toStringWithoutStructName()}" +} + +class GoUtArrayModel( + val value: MutableMap, + typeId: GoArrayTypeId, + packageName: String, +) : GoUtCompositeModel(typeId, packageName) { + val length: Int = typeId.length + + override val typeId: GoArrayTypeId + get() = super.typeId as GoArrayTypeId + + override fun getRequiredImports(): Set { + val elementStructTypeId = typeId.elementTypeId as? GoStructTypeId + val imports = if (elementStructTypeId != null && elementStructTypeId.packageName != packageName) { + mutableSetOf(elementStructTypeId.packagePath) + } else { + mutableSetOf() + } + value.values.map { it.getRequiredImports() }.forEach { imports += it } + return imports + } + + override fun isComparable(): Boolean = value.values.all { it.isComparable() } + + fun getElements(typeId: GoTypeId): List = (0 until length).map { + value[it] ?: typeId.goDefaultValueModel(packageName) + } + + fun toStringWithoutTypeName(): String = when (val typeId = typeId.elementTypeId!!) { + is GoStructTypeId -> getElements(typeId).joinToString(prefix = "{", postfix = "}") { + (it as GoUtStructModel).toStringWithoutStructName() + } + + is GoArrayTypeId -> getElements(typeId).joinToString(prefix = "{", postfix = "}") { + (it as GoUtArrayModel).toStringWithoutTypeName() + } + + else -> getElements(typeId).joinToString(prefix = "{", postfix = "}") + } + + override fun toString(): String = when (val typeId = typeId.elementTypeId!!) { + is GoStructTypeId -> getElements(typeId) + .joinToString(prefix = "[$length]${typeId.getRelativeName(packageName)}{", postfix = "}") { + (it as GoUtStructModel).toStringWithoutStructName() + } + + is GoArrayTypeId -> getElements(typeId) + .joinToString(prefix = "[$length]${typeId.getRelativeName(packageName)}{", postfix = "}") { + (it as GoUtArrayModel).toStringWithoutTypeName() + } + + else -> getElements(typeId) + .joinToString(prefix = "[$length]${typeId.getRelativeName(packageName)}{", postfix = "}") + } +} + +class GoUtFloatNaNModel( + typeId: GoPrimitiveTypeId +) : GoUtPrimitiveModel( + "math.NaN()", + typeId, + explicitCastMode = if (typeId != goFloat64TypeId) { + ExplicitCastMode.REQUIRED + } else { + ExplicitCastMode.NEVER + }, + requiredImports = setOf("math"), +) { + override fun isComparable(): Boolean = false +} + +class GoUtFloatInfModel( + val sign: Int, + typeId: GoPrimitiveTypeId +) : GoUtPrimitiveModel( + "math.Inf($sign)", + typeId, + explicitCastMode = if (typeId != goFloat64TypeId) { + ExplicitCastMode.REQUIRED + } else { + ExplicitCastMode.NEVER + }, + requiredImports = setOf("math"), +) + +class GoUtComplexModel( + var realValue: GoUtPrimitiveModel, + var imagValue: GoUtPrimitiveModel, + typeId: GoPrimitiveTypeId, +) : GoUtPrimitiveModel( + "complex($realValue, $imagValue)", + typeId, + requiredImports = realValue.getRequiredImports() + imagValue.getRequiredImports(), + explicitCastMode = ExplicitCastMode.NEVER +) { + override fun isComparable(): Boolean = realValue.isComparable() && imagValue.isComparable() + override fun toValueGoCode(): String = "complex($realValue, $imagValue)" +} + +class GoUtNilModel( + typeId: GoTypeId +) : GoUtModel(typeId) { + override fun isComparable(): Boolean = true + override fun toString() = "nil" +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt new file mode 100644 index 0000000000..1740891fb2 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt @@ -0,0 +1,124 @@ +package org.utbot.go.api.util + +import org.utbot.go.api.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import kotlin.reflect.KClass + +val goByteTypeId = GoPrimitiveTypeId("byte") +val goBoolTypeId = GoPrimitiveTypeId("bool") + +val goComplex128TypeId = GoPrimitiveTypeId("complex128") +val goComplex64TypeId = GoPrimitiveTypeId("complex64") + +val goFloat32TypeId = GoPrimitiveTypeId("float32") +val goFloat64TypeId = GoPrimitiveTypeId("float64") + +val goIntTypeId = GoPrimitiveTypeId("int") +val goInt16TypeId = GoPrimitiveTypeId("int16") +val goInt32TypeId = GoPrimitiveTypeId("int32") +val goInt64TypeId = GoPrimitiveTypeId("int64") +val goInt8TypeId = GoPrimitiveTypeId("int8") + +val goRuneTypeId = GoPrimitiveTypeId("rune") // = int32 +val goStringTypeId = GoPrimitiveTypeId("string") + +val goUintTypeId = GoPrimitiveTypeId("uint") +val goUint16TypeId = GoPrimitiveTypeId("uint16") +val goUint32TypeId = GoPrimitiveTypeId("uint32") +val goUint64TypeId = GoPrimitiveTypeId("uint64") +val goUint8TypeId = GoPrimitiveTypeId("uint8") +val goUintPtrTypeId = GoPrimitiveTypeId("uintptr") + +val goPrimitives = setOf( + goByteTypeId, + goBoolTypeId, + goComplex128TypeId, + goComplex64TypeId, + goFloat32TypeId, + goFloat64TypeId, + goIntTypeId, + goInt16TypeId, + goInt32TypeId, + goInt64TypeId, + goInt8TypeId, + goRuneTypeId, + goStringTypeId, + goUintTypeId, + goUint16TypeId, + goUint32TypeId, + goUint64TypeId, + goUint8TypeId, + goUintPtrTypeId, +) + +val GoTypeId.isPrimitiveGoType: Boolean + get() = this in goPrimitives + +private val goTypesNeverRequireExplicitCast = setOf( + goBoolTypeId, + goComplex128TypeId, + goComplex64TypeId, + goFloat64TypeId, + goIntTypeId, + goStringTypeId, +) + +val GoPrimitiveTypeId.neverRequiresExplicitCast: Boolean + get() = this in goTypesNeverRequireExplicitCast + +/** + * This method is useful for converting the string representation of a Go value to its more accurate representation. + * For example, to build more proper GoUtPrimitiveModel-s with GoFuzzedFunctionsExecutor. + * Note, that for now such conversion is not required and is done for convenience only. + * + * About corresponding types: int and uint / uintptr types sizes in Go are platform dependent, + * but are supposed to fit in Long and ULong respectively. + */ +val GoPrimitiveTypeId.correspondingKClass: KClass + get() = when (this) { + goByteTypeId, goUint8TypeId -> UByte::class + goBoolTypeId -> Boolean::class + goFloat32TypeId -> Float::class + goFloat64TypeId -> Double::class + goInt16TypeId -> Short::class + goInt32TypeId, goRuneTypeId -> Int::class + goIntTypeId, goInt64TypeId -> Long::class + goInt8TypeId -> Byte::class + goStringTypeId -> String::class + goUint32TypeId -> UInt::class + goUint16TypeId -> UShort::class + goUintTypeId, goUint64TypeId, goUintPtrTypeId -> ULong::class + else -> String::class // default way to hold GoUtPrimitiveModel's value is to use String + } + +fun GoTypeId.goDefaultValueModel(packageName: String): GoUtModel = when (this) { + is GoPrimitiveTypeId -> when (this) { + goBoolTypeId -> GoUtPrimitiveModel(false, this) + goRuneTypeId, goIntTypeId, goInt8TypeId, goInt16TypeId, goInt32TypeId, goInt64TypeId -> GoUtPrimitiveModel( + 0, + this + ) + + goByteTypeId, goUintTypeId, goUint8TypeId, goUint16TypeId, goUint32TypeId, goUint64TypeId -> GoUtPrimitiveModel( + 0, + this + ) + + goFloat32TypeId, goFloat64TypeId -> GoUtPrimitiveModel(0.0, this) + goComplex64TypeId, goComplex128TypeId -> GoUtComplexModel( + goFloat64TypeId.goDefaultValueModel(packageName) as GoUtPrimitiveModel, + goFloat64TypeId.goDefaultValueModel(packageName) as GoUtPrimitiveModel, + this + ) + + goStringTypeId -> GoUtPrimitiveModel("", this) + goUintPtrTypeId -> GoUtPrimitiveModel(0, this) + + else -> error("Go primitive ${this.javaClass} is not supported") + } + + is GoStructTypeId -> GoUtStructModel(listOf(), this, packageName) + is GoArrayTypeId -> GoUtArrayModel(hashMapOf(), this, packageName) + else -> GoUtNilModel(this) +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt new file mode 100644 index 0000000000..602fb24c01 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt @@ -0,0 +1,16 @@ +package org.utbot.go.api.util + +import org.utbot.go.api.GoUtComplexModel +import org.utbot.go.api.GoUtFloatInfModel +import org.utbot.go.api.GoUtFloatNaNModel +import org.utbot.go.framework.api.go.GoUtModel + +fun GoUtModel.isNaNOrInf(): Boolean = this is GoUtFloatNaNModel || this is GoUtFloatInfModel + +fun GoUtModel.doesNotContainNaNOrInf(): Boolean { + if (this.isNaNOrInf()) return false + val asComplexModel = (this as? GoUtComplexModel) ?: return true + return !(asComplexModel.realValue.isNaNOrInf() || asComplexModel.imagValue.isNaNOrInf()) +} + +fun GoUtModel.containsNaNOrInf(): Boolean = !this.doesNotContainNaNOrInf() \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/framework/api/go/GoApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/framework/api/go/GoApi.kt new file mode 100644 index 0000000000..424e611318 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/framework/api/go/GoApi.kt @@ -0,0 +1,51 @@ +package org.utbot.go.framework.api.go + +/** + * Parent class for all Go types for compatibility with UTBot framework. + * + * To see its children check GoTypesApi.kt at org.utbot.go.api. + */ +abstract class GoTypeId( + val name: String, + val elementTypeId: GoTypeId? = null, + val implementsError: Boolean = false +) { + open val packageName: String = "" + val simpleName: String = name + abstract val canonicalName: String + + abstract fun getRelativeName(packageName: String): String + override fun toString(): String = canonicalName +} + +/** + * Parent class for all Go models. + * + * To see its children check GoUtModelsApi.kt at org.utbot.go.api. + */ +abstract class GoUtModel( + open val typeId: GoTypeId, +) { + open fun getRequiredImports(): Set = emptySet() + abstract fun isComparable(): Boolean +} + +/** + * Class for Go struct field model. + */ +class GoUtFieldModel( + val model: GoUtModel, + val fieldId: GoFieldId, +) : GoUtModel(fieldId.declaringType) { + override fun getRequiredImports(): Set = model.getRequiredImports() + override fun isComparable(): Boolean = model.isComparable() +} + +/** + * Class for Go struct field. + */ +class GoFieldId( + val declaringType: GoTypeId, + val name: String, + val isExported: Boolean +) diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoArrayValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoArrayValueProvider.kt new file mode 100644 index 0000000000..f47e78c12a --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoArrayValueProvider.kt @@ -0,0 +1,39 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoArrayTypeId +import org.utbot.go.api.GoUtArrayModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoArrayValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoArrayTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoArrayTypeId }.also { arrayType -> + val packageName = description.methodUnderTest.getPackageName() + yield( + Seed.Collection( + construct = Routine.Collection { + GoUtArrayModel( + value = hashMapOf(), + typeId = arrayType, + packageName = packageName + ) + }, + modify = Routine.ForEach(listOf(arrayType.elementTypeId!!)) { self, i, values -> + val model = self as GoUtArrayModel + if (i >= model.length) { + return@ForEach + } + model.value[i] = values.first() + } + ) + ) + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesValueProvider.kt new file mode 100644 index 0000000000..8999f7524e --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesValueProvider.kt @@ -0,0 +1,203 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.fuzzing.seeds.* +import org.utbot.go.GoDescription +import org.utbot.go.api.GoPrimitiveTypeId +import org.utbot.go.api.GoUtComplexModel +import org.utbot.go.api.GoUtPrimitiveModel +import org.utbot.go.api.util.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtModel +import java.util.* +import kotlin.properties.Delegates + +object GoPrimitivesValueProvider : ValueProvider { + var intSize by Delegates.notNull() + private val random = Random(0) + + override fun accept(type: GoTypeId): Boolean = type in goPrimitives + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoPrimitiveTypeId }.also { primitiveType -> + val primitives: List> = when (primitiveType) { + goBoolTypeId -> listOf( + Seed.Known(Bool.FALSE.invoke()) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toBoolean(), + primitiveType + ) + }, + Seed.Known(Bool.TRUE.invoke()) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toBoolean(), + primitiveType + ) + } + ) + + goRuneTypeId, goIntTypeId, goInt8TypeId, goInt16TypeId, goInt32TypeId, goInt64TypeId -> Signed.values() + .map { + when (type) { + goInt8TypeId -> Seed.Known(it.invoke(8)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toByte(), + primitiveType + ) + } + + goInt16TypeId -> Seed.Known(it.invoke(16)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toShort(), + primitiveType + ) + } + + goInt32TypeId, goRuneTypeId -> Seed.Known(it.invoke(32)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toInt(), + primitiveType + ) + } + + goIntTypeId -> Seed.Known(it.invoke(intSize)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + if (intSize == 32) obj.toInt() else obj.toLong(), + primitiveType + ) + } + + goInt64TypeId -> Seed.Known(it.invoke(64)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toLong(), + primitiveType + ) + } + + else -> return@sequence + } + } + + goByteTypeId, goUintTypeId, goUintPtrTypeId, goUint8TypeId, goUint16TypeId, goUint32TypeId, goUint64TypeId -> Unsigned.values() + .map { + when (type) { + goByteTypeId, goUint8TypeId -> Seed.Known(it.invoke(8)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUByte(), + primitiveType + ) + } + + goUint16TypeId -> Seed.Known(it.invoke(16)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUShort(), + primitiveType + ) + } + + goUint32TypeId -> Seed.Known(it.invoke(32)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toUInt(), + primitiveType + ) + } + + goUintTypeId, goUintPtrTypeId -> Seed.Known(it.invoke(intSize)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + if (intSize == 32) obj.toUInt() else obj.toULong(), + primitiveType + ) + } + + goUint64TypeId -> Seed.Known(it.invoke(64)) { obj: BitVectorValue -> + GoUtPrimitiveModel( + obj.toULong(), + primitiveType + ) + } + + else -> return@sequence + } + } + + goFloat32TypeId -> generateFloat32Seeds(primitiveType) + + goFloat64TypeId -> generateFloat64Seeds(primitiveType) + + goComplex64TypeId -> generateComplexSeeds(primitiveType, goFloat32TypeId) + + goComplex128TypeId -> generateComplexSeeds(primitiveType, goFloat64TypeId) + + goStringTypeId -> listOf( + Seed.Known(StringValue("")) { obj: StringValue -> + GoUtPrimitiveModel( + obj.value, + primitiveType + ) + }, + Seed.Known(StringValue("hello")) { obj: StringValue -> + GoUtPrimitiveModel( + obj.value, + primitiveType + ) + }) + + else -> emptyList() + } + + primitives.forEach { fuzzedValue -> + yield(fuzzedValue) + } + } + } + + private fun generateFloat32Seeds(typeId: GoPrimitiveTypeId): List> { + return listOf( + Seed.Known(IEEE754Value.fromFloat(random.nextFloat())) { obj: IEEE754Value -> + GoUtPrimitiveModel(obj.toFloat(), typeId) + } + ) + } + + private fun generateFloat64Seeds(typeId: GoPrimitiveTypeId): List> { + return listOf( + Seed.Known(IEEE754Value.fromDouble(random.nextDouble())) { obj: IEEE754Value -> + GoUtPrimitiveModel(obj.toDouble(), typeId) + } + ) + } + + private fun generateComplexSeeds( + typeId: GoPrimitiveTypeId, + floatTypeId: GoPrimitiveTypeId + ): List> { + return listOf( + Seed.Recursive( + construct = Routine.Create(listOf(floatTypeId, floatTypeId)) { values -> + GoUtComplexModel( + realValue = values[0] as GoUtPrimitiveModel, + imagValue = values[1] as GoUtPrimitiveModel, + typeId = typeId + ) + }, + modify = sequence { + yield(Routine.Call(listOf(floatTypeId)) { self, values -> + val model = self as GoUtComplexModel + val value = values.first() as GoUtPrimitiveModel + model.realValue = value + }) + }, + empty = Routine.Empty { + GoUtComplexModel( + realValue = GoUtPrimitiveModel(0.0, floatTypeId), + imagValue = GoUtPrimitiveModel(0.0, floatTypeId), + typeId = typeId + ) + } + ) + ) + } +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStructValueProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStructValueProvider.kt new file mode 100644 index 0000000000..9fc47d5693 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStructValueProvider.kt @@ -0,0 +1,51 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider +import org.utbot.go.GoDescription +import org.utbot.go.api.GoStructTypeId +import org.utbot.go.api.GoUtStructModel +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtFieldModel +import org.utbot.go.framework.api.go.GoUtModel + +object GoStructValueProvider : ValueProvider { + override fun accept(type: GoTypeId): Boolean = type is GoStructTypeId + + override fun generate(description: GoDescription, type: GoTypeId): Sequence> = + sequence { + type.let { it as GoStructTypeId }.also { structType -> + val packageName = description.methodUnderTest.getPackageName() + val fields = structType.fields + .filter { structType.packageName == packageName || it.isExported } + yield(Seed.Recursive( + construct = Routine.Create(fields.map { it.declaringType }) { values -> + GoUtStructModel( + value = fields.zip(values).map { (field, value) -> + GoUtFieldModel(value, field) + }, + typeId = structType, + packageName = packageName, + ) + }, + modify = sequence { + fields.forEachIndexed { index, field -> + yield(Routine.Call(listOf(field.declaringType)) { self, values -> + val model = self as GoUtStructModel + val value = values.first() + (model.value as MutableList)[index] = GoUtFieldModel(value, field) + }) + } + }, + empty = Routine.Empty { + GoUtStructModel( + value = emptyList(), + typeId = structType, + packageName = packageName + ) + } + )) + } + } +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt new file mode 100644 index 0000000000..9f47c3b177 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt @@ -0,0 +1,109 @@ +package org.utbot.go.gocodeanalyzer + +import com.beust.klaxon.TypeAdapter +import com.beust.klaxon.TypeFor +import org.utbot.go.api.GoArrayTypeId +import org.utbot.go.api.GoInterfaceTypeId +import org.utbot.go.api.GoPrimitiveTypeId +import org.utbot.go.api.GoStructTypeId +import org.utbot.go.api.util.goPrimitives +import org.utbot.go.framework.api.go.GoFieldId +import org.utbot.go.framework.api.go.GoTypeId +import kotlin.reflect.KClass + +data class AnalyzedInterfaceType( + override val name: String, + val implementsError: Boolean, + val packageName: String, + val packagePath: String +) : AnalyzedType(name) { + override fun toGoTypeId(): GoTypeId = + GoInterfaceTypeId( + name = simpleName, + implementsError = implementsError, + packageName = packageName, + packagePath = packagePath + ) + + private val simpleName: String = name.replaceFirst("interface ", "") +} + +data class AnalyzedPrimitiveType( + override val name: String +) : AnalyzedType(name) { + override fun toGoTypeId(): GoTypeId = GoPrimitiveTypeId(name = name) +} + +data class AnalyzedStructType( + override val name: String, + val packageName: String, + val packagePath: String, + val implementsError: Boolean, + val fields: List +) : AnalyzedType(name) { + data class AnalyzedField( + val name: String, + val type: AnalyzedType, + val isExported: Boolean + ) + + override fun toGoTypeId(): GoTypeId = GoStructTypeId( + name = name, + packageName = packageName, + packagePath = packagePath, + implementsError = implementsError, + fields = fields.map { field -> GoFieldId(field.type.toGoTypeId(), field.name, field.isExported) } + ) +} + +data class AnalyzedArrayType( + override val name: String, + val elementType: AnalyzedType, + val length: Int +) : AnalyzedType(name) { + override fun toGoTypeId(): GoTypeId = GoArrayTypeId( + name = name, + elementTypeId = elementType.toGoTypeId(), + length = length + ) +} + +@TypeFor(field = "name", adapter = AnalyzedTypeAdapter::class) +abstract class AnalyzedType(open val name: String) { + abstract fun toGoTypeId(): GoTypeId +} + +class AnalyzedTypeAdapter : TypeAdapter { + override fun classFor(type: Any): KClass { + val typeName = type as String + return when { + typeName.startsWith("interface ") -> AnalyzedInterfaceType::class + typeName.startsWith("map[") -> error("Map type not yet supported") + typeName.startsWith("[]") -> error("Slice type not yet supported") + typeName.startsWith("[") -> AnalyzedArrayType::class + goPrimitives.map { it.name }.contains(typeName) -> AnalyzedPrimitiveType::class + else -> AnalyzedStructType::class + } + } +} + +internal data class AnalyzedFunctionParameter(val name: String, val type: AnalyzedType) + +internal data class AnalyzedFunction( + val name: String, + val modifiedName: String, + val parameters: List, + val resultTypes: List, + val modifiedFunctionForCollectingTraces: String, + val numberOfAllStatements: Int +) + +internal data class AnalysisResult( + val absoluteFilePath: String, + val packageName: String, + val analyzedFunctions: List, + val notSupportedFunctionsNames: List, + val notFoundFunctionsNames: List +) + +internal data class AnalysisResults(val intSize: Int, val results: List) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt new file mode 100644 index 0000000000..0a56357b1b --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt @@ -0,0 +1,5 @@ +package org.utbot.go.gocodeanalyzer + +internal data class AnalysisTarget(val absoluteFilePath: String, val targetFunctionsNames: List) + +internal data class AnalysisTargets(val targets: List) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt new file mode 100644 index 0000000000..cc394dc2a5 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt @@ -0,0 +1,128 @@ +package org.utbot.go.gocodeanalyzer + +import org.utbot.common.FileUtil.extractDirectoryFromArchive +import org.utbot.common.scanForResourcesContaining +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFunctionParameter +import org.utbot.go.fuzzer.providers.GoPrimitivesValueProvider +import org.utbot.go.util.executeCommandByNewProcessOrFail +import org.utbot.go.util.parseFromJsonOrFail +import org.utbot.go.util.writeJsonToFileOrFail +import java.io.File +import java.nio.file.Paths + +object GoSourceCodeAnalyzer { + + data class GoSourceFileAnalysisResult( + val functions: List, + val notSupportedFunctionsNames: List, + val notFoundFunctionsNames: List + ) + + /** + * Takes map from absolute paths of Go source files to names of their selected functions. + * + * Returns GoSourceFileAnalysisResult-s grouped by their source files. + */ + fun analyzeGoSourceFilesForFunctions( + targetFunctionsNamesBySourceFiles: Map>, + goExecutableAbsolutePath: String + ): Map { + val analysisTargets = AnalysisTargets( + targetFunctionsNamesBySourceFiles.map { (absoluteFilePath, targetFunctionsNames) -> + AnalysisTarget(absoluteFilePath, targetFunctionsNames) + } + ) + val analysisTargetsFileName = createAnalysisTargetsFileName() + val analysisResultsFileName = createAnalysisResultsFileName() + + val goCodeAnalyzerSourceDir = extractGoCodeAnalyzerSourceDirectory() + val analysisTargetsFile = goCodeAnalyzerSourceDir.resolve(analysisTargetsFileName) + val analysisResultsFile = goCodeAnalyzerSourceDir.resolve(analysisResultsFileName) + + val goCodeAnalyzerRunCommand = listOf( + goExecutableAbsolutePath, + "run" + ) + getGoCodeAnalyzerSourceFilesNames() + listOf( + "-targets", + analysisTargetsFileName, + "-results", + analysisResultsFileName, + ) + + try { + writeJsonToFileOrFail(analysisTargets, analysisTargetsFile) + val environment = System.getenv().toMutableMap().apply { + this["Path"] = (this["Path"] ?: "") + File.pathSeparator + Paths.get(goExecutableAbsolutePath).parent + } + executeCommandByNewProcessOrFail( + goCodeAnalyzerRunCommand, + goCodeAnalyzerSourceDir, + "GoSourceCodeAnalyzer for $analysisTargets", + environment + ) + val analysisResults = parseFromJsonOrFail(analysisResultsFile) + GoPrimitivesValueProvider.intSize = analysisResults.intSize + return analysisResults.results.map { analysisResult -> + GoUtFile(analysisResult.absoluteFilePath, analysisResult.packageName) to analysisResult + }.associateBy({ (sourceFile, _) -> sourceFile }) { (sourceFile, analysisResult) -> + val functions = analysisResult.analyzedFunctions.map { analyzedFunction -> + val parameters = analyzedFunction.parameters.map { analyzedFunctionParameter -> + GoUtFunctionParameter( + analyzedFunctionParameter.name, + analyzedFunctionParameter.type.toGoTypeId() + ) + } + val resultTypes = analyzedFunction.resultTypes.map { analyzedType -> analyzedType.toGoTypeId() } + GoUtFunction( + analyzedFunction.name, + analyzedFunction.modifiedName, + parameters, + resultTypes, + analyzedFunction.modifiedFunctionForCollectingTraces, + analyzedFunction.numberOfAllStatements, + sourceFile + ) + } + GoSourceFileAnalysisResult( + functions, + analysisResult.notSupportedFunctionsNames, + analysisResult.notFoundFunctionsNames + ) + } + } finally { + // TODO correctly? + analysisTargetsFile.delete() + analysisResultsFile.delete() + goCodeAnalyzerSourceDir.deleteRecursively() + } + } + + private fun extractGoCodeAnalyzerSourceDirectory(): File { + val sourceDirectoryName = "go_source_code_analyzer" + val classLoader = GoSourceCodeAnalyzer::class.java.classLoader + + val containingResourceFile = classLoader.scanForResourcesContaining(sourceDirectoryName).firstOrNull() + ?: error("Can't find resource containing $sourceDirectoryName directory.") + if (containingResourceFile.extension != "jar") { + error("Resource for $sourceDirectoryName directory is expected to be JAR: others are not supported yet.") + } + + val archiveFilePath = containingResourceFile.toPath() + return extractDirectoryFromArchive(archiveFilePath, sourceDirectoryName)?.toFile() + ?: error("Can't find $sourceDirectoryName directory at the top level of JAR ${archiveFilePath.toAbsolutePath()}.") + } + + private fun getGoCodeAnalyzerSourceFilesNames(): List { + return listOf("main.go", "analyzer_core.go", "analysis_targets.go", "analysis_results.go", "cover.go") + } + + private fun createAnalysisTargetsFileName(): String { + return "ut_go_analysis_targets.json" + } + + private fun createAnalysisResultsFileName(): String { + return "ut_go_analysis_results.json" + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..3bc3b866fd --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt @@ -0,0 +1,101 @@ +package org.utbot.go.logic + +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.simplecodegeneration.GoTestCasesCodeGenerator + +abstract class AbstractGoUtTestsGenerationController { + + fun generateTests( + selectedFunctionsNamesBySourceFiles: Map>, + testsGenerationConfig: GoUtTestsGenerationConfig, + isCanceled: () -> Boolean = { false } + ) { + if (!onSourceCodeAnalysisStart(selectedFunctionsNamesBySourceFiles)) return + val analysisResults = GoSourceCodeAnalyzer.analyzeGoSourceFilesForFunctions( + selectedFunctionsNamesBySourceFiles, + testsGenerationConfig.goExecutableAbsolutePath + ) + if (!onSourceCodeAnalysisFinished(analysisResults)) return + + val numOfFunctions = analysisResults.values + .map { it.functions.size } + .reduce { acc, numOfFunctions -> acc + numOfFunctions } + val functionTimeoutStepMillis = testsGenerationConfig.allExecutionTimeoutsMillisConfig / numOfFunctions + var startTimeMillis = System.currentTimeMillis() + val testCasesBySourceFiles = analysisResults.mapValues { (sourceFile, analysisResult) -> + val functions = analysisResult.functions + if (!onTestCasesGenerationForGoSourceFileFunctionsStart(sourceFile, functions)) return + GoTestCasesGenerator.generateTestCasesForGoSourceFileFunctions( + sourceFile, + functions, + testsGenerationConfig.goExecutableAbsolutePath, + testsGenerationConfig.eachExecutionTimeoutsMillisConfig + ) { index -> isCanceled() || System.currentTimeMillis() - (startTimeMillis + (index + 1) * functionTimeoutStepMillis) > 0 } + .also { + startTimeMillis += functionTimeoutStepMillis * functions.size + if (!onTestCasesGenerationForGoSourceFileFunctionsFinished(sourceFile, it)) return + } + } + + testCasesBySourceFiles.forEach { (sourceFile, testCases) -> + if (!onTestCasesFileCodeGenerationStart(sourceFile, testCases)) return + val generatedTestsFileCode = GoTestCasesCodeGenerator.generateTestCasesFileCode(sourceFile, testCases) + if (!onTestCasesFileCodeGenerationFinished(sourceFile, generatedTestsFileCode)) return + } + } + + protected abstract fun onSourceCodeAnalysisStart( + targetFunctionsNamesBySourceFiles: Map> + ): Boolean + + protected abstract fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean + + protected abstract fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean + + protected abstract fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean + + protected abstract fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean + + protected abstract fun onTestCasesFileCodeGenerationFinished( + sourceFile: GoUtFile, + generatedTestsFileCode: String + ): Boolean + + protected fun generateMissingSelectedFunctionsListMessage( + analysisResults: Map, + ): String? { + val missingSelectedFunctions = analysisResults.filter { (_, analysisResult) -> + analysisResult.notSupportedFunctionsNames.isNotEmpty() || analysisResult.notFoundFunctionsNames.isNotEmpty() + } + if (missingSelectedFunctions.isEmpty()) { + return null + } + return missingSelectedFunctions.map { (sourceFile, analysisResult) -> + val notSupportedFunctions = analysisResult.notSupportedFunctionsNames.joinToString(separator = ", ") + val notFoundFunctions = analysisResult.notFoundFunctionsNames.joinToString(separator = ", ") + val messageSb = StringBuilder() + messageSb.append("File ${sourceFile.absolutePath}") + if (notSupportedFunctions.isNotEmpty()) { + messageSb.append("\n-- contains currently unsupported functions: $notSupportedFunctions") + } + if (notFoundFunctions.isNotEmpty()) { + messageSb.append("\n-- does not contain functions: $notFoundFunctions") + } + messageSb.toString() + }.joinToString(separator = "\n\n", prefix = "\n\n", postfix = "\n\n") + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt new file mode 100644 index 0000000000..50350c635b --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt @@ -0,0 +1,45 @@ +package org.utbot.go.logic + +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import org.utbot.go.GoEngine +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import kotlin.system.measureTimeMillis + +val logger = KotlinLogging.logger {} + +object GoTestCasesGenerator { + + fun generateTestCasesForGoSourceFileFunctions( + sourceFile: GoUtFile, + functions: List, + goExecutableAbsolutePath: String, + eachExecutionTimeoutsMillisConfig: EachExecutionTimeoutsMillisConfig, + timeoutExceededOrIsCanceled: (index: Int) -> Boolean + ): List = runBlocking { + return@runBlocking functions.flatMapIndexed { index, function -> + val testCases = mutableListOf() + if (timeoutExceededOrIsCanceled(index)) return@flatMapIndexed testCases + val engine = GoEngine( + function, + sourceFile, + goExecutableAbsolutePath, + eachExecutionTimeoutsMillisConfig, + { timeoutExceededOrIsCanceled(index) } + ) + logger.info { "Fuzzing for function [${function.name}] - started" } + val totalFuzzingTime = measureTimeMillis { + engine.fuzzing().catch { + logger.error { "Error in flow: ${it.message}" } + }.collect { (fuzzedFunction, executionResult) -> + testCases.add(GoUtFuzzedFunctionTestCase(fuzzedFunction, executionResult)) + } + } + logger.info { "Fuzzing for function [${function.name}] - completed in $totalFuzzingTime ms. Generated ${testCases.size} test cases" } + testCases + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/GoUtTestsGenerationConfig.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoUtTestsGenerationConfig.kt new file mode 100644 index 0000000000..4418a97b45 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoUtTestsGenerationConfig.kt @@ -0,0 +1,25 @@ +package org.utbot.go.logic + +import org.utbot.go.api.GoUtFunction + +data class EachExecutionTimeoutsMillisConfig(private val eachFunctionExecutionTimeoutMillis: Long) { + @Suppress("UNUSED_PARAMETER") // TODO: support finer tuning + operator fun get(function: GoUtFunction): Long = eachFunctionExecutionTimeoutMillis +} + +class GoUtTestsGenerationConfig( + val goExecutableAbsolutePath: String, + eachFunctionExecutionTimeoutMillis: Long, + allFunctionExecutionTimeoutMillis: Long +) { + + companion object Constants { + const val DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS: Long = 60000 + const val DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS: Long = 1000 + } + + val eachExecutionTimeoutsMillisConfig: EachExecutionTimeoutsMillisConfig = + EachExecutionTimeoutsMillisConfig(eachFunctionExecutionTimeoutMillis) + + val allExecutionTimeoutsMillisConfig: Long = allFunctionExecutionTimeoutMillis +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoCodeGenerationUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoCodeGenerationUtil.kt new file mode 100644 index 0000000000..8042203c6c --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoCodeGenerationUtil.kt @@ -0,0 +1,42 @@ +package org.utbot.go.simplecodegeneration + +import org.utbot.go.api.ExplicitCastMode +import org.utbot.go.api.GoPrimitiveTypeId +import org.utbot.go.api.GoUtFuzzedFunction +import org.utbot.go.api.GoUtPrimitiveModel +import org.utbot.go.framework.api.go.GoUtModel + + +fun generateFuzzedFunctionCall(functionName: String, parameters: List): String { + val fuzzedParametersToString = parameters.joinToString(prefix = "(", postfix = ")") { it.toString() } + return "$functionName$fuzzedParametersToString" +} + +fun generateVariablesDeclarationTo(variablesNames: List, expression: String): String { + val variables = variablesNames.joinToString() + return "$variables := $expression" +} + +fun generateFuzzedFunctionCallSavedToVariables( + variablesNames: List, + fuzzedFunction: GoUtFuzzedFunction +): String = generateVariablesDeclarationTo( + variablesNames, + generateFuzzedFunctionCall(fuzzedFunction.function.name, fuzzedFunction.parametersValues) +) + +fun generateCastIfNeed(toTypeId: GoPrimitiveTypeId, expressionType: GoPrimitiveTypeId, expression: String): String { + return if (expressionType != toTypeId) { + "${toTypeId.name}($expression)" + } else { + expression + } +} + +fun generateCastedValueIfPossible(model: GoUtPrimitiveModel): String { + return if (model.explicitCastMode == ExplicitCastMode.NEVER) { + model.toValueGoCode() + } else { + model.toCastedValueGoCode() + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt new file mode 100644 index 0000000000..53538c618d --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt @@ -0,0 +1,33 @@ +package org.utbot.go.simplecodegeneration + +class GoFileCodeBuilder( + packageName: String, + importNames: Set, +) { + private val packageLine: String = "package $packageName" + private val importLines: String = importLines(importNames) + private val topLevelElements: MutableList = mutableListOf() + + private fun importLines(importNames: Set): String { + val sortedImportNames = importNames.toList().sorted() + if (sortedImportNames.isEmpty()) return "" + if (sortedImportNames.size == 1) { + return "import ${sortedImportNames.first()}" + } + return sortedImportNames.joinToString(separator = "", prefix = "import (\n", postfix = ")") { + "\t\"$it\"\n" + } + } + + fun buildCodeString(): String { + return "$packageLine\n\n$importLines\n\n${topLevelElements.joinToString(separator = "\n\n")}" + } + + fun addTopLevelElements(vararg elements: String) { + topLevelElements.addAll(elements) + } + + fun addTopLevelElements(elements: Iterable) { + topLevelElements.addAll(elements) + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt new file mode 100644 index 0000000000..639984aca3 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt @@ -0,0 +1,209 @@ +package org.utbot.go.simplecodegeneration + +import org.utbot.go.api.* +import org.utbot.go.api.util.containsNaNOrInf +import org.utbot.go.api.util.doesNotContainNaNOrInf +import org.utbot.go.api.util.goFloat64TypeId +import org.utbot.go.api.util.goStringTypeId +import org.utbot.go.framework.api.go.GoUtModel + +object GoTestCasesCodeGenerator { + + fun generateTestCasesFileCode(sourceFile: GoUtFile, testCases: List): String { + val imports = if (testCases.isNotEmpty()) { + mutableSetOf("github.com/stretchr/testify/assert", "testing") + } else { + mutableSetOf() + } + testCases.forEach { testCase -> + testCase.parametersValues.forEach { + imports += it.getRequiredImports() + } + when (val executionResult = testCase.executionResult) { + is GoUtExecutionCompleted -> { + executionResult.models.forEach { + imports += it.getRequiredImports() + } + } + + is GoUtPanicFailure -> { + imports += executionResult.panicValue.getRequiredImports() + } + } + } + + val fileBuilder = GoFileCodeBuilder(sourceFile.packageName, imports) + + fun List.generateTestFunctions( + generateTestFunctionForTestCase: (GoUtFuzzedFunctionTestCase, Int?) -> String + ) { + this.forEachIndexed { testIndex, testCase -> + val testIndexToShow = if (this.size == 1) null else testIndex + 1 + val testFunctionCode = generateTestFunctionForTestCase(testCase, testIndexToShow) + fileBuilder.addTopLevelElements(testFunctionCode) + } + } + + testCases.groupBy { it.function }.forEach { (_, functionTestCases) -> + functionTestCases.filter { it.executionResult is GoUtExecutionCompleted } + .generateTestFunctions(::generateTestFunctionForCompletedExecutionTestCase) + functionTestCases.filter { it.executionResult is GoUtPanicFailure } + .generateTestFunctions(::generateTestFunctionForPanicFailureTestCase) + functionTestCases.filter { it.executionResult is GoUtTimeoutExceeded } + .generateTestFunctions(::generateTestFunctionForTimeoutExceededTestCase) + } + + return fileBuilder.buildCodeString() + } + + private fun generateTestFunctionForCompletedExecutionTestCase( + testCase: GoUtFuzzedFunctionTestCase, testIndexToShow: Int? + ): String { + val (fuzzedFunction, executionResult) = testCase + val function = fuzzedFunction.function + + val testFunctionNamePostfix = if (executionResult is GoUtExecutionWithNonNilError) { + "WithNonNilError" + } else { + "" + } + val testIndexToShowString = testIndexToShow ?: "" + val testFunctionSignatureDeclaration = + "func Test${function.name.replaceFirstChar(Char::titlecaseChar)}${testFunctionNamePostfix}ByUtGoFuzzer$testIndexToShowString(t *testing.T)" + + if (function.resultTypes.isEmpty()) { + val actualFunctionCall = + generateFuzzedFunctionCall(fuzzedFunction.function.name, fuzzedFunction.parametersValues) + val testFunctionBody = "\tassert.NotPanics(t, func() { $actualFunctionCall })\n" + return "$testFunctionSignatureDeclaration {\n$testFunctionBody}" + } + + val testFunctionBodySb = StringBuilder() + + val resultTypes = function.resultTypes + val doResultTypesImplementError = resultTypes.map { it.implementsError } + val actualResultVariablesNames = run { + val errorVariablesNumber = doResultTypesImplementError.count { it } + val commonVariablesNumber = resultTypes.size - errorVariablesNumber + + var errorVariablesIndex = 0 + var commonVariablesIndex = 0 + doResultTypesImplementError.map { implementsError -> + if (implementsError) { + "actualErr${if (errorVariablesNumber > 1) errorVariablesIndex++ else ""}" + } else { + "actualVal${if (commonVariablesNumber > 1) commonVariablesIndex++ else ""}" + } + } + } + val actualFunctionCall = generateFuzzedFunctionCallSavedToVariables(actualResultVariablesNames, fuzzedFunction) + testFunctionBodySb.append("\t$actualFunctionCall\n\n") + + val expectedModels = (executionResult as GoUtExecutionCompleted).models + val (assertionName, assertionTParameter) = if (expectedModels.size > 1 || expectedModels.any { it.isComplexModelAndNeedsSeparateAssertions() }) { + testFunctionBodySb.append("\tassertMultiple := assert.New(t)\n") + "assertMultiple" to "" + } else { + "assert" to "t, " + } + actualResultVariablesNames.zip(expectedModels).zip(doResultTypesImplementError) + .forEach { (resultVariableAndModel, doesResultTypeImplementError) -> + val (actualResultVariableName, expectedModel) = resultVariableAndModel + + val assertionCalls = mutableListOf() + fun generateAssertionCallHelper(refinedExpectedModel: GoUtModel, actualResultCode: String) { + val code = generateCompletedExecutionAssertionCall( + refinedExpectedModel, actualResultCode, doesResultTypeImplementError, assertionTParameter + ) + assertionCalls.add(code) + } + + if (expectedModel.isComplexModelAndNeedsSeparateAssertions()) { + val complexModel = expectedModel as GoUtComplexModel + generateAssertionCallHelper(complexModel.realValue, "real($actualResultVariableName)") + generateAssertionCallHelper(complexModel.imagValue, "imag($actualResultVariableName)") + } else { + generateAssertionCallHelper(expectedModel, actualResultVariableName) + } + assertionCalls.forEach { testFunctionBodySb.append("\t$assertionName.$it\n") } + } + val testFunctionBody = testFunctionBodySb.toString() + + return "$testFunctionSignatureDeclaration {\n$testFunctionBody}" + } + + private fun GoUtModel.isComplexModelAndNeedsSeparateAssertions(): Boolean = + this is GoUtComplexModel && this.containsNaNOrInf() + + private fun generateCompletedExecutionAssertionCall( + expectedModel: GoUtModel, + actualResultCode: String, + doesReturnTypeImplementError: Boolean, + assertionTParameter: String + ): String { + if (expectedModel is GoUtNilModel) { + return "Nil($assertionTParameter$actualResultCode)" + } + if (doesReturnTypeImplementError && expectedModel.typeId == goStringTypeId) { + return "ErrorContains($assertionTParameter$actualResultCode, $expectedModel)" + } + if (expectedModel is GoUtFloatNaNModel) { + val castedActualResultCode = generateCastIfNeed(goFloat64TypeId, expectedModel.typeId, actualResultCode) + return "True(${assertionTParameter}math.IsNaN($castedActualResultCode))" + } + if (expectedModel is GoUtFloatInfModel) { + val castedActualResultCode = generateCastIfNeed(goFloat64TypeId, expectedModel.typeId, actualResultCode) + return "True(${assertionTParameter}math.IsInf($castedActualResultCode, ${expectedModel.sign}))" + } + val prefix = if (!expectedModel.isComparable()) "Not" else "" + val castedExpectedResultCode = + if (expectedModel is GoUtPrimitiveModel) { + generateCastedValueIfPossible(expectedModel) + } else { + expectedModel.toString() + } + return "${prefix}Equal($assertionTParameter$castedExpectedResultCode, $actualResultCode)" + } + + private fun generateTestFunctionForPanicFailureTestCase( + testCase: GoUtFuzzedFunctionTestCase, testIndexToShow: Int? + ): String { + val (fuzzedFunction, executionResult) = testCase + val function = fuzzedFunction.function + + val testIndexToShowString = testIndexToShow ?: "" + val testFunctionSignatureDeclaration = + "func Test${function.name.capitalize()}PanicsByUtGoFuzzer$testIndexToShowString(t *testing.T)" + + val actualFunctionCall = + generateFuzzedFunctionCall(fuzzedFunction.function.name, fuzzedFunction.parametersValues) + val actualFunctionCallLambda = "func() { $actualFunctionCall }" + val (expectedPanicValue, isErrorMessage) = (executionResult as GoUtPanicFailure) + val isPrimitiveWithOkEquals = + expectedPanicValue is GoUtPrimitiveModel && expectedPanicValue.doesNotContainNaNOrInf() + val testFunctionBody = if (isErrorMessage) { + "\tassert.PanicsWithError(t, $expectedPanicValue, $actualFunctionCallLambda)" + } else if (isPrimitiveWithOkEquals || expectedPanicValue is GoUtNilModel) { + val expectedPanicValueCode = if (expectedPanicValue is GoUtNilModel) { + "$expectedPanicValue" + } else { + generateCastedValueIfPossible(expectedPanicValue as GoUtPrimitiveModel) + } + "\tassert.PanicsWithValue(t, $expectedPanicValueCode, $actualFunctionCallLambda)" + } else { + "\tassert.Panics(t, $actualFunctionCallLambda)" + } + + return "$testFunctionSignatureDeclaration {\n$testFunctionBody\n}" + } + + private fun generateTestFunctionForTimeoutExceededTestCase( + testCase: GoUtFuzzedFunctionTestCase, @Suppress("UNUSED_PARAMETER") testIndexToShow: Int? + ): String { + val (fuzzedFunction, executionResult) = testCase + val actualFunctionCall = + generateFuzzedFunctionCall(fuzzedFunction.function.name, fuzzedFunction.parametersValues) + val exceededTimeoutMillis = (executionResult as GoUtTimeoutExceeded).timeoutMillis + return "// $actualFunctionCall exceeded $exceededTimeoutMillis ms timeout" + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt new file mode 100644 index 0000000000..47cf0927da --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt @@ -0,0 +1,26 @@ +package org.utbot.go.util + +import com.beust.klaxon.Klaxon +import java.io.File + +fun convertObjectToJsonString(targetObject: T): String = Klaxon().toJsonString(targetObject) + +fun writeJsonToFileOrFail(targetObject: T, jsonFile: File) { + val targetObjectAsJson = convertObjectToJsonString(targetObject) + jsonFile.writeText(targetObjectAsJson) +} + +inline fun parseFromJsonOrFail(jsonFile: File): T { + val result = Klaxon().parse(jsonFile) + if (result == null) { + val rawResults = try { + jsonFile.readText() + } catch (exception: Exception) { + null + } + throw RuntimeException( + "Failed to deserialize results: $rawResults" + ) + } + return result +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt new file mode 100644 index 0000000000..bf7d170e64 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt @@ -0,0 +1,50 @@ +package org.utbot.go.util + +import java.io.File +import java.io.InputStreamReader + +fun executeCommandByNewProcessOrFail( + command: List, + workingDirectory: File, + executionTargetName: String, + environment: Map = System.getenv(), + helpMessage: String? = null +) { + val helpMessageLine = if (helpMessage == null) "" else "\n\nHELP: $helpMessage" + val executedProcess = runCatching { + val process = executeCommandByNewProcessOrFailWithoutWaiting(command, workingDirectory, environment) + process.waitFor() + process + }.getOrElse { + throw RuntimeException( + StringBuilder() + .append("Execution of $executionTargetName in child process failed with throwable: ") + .append("$it").append(helpMessageLine) + .toString() + ) + } + val exitCode = executedProcess.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(executedProcess.inputStream).readText() + throw RuntimeException( + StringBuilder() + .append("Execution of $executionTargetName in child process failed with non-zero exit code = $exitCode: ") + .append("\n$processOutput").append(helpMessageLine) + .toString() + ) + } +} + +fun executeCommandByNewProcessOrFailWithoutWaiting( + command: List, + workingDirectory: File, + environment: Map = System.getenv() +): Process { + val processBuilder = ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectErrorStream(true) + .directory(workingDirectory) + processBuilder.environment().clear() + processBuilder.environment().putAll(environment) + return processBuilder.start() +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/GoCodeTemplates.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoCodeTemplates.kt new file mode 100644 index 0000000000..261d16dbc2 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoCodeTemplates.kt @@ -0,0 +1,687 @@ +package org.utbot.go.worker + +import org.utbot.go.api.GoStructTypeId + +object GoCodeTemplates { + + private val traces = """ + var __traces__ []int + """.trimIndent() + + private val rawValueInterface = """ + type __RawValue__ interface { + __toReflectValue__() (reflect.Value, error) + } + """.trimIndent() + + private val primitiveValueStruct = """ + type __PrimitiveValue__ struct { + Type string `json:"type"` + Value string `json:"value"` + } + """.trimIndent() + + private val primitiveValueToReflectValueMethod = """ + func (v __PrimitiveValue__) __toReflectValue__() (reflect.Value, error) { + const complexPartsDelimiter = "@" + + switch v.Type { + case "bool": + value, err := strconv.ParseBool(v.Value) + __checkErrorAndExit__(err) + + return reflect.ValueOf(value), nil + case "int": + value, err := strconv.Atoi(v.Value) + __checkErrorAndExit__(err) + + return reflect.ValueOf(value), nil + case "int8": + value, err := strconv.ParseInt(v.Value, 10, 8) + __checkErrorAndExit__(err) + + return reflect.ValueOf(int8(value)), nil + case "int16": + value, err := strconv.ParseInt(v.Value, 10, 16) + __checkErrorAndExit__(err) + + return reflect.ValueOf(int16(value)), nil + case "int32": + value, err := strconv.ParseInt(v.Value, 10, 32) + __checkErrorAndExit__(err) + + return reflect.ValueOf(int32(value)), nil + case "rune": + value, err := strconv.ParseInt(v.Value, 10, 32) + __checkErrorAndExit__(err) + + return reflect.ValueOf(rune(value)), nil + case "int64": + value, err := strconv.ParseInt(v.Value, 10, 64) + __checkErrorAndExit__(err) + + return reflect.ValueOf(value), nil + case "byte": + value, err := strconv.ParseUint(v.Value, 10, 8) + __checkErrorAndExit__(err) + + return reflect.ValueOf(byte(value)), nil + case "uint": + value, err := strconv.ParseUint(v.Value, 10, strconv.IntSize) + __checkErrorAndExit__(err) + + return reflect.ValueOf(uint(value)), nil + case "uint8": + value, err := strconv.ParseUint(v.Value, 10, 8) + __checkErrorAndExit__(err) + + return reflect.ValueOf(uint8(value)), nil + case "uint16": + value, err := strconv.ParseUint(v.Value, 10, 16) + __checkErrorAndExit__(err) + + return reflect.ValueOf(uint16(value)), nil + case "uint32": + value, err := strconv.ParseUint(v.Value, 10, 32) + __checkErrorAndExit__(err) + + return reflect.ValueOf(uint32(value)), nil + case "uint64": + value, err := strconv.ParseUint(v.Value, 10, 64) + __checkErrorAndExit__(err) + + return reflect.ValueOf(value), nil + case "float32": + value, err := strconv.ParseFloat(v.Value, 32) + __checkErrorAndExit__(err) + + return reflect.ValueOf(float32(value)), nil + case "float64": + value, err := strconv.ParseFloat(v.Value, 64) + __checkErrorAndExit__(err) + + return reflect.ValueOf(value), nil + case "complex64": + splittedValue := strings.Split(v.Value, complexPartsDelimiter) + if len(splittedValue) != 2 { + return reflect.Value{}, fmt.Errorf("not correct complex64 value") + } + realPart, err := strconv.ParseFloat(splittedValue[0], 32) + __checkErrorAndExit__(err) + + imaginaryPart, err := strconv.ParseFloat(splittedValue[1], 32) + __checkErrorAndExit__(err) + + return reflect.ValueOf(complex(float32(realPart), float32(imaginaryPart))), nil + case "complex128": + splittedValue := strings.Split(v.Value, complexPartsDelimiter) + if len(splittedValue) != 2 { + return reflect.Value{}, fmt.Errorf("not correct complex128 value") + } + + realPart, err := strconv.ParseFloat(splittedValue[0], 64) + __checkErrorAndExit__(err) + + imaginaryPart, err := strconv.ParseFloat(splittedValue[1], 64) + __checkErrorAndExit__(err) + + return reflect.ValueOf(complex(realPart, imaginaryPart)), nil + case "string": + return reflect.ValueOf(v.Value), nil + case "uintptr": + value, err := strconv.ParseUint(v.Value, 10, strconv.IntSize) + __checkErrorAndExit__(err) + + return reflect.ValueOf(uintptr(value)), nil + } + return reflect.Value{}, fmt.Errorf("not supported type %s", v.Type) + } + """.trimIndent() + + private val fieldValueStruct = """ + type __FieldValue__ struct { + Name string `json:"name"` + Value __RawValue__ `json:"value"` + IsExported bool `json:"isExported"` + } + """.trimIndent() + + private val structValueStruct = """ + type __StructValue__ struct { + Type string `json:"type"` + Value []__FieldValue__ `json:"value"` + } + """.trimIndent() + + private val structValueToReflectValueMethod = """ + func (v __StructValue__) __toReflectValue__() (reflect.Value, error) { + structType, err := __convertStringToReflectType__(v.Type) + __checkErrorAndExit__(err) + + structPtr := reflect.New(structType) + + for _, f := range v.Value { + field := structPtr.Elem().FieldByName(f.Name) + + reflectValue, err := f.Value.__toReflectValue__() + __checkErrorAndExit__(err) + + if field.Type().Kind() == reflect.Uintptr { + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().SetUint(reflectValue.Uint()) + } else { + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflectValue) + } + } + + return structPtr.Elem(), nil + } + """.trimIndent() + + private val arrayValueStruct = """ + type __ArrayValue__ struct { + Type string `json:"type"` + ElementType string `json:"elementType"` + Length int `json:"length"` + Value []__RawValue__ `json:"value"` + } + """.trimIndent() + + private val arrayValueToReflectValueMethod = """ + func (v __ArrayValue__) __toReflectValue__() (reflect.Value, error) { + elementType, err := __convertStringToReflectType__(v.ElementType) + __checkErrorAndExit__(err) + + arrayType := reflect.ArrayOf(v.Length, elementType) + arrayPtr := reflect.New(arrayType) + + for i := 0; i < v.Length; i++ { + element := arrayPtr.Elem().Index(i) + + reflectValue, err := v.Value[i].__toReflectValue__() + __checkErrorAndExit__(err) + + element.Set(reflectValue) + } + + return arrayPtr.Elem(), nil + } + """.trimIndent() + + private fun convertStringToReflectType(structTypes: Set, packageName: String) = """ + func __convertStringToReflectType__(typeName string) (reflect.Type, error) { + var result reflect.Type + + switch { + case strings.HasPrefix(typeName, "map["): + return nil, fmt.Errorf("map type not supported") + case strings.HasPrefix(typeName, "[]"): + return nil, fmt.Errorf("slice type not supported") + case strings.HasPrefix(typeName, "["): + index := strings.IndexRune(typeName, ']') + if index == -1 { + return nil, fmt.Errorf("not correct type name '%s'", typeName) + } + + lengthStr := typeName[1:index] + length, err := strconv.Atoi(lengthStr) + if err != nil { + return nil, err + } + + res, err := __convertStringToReflectType__(typeName[index+1:]) + if err != nil { + return nil, err + } + + result = reflect.ArrayOf(length, res) + default: + switch typeName { + case "bool": + result = reflect.TypeOf(true) + case "int": + result = reflect.TypeOf(0) + case "int8": + result = reflect.TypeOf(int8(0)) + case "int16": + result = reflect.TypeOf(int16(0)) + case "int32": + result = reflect.TypeOf(int32(0)) + case "rune": + result = reflect.TypeOf(rune(0)) + case "int64": + result = reflect.TypeOf(int64(0)) + case "byte": + result = reflect.TypeOf(byte(0)) + case "uint": + result = reflect.TypeOf(uint(0)) + case "uint8": + result = reflect.TypeOf(uint8(0)) + case "uint16": + result = reflect.TypeOf(uint16(0)) + case "uint32": + result = reflect.TypeOf(uint32(0)) + case "uint64": + result = reflect.TypeOf(uint64(0)) + case "float32": + result = reflect.TypeOf(float32(0)) + case "float64": + result = reflect.TypeOf(float64(0)) + case "complex64": + result = reflect.TypeOf(complex(float32(0), float32(0))) + case "complex128": + result = reflect.TypeOf(complex(float64(0), float64(0))) + case "string": + result = reflect.TypeOf("") + case "uintptr": + result = reflect.TypeOf(uintptr(0)) + ${structTypes.joinToString(separator = "\n") { "case \"${it.canonicalName}\": result = reflect.TypeOf(${it.getRelativeName(packageName)}{})" }} + default: + return nil, fmt.Errorf("type '%s' not supported", typeName) + } + } + return result, nil + } + """.trimIndent() + + private val panicMessageStruct = """ + type __RawPanicMessage__ struct { + RawResultValue __RawValue__ `json:"rawResultValue"` + ImplementsError bool `json:"implementsError"` + } + """.trimIndent() + + private val rawExecutionResultStruct = """ + type __RawExecutionResult__ struct { + TimeoutExceeded bool `json:"timeoutExceeded"` + RawResultValues []__RawValue__ `json:"rawResultValues"` + PanicMessage *__RawPanicMessage__ `json:"panicMessage"` + Trace []int `json:"trace"` + } + """.trimIndent() + + private val checkErrorFunction = """ + func __checkErrorAndExit__(err error) { + if err != nil { + log.Fatal(err) + } + } + """.trimIndent() + + private val convertFloat64ValueToStringFunction = """ + func __convertFloat64ValueToString__(value float64) string { + const outputNaN = "NaN" + const outputPosInf = "+Inf" + const outputNegInf = "-Inf" + switch { + case math.IsNaN(value): + return fmt.Sprint(outputNaN) + case math.IsInf(value, 1): + return fmt.Sprint(outputPosInf) + case math.IsInf(value, -1): + return fmt.Sprint(outputNegInf) + default: + return fmt.Sprintf("%v", value) + } + } + """.trimIndent() + + private val convertReflectValueToRawValueFunction = """ + //goland:noinspection GoPreferNilSlice + func __convertReflectValueToRawValue__(valueOfRes reflect.Value) (__RawValue__, error) { + const outputComplexPartsDelimiter = "@" + + switch valueOfRes.Kind() { + case reflect.Bool: + return __PrimitiveValue__{ + Type: valueOfRes.Kind().String(), + Value: fmt.Sprintf("%#v", valueOfRes.Bool()), + }, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return __PrimitiveValue__{ + Type: valueOfRes.Kind().String(), + Value: fmt.Sprintf("%#v", valueOfRes.Int()), + }, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return __PrimitiveValue__{ + Type: valueOfRes.Kind().String(), + Value: fmt.Sprintf("%v", valueOfRes.Uint()), + }, nil + case reflect.Float32, reflect.Float64: + return __PrimitiveValue__{ + Type: valueOfRes.Kind().String(), + Value: __convertFloat64ValueToString__(valueOfRes.Float()), + }, nil + case reflect.Complex64, reflect.Complex128: + value := valueOfRes.Complex() + realPartString := __convertFloat64ValueToString__(real(value)) + imagPartString := __convertFloat64ValueToString__(imag(value)) + return __PrimitiveValue__{ + Type: valueOfRes.Kind().String(), + Value: fmt.Sprintf("%v%v%v", realPartString, outputComplexPartsDelimiter, imagPartString), + }, nil + case reflect.String: + return __PrimitiveValue__{ + Type: reflect.String.String(), + Value: fmt.Sprintf("%v", valueOfRes.String()), + }, nil + case reflect.Struct: + fields := reflect.VisibleFields(valueOfRes.Type()) + resultValues := make([]__FieldValue__, len(fields)) + for i, field := range fields { + res, err := __convertReflectValueToRawValue__(valueOfRes.FieldByName(field.Name)) + __checkErrorAndExit__(err) + + resultValues[i] = __FieldValue__{ + Name: field.Name, + Value: res, + IsExported: field.IsExported(), + } + } + return __StructValue__{ + Type: valueOfRes.Type().String(), + Value: resultValues, + }, nil + case reflect.Array: + elem := valueOfRes.Type().Elem() + elementType := elem.String() + arrayElementValues := []__RawValue__{} + for i := 0; i < valueOfRes.Len(); i++ { + arrayElementValue, err := __convertReflectValueToRawValue__(valueOfRes.Index(i)) + __checkErrorAndExit__(err) + + arrayElementValues = append(arrayElementValues, arrayElementValue) + } + length := len(arrayElementValues) + return __ArrayValue__{ + Type: fmt.Sprintf("[%d]%s", length, elementType), + ElementType: elementType, + Length: length, + Value: arrayElementValues, + }, nil + case reflect.Interface: + if valueOfRes.Interface() == nil { + return nil, nil + } + if e, ok := valueOfRes.Interface().(error); ok { + return __convertReflectValueToRawValue__(reflect.ValueOf(e.Error())) + } + return nil, errors.New("unsupported result type: " + valueOfRes.Type().String()) + default: + return nil, errors.New("unsupported result type: " + valueOfRes.Type().String()) + } + } + """.trimIndent() + + private val executeFunctionFunction = """ + func __executeFunction__( + timeoutMillis time.Duration, wrappedFunction func() []__RawValue__, + ) __RawExecutionResult__ { + ctxWithTimeout, cancel := context.WithTimeout(context.Background(), timeoutMillis) + defer cancel() + + done := make(chan __RawExecutionResult__, 1) + go func() { + executionResult := __RawExecutionResult__{ + TimeoutExceeded: false, + RawResultValues: []__RawValue__{}, + PanicMessage: nil, + } + panicked := true + defer func() { + panicMessage := recover() + if panicked { + panicAsError, implementsError := panicMessage.(error) + var ( + resultValue __RawValue__ + err error + ) + if implementsError { + resultValue, err = __convertReflectValueToRawValue__(reflect.ValueOf(panicAsError.Error())) + } else { + resultValue, err = __convertReflectValueToRawValue__(reflect.ValueOf(panicMessage)) + } + __checkErrorAndExit__(err) + + executionResult.PanicMessage = &__RawPanicMessage__{ + RawResultValue: resultValue, + ImplementsError: implementsError, + } + } + executionResult.Trace = __traces__ + done <- executionResult + }() + + resultValues := wrappedFunction() + executionResult.RawResultValues = resultValues + panicked = false + }() + + select { + case timelyExecutionResult := <-done: + return timelyExecutionResult + case <-ctxWithTimeout.Done(): + return __RawExecutionResult__{ + TimeoutExceeded: true, + RawResultValues: []__RawValue__{}, + PanicMessage: nil, + Trace: __traces__, + } + } + } + """.trimIndent() + + private val wrapResultValuesForWorkerFunction = """ + //goland:noinspection GoPreferNilSlice + func __wrapResultValuesForUtBotGoWorker__(values []reflect.Value) []__RawValue__ { + rawValues := []__RawValue__{} + for _, value := range values { + resultValue, err := __convertReflectValueToRawValue__(value) + __checkErrorAndExit__(err) + + rawValues = append(rawValues, resultValue) + } + return rawValues + } + """.trimIndent() + + private val convertRawValuesToReflectValuesFunction = """ + //goland:noinspection GoPreferNilSlice + func __convertRawValuesToReflectValues__(values []__RawValue__) []reflect.Value { + parameters := []reflect.Value{} + + for _, value := range values { + reflectValue, err := value.__toReflectValue__() + __checkErrorAndExit__(err) + + parameters = append(parameters, reflectValue) + } + + return parameters + } + """.trimIndent() + + private val parseJsonToRawValuesFunction = """ + //goland:noinspection GoPreferNilSlice + func __parseJsonToRawValues__(decoder *json.Decoder) ([]__RawValue__, error) { + result := []__RawValue__{} + + // read '[' + _, err := decoder.Token() + if err == io.EOF { + return nil, err + } + __checkErrorAndExit__(err) + + for decoder.More() { + var p map[string]interface{} + err = decoder.Decode(&p) + __checkErrorAndExit__(err) + + rawValue, err := __convertParsedJsonToRawValue__(p) + __checkErrorAndExit__(err) + + result = append(result, rawValue) + } + + // read ']' + _, err = decoder.Token() + __checkErrorAndExit__(err) + + return result, nil + } + """.trimIndent() + + private val convertParsedJsonToRawValueFunction = """ + //goland:noinspection GoPreferNilSlice + func __convertParsedJsonToRawValue__(p map[string]interface{}) (__RawValue__, error) { + rawValue := p + + typeName, ok := rawValue["type"] + if !ok { + return nil, fmt.Errorf("every rawValue must contain field 'type'") + } + typeNameStr, ok := typeName.(string) + if !ok { + return nil, fmt.Errorf("field 'type' must be string") + } + + v, ok := rawValue["value"] + if !ok { + return nil, fmt.Errorf("every rawValue must contain field 'value'") + } + + switch { + case strings.HasPrefix(typeNameStr, "map["): + return nil, fmt.Errorf("map type not supported") + case strings.HasPrefix(typeNameStr, "[]"): + return nil, fmt.Errorf("slice type not supported") + case strings.HasPrefix(typeNameStr, "["): + elementType, ok := rawValue["elementType"] + if !ok { + return nil, fmt.Errorf("arrayValue must contain field 'elementType") + } + elementTypeStr, ok := elementType.(string) + if !ok { + return nil, fmt.Errorf("arrayValue field 'elementType' must be string") + } + + if _, ok := rawValue["length"]; !ok { + return nil, fmt.Errorf("arrayValue must contain field 'length'") + } + length, ok := rawValue["length"].(float64) + if !ok { + return nil, fmt.Errorf("arrayValue field 'length' must be float64") + } + + value, ok := v.([]interface{}) + if !ok || len(value) != int(length) { + return nil, fmt.Errorf("arrayValue field 'value' must be array of length %d", int(length)) + } + + values := []__RawValue__{} + for _, v := range value { + nextValue, err := __convertParsedJsonToRawValue__(v.(map[string]interface{})) + __checkErrorAndExit__(err) + + values = append(values, nextValue) + } + + return __ArrayValue__{ + Type: typeNameStr, + ElementType: elementTypeStr, + Length: int(length), + Value: values, + }, nil + default: + switch typeNameStr { + case "bool", "rune", "int", "int8", "int16", "int32", "int64", "byte", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "complex64", "complex128", "string", "uintptr": + value, ok := v.(string) + if !ok { + return nil, fmt.Errorf("primitiveValue field 'value' must be string") + } + + return __PrimitiveValue__{ + Type: typeNameStr, + Value: value, + }, nil + default: + value, ok := v.([]interface{}) + if !ok { + return nil, fmt.Errorf("structValue field 'value' must be array") + } + + values := []__FieldValue__{} + for _, v := range value { + nextValue, err := __convertParsedJsonToFieldValue__(v.(map[string]interface{})) + __checkErrorAndExit__(err) + + values = append(values, nextValue) + } + + return __StructValue__{ + Type: typeNameStr, + Value: values, + }, nil + } + } + } + """.trimIndent() + + private val convertParsedJsonToFieldValueFunction = """ + func __convertParsedJsonToFieldValue__(p map[string]interface{}) (__FieldValue__, error) { + name, ok := p["name"] + if !ok { + return __FieldValue__{}, fmt.Errorf("fieldValue must contain field 'name'") + } + nameStr, ok := name.(string) + if !ok { + return __FieldValue__{}, fmt.Errorf("fieldValue 'name' must be string") + } + + isExported, ok := p["isExported"] + if !ok { + return __FieldValue__{}, fmt.Errorf("fieldValue must contain field 'isExported'") + } + isExportedBool, ok := isExported.(bool) + if !ok { + return __FieldValue__{}, fmt.Errorf("fieldValue 'isExported' must be bool") + } + + if _, ok := p["value"]; !ok { + return __FieldValue__{}, fmt.Errorf("fieldValue must contain field 'value'") + } + value, err := __convertParsedJsonToRawValue__(p["value"].(map[string]interface{})) + __checkErrorAndExit__(err) + + return __FieldValue__{ + Name: nameStr, + Value: value, + IsExported: isExportedBool, + }, nil + } + """.trimIndent() + + fun getTopLevelHelperStructsAndFunctionsForWorker(structTypes: Set, packageName: String) = listOf( + traces, + rawValueInterface, + primitiveValueStruct, + primitiveValueToReflectValueMethod, + fieldValueStruct, + structValueStruct, + structValueToReflectValueMethod, + arrayValueStruct, + arrayValueToReflectValueMethod, + convertStringToReflectType(structTypes, packageName), + panicMessageStruct, + rawExecutionResultStruct, + checkErrorFunction, + convertFloat64ValueToStringFunction, + convertReflectValueToRawValueFunction, + executeFunctionFunction, + wrapResultValuesForWorkerFunction, + convertRawValuesToReflectValuesFunction, + parseJsonToRawValuesFunction, + convertParsedJsonToRawValueFunction, + convertParsedJsonToFieldValueFunction + ) +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorker.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorker.kt new file mode 100644 index 0000000000..902c3acf55 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorker.kt @@ -0,0 +1,64 @@ +package org.utbot.go.worker + +import com.beust.klaxon.Klaxon +import org.utbot.go.api.GoUtArrayModel +import org.utbot.go.api.GoUtComplexModel +import org.utbot.go.api.GoUtPrimitiveModel +import org.utbot.go.api.GoUtStructModel +import org.utbot.go.framework.api.go.GoUtModel +import org.utbot.go.util.convertObjectToJsonString +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.net.Socket + +class GoWorker( + socket: Socket, +) { + private val reader: BufferedReader = BufferedReader(InputStreamReader(socket.getInputStream())) + private val writer: BufferedWriter = BufferedWriter(OutputStreamWriter(socket.getOutputStream())) + + private fun GoUtModel.convertToRawValue(): RawValue = when (val model = this) { + is GoUtComplexModel -> PrimitiveValue( + model.typeId.name, + "${model.realValue.toValueGoCode()}@${model.imagValue.toValueGoCode()}" + ) + + is GoUtArrayModel -> ArrayValue( + model.typeId.name, + model.typeId.elementTypeId!!.canonicalName, + model.length, + model.getElements(model.typeId.elementTypeId!!).map { it.convertToRawValue() } + ) + + is GoUtStructModel -> StructValue( + model.typeId.canonicalName, + model.value.map { + StructValue.FieldValue( + it.fieldId.name, + it.model.convertToRawValue(), + it.fieldId.isExported + ) + } + ) + + is GoUtPrimitiveModel -> PrimitiveValue(model.typeId.name, model.value.toString()) + + else -> error("Converting ${model.javaClass} to RawValue is not supported") + } + + fun sendFuzzedParametersValues(parameters: List) { + val rawValues = parameters.map { it.convertToRawValue() } + val json = convertObjectToJsonString(rawValues) + writer.write(json) + writer.flush() + } + + fun receiveRawExecutionResult(): RawExecutionResult { + val length = reader.readLine().toInt() + val buffer = CharArray(length) + reader.read(buffer) + return Klaxon().parse(String(buffer)) ?: error("") + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorkerCodeGenerationHelper.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorkerCodeGenerationHelper.kt new file mode 100644 index 0000000000..e5c71f9d92 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/GoWorkerCodeGenerationHelper.kt @@ -0,0 +1,130 @@ +package org.utbot.go.worker + +import org.utbot.go.api.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.logic.EachExecutionTimeoutsMillisConfig +import org.utbot.go.simplecodegeneration.GoFileCodeBuilder +import java.io.File + +internal object GoWorkerCodeGenerationHelper { + + const val workerTestFunctionName = "TestGoFileFuzzedFunctionsByUtGoWorker" + + private val alwaysRequiredImports = setOf( + "io", + "context", + "encoding/json", + "errors", + "fmt", + "log", + "math", + "net", + "reflect", + "strconv", + "strings", + "testing", + "time", + "unsafe" + ) + + fun createFileToExecute( + sourceFile: GoUtFile, + function: GoUtFunction, + eachExecutionTimeoutsMillisConfig: EachExecutionTimeoutsMillisConfig, + port: Int + ): File { + val fileToExecuteName = createFileToExecuteName(sourceFile) + val sourceFileDir = File(sourceFile.absoluteDirectoryPath) + val fileToExecute = sourceFileDir.resolve(fileToExecuteName) + + val fileToExecuteGoCode = generateWorkerTestFileGoCode( + sourceFile, function, eachExecutionTimeoutsMillisConfig, port + ) + fileToExecute.writeText(fileToExecuteGoCode) + return fileToExecute + } + + private fun createFileToExecuteName(sourceFile: GoUtFile): String { + return "utbot_go_worker_${sourceFile.fileNameWithoutExtension}_test.go" + } + + private fun generateWorkerTestFileGoCode( + sourceFile: GoUtFile, + function: GoUtFunction, + eachExecutionTimeoutsMillisConfig: EachExecutionTimeoutsMillisConfig, + port: Int + ): String { + fun GoTypeId.getAllStructTypes(): Set = when (this) { + is GoStructTypeId -> fields.fold(setOf(this)) { acc: Set, field -> + acc + (field.declaringType).getAllStructTypes() + } + + is GoArrayTypeId -> elementTypeId!!.getAllStructTypes() + else -> emptySet() + } + + val structTypes = + function.parameters.fold(emptySet()) { acc: Set, functionParameter: GoUtFunctionParameter -> + acc + functionParameter.type.getAllStructTypes() + } + val imports = structTypes.fold(emptySet()) { acc: Set, goStructTypeId -> + if (goStructTypeId.packageName != sourceFile.packageName) acc + goStructTypeId.packagePath else acc + } + val fileCodeBuilder = GoFileCodeBuilder(sourceFile.packageName, alwaysRequiredImports + imports) + + val workerTestFunctionCode = generateWorkerTestFunctionCode(function, eachExecutionTimeoutsMillisConfig, port) + val modifiedFunction = function.modifiedFunctionForCollectingTraces + fileCodeBuilder.addTopLevelElements( + GoCodeTemplates.getTopLevelHelperStructsAndFunctionsForWorker( + structTypes, sourceFile.packageName + ) + modifiedFunction + workerTestFunctionCode + ) + + return fileCodeBuilder.buildCodeString() + } + + private fun generateWorkerTestFunctionCode( + function: GoUtFunction, eachExecutionTimeoutsMillisConfig: EachExecutionTimeoutsMillisConfig, port: Int + ): String { + val timeoutMillis = eachExecutionTimeoutsMillisConfig[function] + return """ + func $workerTestFunctionName(t *testing.T) { + con, err := net.Dial("tcp", ":$port") + __checkErrorAndExit__(err) + + defer func(con net.Conn) { + err := con.Close() + if err != nil { + __checkErrorAndExit__(err) + } + }(con) + + jsonDecoder := json.NewDecoder(con) + for { + rawValues, err := __parseJsonToRawValues__(jsonDecoder) + if err == io.EOF { + break + } + __checkErrorAndExit__(err) + + parameters := __convertRawValuesToReflectValues__(rawValues) + function := reflect.ValueOf(${function.modifiedName}) + + executionResult := __executeFunction__($timeoutMillis*time.Millisecond, func() []__RawValue__ { + __traces__ = []int{} + return __wrapResultValuesForUtBotGoWorker__(function.Call(parameters)) + }) + + jsonBytes, toJsonErr := json.MarshalIndent(executionResult, "", " ") + __checkErrorAndExit__(toJsonErr) + + _, err = con.Write([]byte(strconv.Itoa(len(jsonBytes)) + "\n")) + __checkErrorAndExit__(err) + + _, err = con.Write(jsonBytes) + __checkErrorAndExit__(err) + } + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/worker/RawExecutionResults.kt b/utbot-go/src/main/kotlin/org/utbot/go/worker/RawExecutionResults.kt new file mode 100644 index 0000000000..068ee06499 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/worker/RawExecutionResults.kt @@ -0,0 +1,255 @@ +package org.utbot.go.worker + +import com.beust.klaxon.TypeAdapter +import com.beust.klaxon.TypeFor +import org.utbot.go.api.* +import org.utbot.go.api.util.* +import org.utbot.go.framework.api.go.GoTypeId +import org.utbot.go.framework.api.go.GoUtFieldModel +import org.utbot.go.framework.api.go.GoUtModel +import kotlin.reflect.KClass + +data class PrimitiveValue( + override val type: String, + override val value: String, +) : RawValue(type, value) { + override fun checkIsEqualTypes(type: GoTypeId): Boolean { + if (!type.isPrimitiveGoType && type !is GoInterfaceTypeId && !type.implementsError) { + return false + } + // byte is an alias for uint8 and rune is an alias for int32 + if (this.type == "uint8" && type == goByteTypeId || this.type == "int32" && type == goRuneTypeId) { + return true + } + // for error support + if (this.type == "string" && type is GoInterfaceTypeId && type.implementsError) { + return true + } + return this.type == type.simpleName + } +} + +data class StructValue( + override val type: String, + override val value: List +) : RawValue(type, value) { + data class FieldValue( + val name: String, + val value: RawValue, + val isExported: Boolean + ) + + override fun checkIsEqualTypes(type: GoTypeId): Boolean { + if (type !is GoStructTypeId) { + return false + } + if (this.type != type.canonicalName) { + return false + } + if (value.size != type.fields.size) { + return false + } + value.zip(type.fields).forEach { (fieldValue, fieldId) -> + if (fieldValue.name != fieldId.name) { + return false + } + if (!fieldValue.value.checkIsEqualTypes(fieldId.declaringType)) { + return false + } + if (fieldValue.isExported != fieldId.isExported) { + return false + } + } + return true + } +} + +data class ArrayValue( + override val type: String, + val elementType: String, + val length: Int, + override val value: List +) : RawValue(type, value) { + override fun checkIsEqualTypes(type: GoTypeId): Boolean { + if (type !is GoArrayTypeId) { + return false + } + if (length != type.length || elementType != type.elementTypeId!!.canonicalName) { + return false + } + return value.all { it.checkIsEqualTypes(type.elementTypeId) } + } +} + +@TypeFor(field = "type", adapter = RawResultValueAdapter::class) +abstract class RawValue(open val type: String, open val value: Any) { + abstract fun checkIsEqualTypes(type: GoTypeId): Boolean +} + +class RawResultValueAdapter : TypeAdapter { + override fun classFor(type: Any): KClass { + val typeName = type as String + return when { + typeName.startsWith("map[") -> error("Map result type not supported") + typeName.startsWith("[]") -> error("Slice result type not supported") + typeName.startsWith("[") -> ArrayValue::class + goPrimitives.map { it.name }.contains(typeName) -> PrimitiveValue::class + else -> StructValue::class + } + } +} + +data class RawPanicMessage( + val rawResultValue: RawValue, + val implementsError: Boolean +) + +data class RawExecutionResult( + val timeoutExceeded: Boolean, + val rawResultValues: List, + val panicMessage: RawPanicMessage?, + val trace: List +) + +private object RawValuesCodes { + const val NAN_VALUE = "NaN" + const val POS_INF_VALUE = "+Inf" + const val NEG_INF_VALUE = "-Inf" + const val COMPLEX_PARTS_DELIMITER = "@" +} + +fun convertRawExecutionResultToExecutionResult( + packageName: String, + rawExecutionResult: RawExecutionResult, + functionResultTypes: List, + timeoutMillis: Long +): GoUtExecutionResult { + if (rawExecutionResult.timeoutExceeded) { + return GoUtTimeoutExceeded(timeoutMillis, rawExecutionResult.trace) + } + if (rawExecutionResult.panicMessage != null) { + val (rawResultValue, implementsError) = rawExecutionResult.panicMessage + val panicValue = if (goPrimitives.map { it.simpleName }.contains(rawResultValue.type)) { + createGoUtPrimitiveModelFromRawValue( + rawResultValue as PrimitiveValue, + GoPrimitiveTypeId(rawResultValue.type) + ) + } else { + error("Only primitive panic value is currently supported") + } + return GoUtPanicFailure(panicValue, implementsError, rawExecutionResult.trace) + } + if (rawExecutionResult.rawResultValues.size != functionResultTypes.size) { + error("Function completed execution must have as many result raw values as result types.") + } + rawExecutionResult.rawResultValues.zip(functionResultTypes).forEach { (rawResultValue, resultType) -> + if (rawResultValue == null) { + if (resultType !is GoInterfaceTypeId) { + error("Result of function execution must have same type as function result") + } + return@forEach + } + if (!rawResultValue.checkIsEqualTypes(resultType)) { + error("Result of function execution must have same type as function result") + } + } + var executedWithNonNilErrorString = false + val resultValues = + rawExecutionResult.rawResultValues.zip(functionResultTypes).map { (rawResultValue, resultType) -> + if (resultType.implementsError && rawResultValue != null) { + executedWithNonNilErrorString = true + } + createGoUtModelFromRawValue(rawResultValue, resultType, packageName) + } + return if (executedWithNonNilErrorString) { + GoUtExecutionWithNonNilError(resultValues, rawExecutionResult.trace) + } else { + GoUtExecutionSuccess(resultValues, rawExecutionResult.trace) + } +} + +private fun createGoUtModelFromRawValue( + rawValue: RawValue?, typeId: GoTypeId, packageName: String +): GoUtModel = when (typeId) { + // Only for error interface + is GoInterfaceTypeId -> if (rawValue == null) { + GoUtNilModel(typeId) + } else { + GoUtPrimitiveModel((rawValue as PrimitiveValue).value, goStringTypeId) + } + + is GoStructTypeId -> createGoUtStructModelFromRawValue(rawValue as StructValue, typeId, packageName) + + is GoArrayTypeId -> createGoUtArrayModelFromRawValue(rawValue as ArrayValue, typeId, packageName) + + is GoPrimitiveTypeId -> createGoUtPrimitiveModelFromRawValue(rawValue as PrimitiveValue, typeId) + + else -> error("Creating a model from raw value of [${typeId.javaClass}] type is not supported") +} + +private fun createGoUtPrimitiveModelFromRawValue( + resultValue: PrimitiveValue, typeId: GoPrimitiveTypeId +): GoUtPrimitiveModel { + val rawValue = resultValue.value + if (typeId == goFloat64TypeId || typeId == goFloat32TypeId) { + return convertRawFloatValueToGoUtPrimitiveModel(rawValue, typeId) + } + if (typeId == goComplex128TypeId || typeId == goComplex64TypeId) { + val correspondingFloatType = if (typeId == goComplex128TypeId) goFloat64TypeId else goFloat32TypeId + val (realPartModel, imagPartModel) = rawValue.split(RawValuesCodes.COMPLEX_PARTS_DELIMITER).map { + convertRawFloatValueToGoUtPrimitiveModel(it, correspondingFloatType, typeId == goComplex64TypeId) + } + return GoUtComplexModel(realPartModel, imagPartModel, typeId) + } + val value = when (typeId.correspondingKClass) { + UByte::class -> rawValue.toUByte() + Boolean::class -> rawValue.toBoolean() + Float::class -> rawValue.toFloat() + Double::class -> rawValue.toDouble() + Int::class -> rawValue.toInt() + Short::class -> rawValue.toShort() + Long::class -> rawValue.toLong() + Byte::class -> rawValue.toByte() + UInt::class -> rawValue.toUInt() + UShort::class -> rawValue.toUShort() + ULong::class -> rawValue.toULong() + else -> rawValue + } + return GoUtPrimitiveModel(value, typeId) +} + +private fun convertRawFloatValueToGoUtPrimitiveModel( + rawValue: String, typeId: GoPrimitiveTypeId, explicitCastRequired: Boolean = false +): GoUtPrimitiveModel { + return when (rawValue) { + RawValuesCodes.NAN_VALUE -> GoUtFloatNaNModel(typeId) + RawValuesCodes.POS_INF_VALUE -> GoUtFloatInfModel(1, typeId) + RawValuesCodes.NEG_INF_VALUE -> GoUtFloatInfModel(-1, typeId) + else -> { + val typedValue = if (typeId == goFloat64TypeId) rawValue.toDouble() else rawValue.toFloat() + if (explicitCastRequired) { + GoUtPrimitiveModel(typedValue, typeId, explicitCastMode = ExplicitCastMode.REQUIRED) + } else { + GoUtPrimitiveModel(typedValue, typeId) + } + } + } +} + +private fun createGoUtStructModelFromRawValue( + resultValue: StructValue, resultTypeId: GoStructTypeId, packageName: String +): GoUtStructModel { + val value = resultValue.value.zip(resultTypeId.fields).map { (value, fieldId) -> + GoUtFieldModel(createGoUtModelFromRawValue(value.value, fieldId.declaringType, packageName), fieldId) + } + return GoUtStructModel(value, resultTypeId, packageName) +} + +private fun createGoUtArrayModelFromRawValue( + resultValue: ArrayValue, resultTypeId: GoArrayTypeId, packageName: String +): GoUtArrayModel { + val value = (0 until resultTypeId.length).associateWith { index -> + createGoUtModelFromRawValue(resultValue.value[index], resultTypeId.elementTypeId!!, packageName) + }.toMutableMap() + return GoUtArrayModel(value, resultTypeId, packageName) +} \ No newline at end of file diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go new file mode 100644 index 0000000000..79bccdc080 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go @@ -0,0 +1,82 @@ +package main + +import "go/token" + +type AnalyzedType interface { + GetName() string +} + +type AnalyzedInterfaceType struct { + Name string `json:"name"` + ImplementsError bool `json:"implementsError"` + PackageName string `json:"packageName"` + PackagePath string `json:"packagePath"` +} + +func (t AnalyzedInterfaceType) GetName() string { + return t.Name +} + +type AnalyzedPrimitiveType struct { + Name string `json:"name"` +} + +func (t AnalyzedPrimitiveType) GetName() string { + return t.Name +} + +type AnalyzedField struct { + Name string `json:"name"` + Type AnalyzedType `json:"type"` + IsExported bool `json:"isExported"` +} + +type AnalyzedStructType struct { + Name string `json:"name"` + PackageName string `json:"packageName"` + PackagePath string `json:"packagePath"` + ImplementsError bool `json:"implementsError"` + Fields []AnalyzedField `json:"fields"` +} + +func (t AnalyzedStructType) GetName() string { + return t.Name +} + +type AnalyzedArrayType struct { + Name string `json:"name"` + ElementType AnalyzedType `json:"elementType"` + Length int64 `json:"length"` +} + +func (t AnalyzedArrayType) GetName() string { + return t.Name +} + +type AnalyzedFunctionParameter struct { + Name string `json:"name"` + Type AnalyzedType `json:"type"` +} + +type AnalyzedFunction struct { + Name string `json:"name"` + ModifiedName string `json:"modifiedName"` + Parameters []AnalyzedFunctionParameter `json:"parameters"` + ResultTypes []AnalyzedType `json:"resultTypes"` + ModifiedFunctionForCollectingTraces string `json:"modifiedFunctionForCollectingTraces"` + NumberOfAllStatements int `json:"numberOfAllStatements"` + position token.Pos +} + +type AnalysisResult struct { + AbsoluteFilePath string `json:"absoluteFilePath"` + PackageName string `json:"packageName"` + AnalyzedFunctions []AnalyzedFunction `json:"analyzedFunctions"` + NotSupportedFunctionsNames []string `json:"notSupportedFunctionsNames"` + NotFoundFunctionsNames []string `json:"notFoundFunctionsNames"` +} + +type AnalysisResults struct { + IntSize int `json:"intSize"` + Results []AnalysisResult `json:"results"` +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go new file mode 100644 index 0000000000..b925cc8ff5 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go @@ -0,0 +1,10 @@ +package main + +type AnalysisTarget struct { + AbsoluteFilePath string `json:"absoluteFilePath"` + TargetFunctionsNames []string `json:"targetFunctionsNames"` +} + +type AnalysisTargets struct { + Targets []AnalysisTarget `json:"targets"` +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go b/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go new file mode 100644 index 0000000000..3d650c5710 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go @@ -0,0 +1,272 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/printer" + "go/token" + "go/types" + "sort" +) + +var errorInterface = func() *types.Interface { + // TODO not sure if it's best way + src := `package src + +import "errors" + +var x = errors.New("") +` + fset := token.NewFileSet() + fileAst, astErr := parser.ParseFile(fset, "", src, 0) + checkError(astErr) + typesConfig := types.Config{Importer: importer.Default()} + info := &types.Info{ + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Types: make(map[ast.Expr]types.TypeAndValue), + } + _, typesCheckErr := typesConfig.Check("", fset, []*ast.File{fileAst}, info) + checkError(typesCheckErr) + for _, obj := range info.Defs { + if obj != nil { + return obj.Type().Underlying().(*types.Interface) + } + } + return nil +}() + +func implementsError(typ *types.Named) bool { + return types.Implements(typ, errorInterface) +} + +//goland:noinspection GoPreferNilSlice +func toAnalyzedType(typ types.Type) (AnalyzedType, error) { + var result AnalyzedType + underlyingType := typ.Underlying() + switch underlyingType.(type) { + case *types.Basic: + basicType := underlyingType.(*types.Basic) + name := basicType.Name() + result = AnalyzedPrimitiveType{Name: name} + case *types.Struct: + namedType := typ.(*types.Named) + name := namedType.Obj().Name() + pkg := namedType.Obj().Pkg() + isError := implementsError(namedType) + + structType := underlyingType.(*types.Struct) + fields := []AnalyzedField{} + for i := 0; i < structType.NumFields(); i++ { + field := structType.Field(i) + + fieldType, err := toAnalyzedType(field.Type()) + checkError(err) + + fields = append(fields, AnalyzedField{field.Name(), fieldType, field.Exported()}) + } + + result = AnalyzedStructType{ + Name: name, + PackageName: pkg.Name(), + PackagePath: pkg.Path(), + ImplementsError: isError, + Fields: fields, + } + case *types.Array: + arrayType := typ.(*types.Array) + + arrayElemType, err := toAnalyzedType(arrayType.Elem()) + checkError(err) + + elemTypeName := arrayElemType.GetName() + + length := arrayType.Len() + name := fmt.Sprintf("[%d]%s", length, elemTypeName) + + result = AnalyzedArrayType{ + Name: name, + ElementType: arrayElemType, + Length: length, + } + case *types.Interface: + namedType := typ.(*types.Named) + name := namedType.Obj().Name() + pkg := namedType.Obj().Pkg() + packageName, packagePath := "", "" + if pkg != nil { + packageName = pkg.Name() + packagePath = pkg.Path() + } + + isError := implementsError(namedType) + if !isError { + return nil, errors.New("currently only error interface is supported") + } + result = AnalyzedInterfaceType{ + Name: fmt.Sprintf("interface %s", name), + ImplementsError: isError, + PackageName: packageName, + PackagePath: packagePath, + } + } + return result, nil +} + +// for now supports only basic and error result types +func checkTypeIsSupported(typ types.Type, isResultType bool) bool { + underlyingType := typ.Underlying() // analyze real type, not alias or defined type + if _, ok := underlyingType.(*types.Basic); ok { + return true + } + if structType, ok := underlyingType.(*types.Struct); ok { + for i := 0; i < structType.NumFields(); i++ { + if !checkTypeIsSupported(structType.Field(i).Type(), isResultType) { + return false + } + } + return true + } + if arrayType, ok := underlyingType.(*types.Array); ok { + return checkTypeIsSupported(arrayType.Elem(), isResultType) + } + if interfaceType, ok := underlyingType.(*types.Interface); ok && isResultType { + return interfaceType == errorInterface + } + return false +} + +func checkIsSupported(signature *types.Signature) bool { + if signature.Recv() != nil { // is method + return false + } + if signature.TypeParams() != nil { // has type params + return false + } + if signature.Variadic() { // is variadic + return false + } + if results := signature.Results(); results != nil { + for i := 0; i < results.Len(); i++ { + result := results.At(i) + if !checkTypeIsSupported(result.Type(), true) { + return false + } + } + } + if parameters := signature.Params(); parameters != nil { + for i := 0; i < parameters.Len(); i++ { + parameter := parameters.At(i) + if !checkTypeIsSupported(parameter.Type(), false) { + return false + } + } + } + return true +} + +func collectTargetAnalyzedFunctions(fset *token.FileSet, info *types.Info, targetFunctionsNames []string) ( + analyzedFunctions []AnalyzedFunction, + notSupportedFunctionsNames []string, + notFoundFunctionsNames []string, +) { + analyzedFunctions = []AnalyzedFunction{} + notSupportedFunctionsNames = []string{} + notFoundFunctionsNames = []string{} + + selectAll := len(targetFunctionsNames) == 0 + foundTargetFunctionsNamesMap := map[string]bool{} + for _, functionName := range targetFunctionsNames { + foundTargetFunctionsNamesMap[functionName] = false + } + + for ident, obj := range info.Defs { + switch typedObj := obj.(type) { + case *types.Func: + analyzedFunction := AnalyzedFunction{ + Name: typedObj.Name(), + ModifiedName: createNewFunctionName(typedObj.Name()), + Parameters: []AnalyzedFunctionParameter{}, + ResultTypes: []AnalyzedType{}, + position: typedObj.Pos(), + } + + if !selectAll { + if isFound, ok := foundTargetFunctionsNamesMap[analyzedFunction.Name]; !ok || isFound { + continue + } else { + foundTargetFunctionsNamesMap[analyzedFunction.Name] = true + } + } + + signature := typedObj.Type().(*types.Signature) + if !checkIsSupported(signature) { + notSupportedFunctionsNames = append(notSupportedFunctionsNames, analyzedFunction.Name) + continue + } + if parameters := signature.Params(); parameters != nil { + for i := 0; i < parameters.Len(); i++ { + parameter := parameters.At(i) + + parameterType, err := toAnalyzedType(parameter.Type()) + checkError(err) + + analyzedFunction.Parameters = append(analyzedFunction.Parameters, + AnalyzedFunctionParameter{ + Name: parameter.Name(), + Type: parameterType, + }) + } + } + if results := signature.Results(); results != nil { + for i := 0; i < results.Len(); i++ { + result := results.At(i) + + resultType, err := toAnalyzedType(result.Type()) + checkError(err) + + analyzedFunction.ResultTypes = append(analyzedFunction.ResultTypes, resultType) + } + } + + funcDecl := ident.Obj.Decl.(*ast.FuncDecl) + funcDecl.Name = ast.NewIdent(analyzedFunction.ModifiedName) + + visitor := Visitor{ + counter: 0, + newFunctionName: analyzedFunction.ModifiedName, + } + ast.Walk(&visitor, funcDecl) + + var modifiedFunction bytes.Buffer + cfg := printer.Config{ + Mode: printer.TabIndent, + Tabwidth: 4, + Indent: 0, + } + err := cfg.Fprint(&modifiedFunction, fset, funcDecl) + checkError(err) + + analyzedFunction.ModifiedFunctionForCollectingTraces = modifiedFunction.String() + analyzedFunction.NumberOfAllStatements = visitor.counter + analyzedFunctions = append(analyzedFunctions, analyzedFunction) + } + } + + for functionName, isFound := range foundTargetFunctionsNamesMap { + if !isFound { + notFoundFunctionsNames = append(notFoundFunctionsNames, functionName) + } + } + sort.Slice(analyzedFunctions, func(i, j int) bool { + return analyzedFunctions[i].position < analyzedFunctions[j].position + }) + sort.Sort(sort.StringSlice(notSupportedFunctionsNames)) + sort.Sort(sort.StringSlice(notFoundFunctionsNames)) + + return analyzedFunctions, notSupportedFunctionsNames, notFoundFunctionsNames +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/cover.go b/utbot-go/src/main/resources/go_source_code_analyzer/cover.go new file mode 100644 index 0000000000..0050e2f4af --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/cover.go @@ -0,0 +1,141 @@ +package main + +import ( + "go/ast" + "go/token" + "strconv" +) + +type Visitor struct { + counter int + newFunctionName string +} + +func (v *Visitor) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.BlockStmt: + if n == nil { + n = &ast.BlockStmt{} + } + n.List = v.addLinesWithLoggingInTraceBeforeFirstReturnStatement(n.List) + return nil + case *ast.IfStmt: + if n.Body == nil { + return nil + } + ast.Walk(v, n.Body) + if n.Else == nil { + n.Else = &ast.BlockStmt{} + } + switch stmt := n.Else.(type) { + case *ast.IfStmt: + n.Else = &ast.BlockStmt{List: []ast.Stmt{stmt}} + } + ast.Walk(v, n.Else) + return nil + case *ast.ForStmt: + if n.Body == nil { + return nil + } + ast.Walk(v, n.Body) + return nil + case *ast.RangeStmt: + if n.Body == nil { + return nil + } + ast.Walk(v, n.Body) + return nil + case *ast.SwitchStmt: + hasDefault := false + if n.Body == nil { + n.Body = &ast.BlockStmt{} + } + for _, stmt := range n.Body.List { + if cas, ok := stmt.(*ast.CaseClause); ok && cas.List == nil { + hasDefault = true + break + } + } + if !hasDefault { + n.Body.List = append(n.Body.List, &ast.CaseClause{}) + } + for _, stmt := range n.Body.List { + ast.Walk(v, stmt) + } + return nil + case *ast.TypeSwitchStmt: + hasDefault := false + if n.Body == nil { + n.Body = &ast.BlockStmt{} + } + for _, stmt := range n.Body.List { + if cas, ok := stmt.(*ast.CaseClause); ok && cas.List == nil { + hasDefault = true + break + } + } + if !hasDefault { + n.Body.List = append(n.Body.List, &ast.CaseClause{}) + } + for _, stmt := range n.Body.List { + ast.Walk(v, stmt) + } + return nil + case *ast.SelectStmt: + if n.Body == nil { + return nil + } + for _, stmt := range n.Body.List { + ast.Walk(v, stmt) + } + return nil + case *ast.CaseClause: + for _, expr := range n.List { + ast.Walk(v, expr) + } + n.Body = v.addLinesWithLoggingInTraceBeforeFirstReturnStatement(n.Body) + return nil + case *ast.CommClause: + ast.Walk(v, n.Comm) + n.Body = v.addLinesWithLoggingInTraceBeforeFirstReturnStatement(n.Body) + return nil + } + return v +} + +func (v *Visitor) addLinesWithLoggingInTraceBeforeFirstReturnStatement(stmts []ast.Stmt) []ast.Stmt { + if len(stmts) == 0 { + return []ast.Stmt{v.newLineWithLoggingInTrace()} + } + + var newList []ast.Stmt + for _, stmt := range stmts { + newList = append(newList, v.newLineWithLoggingInTrace()) + ast.Walk(v, stmt) + newList = append(newList, stmt) + if _, ok := stmt.(*ast.ReturnStmt); ok { + break + } + } + return newList +} + +func (v *Visitor) newLineWithLoggingInTrace() ast.Stmt { + v.counter++ + + traces := ast.NewIdent("__traces__") + return &ast.AssignStmt{ + Lhs: []ast.Expr{traces}, + Tok: token.ASSIGN, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: ast.NewIdent("append"), + Args: []ast.Expr{traces, ast.NewIdent(strconv.Itoa(v.counter))}, + }, + }, + } +} + +func createNewFunctionName(funcName string) string { + return "__" + funcName + "__" +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/go.mod b/utbot-go/src/main/resources/go_source_code_analyzer/go.mod new file mode 100644 index 0000000000..cf28715f70 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/go.mod @@ -0,0 +1,10 @@ +module go_source_code_analyzer + +go 1.19 + +require golang.org/x/tools v0.4.0 + +require ( + golang.org/x/mod v0.7.0 // indirect + golang.org/x/sys v0.3.0 // indirect +) diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/go.sum b/utbot-go/src/main/resources/go_source_code_analyzer/go.sum new file mode 100644 index 0000000000..a20f737720 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/go.sum @@ -0,0 +1,7 @@ +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/main.go b/utbot-go/src/main/resources/go_source_code_analyzer/main.go new file mode 100644 index 0000000000..c44074294e --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "go/parser" + "go/token" + "log" + "os" + "path/filepath" + "strconv" + + "golang.org/x/tools/go/packages" +) + +func checkError(err error) { + if err != nil { + log.Fatal(err.Error()) + } +} + +func getPackageName(path string) string { + fset := token.NewFileSet() + + astFile, astErr := parser.ParseFile(fset, path, nil, parser.PackageClauseOnly) + checkError(astErr) + + return astFile.Name.Name +} + +func analyzeTarget(target AnalysisTarget) (*AnalysisResult, error) { + if len(target.TargetFunctionsNames) == 0 { + return nil, fmt.Errorf("target must contain target functions") + } + + packageName := getPackageName(target.AbsoluteFilePath) + + dir, _ := filepath.Split(target.AbsoluteFilePath) + cfg := packages.Config{ + Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedDeps | packages.NeedImports, + Dir: dir, + } + pkgs, err := packages.Load(&cfg, "") + if err != nil { + return nil, err + } + + for _, pkg := range pkgs { + if pkg.Name == packageName { + // collect required info about selected functions + analyzedFunctions, notSupportedFunctionsNames, notFoundFunctionsNames := + collectTargetAnalyzedFunctions(pkg.Fset, pkg.TypesInfo, target.TargetFunctionsNames) + + return &AnalysisResult{ + AbsoluteFilePath: target.AbsoluteFilePath, + PackageName: packageName, + AnalyzedFunctions: analyzedFunctions, + NotSupportedFunctionsNames: notSupportedFunctionsNames, + NotFoundFunctionsNames: notFoundFunctionsNames, + }, nil + } + } + return nil, fmt.Errorf("package %s not found", packageName) +} + +func main() { + var targetsFilePath, resultsFilePath string + flag.StringVar(&targetsFilePath, "targets", "", "path to JSON file to read analysis targets from") + flag.StringVar(&resultsFilePath, "results", "", "path to JSON file to write analysis results to") + flag.Parse() + + // read and deserialize targets + targetsBytes, readErr := os.ReadFile(targetsFilePath) + checkError(readErr) + + var analysisTargets AnalysisTargets + fromJsonErr := json.Unmarshal(targetsBytes, &analysisTargets) + checkError(fromJsonErr) + + // parse each requested Go source file + analysisResults := AnalysisResults{ + IntSize: strconv.IntSize, + Results: []AnalysisResult{}, + } + for _, target := range analysisTargets.Targets { + result, err := analyzeTarget(target) + checkError(err) + + analysisResults.Results = append(analysisResults.Results, *result) + } + + // serialize and write results + jsonBytes, toJsonErr := json.MarshalIndent(analysisResults, "", " ") + checkError(toJsonErr) + + writeErr := os.WriteFile(resultsFilePath, jsonBytes, os.ModePerm) + checkError(writeErr) +} diff --git a/utbot-intellij-go/build.gradle.kts b/utbot-intellij-go/build.gradle.kts new file mode 100644 index 0000000000..a3aadc4d5a --- /dev/null +++ b/utbot-intellij-go/build.gradle.kts @@ -0,0 +1,82 @@ +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 goPluginVersion: 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-go")) +} + +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" + ) + + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + + plugins.set( + when (ideType) { + "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + goPlugins + androidPlugins + "PC" -> pythonCommunityPlugins + "PU" -> pythonUltimatePlugins // something else, JS? + else -> jvmPlugins + } + ) + + version.set(ideVersion) + type.set(ideType) +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/GoLanguageAssistant.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/GoLanguageAssistant.kt new file mode 100644 index 0000000000..fde7f9334e --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/GoLanguageAssistant.kt @@ -0,0 +1,101 @@ +package org.utbot.intellij.plugin.language.go + +import com.goide.psi.GoFile +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.goide.psi.GoMethodDeclaration +import com.goide.psi.GoPointerType +import com.goide.psi.GoStructType +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.intellij.psi.util.PsiTreeUtil +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import org.utbot.intellij.plugin.language.go.generator.GoUtTestsDialogProcessor + +@Suppress("unused") // is used in org.utbot.intellij.plugin.language.agnostic.LanguageAssistant via reflection +object GoLanguageAssistant : LanguageAssistant() { + + private const val goId = "go" + val language: Language = Language.findLanguageByID(goId) ?: error("Go language wasn't found") + + private data class PsiTargets( + val targetFunctions: Set, + val focusedTargetFunctions: Set, + ) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val (targetFunctions, focusedTargetFunctions) = getPsiTargets(e) ?: return + GoUtTestsDialogProcessor.createDialogAndGenerateTests( + project, + targetFunctions, + focusedTargetFunctions + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): PsiTargets? { + e.project ?: return null + + // The action is being called from editor or return. TODO: support other cases instead of return. + val editor = e.getData(CommonDataKeys.EDITOR) ?: return null + + val file = e.getData(CommonDataKeys.PSI_FILE) as? GoFile ?: return null + val element = findPsiElement(file, editor) ?: return null + + val containingFunction = getContainingFunction(element) + val targetFunctions = extractTargetFunctionsOrMethods(file) + val focusedTargetFunctions = if (containingFunction != null) { + setOf(containingFunction) + } else { + emptySet() + } + + return PsiTargets(targetFunctions, focusedTargetFunctions) + } + + // TODO: logic can be modified. For example, maybe suggest methods of the containing struct if present. + private fun extractTargetFunctionsOrMethods(file: GoFile): Set { + return file.functions.toSet() union file.methods.toSet() + } + + private fun getContainingFunction(element: PsiElement): GoFunctionOrMethodDeclaration? { + if (element is GoFunctionOrMethodDeclaration) + return element + + val parent = element.parent ?: return null + return getContainingFunction(parent) + } + + // Unused for now, but may be used for more complicated extract logic in the future. + @Suppress("unused") + private fun getContainingStruct(element: PsiElement): GoStructType? = + PsiTreeUtil.getParentOfType(element, GoStructType::class.java, false) + + // Unused for now, but may be used to access all methods of receiver's struct. + @Suppress("unused") + private fun getMethodReceiverStruct(method: GoMethodDeclaration): GoStructType { + val receiverType = method.receiverType + if (receiverType is GoPointerType) { + return receiverType.type as GoStructType + } + return receiverType as GoStructType + } + + // This method is cloned 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-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/GoUtTestsCodeFileWriter.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/GoUtTestsCodeFileWriter.kt new file mode 100644 index 0000000000..14dca93611 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/GoUtTestsCodeFileWriter.kt @@ -0,0 +1,61 @@ +package org.utbot.intellij.plugin.language.go.generator + +import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiManager +import com.intellij.util.IncorrectOperationException +import org.utbot.go.api.GoUtFile +import org.utbot.intellij.plugin.language.go.GoLanguageAssistant +import org.utbot.intellij.plugin.language.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import java.nio.file.Paths + +// This class is highly inspired by CodeGenerationController. +object GoUtTestsCodeFileWriter { + + fun createTestsFileWithGeneratedCode( + model: GenerateGoTestsModel, + sourceFile: GoUtFile, + generatedTestsFileCode: String + ) { + val testsFileName = createTestsFileName(sourceFile) + try { + runWriteAction { + val sourcePsiFile = findPsiFile(model, sourceFile) + val sourceFileDir = sourcePsiFile.containingDirectory + + val testsFileNameWithExtension = "$testsFileName.go" + val testPsiFile = PsiFileFactory.getInstance(model.project) + .createFileFromText( + testsFileNameWithExtension, GoLanguageAssistant.language, generatedTestsFileCode + ) + sourceFileDir.findFile(testsFileNameWithExtension)?.delete() + sourceFileDir.add(testPsiFile) + + val testFile = sourceFileDir.findFile(testsFileNameWithExtension)!! + CodeInsightUtil.positionCursor(model.project, testFile, testFile) + } + } catch (e: IncorrectOperationException) { + showCreatingFileError(model.project, testsFileName) + } + } + + private fun findPsiFile(model: GenerateGoTestsModel, sourceFile: GoUtFile): PsiFile { + val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(Paths.get(sourceFile.absolutePath))!! + return PsiManager.getInstance(model.project).findFile(virtualFile)!! + } + + private fun createTestsFileName(sourceFile: GoUtFile) = sourceFile.fileNameWithoutExtension + "_go_ut_test" + + private fun showCreatingFileError(project: Project, testFileName: String) { + showErrorDialogLater( + project, + message = "Cannot Create File '$testFileName'", + title = "Failed to Create File" + ) + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/GoUtTestsDialogProcessor.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/GoUtTestsDialogProcessor.kt new file mode 100644 index 0000000000..5bb39fee47 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/GoUtTestsDialogProcessor.kt @@ -0,0 +1,60 @@ +package org.utbot.intellij.plugin.language.go.generator + +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import org.utbot.go.logic.GoUtTestsGenerationConfig +import org.utbot.intellij.plugin.language.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.language.go.ui.GenerateGoTestsDialogWindow + +object GoUtTestsDialogProcessor { + + fun createDialogAndGenerateTests( + project: Project, + targetFunctions: Set, + focusedTargetFunctions: Set, + ) { + val dialogProcessor = createDialog(project, targetFunctions, focusedTargetFunctions) + if (!dialogProcessor.showAndGet()) return + + createTests(dialogProcessor.model) + } + + private fun createDialog( + project: Project, + targetFunctions: Set, + focusedTargetFunctions: Set, + ): GenerateGoTestsDialogWindow { + return GenerateGoTestsDialogWindow( + GenerateGoTestsModel( + project, + targetFunctions, + focusedTargetFunctions, + ) + ) + } + + private fun createTests(model: GenerateGoTestsModel) { + ProgressManager.getInstance().run(object : Task.Backgroundable(model.project, "Generate Go tests") { + override fun run(indicator: ProgressIndicator) { + // readAction is required to read PSI-tree or else "Read access" exception occurs. + val selectedFunctionsNamesBySourceFiles = runReadAction { + model.selectedFunctions.groupBy({ it.containingFile.virtualFile.canonicalPath!! }) { it.name!! } + } + val testsGenerationConfig = GoUtTestsGenerationConfig( + model.goExecutableAbsolutePath, + model.eachFunctionExecutionTimeoutMillis, + model.allFunctionExecutionTimeoutMillis + ) + + IntellijGoUtTestsGenerationController(model, indicator).generateTests( + selectedFunctionsNamesBySourceFiles, + testsGenerationConfig + ) { indicator.isCanceled } + } + }) + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/IntellijGoUtTestsGenerationController.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/IntellijGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..3812b8aaee --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/generator/IntellijGoUtTestsGenerationController.kt @@ -0,0 +1,141 @@ +package org.utbot.intellij.plugin.language.go.generator + +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.progress.ProgressIndicator +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.logic.AbstractGoUtTestsGenerationController +import org.utbot.intellij.plugin.language.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.intellij.plugin.ui.utils.showWarningDialogLater + +class IntellijGoUtTestsGenerationController( + private val model: GenerateGoTestsModel, + private val indicator: ProgressIndicator +) : AbstractGoUtTestsGenerationController() { + + private object ProgressIndicatorConstants { + const val START_FRACTION = 0.05 // is needed to prevent infinite indicator that appears for 0.0 + const val SOURCE_CODE_ANALYSIS_FRACTION = 0.25 + const val TEST_CASES_CODE_GENERATION_FRACTION = 0.1 + const val TEST_CASES_GENERATION_FRACTION = + 1.0 - SOURCE_CODE_ANALYSIS_FRACTION - TEST_CASES_CODE_GENERATION_FRACTION + } + + private lateinit var testCasesGenerationCounter: ProcessedFilesCounter + private lateinit var testCasesCodeGenerationCounter: ProcessedFilesCounter + + private data class ProcessedFilesCounter( + private val toProcessTotalFilesNumber: Int, + private var processedFiles: Int = 0 + ) { + val processedFilesRatio: Double get() = processedFiles.toDouble() / toProcessTotalFilesNumber + fun addProcessedFile() { + processedFiles++ + } + } + + override fun onSourceCodeAnalysisStart(targetFunctionsNamesBySourceFiles: Map>): Boolean { + indicator.isIndeterminate = false + indicator.text = "Analyze source files" + indicator.fraction = ProgressIndicatorConstants.START_FRACTION + return true + } + + override fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean { + indicator.fraction = indicator.fraction.coerceAtLeast( + ProgressIndicatorConstants.START_FRACTION + ProgressIndicatorConstants.SOURCE_CODE_ANALYSIS_FRACTION + ) + if (!handleMissingSelectedFunctions(analysisResults)) return false + + val filesToProcessTotalNumber = + analysisResults.count { (_, analysisResult) -> analysisResult.functions.isNotEmpty() } + testCasesGenerationCounter = ProcessedFilesCounter(filesToProcessTotalNumber) + testCasesCodeGenerationCounter = ProcessedFilesCounter(filesToProcessTotalNumber) + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean { + indicator.text = "Generate test cases for ${sourceFile.fileName}" + indicator.fraction = indicator.fraction.coerceAtLeast( + ProgressIndicatorConstants.START_FRACTION + + ProgressIndicatorConstants.SOURCE_CODE_ANALYSIS_FRACTION + + ProgressIndicatorConstants.TEST_CASES_GENERATION_FRACTION + * testCasesGenerationCounter.processedFilesRatio + ) + indicator.checkCanceled() // allow user to cancel possibly slow unit test generation + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + testCasesGenerationCounter.addProcessedFile() + return true + } + + override fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + indicator.text = "Generate tests code for ${sourceFile.fileName}" + indicator.fraction = indicator.fraction.coerceAtLeast( + ProgressIndicatorConstants.START_FRACTION + + ProgressIndicatorConstants.SOURCE_CODE_ANALYSIS_FRACTION + + ProgressIndicatorConstants.TEST_CASES_GENERATION_FRACTION + + ProgressIndicatorConstants.TEST_CASES_CODE_GENERATION_FRACTION + * testCasesCodeGenerationCounter.processedFilesRatio + ) + return true + } + + override fun onTestCasesFileCodeGenerationFinished( + sourceFile: GoUtFile, + generatedTestsFileCode: String + ): Boolean { + invokeLater { + GoUtTestsCodeFileWriter.createTestsFileWithGeneratedCode(model, sourceFile, generatedTestsFileCode) + } + testCasesCodeGenerationCounter.addProcessedFile() + return true + } + + private fun handleMissingSelectedFunctions( + analysisResults: Map + ): Boolean { + val missingSelectedFunctionsListMessage = generateMissingSelectedFunctionsListMessage(analysisResults) + val okSelectedFunctionsArePresent = + analysisResults.any { (_, analysisResult) -> analysisResult.functions.isNotEmpty() } + + if (missingSelectedFunctionsListMessage == null) { + return okSelectedFunctionsArePresent + } + + val errorMessageSb = StringBuilder() + .append("Some selected functions were skipped during source code analysis.") + .append(missingSelectedFunctionsListMessage) + if (okSelectedFunctionsArePresent) { + showWarningDialogLater( + model.project, + errorMessageSb.append("Unit test generation for other selected functions will be performed as usual.") + .toString(), + title = "Skipped some functions for unit tests generation" + ) + } else { + showErrorDialogLater( + model.project, + errorMessageSb.append("Unit test generation is cancelled: no other selected functions.").toString(), + title = "Unit tests generation is cancelled" + ) + } + return okSelectedFunctionsArePresent + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/models/GenerateGoTestsModel.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/models/GenerateGoTestsModel.kt new file mode 100644 index 0000000000..5a398efd7a --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/models/GenerateGoTestsModel.kt @@ -0,0 +1,25 @@ +package org.utbot.intellij.plugin.language.go.models + +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.intellij.openapi.project.Project +import org.utbot.go.logic.GoUtTestsGenerationConfig + +/** + * Contains information about Go tests generation task required for intellij plugin logic. + * + * targetFunctions: all possible functions to generate tests for; + * focusedTargetFunctions: such target functions that user is focused on while plugin execution; + * selectedFunctions: finally selected functions to generate tests for; + * goExecutableAbsolutePath: self-explanatory; + * eachFunctionExecutionTimeoutMillis: timeout in milliseconds for each fuzzed function execution. + */ +data class GenerateGoTestsModel( + val project: Project, + val targetFunctions: Set, + val focusedTargetFunctions: Set, +) { + lateinit var selectedFunctions: Set + lateinit var goExecutableAbsolutePath: String + var eachFunctionExecutionTimeoutMillis: Long = GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS + var allFunctionExecutionTimeoutMillis: Long = GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/GenerateGoTestsDialogWindow.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/GenerateGoTestsDialogWindow.kt new file mode 100644 index 0000000000..3fe899f484 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/GenerateGoTestsDialogWindow.kt @@ -0,0 +1,139 @@ +package org.utbot.intellij.plugin.language.go.ui + +import com.goide.psi.GoFunctionOrMethodDeclaration +import com.goide.refactor.ui.GoDeclarationInfo +import com.goide.sdk.combobox.GoSdkChooserCombo +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.JBLabel +import com.intellij.ui.layout.panel +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import org.utbot.go.logic.GoUtTestsGenerationConfig +import org.utbot.intellij.plugin.language.go.models.GenerateGoTestsModel +import org.utbot.intellij.plugin.language.go.ui.utils.resolveGoExecutablePath +import java.text.ParseException +import java.util.concurrent.TimeUnit +import javax.swing.JComponent + +private const val MINIMUM_EACH_EXECUTION_TIMEOUT_MILLIS = 1 +private const val EACH_EXECUTION_TIMEOUT_MILLIS_SPINNER_STEP = 10 + +private const val MINIMUM_ALL_EXECUTION_TIMEOUT_SECONDS = 1 +private const val ALL_EXECUTION_TIMEOUT_SECONDS_SPINNER_STEP = 10 + +// This class is highly inspired by GenerateTestsDialogWindow. +class GenerateGoTestsDialogWindow(val model: GenerateGoTestsModel) : DialogWrapper(model.project) { + + private val targetInfos = model.targetFunctions.toInfos() + private val targetFunctionsTable = GoFunctionsSelectionTable(targetInfos).apply { + val height = this.rowHeight * (targetInfos.size.coerceAtMost(12) + 1) + this.preferredScrollableViewportSize = JBUI.size(-1, height) + } + + private val projectGoSdkField = GoSdkChooserCombo() + private val allFunctionExecutionTimeoutSecondsSpinner = + JBIntSpinner( + TimeUnit.MILLISECONDS.toSeconds(GoUtTestsGenerationConfig.DEFAULT_ALL_EXECUTION_TIMEOUT_MILLIS).toInt(), + MINIMUM_ALL_EXECUTION_TIMEOUT_SECONDS, + Int.MAX_VALUE, + ALL_EXECUTION_TIMEOUT_SECONDS_SPINNER_STEP + ) + private val eachFunctionExecutionTimeoutMillisSpinner = + JBIntSpinner( + GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS.toInt(), + MINIMUM_EACH_EXECUTION_TIMEOUT_MILLIS, + Int.MAX_VALUE, + EACH_EXECUTION_TIMEOUT_MILLIS_SPINNER_STEP + ) + + private lateinit var panel: DialogPanel + + init { + title = "Generate Tests with UtBot" + isResizable = false + init() + } + + override fun createCenterPanel(): JComponent { + panel = panel { + row("Test source root: near to source files") {} + row("Project Go SDK:") { + component(projectGoSdkField) + } + row("Generate test methods for:") {} + row { + scrollPane(targetFunctionsTable) + } + row("Timeout for all functions:") { + component(allFunctionExecutionTimeoutSecondsSpinner) + component(JBLabel("seconds")) + } + row("Timeout for each function execution:") { + component(eachFunctionExecutionTimeoutMillisSpinner) + component(JBLabel("ms")) + } + } + updateFunctionsOrMethodsTable() + return panel + } + + override fun doOKAction() { + model.selectedFunctions = targetFunctionsTable.selectedMemberInfos.fromInfos() + model.goExecutableAbsolutePath = projectGoSdkField.sdk.resolveGoExecutablePath()!! + try { + eachFunctionExecutionTimeoutMillisSpinner.commitEdit() + allFunctionExecutionTimeoutSecondsSpinner.commitEdit() + } catch (_: ParseException) { + } + model.eachFunctionExecutionTimeoutMillis = eachFunctionExecutionTimeoutMillisSpinner.number.toLong() + model.allFunctionExecutionTimeoutMillis = + TimeUnit.SECONDS.toMillis(allFunctionExecutionTimeoutSecondsSpinner.number.toLong()) + super.doOKAction() + } + + private fun updateFunctionsOrMethodsTable() { + val focusedTargetFunctionsNames = model.focusedTargetFunctions.map { it.name }.toSet() + val selectedInfos = targetInfos.filter { + it.declaration.name in focusedTargetFunctionsNames + } + if (selectedInfos.isEmpty()) { + checkInfos(targetInfos) + } else { + checkInfos(selectedInfos) + } + targetFunctionsTable.setMemberInfos(targetInfos) + } + + private fun checkInfos(infos: Collection) { + infos.forEach { it.isChecked = true } + } + + private fun Collection.toInfos(): Set = + this.map { GoDeclarationInfo(it) }.toSet() + + private fun Collection.fromInfos(): Set = + this.map { it.declaration as GoFunctionOrMethodDeclaration }.toSet() + + @Suppress("DuplicatedCode") // This method is highly inspired by GenerateTestsDialogWindow.doValidate(). + override fun doValidate(): ValidationInfo? { + projectGoSdkField.sdk.resolveGoExecutablePath() + ?: return ValidationInfo( + "Go SDK is not configured", + projectGoSdkField.childComponent + ) + + targetFunctionsTable.tableHeader?.background = UIUtil.getTableBackground() + targetFunctionsTable.background = UIUtil.getTableBackground() + if (targetFunctionsTable.selectedMemberInfos.isEmpty()) { + targetFunctionsTable.tableHeader?.background = JBUI.CurrentTheme.Validator.errorBackgroundColor() + targetFunctionsTable.background = JBUI.CurrentTheme.Validator.errorBackgroundColor() + return ValidationInfo( + "Tick any methods to generate tests for", targetFunctionsTable.componentPopupMenu + ) + } + return null + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/GoFunctionsSelectionTable.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/GoFunctionsSelectionTable.kt new file mode 100644 index 0000000000..cd7e93eb7d --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/GoFunctionsSelectionTable.kt @@ -0,0 +1,32 @@ +package org.utbot.intellij.plugin.language.go.ui + +import com.goide.psi.GoNamedElement +import com.goide.refactor.ui.GoDeclarationInfo +import com.intellij.refactoring.ui.AbstractMemberSelectionTable +import com.intellij.ui.RowIcon +import com.intellij.util.PlatformIcons +import javax.swing.Icon + +class GoFunctionsSelectionTable(infos: Set) : + AbstractMemberSelectionTable(infos, null, null) { + + override fun getAbstractColumnValue(info: GoDeclarationInfo): Boolean { + return info.isToAbstract + } + + override fun isAbstractColumnEditable(rowIndex: Int): Boolean { + return myMemberInfoModel.isAbstractEnabled(myMemberInfos[rowIndex] as GoDeclarationInfo) + } + + override fun getOverrideIcon(memberInfo: GoDeclarationInfo): Icon? = null + + override fun setVisibilityIcon(memberInfo: GoDeclarationInfo, icon_: com.intellij.ui.icons.RowIcon?) { + val icon = icon_ as RowIcon + val iconToSet = if (memberInfo.declaration.isPublic) { + PlatformIcons.PUBLIC_ICON + } else { + PlatformIcons.PRIVATE_ICON + } + icon.setIcon(iconToSet, VISIBILITY_ICON_POSITION) + } +} \ No newline at end of file diff --git a/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/utils/GoSdkUtils.kt b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/utils/GoSdkUtils.kt new file mode 100644 index 0000000000..049098ed64 --- /dev/null +++ b/utbot-intellij-go/src/main/kotlin/org/utbot/intellij/plugin/language/go/ui/utils/GoSdkUtils.kt @@ -0,0 +1,9 @@ +package org.utbot.intellij.plugin.language.go.ui.utils + +import com.goide.sdk.GoSdk +import java.nio.file.Paths + +fun GoSdk.resolveGoExecutablePath(): String? { + val canonicalGoSdkPath = this.executable?.canonicalPath ?: return null + return Paths.get(canonicalGoSdkPath).toAbsolutePath().toString() +} \ No newline at end of file diff --git a/utbot-intellij-js/build.gradle.kts b/utbot-intellij-js/build.gradle.kts index 9af87e9c69..4e60e13656 100644 --- a/utbot-intellij-js/build.gradle.kts +++ b/utbot-intellij-js/build.gradle.kts @@ -7,6 +7,7 @@ val ideVersion: String? by rootProject val kotlinPluginVersion: String? by rootProject val pythonCommunityPluginVersion: String? by rootProject val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject plugins { id("org.jetbrains.intellij") version "1.7.0" @@ -63,10 +64,14 @@ intellij { "JavaScript" ) + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + plugins.set( when (ideType) { "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins - "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + goPlugins + androidPlugins "PC" -> pythonCommunityPlugins "PY" -> pythonUltimatePlugins // something else, JS? else -> jvmPlugins diff --git a/utbot-intellij-python/build.gradle.kts b/utbot-intellij-python/build.gradle.kts index 9797f10378..4767374275 100644 --- a/utbot-intellij-python/build.gradle.kts +++ b/utbot-intellij-python/build.gradle.kts @@ -7,6 +7,7 @@ val ideVersion: String by rootProject val kotlinPluginVersion: String by rootProject val pythonCommunityPluginVersion: String? by rootProject val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject plugins { id("org.jetbrains.intellij") version "1.7.0" @@ -62,10 +63,14 @@ intellij { "JavaScript" ) + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + plugins.set( when (ideType) { "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins - "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + goPlugins + androidPlugins "PC" -> pythonCommunityPlugins "PY" -> pythonUltimatePlugins // something else, JS? else -> jvmPlugins diff --git a/utbot-intellij/build.gradle.kts b/utbot-intellij/build.gradle.kts index d04f438bd4..c3fd6b926a 100644 --- a/utbot-intellij/build.gradle.kts +++ b/utbot-intellij/build.gradle.kts @@ -9,9 +9,11 @@ val kotlinPluginVersion: String? by rootProject val pythonCommunityPluginVersion: String? by rootProject val pythonUltimatePluginVersion: String? by rootProject +val goPluginVersion: String? by rootProject val pythonIde: String? by rootProject val jsIde: String? by rootProject +val goIde: String? by rootProject val sootVersion: String? by rootProject val kryoVersion: String? by rootProject @@ -49,6 +51,10 @@ intellij { "JavaScript" ) + val goPlugins = listOf( + "org.jetbrains.plugins.go:${goPluginVersion}" + ) + val mavenUtilsPlugins = listOf( "org.jetbrains.idea.maven" ) @@ -56,7 +62,7 @@ intellij { plugins.set( when (ideType) { "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins + mavenUtilsPlugins - "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + mavenUtilsPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + goPlugins + androidPlugins + mavenUtilsPlugins "PC" -> pythonCommunityPlugins "PY" -> pythonUltimatePlugins // something else, JS? else -> jvmPlugins @@ -155,6 +161,11 @@ dependencies { implementation(project(":utbot-intellij-js")) } + if (goIde?.split(',')?.contains(ideType) == true) { + implementation(project(":utbot-go")) + implementation(project(":utbot-intellij-go")) + } + implementation(project(":utbot-android-studio")) testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") diff --git a/utbot-intellij/src/main/resources/META-INF/plugin.xml b/utbot-intellij/src/main/resources/META-INF/plugin.xml index 2dbd308903..c21e12800d 100644 --- a/utbot-intellij/src/main/resources/META-INF/plugin.xml +++ b/utbot-intellij/src/main/resources/META-INF/plugin.xml @@ -9,6 +9,7 @@ com.intellij.modules.java org.jetbrains.kotlin com.intellij.modules.python + org.jetbrains.plugins.go org.jetbrains.android org.jetbrains.idea.maven diff --git a/utbot-intellij/src/main/resources/META-INF/withGo.xml b/utbot-intellij/src/main/resources/META-INF/withGo.xml new file mode 100644 index 0000000000..65c848f900 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withGo.xml @@ -0,0 +1,3 @@ + + + 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 index b55c9114d9..98b6c8ab9a 100644 --- 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 @@ -37,7 +37,7 @@ abstract class LanguageAssistant { // The action is being called from 'Project' tool window val language = when (val element = e.getData(CommonDataKeys.PSI_ELEMENT)) { is PsiFileSystemItem -> { - e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { + e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { findLanguageRecursively(project, it) } } @@ -99,6 +99,7 @@ private fun loadWithException(language: Language): Class<*>? { 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") + "go" -> Class.forName("org.utbot.intellij.plugin.language.go.GoLanguageAssistant") "JAVA", "kotlin" -> Class.forName("org.utbot.intellij.plugin.language.JvmLanguageAssistant") else -> error("Unknown language id: ${language.id}") }