Skip to content

Fixed test generation for generics #1846

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 3 commits into from
Feb 26, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions utbot-python/samples/easy_samples/generics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T', bound=bool)
U = TypeVar('U', str, object)


class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str) -> None:
self.name = name
self.value = value

def set(self, new: T) -> None:
self.value = new

def get(self, x: U):
return self.value, x


def func(x: T, y: U):
if x:
return y
else:
return 100
186 changes: 122 additions & 64 deletions utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import org.utbot.python.newtyping.*
import org.utbot.python.newtyping.ast.visitor.Visitor
import org.utbot.python.newtyping.ast.visitor.constants.ConstantCollector
import org.utbot.python.newtyping.ast.visitor.hints.HintCollector
import org.utbot.python.newtyping.general.FunctionType
import org.utbot.python.newtyping.general.Type
import org.utbot.python.newtyping.general.*
import org.utbot.python.newtyping.inference.InferredTypeFeedback
import org.utbot.python.newtyping.inference.InvalidTypeFeedback
import org.utbot.python.newtyping.inference.SuccessFeedback
Expand All @@ -25,6 +24,7 @@ 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.PriorityCartesianProduct
import java.io.File

private val logger = KotlinLogging.logger {}
Expand All @@ -44,16 +44,19 @@ class PythonTestCaseGenerator(
private val mypyStorage: MypyAnnotationStorage,
private val mypyReportLine: List<MypyReportLine>,
private val mypyConfigFile: File,
){
) {

private val storageForMypyMessages: MutableList<MypyAnnotations.MypyReportLine> = mutableListOf()

private fun findMethodByDescription(mypyStorage: MypyAnnotationStorage, method: PythonMethodHeader): PythonMethod {
val containingClass = method.containingPythonClassId
val functionDef = if (containingClass == null) {
var containingClass: CompositeType? = null
val containingClassName = method.containingPythonClassId?.simpleName
val functionDef = if (containingClassName == null) {
mypyStorage.definitions[curModule]!![method.name]!!.getUtBotDefinition()!!
} else {
mypyStorage.definitions[curModule]!![containingClass.simpleName]!!.type.asUtBotType.getPythonAttributes().first {
containingClass =
mypyStorage.definitions[curModule]!![containingClassName]!!.getUtBotType() as CompositeType
mypyStorage.definitions[curModule]!![containingClassName]!!.type.asUtBotType.getPythonAttributes().first {
it.meta.name == method.name
}
} as? PythonFunctionDefinition ?: error("Selected method is not a function definition")
Expand All @@ -64,7 +67,7 @@ class PythonTestCaseGenerator(
return PythonMethod(
name = method.name,
moduleFilename = method.moduleFilename,
containingPythonClassId = method.containingPythonClassId,
containingPythonClass = containingClass,
codeAsString = funcDef.body.source,
definition = functionDef,
ast = funcDef.body
Expand All @@ -84,13 +87,64 @@ class PythonTestCaseGenerator(
} ?: emptyMap()

val namesStorage = GlobalNamesStorage(mypyStorage)
val hintCollector = HintCollector(method.definition, typeStorage, mypyExpressionTypes , namesStorage, curModule)
val hintCollector = HintCollector(method.definition, typeStorage, mypyExpressionTypes, namesStorage, curModule)
val constantCollector = ConstantCollector(typeStorage)
val visitor = Visitor(listOf(hintCollector, constantCollector))
visitor.visit(method.ast)
return Pair(hintCollector, constantCollector)
}

private fun getCandidates(param: TypeParameter, typeStorage: PythonTypeStorage): List<Type> {
val meta = param.pythonDescription() as PythonTypeVarDescription
return when (meta.parameterKind) {
PythonTypeVarDescription.ParameterKind.WithConcreteValues -> {
param.constraints.map { it.boundary }
}
PythonTypeVarDescription.ParameterKind.WithUpperBound -> {
typeStorage.simpleTypes.filter {
if (it.hasBoundedParameters())
return@filter false
val bound = param.constraints.first().boundary
PythonSubtypeChecker.checkIfRightIsSubtypeOfLeft(bound, it, typeStorage)
}
}
}
}

private val maxSubstitutions = 10

private fun generateTypesAfterSubstitution(type: Type, typeStorage: PythonTypeStorage): List<Type> {
val params = type.getBoundedParameters()
return PriorityCartesianProduct(params.map { getCandidates(it, typeStorage) }).getSequence().map { subst ->
DefaultSubstitutionProvider.substitute(type, (params zip subst).associate { it })
}.take(maxSubstitutions).toList()
}

private fun substituteTypeParameters(method: PythonMethod, typeStorage: PythonTypeStorage): List<PythonMethod> {
val newClasses =
if (method.containingPythonClass != null) {
generateTypesAfterSubstitution(method.containingPythonClass, typeStorage)
} else {
listOf(null)
}
return newClasses.flatMap { newClass ->
val funcType = newClass?.getPythonAttributeByName(typeStorage, method.name)?.type as? FunctionType
?: method.definition.type
val newFuncTypes = generateTypesAfterSubstitution(funcType, typeStorage)
newFuncTypes.map { newFuncType ->
val def = PythonFunctionDefinition(method.definition.meta, newFuncType as FunctionType)
PythonMethod(
method.name,
method.moduleFilename,
newClass as? CompositeType,
method.codeAsString,
def,
method.ast
)
}
}.take(maxSubstitutions)
}

fun generate(methodDescription: PythonMethodHeader): PythonTestSet {
storageForMypyMessages.clear()

Expand All @@ -108,65 +162,69 @@ class PythonTestCaseGenerator(
var missingLines: Set<Int>? = null
val coveredLines = mutableSetOf<Int>()
var generated = 0
val typeInferenceCancellation = { isCancelled() || System.currentTimeMillis() >= until || missingLines?.size == 0 }
val typeInferenceCancellation =
{ isCancelled() || System.currentTimeMillis() >= until || missingLines?.size == 0 }

inferAnnotations(
method,
mypyStorage,
typeStorage,
hintCollector,
mypyReportLine,
mypyConfigFile,
typeInferenceCancellation
) { functionType ->
val args = (functionType as FunctionType).arguments

logger.info { "Inferred annotations: ${ args.joinToString { it.pythonTypeRepresentation() } }" }

val engine = PythonEngine(
method,
directoriesForSysPath,
curModule,
pythonPath,
constants,
timeoutForRun,
coveredLines,
PythonTypeStorage.get(mypyStorage)
)

var coverageLimit = COVERAGE_LIMIT
var coveredBefore = coveredLines.size

var feedback: InferredTypeFeedback = SuccessFeedback

val fuzzerCancellation = { typeInferenceCancellation() || coverageLimit == 0 } // || feedback is InvalidTypeFeedback }

engine.fuzzing(args, fuzzerCancellation, until).collect {
generated += 1
when (it) {
is ValidExecution -> {
executions += it.utFuzzedExecution
missingLines = updateCoverage(it.utFuzzedExecution, coveredLines, missingLines)
feedback = SuccessFeedback
}
is InvalidExecution -> {
errors += it.utError
feedback = SuccessFeedback
}
is ArgumentsTypeErrorFeedback -> {
feedback = InvalidTypeFeedback
substituteTypeParameters(method, typeStorage).forEach { newMethod ->
inferAnnotations(
newMethod,
mypyStorage,
typeStorage,
hintCollector,
mypyReportLine,
mypyConfigFile,
typeInferenceCancellation
) { functionType ->
val args = (functionType as FunctionType).arguments

logger.info { "Inferred annotations: ${args.joinToString { it.pythonTypeRepresentation() }}" }

val engine = PythonEngine(
newMethod,
directoriesForSysPath,
curModule,
pythonPath,
constants,
timeoutForRun,
coveredLines,
PythonTypeStorage.get(mypyStorage)
)

var coverageLimit = COVERAGE_LIMIT
var coveredBefore = coveredLines.size

var feedback: InferredTypeFeedback = SuccessFeedback

val fuzzerCancellation =
{ typeInferenceCancellation() || coverageLimit == 0 } // || feedback is InvalidTypeFeedback }

engine.fuzzing(args, fuzzerCancellation, until).collect {
generated += 1
when (it) {
is ValidExecution -> {
executions += it.utFuzzedExecution
missingLines = updateCoverage(it.utFuzzedExecution, coveredLines, missingLines)
feedback = SuccessFeedback
}
is InvalidExecution -> {
errors += it.utError
feedback = SuccessFeedback
}
is ArgumentsTypeErrorFeedback -> {
feedback = InvalidTypeFeedback
}
is TypeErrorFeedback -> {
feedback = InvalidTypeFeedback
}
}
is TypeErrorFeedback -> {
feedback = InvalidTypeFeedback
val coveredAfter = coveredLines.size
if (coveredAfter == coveredBefore) {
coverageLimit -= 1
}
coveredBefore = coveredAfter
}
val coveredAfter = coveredLines.size
if (coveredAfter == coveredBefore) {
coverageLimit -= 1
}
coveredBefore = coveredAfter
feedback
}
feedback
}


Expand Down Expand Up @@ -231,13 +289,13 @@ class PythonTestCaseGenerator(
mypyConfigFile
)

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

val existsAnnotation = method.definition.type
if (existsAnnotation.arguments.all {it.pythonTypeName() != "typing.Any"}) {
if (existsAnnotation.arguments.all { it.pythonTypeName() != "typing.Any" }) {
annotationHandler(existsAnnotation)
}

Expand Down
5 changes: 3 additions & 2 deletions utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.utbot.python.framework.api.python.PythonClassId
import org.utbot.python.framework.api.python.PythonModel
import org.utbot.python.framework.api.python.util.pythonAnyClassId
import org.utbot.python.newtyping.*
import org.utbot.python.newtyping.general.CompositeType
import org.utbot.python.typing.MypyAnnotations

data class PythonArgument(val name: String, val annotation: String?)
Expand All @@ -20,7 +21,7 @@ class PythonMethodHeader(
class PythonMethod(
val name: String,
val moduleFilename: String,
val containingPythonClassId: PythonClassId?,
val containingPythonClass: CompositeType?,
val codeAsString: String,
var definition: PythonFunctionDefinition,
val ast: Block
Expand All @@ -34,7 +35,7 @@ class PythonMethod(
TODO: Now we think that all class methods has `self` argument! We should support `@property` decorator
*/
val hasThisArgument: Boolean
get() = containingPythonClassId != null
get() = containingPythonClass != null

val arguments: List<PythonArgument>
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.utbot.python.FunctionArguments
import org.utbot.python.PythonMethod
import org.utbot.python.code.PythonCodeGenerator
import org.utbot.python.framework.api.python.util.pythonAnyClassId
import org.utbot.python.newtyping.pythonTypeRepresentation
import org.utbot.python.utils.TemporaryFileManager
import org.utbot.python.utils.getResult
import org.utbot.python.utils.startProcess
Expand Down Expand Up @@ -63,7 +64,7 @@ class PythonCodeExecutorImpl(
return Coverage(
coveredInstructions=covered.map {
Instruction(
method.containingPythonClassId?.name ?: pythonAnyClassId.name,
method.containingPythonClass?.pythonTypeRepresentation() ?: pythonAnyClassId.name,
method.methodSignature(),
it,
it.toLong()
Expand All @@ -72,7 +73,7 @@ class PythonCodeExecutorImpl(
instructionsCount = statements.size.toLong(),
missedInstructions = missedStatements.map {
Instruction(
method.containingPythonClassId?.name ?: pythonAnyClassId.name,
method.containingPythonClass?.pythonTypeRepresentation() ?: pythonAnyClassId.name,
method.methodSignature(),
it,
it.toLong()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import org.utbot.python.newtyping.pythonAnyType
import org.utbot.python.newtyping.pythonModules
import org.utbot.python.newtyping.pythonTypeRepresentation
import org.utbot.python.framework.codegen.toPythonRawString
import org.utbot.python.newtyping.pythonDescription

class PythonCodeGenerator(
classUnderTest: ClassId,
Expand Down Expand Up @@ -113,7 +114,7 @@ class PythonCodeGenerator(
val executorModuleName = "utbot_executor.executor"
val executorModuleNameAlias = "__utbot_executor"
val executorFunctionName = "$executorModuleNameAlias.run_calculate_function_value"
val failArgumentsFunctionName = "$executorModuleNameAlias.fail_arguments_initialization"
val failArgumentsFunctionName = "$executorModuleNameAlias.fail_argument_initialization"

val importExecutor = PythonUserImport(executorModuleName, alias_ = executorModuleNameAlias)
val importSys = PythonSystemImport("sys")
Expand All @@ -133,12 +134,12 @@ class PythonCodeGenerator(
val outputPath = CgLiteral(pythonStrClassId, fileForOutputName.toPythonRawString())
val databasePath = CgLiteral(pythonStrClassId, coverageDatabasePath.toPythonRawString())

val containingClass = method.containingPythonClassId
val containingClass = method.containingPythonClass
var functionTextName =
if (containingClass == null)
method.name
else
"${containingClass.simpleName}.${method.name}"
"${containingClass.pythonDescription().name.name}.${method.name}"
if (functionModule.isNotEmpty()) {
functionTextName = "$functionModule.$functionTextName"
}
Expand Down
18 changes: 2 additions & 16 deletions utbot-python/src/main/kotlin/org/utbot/python/fuzzing/PythonApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,7 @@ import org.utbot.fuzzing.Seed
import org.utbot.fuzzing.Statistic
import org.utbot.fuzzing.utils.Trie
import org.utbot.python.framework.api.python.PythonTree
import org.utbot.python.fuzzing.provider.BoolValueProvider
import org.utbot.python.fuzzing.provider.BytearrayValueProvider
import org.utbot.python.fuzzing.provider.BytesValueProvider
import org.utbot.python.fuzzing.provider.ComplexValueProvider
import org.utbot.python.fuzzing.provider.ConstantValueProvider
import org.utbot.python.fuzzing.provider.DictValueProvider
import org.utbot.python.fuzzing.provider.FloatValueProvider
import org.utbot.python.fuzzing.provider.IntValueProvider
import org.utbot.python.fuzzing.provider.ListValueProvider
import org.utbot.python.fuzzing.provider.NoneValueProvider
import org.utbot.python.fuzzing.provider.ReduceValueProvider
import org.utbot.python.fuzzing.provider.SetValueProvider
import org.utbot.python.fuzzing.provider.StrValueProvider
import org.utbot.python.fuzzing.provider.TupleFixSizeValueProvider
import org.utbot.python.fuzzing.provider.TupleValueProvider
import org.utbot.python.fuzzing.provider.UnionValueProvider
import org.utbot.python.fuzzing.provider.*
import org.utbot.python.fuzzing.provider.utils.isAny
import org.utbot.python.newtyping.PythonProtocolDescription
import org.utbot.python.newtyping.PythonSubtypeChecker
Expand Down Expand Up @@ -79,6 +64,7 @@ fun pythonDefaultValueProviders(idGenerator: IdGenerator<Long>) = listOf(
BytearrayValueProvider,
ReduceValueProvider(idGenerator),
ConstantValueProvider,
TypeAliasValueProvider
)

class PythonFuzzing(
Expand Down
Loading