Skip to content

Commit 227adcf

Browse files
committed
Add UTBot Python Java Api
1 parent be47d83 commit 227adcf

File tree

15 files changed

+406
-9
lines changed

15 files changed

+406
-9
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ class PythonGenerateTestsCommand : CliktCommand(
275275
testSet.method,
276276
testSet.executions.filterNot { it.result is UtExecutionSuccess },
277277
testSet.errors,
278-
testSet.mypyReport,
279278
testSet.classId,
280279
testSet.executionsNumber
281280
)

utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ class PythonTestCaseGenerator(
4242
private val mypyStorage: MypyInfoBuild,
4343
private val mypyReportLine: List<MypyReportLine>
4444
) {
45-
46-
private val storageForMypyMessages: MutableList<MypyReportLine> = mutableListOf()
47-
4845
private fun constructCollectors(
4946
mypyStorage: MypyInfoBuild,
5047
typeStorage: PythonTypeHintsStorage,
@@ -230,8 +227,6 @@ class PythonTestCaseGenerator(
230227
}
231228

232229
fun generate(method: PythonMethod, until: Long): PythonTestSet {
233-
storageForMypyMessages.clear()
234-
235230
val typeStorage = PythonTypeHintsStorage.get(mypyStorage)
236231

237232
val executions = mutableListOf<PythonUtExecution>()
@@ -279,7 +274,6 @@ class PythonTestCaseGenerator(
279274
else
280275
coverageExecutions + emptyCoverageExecutions.take(MAX_EMPTY_COVERAGE_TESTS),
281276
errors,
282-
storageForMypyMessages,
283277
executionsNumber = executions.size,
284278
)
285279
}

utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import org.utbot.python.framework.api.python.PythonTreeModel
88
import org.utbot.python.framework.api.python.util.pythonAnyClassId
99
import org.utbot.python.newtyping.*
1010
import org.utbot.python.newtyping.general.CompositeType
11-
import org.utbot.python.newtyping.mypy.MypyReportLine
1211
import org.utbot.python.newtyping.utils.isNamed
1312

1413
data class PythonArgument(
@@ -69,7 +68,6 @@ data class PythonTestSet(
6968
val method: PythonMethod,
7069
val executions: List<UtExecution>,
7170
val errors: List<UtError>,
72-
val mypyReport: List<MypyReportLine>,
7371
val classId: PythonClassId? = null,
7472
val executionsNumber: Int = 0,
7573
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.utbot.python.framework.external
2+
3+
import org.utbot.python.PythonTestGenerationConfig
4+
import org.utbot.python.PythonTestGenerationProcessor
5+
import org.utbot.python.PythonTestSet
6+
7+
class JavaApiProcessor(
8+
override val configuration: PythonTestGenerationConfig
9+
) : PythonTestGenerationProcessor() {
10+
override fun saveTests(testsCode: String) {
11+
}
12+
13+
override fun notGeneratedTestsAction(testedFunctions: List<String>) {
14+
}
15+
16+
override fun processCoverageInfo(testSets: List<PythonTestSet>) {
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.utbot.python.framework.external
2+
3+
import org.utbot.python.framework.api.python.pythonBuiltinsModuleName
4+
import org.utbot.python.framework.api.python.util.moduleOfType
5+
6+
data class PythonObjectName(
7+
val moduleName: String,
8+
val name: String,
9+
) {
10+
constructor(fullName: String) : this(
11+
moduleOfType(fullName) ?: pythonBuiltinsModuleName,
12+
fullName.removePrefix(moduleOfType(fullName) ?: pythonBuiltinsModuleName).removePrefix(".")
13+
)
14+
val fullName = "$moduleName.$name"
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.utbot.python.framework.external
2+
3+
import org.utbot.python.PythonMethod
4+
import org.utbot.python.newtyping.pythonTypeName
5+
6+
class PythonTestMethodInfo(
7+
val methodName: PythonObjectName,
8+
val moduleFilename: String,
9+
val containingClassName: PythonObjectName? = null
10+
)
11+
12+
fun PythonMethod.toPythonMethodInfo() = PythonTestMethodInfo(
13+
PythonObjectName(this.name),
14+
this.moduleFilename,
15+
this.containingPythonClass?.let { PythonObjectName(it.pythonTypeName()) }
16+
)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package org.utbot.python.framework.external
2+
3+
import mu.KLogger
4+
import mu.KotlinLogging
5+
import org.utbot.common.PathUtil.toPath
6+
import org.utbot.framework.UtSettings
7+
import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour
8+
import org.utbot.framework.codegen.domain.TestFramework
9+
import org.utbot.python.*
10+
import org.utbot.python.framework.api.python.PythonClassId
11+
import org.utbot.python.framework.codegen.model.Pytest
12+
import org.utbot.python.framework.codegen.model.Unittest
13+
import org.utbot.python.utils.RequirementsInstaller
14+
import org.utbot.python.utils.Success
15+
import org.utbot.python.utils.findCurrentPythonModule
16+
import java.io.File
17+
18+
object PythonUtBotJavaApi {
19+
private val logger: KLogger = KotlinLogging.logger {}
20+
21+
/**
22+
* Generate test sets
23+
*
24+
* @param testMethods methods for test generation
25+
* @param pythonPath a path to the Python executable file
26+
* @param pythonRunRoot a path to the directory where test sets will be executed
27+
* @param directoriesForSysPath a collection of strings that specifies the additional search path for modules, usually it is only project root
28+
* @param timeout a timeout to the test generation process (in milliseconds)
29+
* @param executionTimeout a timeout to one concrete execution
30+
*/
31+
@JvmStatic
32+
fun generateTestSets (
33+
testMethods: List<PythonTestMethodInfo>,
34+
pythonPath: String,
35+
pythonRunRoot: String,
36+
directoriesForSysPath: Collection<String>,
37+
timeout: Long,
38+
executionTimeout: Long = UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis,
39+
): List<PythonTestSet> {
40+
logger.info("Checking requirements...")
41+
42+
val installer = RequirementsInstaller()
43+
RequirementsInstaller.checkRequirements(
44+
installer,
45+
pythonPath,
46+
emptyList()
47+
)
48+
val processor = initPythonTestGeneratorProcessor(
49+
testMethods,
50+
pythonPath,
51+
pythonRunRoot,
52+
directoriesForSysPath.toSet(),
53+
timeout,
54+
executionTimeout,
55+
)
56+
logger.info("Loading information about Python types...")
57+
val (mypyStorage, _) = processor.sourceCodeAnalyze()
58+
logger.info("Generating tests...")
59+
return processor.testGenerate(mypyStorage)
60+
}
61+
62+
/**
63+
* Generate test sets code
64+
*
65+
* @param testSets a list of test sets
66+
* @param pythonRunRoot a path to the directory where test sets will be executed
67+
* @param directoriesForSysPath a collection of strings that specifies the additional search path for modules, usually it is only project root
68+
* @param testFramework a test framework (Unittest or Pytest)
69+
*/
70+
@JvmStatic
71+
fun renderTestSets (
72+
testSets: List<PythonTestSet>,
73+
pythonRunRoot: String,
74+
directoriesForSysPath: Collection<String>,
75+
testFramework: TestFramework = Unittest,
76+
): String {
77+
if (testSets.isEmpty()) return ""
78+
79+
require(testFramework is Unittest || testFramework is Pytest) { "TestFramework should be Unittest or Pytest" }
80+
81+
testSets.map { it.method.containingPythonClass } .toSet().let {
82+
require(it.size == 1) { "All test methods should be from one class or only top level" }
83+
it.first()
84+
}
85+
86+
val containingFile = testSets.map { it.method.moduleFilename } .toSet().let {
87+
require(it.size == 1) { "All test methods should be from one module" }
88+
it.first()
89+
}
90+
val moduleUnderTest = findCurrentPythonModule(directoriesForSysPath, containingFile)
91+
require(moduleUnderTest is Success)
92+
93+
val testMethods = testSets.map { it.method.toPythonMethodInfo() }.toSet().toList()
94+
95+
val processor = initPythonTestGeneratorProcessor(
96+
testMethods = testMethods,
97+
pythonRunRoot = pythonRunRoot,
98+
directoriesForSysPath = directoriesForSysPath.toSet(),
99+
testFramework = testFramework,
100+
)
101+
return processor.testCodeGenerate(testSets)
102+
}
103+
104+
/**
105+
* Generate test sets and render code
106+
*
107+
* @param testMethods methods for test generation
108+
* @param pythonPath a path to the Python executable file
109+
* @param pythonRunRoot a path to the directory where test sets will be executed
110+
* @param directoriesForSysPath a collection of strings that specifies the additional search path for modules, usually it is only project root
111+
* @param timeout a timeout to the test generation process (in milliseconds)
112+
* @param executionTimeout a timeout to one concrete execution
113+
* @param testFramework a test framework (Unittest or Pytest)
114+
*/
115+
@JvmStatic
116+
fun generate(
117+
testMethods: List<PythonTestMethodInfo>,
118+
pythonPath: String,
119+
pythonRunRoot: String,
120+
directoriesForSysPath: Collection<String>,
121+
timeout: Long,
122+
executionTimeout: Long = UtSettings.concreteExecutionDefaultTimeoutInInstrumentedProcessMillis,
123+
testFramework: TestFramework = Unittest,
124+
): String {
125+
val testSets =
126+
generateTestSets(testMethods, pythonPath, pythonRunRoot, directoriesForSysPath, timeout, executionTimeout)
127+
return renderTestSets(testSets, pythonRunRoot, directoriesForSysPath, testFramework)
128+
}
129+
130+
private fun initPythonTestGeneratorProcessor (
131+
testMethods: List<PythonTestMethodInfo>,
132+
pythonPath: String = "",
133+
pythonRunRoot: String,
134+
directoriesForSysPath: Set<String>,
135+
timeout: Long = 60_000,
136+
timeoutForRun: Long = 2_000,
137+
testFramework: TestFramework = Unittest,
138+
): PythonTestGenerationProcessor {
139+
140+
val pythonFilePath = testMethods.map { it.moduleFilename }.let {
141+
require(it.size == 1) {"All test methods should be from one file"}
142+
it.first()
143+
}
144+
val contentFile = File(pythonFilePath)
145+
val pythonFileContent = contentFile.readText()
146+
147+
val pythonModule = testMethods.map { it.methodName.moduleName }.let {
148+
require(it.size == 1) {"All test methods should be from one module"}
149+
it.first()
150+
}
151+
152+
val pythonMethods = testMethods.map {
153+
PythonMethodHeader(
154+
it.methodName.name,
155+
it.moduleFilename,
156+
it.containingClassName?.let { objName ->
157+
PythonClassId(objName.moduleName, objName.name)
158+
})
159+
}
160+
161+
return JavaApiProcessor(
162+
PythonTestGenerationConfig(
163+
pythonPath,
164+
TestFileInformation(pythonFilePath, pythonFileContent, pythonModule),
165+
directoriesForSysPath,
166+
pythonMethods,
167+
timeout,
168+
timeoutForRun,
169+
testFramework,
170+
pythonRunRoot.toPath(),
171+
true,
172+
{ false },
173+
RuntimeExceptionTestsBehaviour.FAIL
174+
)
175+
)
176+
}
177+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.utbot.python.framework.external
2+
3+
import org.utbot.python.utils.RequirementsInstaller
4+
import org.utbot.python.utils.RequirementsUtils
5+
6+
class RequirementsInstaller : RequirementsInstaller {
7+
override fun checkRequirements(pythonPath: String, requirements: List<String>): Boolean {
8+
return RequirementsUtils.requirementsAreInstalled(pythonPath, requirements)
9+
}
10+
11+
override fun installRequirements(pythonPath: String, requirements: List<String>) {
12+
val result = RequirementsUtils.installRequirements(pythonPath, requirements)
13+
if (result.exitValue != 0) {
14+
System.err.println(result.stderr)
15+
error("Failed to install requirements: ${requirements.joinToString()}.")
16+
}
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.utbot.python.utils
2+
3+
import java.io.File
4+
5+
fun findCurrentPythonModule(
6+
directoriesForSysPath: Collection<String>,
7+
sourceFile: String
8+
): Optional<String> {
9+
directoriesForSysPath.forEach { path ->
10+
val module = getModuleName(path.toAbsolutePath(), sourceFile.toAbsolutePath())
11+
if (module != null)
12+
return Success(module)
13+
}
14+
return Fail("Couldn't find path for $sourceFile in --sys-path option. Please, specify it.")
15+
}
16+
17+
fun String.toAbsolutePath(): String =
18+
File(this).canonicalPath

utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ object TemporaryFileManager {
1010
private var nextId = 0
1111

1212
init {
13+
tmpDirectory = initialize()
14+
}
15+
16+
fun initialize(): Path {
1317
tmpDirectory = FileUtil.createTempDirectory("python-test-generation-${nextId++}")
1418
Cleaner.addFunction { tmpDirectory.toFile().deleteRecursively() }
19+
return tmpDirectory
1520
}
1621

1722
fun assignTemporaryFile(fileName_: String? = null, tag: String? = null, addToCleaner: Boolean = true): File {

utbot-python/src/main/resources/example_code/__init__.py

Whitespace-only changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import math
2+
3+
4+
def calculate_function_value(x, y):
5+
"""
6+
Calculate value `f`
7+
| sqrt(x - 2y) , x > 100
8+
f(x, y) = | (3x^2 - 2xy + y^2) / sin(x) , -100 < x <= 100
9+
| (0.01 * x) ^ log2(y) , x < -100
10+
"""
11+
12+
if x > 100:
13+
return math.sqrt(x - 2 * y)
14+
elif -100 < x <= 100:
15+
return (3*x**2 - 2*x*y + y**2) / math.sin(x)
16+
else:
17+
return (0.01 * x) ** math.log2(y)

utbot-python/src/main/resources/example_code/inner_dir/__init__.py

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class InnerClass:
2+
x: int
3+
4+
def __init__(self, x: int):
5+
self.x = x
6+
7+
def f(self, y: int):
8+
return y**2 + self.x*y + 1

0 commit comments

Comments
 (0)