Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f6bc749

Browse files
JavaApi for UtBot Python (#2140)
1 parent 1fa451f commit f6bc749

File tree

12 files changed

+406
-0
lines changed

12 files changed

+406
-0
lines changed
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.

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /