Skip to content

Commit 56f76da

Browse files
tamarinvs19tochilinakMarkoutte
authored
Update python unit test generation (#1763)
Co-authored-by: Ekaterina Tochilina <katerina_t_n@mail.ru> Co-authored-by: Vyacheslav Tamarin <vyacheslav.tamarin@yandex.ru> Co-authored-by: Maksim Pelevin <maks.pelevin@gmail.com>
1 parent 290b930 commit 56f76da

File tree

266 files changed

+41850
-3471
lines changed

Some content is hidden

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

266 files changed

+41850
-3471
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ target/
88
.gradle/
99
*.log
1010
*.rdgen
11-
utbot-intellij/src/main/resources/settings.properties
11+
utbot-intellij/src/main/resources/settings.properties
12+
__pycache__
13+
.dmypy.json

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ if (pythonIde.split(",").contains(ideType)) {
5353
include("utbot-python")
5454
include("utbot-cli-python")
5555
include("utbot-intellij-python")
56+
include("utbot-python-parser")
5657
}
5758

5859
if (jsIde.split(",").contains(ideType)) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class UtBotPythonCli : CliktCommand(name = "UnitTestBot Python Command Line Inte
2626
fun main(args: Array<String>) = try {
2727
UtBotPythonCli().subcommands(
2828
PythonGenerateTestsCommand(),
29-
PythonRunTestsCommand()
29+
PythonRunTestsCommand(),
30+
PythonTypeInferenceCommand()
3031
).main(args)
3132
} catch (ex: Throwable) {
3233
ex.printStackTrace()

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

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import com.github.ajalt.clikt.parameters.options.*
66
import com.github.ajalt.clikt.parameters.types.choice
77
import com.github.ajalt.clikt.parameters.types.long
88
import mu.KotlinLogging
9+
import org.parsers.python.PythonParser
910
import org.utbot.framework.codegen.domain.TestFramework
10-
import org.utbot.python.PythonMethod
11+
import org.utbot.python.PythonMethodHeader
1112
import org.utbot.python.PythonTestGenerationProcessor
1213
import org.utbot.python.PythonTestGenerationProcessor.processTestGeneration
13-
import org.utbot.python.code.PythonClass
1414
import org.utbot.python.code.PythonCode
15+
import org.utbot.python.framework.api.python.PythonClassId
1516
import org.utbot.python.framework.codegen.model.Pytest
1617
import org.utbot.python.framework.codegen.model.Unittest
18+
import org.utbot.python.newtyping.ast.parseClassDefinition
19+
import org.utbot.python.newtyping.ast.parseFunctionDefinition
20+
import org.utbot.python.utils.*
1721
import org.utbot.python.utils.RequirementsUtils.installRequirements
1822
import org.utbot.python.utils.RequirementsUtils.requirements
19-
import org.utbot.python.utils.getModuleName
2023
import java.io.File
2124
import java.nio.file.Paths
2225

@@ -80,11 +83,6 @@ class PythonGenerateTestsCommand : CliktCommand(
8083
help = "Turn off Python requirements check (to speed up)."
8184
).flag(default = false)
8285

83-
private val visitOnlySpecifiedSource by option(
84-
"--visit-only-specified-source",
85-
help = "Do not search for classes and imported modules in other Python files from sys.path."
86-
).flag(default = false)
87-
8886
private val timeout by option(
8987
"-t", "--timeout",
9088
help = "Specify the maximum time in milliseconds to spend on generating tests ($DEFAULT_TIMEOUT_IN_MILLIS by default)."
@@ -107,36 +105,33 @@ class PythonGenerateTestsCommand : CliktCommand(
107105
else -> error("Not reachable")
108106
}
109107

110-
private fun findCurrentPythonModule(): Optional<String> {
111-
directoriesForSysPath.forEach { path ->
112-
val module = getModuleName(path.toAbsolutePath(), sourceFile.toAbsolutePath())
113-
if (module != null)
114-
return Success(module)
115-
}
116-
return Fail("Couldn't find path for $sourceFile in --sys-path option. Please, specify it.")
117-
}
118-
119108
private val forbiddenMethods = listOf("__init__", "__new__")
120109

121-
private fun getClassMethods(pythonClassFromSources: PythonClass): List<PythonMethod> =
122-
pythonClassFromSources.methods.filter { method -> method.name !in forbiddenMethods }
110+
private fun getPythonMethods(): Optional<List<PythonMethodHeader>> {
111+
val parsedModule = PythonParser(sourceFileContent).Module()
123112

124-
private fun getPythonMethods(sourceCodeContent: String, currentModule: String): Optional<List<PythonMethod>> {
125-
val code = PythonCode.getFromString(
126-
sourceCodeContent,
127-
sourceFile.toAbsolutePath(),
128-
pythonModule = currentModule
129-
)
130-
?: return Fail("Couldn't parse source file. Maybe it contains syntax error?")
113+
val topLevelFunctions = PythonCode.getTopLevelFunctions(parsedModule)
114+
val topLevelClasses = PythonCode.getTopLevelClasses(parsedModule)
131115

132-
val topLevelFunctions = code.getToplevelFunctions()
133-
val topLevelClasses = code.getToplevelClasses()
134116
val selectedMethods = methods
135117
if (pythonClass == null && methods == null) {
136118
return if (topLevelFunctions.isNotEmpty())
137-
Success(topLevelFunctions)
119+
Success(
120+
topLevelFunctions
121+
.mapNotNull { parseFunctionDefinition(it) }
122+
.map { PythonMethodHeader(it.name.toString(), sourceFile, null) }
123+
)
138124
else {
139-
val topLevelClassMethods = topLevelClasses.flatMap { getClassMethods(it) }
125+
val topLevelClassMethods = topLevelClasses
126+
.mapNotNull { parseClassDefinition(it) }
127+
.flatMap { cls ->
128+
PythonCode.getClassMethods(cls.body)
129+
.mapNotNull { parseFunctionDefinition(it) }
130+
.map { function ->
131+
val parsedClassName = PythonClassId(cls.name.toString())
132+
PythonMethodHeader(function.name.toString(), sourceFile, parsedClassName)
133+
}
134+
}
140135
if (topLevelClassMethods.isNotEmpty()) {
141136
Success(topLevelClassMethods)
142137
} else
@@ -145,19 +140,29 @@ class PythonGenerateTestsCommand : CliktCommand(
145140
} else if (pythonClass == null && selectedMethods != null) {
146141
val pythonMethodsOpt = selectedMethods.map { functionName ->
147142
topLevelFunctions
143+
.mapNotNull { parseFunctionDefinition(it) }
144+
.map { PythonMethodHeader(it.name.toString(), sourceFile, null) }
148145
.find { it.name == functionName }
149146
?.let { Success(it) }
150147
?: Fail("Couldn't find top-level function $functionName in the source file.")
151148
}
152149
return pack(*pythonMethodsOpt.toTypedArray())
153150
}
154151

155-
val pythonClassFromSources = code.getToplevelClasses().find { it.name == pythonClass }
152+
val pythonClassFromSources = topLevelClasses
153+
.mapNotNull { parseClassDefinition(it) }
154+
.find { it.name.toString() == pythonClass }
156155
?.let { Success(it) }
157156
?: Fail("Couldn't find class $pythonClass in the source file.")
158157

159-
val methods = bind(pythonClassFromSources) {
160-
val fineMethods: List<PythonMethod> = it.methods.filter { method -> method.name !in forbiddenMethods }
158+
val methods = bind(pythonClassFromSources) { parsedClass ->
159+
val parsedClassId = PythonClassId(parsedClass.name.toString())
160+
val methods = PythonCode.getClassMethods(parsedClass.body).mapNotNull { parseFunctionDefinition(it) }
161+
val fineMethods = methods
162+
.filter { !forbiddenMethods.contains(it.name.toString()) }
163+
.map {
164+
PythonMethodHeader(it.name.toString(), sourceFile, parsedClassId)
165+
}
161166
if (fineMethods.isNotEmpty())
162167
Success(fineMethods)
163168
else
@@ -178,18 +183,18 @@ class PythonGenerateTestsCommand : CliktCommand(
178183
}
179184

180185
private lateinit var currentPythonModule: String
181-
private lateinit var pythonMethods: List<PythonMethod>
186+
private lateinit var pythonMethods: List<PythonMethodHeader>
182187
private lateinit var sourceFileContent: String
183188

184189
@Suppress("UNCHECKED_CAST")
185190
private fun calculateValues(): Optional<Unit> {
186-
val currentPythonModuleOpt = findCurrentPythonModule()
191+
val currentPythonModuleOpt = findCurrentPythonModule(directoriesForSysPath, sourceFile)
187192
sourceFileContent = File(sourceFile).readText()
188-
val pythonMethodsOpt = bind(currentPythonModuleOpt) { getPythonMethods(sourceFileContent, it) }
193+
val pythonMethodsOpt = bind(currentPythonModuleOpt) { getPythonMethods() }
189194

190195
return bind(pack(currentPythonModuleOpt, pythonMethodsOpt)) {
191196
currentPythonModule = it[0] as String
192-
pythonMethods = it[1] as List<PythonMethod>
197+
pythonMethods = it[1] as List<PythonMethodHeader>
193198
Success(Unit)
194199
}
195200
}
@@ -235,12 +240,12 @@ class PythonGenerateTestsCommand : CliktCommand(
235240
timeout = timeout,
236241
testFramework = testFramework,
237242
timeoutForRun = timeoutForRun,
238-
withMinimization = !doNotMinimize,
239-
doNotCheckRequirements = doNotCheckRequirements,
240-
visitOnlySpecifiedSource = visitOnlySpecifiedSource,
241243
writeTestTextToFile = { generatedCode ->
242244
writeToFileAndSave(output, generatedCode)
243245
},
246+
pythonRunRoot = Paths.get("").toAbsolutePath(),
247+
doNotCheckRequirements = doNotCheckRequirements,
248+
withMinimization = !doNotMinimize,
244249
checkingRequirementsAction = {
245250
logger.info("Checking requirements...")
246251
},
@@ -263,17 +268,12 @@ class PythonGenerateTestsCommand : CliktCommand(
263268
)
264269
},
265270
processMypyWarnings = { messages -> messages.forEach { println(it) } },
266-
finishedAction = {
267-
logger.info("Finished test generation for the following functions: ${it.joinToString()}")
268-
},
269271
processCoverageInfo = { coverageReport ->
270272
val output = coverageOutput ?: return@processTestGeneration
271273
writeToFileAndSave(output, coverageReport)
272-
},
273-
pythonRunRoot = Paths.get("").toAbsolutePath()
274-
)
274+
}
275+
) {
276+
logger.info("Finished test generation for the following functions: ${it.joinToString()}")
277+
}
275278
}
276-
277-
private fun String.toAbsolutePath(): String =
278-
File(this).canonicalPath
279279
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.utbot.cli.language.python
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.parameters.arguments.argument
5+
import com.github.ajalt.clikt.parameters.options.option
6+
import com.github.ajalt.clikt.parameters.options.required
7+
import com.github.ajalt.clikt.parameters.options.split
8+
import com.github.ajalt.clikt.parameters.types.long
9+
import mu.KotlinLogging
10+
import org.utbot.python.newtyping.inference.TypeInferenceProcessor
11+
import org.utbot.python.newtyping.pythonTypeRepresentation
12+
import org.utbot.python.utils.Fail
13+
import org.utbot.python.utils.RequirementsUtils.requirements
14+
import org.utbot.python.utils.Success
15+
16+
private val logger = KotlinLogging.logger {}
17+
18+
class PythonTypeInferenceCommand : CliktCommand(
19+
name = "infer_types",
20+
help = "Infer types for the specified Python top-level function."
21+
) {
22+
private val sourceFile by argument(
23+
help = "File with Python code."
24+
)
25+
26+
private val function by argument(
27+
help = "Function to infer types for."
28+
)
29+
30+
private val className by option(
31+
"-c", "--class",
32+
help = "Class of the function"
33+
)
34+
35+
private val pythonPath by option(
36+
"-p", "--python-path",
37+
help = "(required) Path to Python interpreter. Use only UNC format on Windows."
38+
).required()
39+
40+
private val timeout by option(
41+
"-t", "--timout",
42+
help = "(required) Timeout in milliseconds for type inference."
43+
).long().required()
44+
45+
private val directoriesForSysPath by option(
46+
"-s", "--sys-path",
47+
help = "(required) Directories to add to sys.path. Use only UNC format on Windows. " +
48+
"One of directories must contain the file with the methods under test."
49+
).split(",").required()
50+
51+
private var startTime: Long = 0
52+
53+
override fun run() {
54+
val moduleOpt = findCurrentPythonModule(
55+
directoriesForSysPath.map { it.toAbsolutePath() },
56+
sourceFile.toAbsolutePath()
57+
)
58+
if (moduleOpt is Fail) {
59+
logger.error(moduleOpt.message)
60+
}
61+
val module = (moduleOpt as Success).value
62+
63+
val result = TypeInferenceProcessor(
64+
pythonPath,
65+
directoriesForSysPath.map{ it.toAbsolutePath() }.toSet(),
66+
sourceFile,
67+
module,
68+
function,
69+
className
70+
).inferTypes(
71+
startingTypeInferenceAction = {
72+
startTime = System.currentTimeMillis()
73+
logger.info("Starting type inference...")
74+
},
75+
processSignature = { logger.info("Found signature: " + it.pythonTypeRepresentation()) },
76+
cancel = { System.currentTimeMillis() - startTime > timeout },
77+
checkRequirementsAction = { logger.info("Checking Python requirements...") },
78+
missingRequirementsAction = {
79+
logger.error("Some of the following Python requirements are missing: " +
80+
"${requirements.joinToString()}. Please install them.")
81+
},
82+
loadingInfoAboutTypesAction = { logger.info("Loading information about types...") },
83+
analyzingCodeAction = { logger.info("Analyzing code...") },
84+
pythonMethodExtractionFailAction = { logger.error(it) }
85+
)
86+
87+
result.forEach { println(it.pythonTypeRepresentation()) }
88+
}
89+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.utbot.cli.language.python
2+
3+
import org.utbot.python.utils.Fail
4+
import org.utbot.python.utils.Optional
5+
import org.utbot.python.utils.Success
6+
import org.utbot.python.utils.getModuleName
7+
import java.io.File
8+
9+
fun findCurrentPythonModule(
10+
directoriesForSysPath: Collection<String>,
11+
sourceFile: String
12+
): Optional<String> {
13+
directoriesForSysPath.forEach { path ->
14+
val module = getModuleName(path.toAbsolutePath(), sourceFile.toAbsolutePath())
15+
if (module != null)
16+
return Success(module)
17+
}
18+
return Fail("Couldn't find path for $sourceFile in --sys-path option. Please, specify it.")
19+
}
20+
21+
fun String.toAbsolutePath(): String =
22+
File(this).canonicalPath

utbot-fuzzing/src/main/kotlin/org/utbot/fuzzing/seeds/BitVectorValue.kt

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,7 @@ class BitVectorValue : KnownValue {
101101

102102
@Suppress("MemberVisibilityCanBePrivate")
103103
fun toString(radix: Int, isUnsigned: Boolean = false): String {
104-
val size = if (isUnsigned) size + 1 else size
105-
val array = ByteArray(size / 8 + if (size % 8 != 0) 1 else 0) { index ->
106-
toLong(bits = 8, shift = index * 8).toByte()
107-
}
108-
array.reverse()
109-
return BigInteger(array).toString(radix)
104+
return toBigInteger(isUnsigned).toString(radix)
110105
}
111106

112107
override fun toString() = toString(10)
@@ -126,6 +121,17 @@ class BitVectorValue : KnownValue {
126121
return result
127122
}
128123

124+
private fun toBigInteger(isUnsigned: Boolean): BigInteger {
125+
val size = if (isUnsigned) size + 1 else size
126+
val array = ByteArray(size / 8 + if (size % 8 != 0) 1 else 0) { index ->
127+
toLong(bits = 8, shift = index * 8).toByte()
128+
}
129+
array.reverse()
130+
return BigInteger(array)
131+
}
132+
133+
fun toBigInteger() = toBigInteger(false)
134+
129135
fun toBoolean() = vector[0]
130136

131137
fun toByte() = toLong(8).toByte()
@@ -155,6 +161,7 @@ class BitVectorValue : KnownValue {
155161
is Short -> fromShort(value)
156162
is Int -> fromInt(value)
157163
is Long -> fromLong(value)
164+
is BigInteger -> fromBigInteger(value)
158165
else -> error("unknown type of value $value (${value::class})")
159166
}
160167
}
@@ -190,6 +197,17 @@ class BitVectorValue : KnownValue {
190197
}
191198
return BitVectorValue(size, vector)
192199
}
200+
201+
fun fromBigInteger(value: BigInteger): BitVectorValue {
202+
val size = 128
203+
val bits = value.bitCount()
204+
assert(bits <= size) { "This value $value is too big. Max value is 2^$bits." }
205+
val vector = BitSet(size)
206+
for (i in 0 until size) {
207+
vector[i] = value.testBit(i)
208+
}
209+
return BitVectorValue(size, vector)
210+
}
193211
}
194212
}
195213

0 commit comments

Comments
 (0)