Skip to content

Update time management of python test generation #1893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ class PythonEngine(
private val pythonPath: String,
private val fuzzedConcreteValues: List<PythonFuzzedConcreteValue>,
private val timeoutForRun: Long,
private val initialCoveredLines: Set<Int>,
private val pythonTypeStorage: PythonTypeStorage,
) {

Expand Down Expand Up @@ -141,17 +140,20 @@ class PythonEngine(

fun fuzzing(parameters: List<Type>, isCancelled: () -> Boolean, until: Long): Flow<FuzzingExecutionFeedback> = flow {
val additionalModules = parameters.flatMap { it.pythonModules() }
val coveredLines = initialCoveredLines.toMutableSet()

ServerSocket(0).use { serverSocket ->
logger.info { "Server port: ${serverSocket.localPort}" }
val manager = PythonWorkerManager(
serverSocket,
pythonPath,
until,
{ constructEvaluationInput(it) },
timeoutForRun.toInt()
)
val manager = try {
PythonWorkerManager(
serverSocket,
pythonPath,
until,
{ constructEvaluationInput(it) },
timeoutForRun.toInt()
)
} catch (_: TimeoutException) {
return@flow
}
logger.info { "Executor manager was created successfully" }

fun fuzzingResultHandler(
Expand Down Expand Up @@ -195,7 +197,6 @@ class PythonEngine(

is PythonEvaluationSuccess -> {
val coveredInstructions = evaluationResult.coverage.coveredInstructions
coveredInstructions.forEach { coveredLines.add(it.lineNumber) }

val summary = arguments
.zip(methodUnderTest.arguments)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On line 215 (sorry, that I comment another line, because I was not able to find how to comment 215) there's no check that:

is ValidExecution -> {
  val trieNode: Trie.Node<Instruction> = description.tracer.add(coveredInstructions)

Returns the node the was already found. You can check trieNode.count > 1 to ignore duplicates and minimise the count of tests for user. At the moment similar tests are generated with different arguments, but same trace.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Expand Down Expand Up @@ -277,7 +278,6 @@ class PythonEngine(
cache.add(pair, result)
emit(result.fuzzingExecutionFeedback)
return@PythonFuzzing result.fuzzingPlatformFeedback

}.fuzz(pmd)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ import org.utbot.python.newtyping.mypy.MypyReportLine
import org.utbot.python.newtyping.mypy.getErrorNumber
import org.utbot.python.newtyping.utils.getOffsetLine
import org.utbot.python.typing.MypyAnnotations
import org.utbot.python.utils.ExecutionWithTimeoutMode
import org.utbot.python.utils.GenerationLimitManager
import org.utbot.python.utils.PriorityCartesianProduct
import org.utbot.python.utils.TimeoutMode
import java.io.File

private val logger = KotlinLogging.logger {}

private const val COVERAGE_LIMIT = 150
private const val ADDITIONAL_LIMIT = 5
private const val INVALID_EXECUTION_LIMIT = 10

class PythonTestCaseGenerator(
private val withMinimization: Boolean = true,
private val directoriesForSysPath: Set<String>,
Expand Down Expand Up @@ -120,6 +119,10 @@ class PythonTestCaseGenerator(

fun generate(method: PythonMethod, until: Long): PythonTestSet {
storageForMypyMessages.clear()
val limitManager = GenerationLimitManager(
ExecutionWithTimeoutMode,
until,
)

val typeStorage = PythonTypeStorage.get(mypyStorage)

Expand All @@ -135,10 +138,6 @@ class PythonTestCaseGenerator(
val coveredLines = mutableSetOf<Int>()
var generated = 0

var additionalLimit = ADDITIONAL_LIMIT
val typeInferenceCancellation =
{ isCancelled() || System.currentTimeMillis() >= until || additionalLimit <= 0 }

logger.info("Start test generation for ${method.name}")
substituteTypeParameters(method, typeStorage).forEach { newMethod ->
inferAnnotations(
Expand All @@ -148,7 +147,7 @@ class PythonTestCaseGenerator(
hintCollector,
mypyReportLine,
mypyConfigFile,
typeInferenceCancellation
limitManager,
) { functionType ->
val args = (functionType as FunctionType).arguments

Expand All @@ -161,22 +160,12 @@ class PythonTestCaseGenerator(
pythonPath,
constants,
timeoutForRun,
coveredLines,
PythonTypeStorage.get(mypyStorage)
)

var invalidExecutionLimit = INVALID_EXECUTION_LIMIT
var coverageLimit = COVERAGE_LIMIT
var coveredBefore = coveredLines.size

var feedback: InferredTypeFeedback = SuccessFeedback

val fuzzerCancellation = {
typeInferenceCancellation()
|| coverageLimit == 0
|| additionalLimit == 0
|| invalidExecutionLimit == 0
}
val fuzzerCancellation = { isCancelled() || limitManager.isCancelled() }
val startTime = System.currentTimeMillis()

engine.fuzzing(args, fuzzerCancellation, until).collect {
Expand All @@ -186,30 +175,26 @@ class PythonTestCaseGenerator(
executions += it.utFuzzedExecution
missingLines = updateCoverage(it.utFuzzedExecution, coveredLines, missingLines)
feedback = SuccessFeedback
limitManager.addSuccessExecution()
}
is InvalidExecution -> {
errors += it.utError
feedback = SuccessFeedback
}
is ArgumentsTypeErrorFeedback -> {
invalidExecutionLimit--
feedback = InvalidTypeFeedback
limitManager.addInvalidExecution()
}
is TypeErrorFeedback -> {
invalidExecutionLimit--
feedback = InvalidTypeFeedback
limitManager.addInvalidExecution()
}
}
if (missingLines?.size == 0) {
additionalLimit--
}
val coveredAfter = coveredLines.size
if (coveredAfter == coveredBefore) {
coverageLimit--
}
limitManager.missedLines = missingLines?.size

logger.info { "Time ${System.currentTimeMillis() - startTime}: $generated, $missingLines" }
coveredBefore = coveredAfter
}
limitManager.restart()
feedback
}
}
Expand Down Expand Up @@ -250,7 +235,7 @@ class PythonTestCaseGenerator(
hintCollector: HintCollector,
report: List<MypyReportLine>,
mypyConfigFile: File,
isCancelled: () -> Boolean,
limitManager: GenerationLimitManager,
annotationHandler: suspend (Type) -> InferredTypeFeedback,
) {
val namesInModule = mypyStorage.names
Expand All @@ -259,6 +244,7 @@ class PythonTestCaseGenerator(
.filter {
it.length < 4 || !it.startsWith("__") || !it.endsWith("__")
}
val typeInferenceCancellation = { isCancelled() || limitManager.isCancelled() }

val algo = BaselineAlgorithm(
typeStorage,
Expand All @@ -277,14 +263,15 @@ class PythonTestCaseGenerator(
)

runBlocking breaking@{
if (isCancelled()) {
if (typeInferenceCancellation()) {
return@breaking
}

algo.run(hintCollector.result, isCancelled, annotationHandler)
val iterationNumber = algo.run(hintCollector.result, typeInferenceCancellation, annotationHandler)

val existsAnnotation = method.definition.type
if (existsAnnotation.arguments.all { it.pythonTypeName() != "typing.Any" }) {
if (iterationNumber == 1) {
limitManager.mode = TimeoutMode
val existsAnnotation = method.definition.type
annotationHandler(existsAnnotation)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially this code was needed to run fuzzing with initial types (even when those are not full). Will we run fuzzing with list[Any] for the following function?

def f(x: list): ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. But only if all arguments have not-typing.Any annotations. And if all annotations are full we run fuzzing twice

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? Type inference does not run annotationHandler on initial signature.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I run fuzzing with initial annotations only after type inference) But I can change program and run fuzzing twice with initial annotations, but I think it isn't a good solution

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added handling of an initial annotation after first expansion in BaselineAlgorithm.

  • if annotation is full we cannot expand it and stop type inference and then start fuzzing;
  • if annotation is not full we try to expand and first of all try to fuzz initial annotation (for example, if it is list[typing.Any] we can generate empty list), after that we continue type inference

}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ class PythonWorkerManager(

var timeout: Long = 0
lateinit var process: Process
lateinit var workerSocket: Socket
lateinit var codeExecutor: PythonCodeExecutor
private lateinit var workerSocket: Socket
private lateinit var codeExecutor: PythonCodeExecutor

init {
connect()
Expand All @@ -39,7 +39,7 @@ class PythonWorkerManager(
"localhost",
serverSocket.localPort.toString(),
"--logfile", logfile.absolutePath,
//"--loglevel", "DEBUG",
"--loglevel", "INFO", // "DEBUG", "INFO", "ERROR"
))
timeout = max(until - processStartTime, 0)
workerSocket = try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ abstract class TypeInferenceAlgorithm {
hintCollectorResult: HintCollectorResult,
isCancelled: () -> Boolean,
annotationHandler: suspend (Type) -> InferredTypeFeedback,
)
): Int
}

sealed interface InferredTypeFeedback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,26 @@ class BaselineAlgorithm(
hintCollectorResult: HintCollectorResult,
isCancelled: () -> Boolean,
annotationHandler: suspend (Type) -> InferredTypeFeedback,
) {
): Int {
val generalRating = createGeneralTypeRating(hintCollectorResult, storage)
val initialState = getInitialState(hintCollectorResult, generalRating)
val states: MutableList<BaselineAlgorithmState> = mutableListOf(initialState)
val fileForMypyRuns = TemporaryFileManager.assignTemporaryFile(tag = "mypy.py")
var iterationCounter = 0

run breaking@ {
while (states.isNotEmpty()) {
if (isCancelled())
return@breaking
logger.debug("State number: ${states.size}")
iterationCounter++

val state = chooseState(states)
val newState = expandState(state, storage)
if (newState != null) {
if (iterationCounter == 1) {
annotationHandler(initialState.signature)
}
logger.info("Checking ${newState.signature.pythonTypeRepresentation()}")
if (checkSignature(newState.signature as FunctionType, fileForMypyRuns, configFile)) {
logger.debug("Found new state!")
Expand All @@ -71,6 +77,7 @@ class BaselineAlgorithm(
}
}
}
return iterationCounter
}

private fun checkSignature(signature: FunctionType, fileForMypyRuns: File, configFile: File): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.utbot.python.utils

import kotlin.math.min

class GenerationLimitManager(
// global settings
var mode: LimitManagerMode,
val until: Long,

// local settings: one type inference iteration
var executions: Int = 150,
var invalidExecutions: Int = 10,
var additionalExecutions: Int = 5,
var missedLines: Int? = null,
) {
private val initExecution = executions
private val initInvalidExecutions = invalidExecutions
private val initAdditionalExecutions = additionalExecutions
private val initMissedLines = missedLines

fun restart() {
executions = initExecution
invalidExecutions = initInvalidExecutions
additionalExecutions = initAdditionalExecutions
missedLines = initMissedLines
}

fun addSuccessExecution() {
executions -= 1
}
fun addInvalidExecution() {
invalidExecutions -= 1
}

fun isCancelled(): Boolean {
return mode.isCancelled(this)
}
}

interface LimitManagerMode {
fun isCancelled(manager: GenerationLimitManager): Boolean
}

object MaxCoverageMode : LimitManagerMode {
override fun isCancelled(manager: GenerationLimitManager): Boolean {
return manager.missedLines?.equals(0) == true
}
}

object TimeoutMode : LimitManagerMode {
override fun isCancelled(manager: GenerationLimitManager): Boolean {
return System.currentTimeMillis() >= manager.until
}
}

object ExecutionMode : LimitManagerMode {
override fun isCancelled(manager: GenerationLimitManager): Boolean {
if (manager.invalidExecutions <= 0 || manager.executions <= 0) {
return min(manager.invalidExecutions, 0) + min(manager.executions, 0) <= manager.additionalExecutions
}
return false
}
}

object MaxCoverageWithTimeoutMode : LimitManagerMode {
override fun isCancelled(manager: GenerationLimitManager): Boolean {
return MaxCoverageMode.isCancelled(manager) || TimeoutMode.isCancelled(manager)
}
}

object ExecutionWithTimeoutMode : LimitManagerMode {
override fun isCancelled(manager: GenerationLimitManager): Boolean {
return ExecutionMode.isCancelled(manager) || TimeoutMode.isCancelled(manager)
}
}