diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt index b4beb13441..45da218a54 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt @@ -1,5 +1,6 @@ package org.utbot.framework +import org.utbot.framework.plugin.api.ClassId import soot.SootClass /** @@ -10,7 +11,17 @@ private val isPackageTrusted: MutableMap = mutableMapOf() /** * Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries]. */ -fun SootClass.isFromTrustedLibrary(): Boolean { +fun SootClass.isFromTrustedLibrary(): Boolean = isFromTrustedLibrary(packageName) + +/** + * Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries]. + */ +fun ClassId.isFromTrustedLibrary(): Boolean = isFromTrustedLibrary(packageName) + +/** + * Determines whether [packageName] is from trusted libraries as defined in [TrustedLibraries]. + */ +fun isFromTrustedLibrary(packageName: String): Boolean { isPackageTrusted[packageName]?.let { return it } diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt index 4dd149b0b4..4eb298e64a 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt @@ -468,6 +468,13 @@ val ClassId.allDeclaredFieldIds: Sequence val SootField.fieldId: FieldId get() = FieldId(declaringClass.id, name) +/** + * For some lambdas class names in byte code and in Soot don't match, so we may fail + * to convert some soot fields to Java fields, in such case `null` is returned. + */ +val SootField.jFieldOrNull: Field? + get() = runCatching { fieldId.jField }.getOrNull() + // FieldId utils val FieldId.safeJField: Field? get() = declaringClass.jClass.declaredFields.firstOrNull { it.name == name } diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt index fd05871ba8..2e468b3322 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt @@ -22,6 +22,11 @@ private val logger = KotlinLogging.logger {} object SpringModelUtils { val autowiredClassId = ClassId("org.springframework.beans.factory.annotation.Autowired") + val injectClassIds = getClassIdFromEachAvailablePackage( + packages = listOf("javax", "jakarta"), + classNameFromPackage = "inject.Inject" + ) + val componentClassId = ClassId("org.springframework.stereotype.Component") val applicationContextClassId = ClassId("org.springframework.context.ApplicationContext") val repositoryClassId = ClassId("org.springframework.data.repository.Repository") diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt index 3ba048cffc..39af79204a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt @@ -8,8 +8,8 @@ import org.utbot.engine.ResolvedModels import org.utbot.framework.UtSettings import org.utbot.framework.codegen.util.isAccessibleFrom import org.utbot.modifications.AnalysisMode.SettersAndDirectAccessors -import org.utbot.modifications.ConstructorAnalyzer -import org.utbot.modifications.ConstructorAssembleInfo +import org.utbot.modifications.ExecutableAnalyzer +import org.utbot.modifications.ExecutableAssembleInfo import org.utbot.modifications.UtBotFieldsModificatorsSearcher import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId @@ -77,7 +77,7 @@ class AssembleModelGenerator(private val basePackageName: String) { UtBotFieldsModificatorsSearcher( fieldInvolvementMode = FieldInvolvementMode.WriteOnly ) - private val constructorAnalyzer = ConstructorAnalyzer() + private val executableAnalyzer = ExecutableAnalyzer() /** * Clears state before and after block execution. @@ -284,8 +284,8 @@ class AssembleModelGenerator(private val basePackageName: String) { modelsInAnalysis.add(compositeModel) val constructorInfo = - if (shouldAnalyzeConstructor) constructorAnalyzer.analyze(constructorId) - else ConstructorAssembleInfo(constructorId) + if (shouldAnalyzeConstructor) executableAnalyzer.analyze(constructorId) + else ExecutableAssembleInfo(constructorId) val instantiationCall = constructorCall(compositeModel, constructorInfo) return UtAssembleModel( @@ -406,9 +406,9 @@ class AssembleModelGenerator(private val basePackageName: String) { */ private fun constructorCall( compositeModel: UtCompositeModel, - constructorInfo: ConstructorAssembleInfo, + constructorInfo: ExecutableAssembleInfo, ): UtExecutableCallModel { - val constructorParams = constructorInfo.constructorId.parameters.withIndex() + val constructorParams = constructorInfo.executableId.parameters.withIndex() .map { (index, param) -> val modelOrNull = compositeModel.fields .filter { it.key == constructorInfo.params[index] } @@ -418,7 +418,7 @@ class AssembleModelGenerator(private val basePackageName: String) { assembleModel(fieldModel) } - return UtExecutableCallModel(instance = null, constructorInfo.constructorId, constructorParams) + return UtExecutableCallModel(instance = null, constructorInfo.executableId, constructorParams) } /** @@ -445,11 +445,11 @@ class AssembleModelGenerator(private val basePackageName: String) { val fromUtilPackage = classId.packageName.startsWith("java.util") constructorIds .sortedBy { it.parameters.size } - .firstOrNull { it.parameters.isEmpty() && fromUtilPackage || constructorAnalyzer.isAppropriate(it) } + .firstOrNull { it.parameters.isEmpty() && fromUtilPackage || executableAnalyzer.isAppropriate(it) } } else { constructorIds .sortedByDescending { it.parameters.size } - .firstOrNull { constructorAnalyzer.isAppropriate(it) } + .firstOrNull { executableAnalyzer.isAppropriate(it) } } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/NonNullSpeculator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/NonNullSpeculator.kt index 3c4830cd45..9b5c80df46 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/context/NonNullSpeculator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/NonNullSpeculator.kt @@ -1,17 +1,28 @@ package org.utbot.framework.context import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId import soot.SootField +/** + * Checks whether accessing [field] (with a method invocation or field access) speculatively + * cannot produce [NullPointerException] (according to its finality or accessibility). + * + * @see docs/SpeculativeFieldNonNullability.md for more information. + * + * NOTE: methods for both [FieldId] and [SootField] are provided, because for some lambdas + * class names in byte code and in Soot do not match, making conversion between two field + * representations not always possible, which in turn makes us to support both [FieldId] + * and [SootField] to be useful for both fuzzer and symbolic engine respectively. + */ interface NonNullSpeculator { - /** - * Checks whether accessing [field] (with a method invocation or field access) speculatively - * cannot produce [NullPointerException] (according to its finality or accessibility). - * - * @see docs/SpeculativeFieldNonNullability.md for more information. - */ fun speculativelyCannotProduceNullPointerException( field: SootField, classUnderTest: ClassId, ): Boolean + + fun speculativelyCannotProduceNullPointerException( + field: FieldId, + classUnderTest: ClassId, + ): Boolean } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt index b1b8404190..5a0288fc01 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt @@ -4,6 +4,9 @@ import org.utbot.framework.UtSettings import org.utbot.framework.context.NonNullSpeculator import org.utbot.framework.isFromTrustedLibrary import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.isFinal +import org.utbot.framework.plugin.api.util.isPublic import soot.SootField class SimpleNonNullSpeculator : NonNullSpeculator { @@ -14,4 +17,12 @@ class SimpleNonNullSpeculator : NonNullSpeculator { !UtSettings.maximizeCoverageUsingReflection && field.declaringClass.isFromTrustedLibrary() && (field.isFinal || !field.isPublic) + + override fun speculativelyCannotProduceNullPointerException( + field: FieldId, + classUnderTest: ClassId + ): Boolean = + !UtSettings.maximizeCoverageUsingReflection && + field.declaringClass.isFromTrustedLibrary() && + (field.isFinal || !field.isPublic) } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt index bcb734457a..607015e059 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt @@ -2,6 +2,7 @@ package org.utbot.framework.context.utils import org.utbot.framework.context.ApplicationContext import org.utbot.framework.context.ConcreteExecutionContext +import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams import org.utbot.fuzzing.JavaValueProvider fun ApplicationContext.transformConcreteExecutionContext( @@ -18,5 +19,5 @@ fun ApplicationContext.transformConcreteExecutionContext( } fun ApplicationContext.transformValueProvider( - transformer: (JavaValueProvider) -> JavaValueProvider + transformer: FuzzingContextParams.(JavaValueProvider) -> JavaValueProvider ) = transformConcreteExecutionContext { it.transformValueProvider(transformer) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt index 443a473362..a2ff6a751c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt @@ -4,6 +4,14 @@ import org.utbot.framework.context.ConcreteExecutionContext import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams import org.utbot.framework.context.JavaFuzzingContext import org.utbot.fuzzing.JavaValueProvider +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation + +fun ConcreteExecutionContext.transformInstrumentationFactory( + transformer: (UtExecutionInstrumentation.Factory<*>) -> UtExecutionInstrumentation.Factory<*> +) = object : ConcreteExecutionContext by this { + override val instrumentationFactory: UtExecutionInstrumentation.Factory<*> = + transformer(this@transformInstrumentationFactory.instrumentationFactory) +} fun ConcreteExecutionContext.transformJavaFuzzingContext( transformer: FuzzingContextParams.(JavaFuzzingContext) -> JavaFuzzingContext @@ -14,5 +22,7 @@ fun ConcreteExecutionContext.transformJavaFuzzingContext( } fun ConcreteExecutionContext.transformValueProvider( - transformer: (JavaValueProvider) -> JavaValueProvider -) = transformJavaFuzzingContext { it.transformValueProvider(transformer) } + transformer: FuzzingContextParams.(JavaValueProvider) -> JavaValueProvider +) = transformJavaFuzzingContext { javaFuzzingContext -> + javaFuzzingContext.transformValueProvider { valueProvider -> transformer(valueProvider) } +} diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/JavaLangObject.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/JavaLangObject.kt new file mode 100644 index 0000000000..000fdf7356 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/JavaLangObject.kt @@ -0,0 +1,35 @@ +package org.utbot.fuzzing.spring + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.toTypeParametrizedByTypeVariables +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.providers.nullRoutine +import org.utbot.fuzzing.toFuzzerType + +class JavaLangObjectValueProvider( + private val classesToTryUsingAsJavaLangObject: List, +) : JavaValueProvider { + override fun accept(type: FuzzedType): Boolean { + return type.classId == objectClassId + } + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + classesToTryUsingAsJavaLangObject.map { classToUseAsObject -> + val fuzzedType = toFuzzerType( + type = classToUseAsObject.jClass.toTypeParametrizedByTypeVariables(), + cache = description.typeCache + ) + Seed.Recursive( + construct = Routine.Create(listOf(fuzzedType)) { (value) -> value }, + modify = emptySequence(), + empty = nullRoutine(type.classId) + ) + }.asSequence() +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt index 0ab41c973b..ca439f29bc 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt @@ -1,6 +1,7 @@ package org.utbot.fuzzing.spring.unit import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.UtCompositeModel import org.utbot.framework.plugin.api.util.allDeclaredFieldIds import org.utbot.framework.plugin.api.util.isFinal @@ -30,7 +31,8 @@ val INJECT_MOCK_FLAG = ScopeProperty( */ class InjectMockValueProvider( private val idGenerator: IdGenerator, - private val classUnderTest: ClassId + private val classUnderTest: ClassId, + private val isFieldNonNull: (FieldId) -> Boolean ) : JavaValueProvider { override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) { if (description.description.isStatic == false && scope.parameterIndex == 0 && scope.recursionDepth == 1) { @@ -43,14 +45,24 @@ class InjectMockValueProvider( override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> { if (description.scope?.getProperty(INJECT_MOCK_FLAG) == null) return emptySequence() val fields = type.classId.allDeclaredFieldIds.filterNot { it.isStatic && it.isFinal }.toList() + val (nonNullFields, nullableFields) = fields.partition(isFieldNonNull) return sequenceOf(Seed.Recursive( - construct = Routine.Create(types = fields.map { toFuzzerType(it.jField.genericType, description.typeCache) }) { values -> + construct = Routine.Create( + types = nonNullFields.map { toFuzzerType(it.jField.genericType, description.typeCache) } + ) { values -> emptyFuzzedValue(type.classId).also { (it.model as UtCompositeModel).fields.putAll( - fields.zip(values).associate { (field, value) -> field to value.model } + nonNullFields.zip(values).associate { (field, value) -> field to value.model } ) } }, + modify = nullableFields.map { field -> + Routine.Call( + types = listOf(toFuzzerType(field.jField.genericType, description.typeCache)) + ) { instance, (value) -> + (instance.model as UtCompositeModel).fields[field] = value.model + } + }.asSequence(), empty = Routine.Empty { emptyFuzzedValue(type.classId) } )) } diff --git a/utbot-modificators-analyzer/src/main/kotlin/org/utbot/modifications/ConstructorAnalyzer.kt b/utbot-modificators-analyzer/src/main/kotlin/org/utbot/modifications/ExecutableAnalyzer.kt similarity index 54% rename from utbot-modificators-analyzer/src/main/kotlin/org/utbot/modifications/ConstructorAnalyzer.kt rename to utbot-modificators-analyzer/src/main/kotlin/org/utbot/modifications/ExecutableAnalyzer.kt index 22e7f2fe3e..c3ebf94c6d 100644 --- a/utbot-modificators-analyzer/src/main/kotlin/org/utbot/modifications/ConstructorAnalyzer.kt +++ b/utbot-modificators-analyzer/src/main/kotlin/org/utbot/modifications/ExecutableAnalyzer.kt @@ -1,17 +1,13 @@ package org.utbot.modifications -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.id -import org.utbot.framework.plugin.api.util.isArray -import org.utbot.framework.plugin.api.util.isRefType -import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.util.executableId import org.utbot.modifications.util.kotlinIntrinsicsClassId import org.utbot.modifications.util.retrieveJimpleBody import soot.Scene import soot.SootMethod -import soot.Type import soot.jimple.InvokeExpr import soot.jimple.JimpleBody import soot.jimple.ParameterRef @@ -25,65 +21,69 @@ import soot.jimple.internal.JReturnVoidStmt import soot.jimple.internal.JimpleLocal /** - * Information about constructor required to use it + * Information about [executableId] required to use it * in assemble model construction process. * - * @param params describes the params to call constructor with - * @param setFields describes fields set to required value in constructor - * @param affectedFields describes all fields affected in constructor + * @param params describes the params to call [executableId] with + * @param setFields describes fields set to required value in [executableId] + * @param affectedFields describes all fields affected in [executableId] * */ -data class ConstructorAssembleInfo( - val constructorId: ConstructorId, +data class ExecutableAssembleInfo( + val executableId: ExecutableId, val params: Map = mapOf(), val setFields: Set = setOf(), val affectedFields: Set = setOf() ) /** - * Analyzer of constructors based on Soot. + * Analyzer of constructors and methods based on Soot. */ -class ConstructorAnalyzer { - private val scene = Scene.v() +class ExecutableAnalyzer { + // `Scene.v()` may not yet be initialized when `ExecutableAnalyzer` is created + private val scene get() = Scene.v() /** - * Verifies that [constructorId] can be used in assemble models. - * Analyses Soot representation of constructor for that. + * Verifies that [executableId] can be used in assemble models. + * Analyses Soot representation of [executableId] for that. */ - fun isAppropriate(constructorId: ConstructorId): Boolean { - val sootConstructor = sootConstructor(constructorId) ?: return false - return isAppropriate(sootConstructor) + fun isAppropriate(executableId: ExecutableId): Boolean { + val sootMethod = sootMethod(executableId) ?: return false + return isAppropriate(sootMethod) } /** - * Retrieves information about [constructorId] params and modified fields from Soot. + * Retrieves information about [executableId] params and modified fields from Soot. */ - fun analyze(constructorId: ConstructorId): ConstructorAssembleInfo { + fun analyze(executableId: ExecutableId): ExecutableAssembleInfo { val setFields = mutableSetOf() val affectedFields = mutableSetOf() - val sootConstructor = sootConstructor(constructorId) - ?: error("Soot representation of $constructorId is not found.") - val params = analyze(sootConstructor, setFields, affectedFields) + val sootMethod = sootMethod(executableId) + ?: error("Soot representation of $executableId is not found.") + val params = analyze(sootMethod, setFields, affectedFields) - return ConstructorAssembleInfo(constructorId, params, setFields, affectedFields) + return ExecutableAssembleInfo(executableId, params, setFields, affectedFields) } - //A cache of constructors been analyzed if they are appropriate or not - private val analyzedConstructors: MutableMap = mutableMapOf() + //A cache of executable that has been analyzed if they are appropriate or not + private val analyzedExecutables: MutableMap = mutableMapOf() /** - *Verifies that the body of this constructor + * Verifies that the body of this [sootMethod] * contains only statements matching pattern * this.a = something - * where "a" is an argument of the constructor. + * where "a" is an argument of the [sootMethod]. + * + * NOTE: In case of constructors, calls to other constructors + * (i.e. `super(...)` and `this(...)`) are also allowed. */ - private fun isAppropriate(sootConstructor: SootMethod): Boolean { - if (sootConstructor in analyzedConstructors) { - return analyzedConstructors[sootConstructor]!! + private fun isAppropriate(sootMethod: SootMethod): Boolean { + if (sootMethod in analyzedExecutables) { + return analyzedExecutables[sootMethod]!! } - analyzedConstructors[sootConstructor] = false + analyzedExecutables[sootMethod] = false - val jimpleBody = retrieveJimpleBody(sootConstructor) ?: return false + val jimpleBody = retrieveJimpleBody(sootMethod) ?: return false if (hasSuspiciousInstructions(jimpleBody) || modifiesStatics(jimpleBody)) { return false } @@ -96,7 +96,7 @@ class ConstructorAnalyzer { } } - analyzedConstructors[sootConstructor] = true + analyzedExecutables[sootMethod] = true return true } @@ -111,15 +111,15 @@ class ConstructorAnalyzer { } private fun analyze( - sootConstructor: SootMethod, + sootMethod: SootMethod, setFields: MutableSet, affectedFields: MutableSet, ): Map { - val jimpleBody = retrieveJimpleBody(sootConstructor) ?: return emptyMap() + val jimpleBody = retrieveJimpleBody(sootMethod) ?: return emptyMap() analyzeAssignments(jimpleBody, setFields, affectedFields) val indexOfLocals = jimpleVariableIndices(jimpleBody) - val indexedFields = indexToField(sootConstructor).toMutableMap() + val indexedFields = indexToField(sootMethod).toMutableMap() for (invocation in invocations(jimpleBody)) { val invokedIndexedFields = analyze(invocation.method, setFields, affectedFields) @@ -157,10 +157,10 @@ class ConstructorAnalyzer { } /** - * Matches an index of constructor argument with a [FieldId]. + * Matches an index of executable argument with a [FieldId]. */ - private fun indexToField(sootConstructor: SootMethod): Map { - val jimpleBody = retrieveJimpleBody(sootConstructor) ?: return emptyMap() + private fun indexToField(sootMethod: SootMethod): Map { + val jimpleBody = retrieveJimpleBody(sootMethod) ?: return emptyMap() val assignments = assignments(jimpleBody) val indexedFields = mutableMapOf() @@ -169,6 +169,7 @@ class ConstructorAnalyzer { val field = (assn.leftOp as? JInstanceFieldRef)?.field ?: continue val parameterIndex = jimpleBody.parameterLocals.indexOfFirst { it.name == jimpleLocal.name } + if (parameterIndex == -1) continue indexedFields[parameterIndex] = FieldId(field.declaringClass.id, field.name) } @@ -176,29 +177,19 @@ class ConstructorAnalyzer { } /** - * Matches Jimple variable name with an index in current constructor. + * Matches Jimple variable name with an index in current executable. */ private fun jimpleVariableIndices(jimpleBody: JimpleBody) = jimpleBody.units .filterIsInstance() .filter { it.leftOp is JimpleLocal && it.rightOp is ParameterRef } .associate { it.leftOp as JimpleLocal to (it.rightOp as ParameterRef).index } - private val sootConstructorCache = mutableMapOf() - - private fun sootConstructor(constructorId: ConstructorId): SootMethod? { - if (constructorId in sootConstructorCache) { - return sootConstructorCache[constructorId] - } - val sootClass = scene.getSootClass(constructorId.classId.name) - val allConstructors = sootClass.methods.filter { it.isConstructor } - val sootConstructor = allConstructors.firstOrNull { sameParameterTypes(it, constructorId) } + private val sootMethodCache = mutableMapOf() - if (sootConstructor != null) { - sootConstructorCache[constructorId] = sootConstructor - return sootConstructor + private fun sootMethod(executableId: ExecutableId): SootMethod? = sootMethodCache.getOrPut(executableId) { + scene.getSootClass(executableId.classId.name).methods.firstOrNull { + it.executableId == executableId } - - return null } private fun hasSuspiciousInstructions(jimpleBody: JimpleBody): Boolean = @@ -223,36 +214,4 @@ class ConstructorAnalyzer { .map { it.invokeExpr } // These are instructions inserted by Kotlin compiler to check that arguments are not null, we should ignore them .filterNot { it.method.declaringClass.id == kotlinIntrinsicsClassId } - - private fun sameParameterTypes(sootMethod: SootMethod, constructorId: ConstructorId): Boolean { - val sootConstructorTypes = sootMethod.parameterTypes - val constructorTypes = constructorId.parameters.map { getParameterType(it) } - - val sootConstructorParamsCount = sootConstructorTypes.count() - val constructorParamsCount = constructorTypes.count() - - if (sootConstructorParamsCount != constructorParamsCount) return false - for (i in 0 until sootConstructorParamsCount) { - if (sootConstructorTypes[i] != constructorTypes[i]) return false - } - - return true - } - - /** - * Restores [Type] by [ClassId] if possible. - * - * Note: we return null if restore process failed. Possibly we need to - * enlarge a set of cases types we can deal with in the future. - */ - private fun getParameterType(type: ClassId): Type? = - try { - when { - type.isRefType -> scene.getRefType(type.name) - type.isArray -> scene.getType(type.jClass.canonicalName) - else -> scene.getType(type.name) - } - } catch (e: Exception) { - null - } } \ No newline at end of file diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt index aca0011b85..bdf5745199 100644 --- a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt @@ -15,6 +15,7 @@ import org.utbot.framework.context.TypeReplacer import org.utbot.framework.context.custom.CoverageFilteringConcreteExecutionContext import org.utbot.framework.context.custom.RerunningConcreteExecutionContext import org.utbot.framework.context.custom.useMocks +import org.utbot.framework.context.utils.transformInstrumentationFactory import org.utbot.framework.context.utils.transformJavaFuzzingContext import org.utbot.framework.context.utils.transformValueProvider import org.utbot.framework.plugin.api.BeanDefinitionData @@ -26,13 +27,16 @@ import org.utbot.framework.plugin.api.util.SpringModelUtils.entityClassIds import org.utbot.framework.plugin.api.util.allSuperTypes import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.objectClassId import org.utbot.framework.plugin.api.util.utContext +import org.utbot.fuzzing.spring.JavaLangObjectValueProvider import org.utbot.fuzzing.spring.FuzzedTypeFlag import org.utbot.fuzzing.spring.addProperties import org.utbot.fuzzing.spring.decorators.replaceTypes import org.utbot.fuzzing.spring.properties import org.utbot.fuzzing.spring.unit.InjectMockValueProvider import org.utbot.fuzzing.toFuzzerType +import org.utbot.instrumentation.instrumentation.execution.RemovingConstructFailsUtExecutionInstrumentation class SpringApplicationContextImpl( private val delegateContext: ApplicationContext, @@ -58,7 +62,11 @@ class SpringApplicationContextImpl( var delegateConcreteExecutionContext = delegateContext.createConcreteExecutionContext( fullClasspath, classpathWithoutDependencies - ) + ).transformValueProvider { valueProvider -> + valueProvider.with(JavaLangObjectValueProvider( + classesToTryUsingAsJavaLangObject = listOf(objectClassId, classUnderTest) + )) + } // to avoid filtering out all coverage, we only filter // coverage when `classpathWithoutDependencies` is provided @@ -84,7 +92,10 @@ class SpringApplicationContextImpl( .transformValueProvider { origValueProvider -> InjectMockValueProvider( idGenerator = fuzzingContext.idGenerator, - classUnderTest = fuzzingContext.classUnderTest + classUnderTest = fuzzingContext.classUnderTest, + isFieldNonNull = { fieldId -> + nonNullSpeculator.speculativelyCannotProduceNullPointerException(fieldId, classUnderTest) + }, ) .withFallback(origValueProvider) .replaceTypes { description, type -> @@ -109,6 +120,8 @@ class SpringApplicationContextImpl( springApplicationContext = this ) ) + }.transformInstrumentationFactory { delegateInstrumentationFactory -> + RemovingConstructFailsUtExecutionInstrumentation.Factory(delegateInstrumentationFactory) } } diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestConcreteExecutionContext.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestConcreteExecutionContext.kt index e0ae816987..12fc787d9b 100644 --- a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestConcreteExecutionContext.kt +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringIntegrationTestConcreteExecutionContext.kt @@ -12,7 +12,6 @@ import org.utbot.framework.plugin.api.isSuccess import org.utbot.framework.plugin.api.util.SpringModelUtils import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.getRelevantSpringRepositories -import org.utbot.instrumentation.instrumentation.execution.RemovingConstructFailsUtExecutionInstrumentation import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation import org.utbot.instrumentation.instrumentation.spring.SpringUtExecutionInstrumentation @@ -32,15 +31,13 @@ class SpringIntegrationTestConcreteExecutionContext( } override val instrumentationFactory: UtExecutionInstrumentation.Factory<*> = - RemovingConstructFailsUtExecutionInstrumentation.Factory( - SpringUtExecutionInstrumentation.Factory( - delegateContext.instrumentationFactory, - springSettings, - springApplicationContext.beanDefinitions, - buildDirs = classpathWithoutDependencies.split(File.pathSeparator) - .map { File(it).toURI().toURL() } - .toTypedArray(), - ) + SpringUtExecutionInstrumentation.Factory( + delegateContext.instrumentationFactory, + springSettings, + springApplicationContext.beanDefinitions, + buildDirs = classpathWithoutDependencies.split(File.pathSeparator) + .map { File(it).toURI().toURL() } + .toTypedArray(), ) override fun loadContext( diff --git a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringNonNullSpeculator.kt b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringNonNullSpeculator.kt index 5f49d7800d..4bd44f01a2 100644 --- a/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringNonNullSpeculator.kt +++ b/utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringNonNullSpeculator.kt @@ -1,19 +1,114 @@ package org.utbot.framework.context.spring +import mu.KotlinLogging import org.utbot.framework.context.NonNullSpeculator +import org.utbot.framework.context.spring.SpringNonNullSpeculator.InjectionKind.* import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.classId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.SpringModelUtils.autowiredClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.componentClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.injectClassIds import org.utbot.framework.plugin.api.util.allDeclaredFieldIds +import org.utbot.framework.plugin.api.util.executable import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.jField +import org.utbot.framework.plugin.api.util.jFieldOrNull +import org.utbot.modifications.ExecutableAnalyzer import soot.SootField +import java.lang.reflect.AnnotatedElement class SpringNonNullSpeculator( private val delegateNonNullSpeculator: NonNullSpeculator, private val springApplicationContext: SpringApplicationContext ) : NonNullSpeculator { - override fun speculativelyCannotProduceNullPointerException(field: SootField, classUnderTest: ClassId): Boolean = - // TODO add ` || delegateNonNullSpeculator.speculativelyCannotProduceNullPointerException(field, classUnderTest)` - // (TODO is added as a part of only equivalent transformations refactoring PR and should be completed in the follow up PR) - field.fieldId in classUnderTest.allDeclaredFieldIds && field.type.classId !in springApplicationContext.allInjectedSuperTypes + private val executableAnalyzer = ExecutableAnalyzer() + companion object { + private val logger = KotlinLogging.logger {} + } + + override fun speculativelyCannotProduceNullPointerException( + field: SootField, + classUnderTest: ClassId, + ): Boolean = + ((field.jFieldOrNull?.let { it.fieldId in getInjectedFields(it.declaringClass.id) } ?: false) || + delegateNonNullSpeculator.speculativelyCannotProduceNullPointerException(field, classUnderTest)) + + override fun speculativelyCannotProduceNullPointerException(field: FieldId, classUnderTest: ClassId): Boolean = + (field in getInjectedFields(field.declaringClass) || + delegateNonNullSpeculator.speculativelyCannotProduceNullPointerException(field, classUnderTest)) + + private val injectedFieldsCache = mutableMapOf>() + + /** + * As described in [this issue](https://github.com/UnitTestBot/UTBotJava/issues/2589) we should consider + * `FieldType fieldName` to be non-`null`, iff one of the following statements holds: + * - the field itself is annotated with `@Autowired(required=true)`, `@Autowired` or `@Inject` + * - all the following statements hold: + * - one of the following statements hold: + * - there’s a constructor or a method annotated with `@Autowired(required=true)`, `@Autowired` or `@Inject` + * - there’s only one constructor defined in the class, said constructor is not annotated with + * `@Autowired(required=false)`, while the class itself is annotated with `@Component`-like annotation + * (either `@Component` itself or other annotation that is itself annotated with `@Component`) + * - said constructor/method accepts a parameter of type `FieldType` (or its subtype) + * - said parameter is not annotated with `@Nullable` nor with `@Autowired(required=false)` + * - said constructor/method contains an assignment of said parameter to `fieldName` + */ + private fun getInjectedFields(classId: ClassId): Set = injectedFieldsCache.getOrPut(classId) { + try { + (classId.allDeclaredFieldIds.filter { it.jField.getInjectionAnnotationKind() == INJECTED_AND_REQUIRED } + + classId.getInjectingExecutables().flatMap { injectingExecutable -> + executableAnalyzer.analyze(injectingExecutable).params + .map { (paramIdx, fieldId) -> injectingExecutable.executable.parameters[paramIdx] to fieldId } + .filter { (param, fieldId) -> + fieldId.type.jClass.isAssignableFrom(param.type) && + param.annotations.none { + it.annotationClass.simpleName?.endsWith("Nullable") ?: false + } && + param.getInjectionAnnotationKind() != INJECTED_BUT_NOT_REQUIRED + } + .map { (_, fieldId) -> fieldId } + }).toSet().also { + logger.debug { "Injected fields for $classId: $it" } + } + } catch (e: Throwable) { + logger.warn(e) { "Failed to determine injected fields for class $classId" } + emptySet() + } + } + + private fun ClassId.getInjectingExecutables(): Sequence { + return (allConstructors + allMethods).filter { + it.executable.getInjectionAnnotationKind() == INJECTED_AND_REQUIRED + } + listOfNotNull(allConstructors.singleOrNull()?.takeIf { constructor -> + isBeanDefiningClass() && constructor.executable.getInjectionAnnotationKind() == NOT_INJECTED + }) + } + + private fun ClassId.isBeanDefiningClass(): Boolean = + this in springApplicationContext.injectedTypes || jClass.annotations.any { it.isComponentLike() } + + private fun Annotation.isComponentLike() = + annotationClass.id == componentClassId || + annotationClass.annotations.any { it.annotationClass.id == componentClassId } + + private fun AnnotatedElement.getInjectionAnnotationKind(): InjectionKind { + if(annotations.any { it.annotationClass.id in injectClassIds }) + return INJECTED_AND_REQUIRED + val autowiredAnnotation = annotations.firstOrNull { it.annotationClass.id == autowiredClassId } + ?: return NOT_INJECTED + return if (autowiredAnnotation.annotationClass.java.getMethod("required").invoke(autowiredAnnotation) as Boolean) + INJECTED_AND_REQUIRED + else + INJECTED_BUT_NOT_REQUIRED + } + + private enum class InjectionKind { + NOT_INJECTED, + INJECTED_BUT_NOT_REQUIRED, + INJECTED_AND_REQUIRED, + } } \ No newline at end of file diff --git a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameType.java b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameType.java index bf4f045e80..3c3c9bf239 100644 --- a/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameType.java +++ b/utbot-spring-sample/src/main/java/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameType.java @@ -3,9 +3,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; - @Service public class ServiceOfBeansWithSameType { @Autowired @@ -14,15 +11,21 @@ public class ServiceOfBeansWithSameType { @Autowired private Person personTwo; - public final List baseOrders = new ArrayList<>(); + // A method for testing both cases when the Engine produces + // - two models for two @Autowired fields of the same type + // - one model for two @Autowired fields of the same type + public Boolean checker() { + String name1 = personOne.getName();// shouldn't produce NPE because `personOne` is `@Autowired` + int length = name1.length(); // can produce NPE because `Person.name` isn't `@Autowired` + Integer age2 = personTwo.getAge(); // shouldn't produce NPE because `personTwo` is `@Autowired` + return personOne == personTwo; + } - // a method for testing the case when the Engine produces one model on @Autowired variables of the same type - public Integer ageSum(){ - return personOne.getAge() + personTwo.getAge(); + public Person getPersonOne() { + return personOne; } - // a method for testing the case when the Engine produces two models on @Autowired variables of the same type - public Boolean checker() { - return personOne.getName().equals("k") && personTwo.getName().length() > 5 && baseOrders.isEmpty(); + public Person getPersonTwo() { + return personTwo; } } \ No newline at end of file diff --git a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameTypeTest.kt b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameTypeTest.kt index d57d546470..aa748579c4 100644 --- a/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameTypeTest.kt +++ b/utbot-spring-test/src/test/kotlin/org/utbot/examples/spring/autowiring/twoAndMoreBeansForOneType/ServiceOfBeansWithSameTypeTest.kt @@ -3,7 +3,6 @@ package org.utbot.examples.spring.autowiring.twoAndMoreBeansForOneType import org.junit.jupiter.api.Test import org.utbot.examples.spring.autowiring.SpringNoConfigUtValueTestCaseChecker import org.utbot.examples.spring.utils.* -import org.utbot.framework.plugin.api.UtConcreteValue import org.utbot.testcheckers.eq import org.utbot.testing.* @@ -12,16 +11,15 @@ class ServiceOfBeansWithSameTypeTest : SpringNoConfigUtValueTestCaseChecker( ) { /** - * In this test, we check the case when the Engine produces two models on two @Autowired variables of the same type. - * - * The engine produce two models only in the tests, when `baseOrder` is a testing participant. - * In these tests, we mock all the variables and get the only necessary `mock.values` in each variable. + * In this test, we check both cases when the Engine produces + * - two models for two @Autowired fields of the same type + * - one model for two @Autowired fields of the same type */ @Test fun testChecker() { checkThisMocksAndExceptions( method = ServiceOfBeansWithSameType::checker, - branches = eq(6), + branches = eq(3), {_, mocks, r -> val personOne = mocks.singleMock("personOne", namePersonCall) @@ -31,110 +29,27 @@ class ServiceOfBeansWithSameTypeTest : SpringNoConfigUtValueTestCaseChecker( r1 && r.isException() }, - {_, mocks, r -> - val person = mocks.singleMock("personOne", namePersonCall) - - val personOneName = (person.values[0] as? UtConcreteValue<*>)?.value - val personTwoName = (person.values[1] as? UtConcreteValue<*>)?.value - - val r1 = personOneName == "k" - val r2 = personTwoName == null - - r1 && r2 && r.isException() - }, {_, mocks, r -> val personOne = mocks.singleMock("personOne", namePersonCall) + val personTwo = mocks.singleMockOrNull("personTwo", namePersonCall) val personOneName = personOne.value() - val r1 = personOneName != "k" - - r1 && (r.getOrNull() == false) - }, - {_, mocks, r -> - val person = mocks.singleMock("personOne", namePersonCall) - - val personOneName = (person.values[0] as? UtConcreteValue<*>)?.value - val personTwoName = (person.values[1] as? UtConcreteValue<*>)?.value.toString() - - val r1 = personOneName == "k" - val r2 = personTwoName.length <= 5 + val r1 = personOneName != null + val r2 = personTwo == null // `personTwo.getName()` isn't mocked, meaning `personOne != personTwo` r1 && r2 && (r.getOrNull() == false) }, - - //In this test Engine produces two models on two @Autowired variables of the same type - {thisInstance, mocks, r -> - val personOne = mocks.singleMock("personOne", namePersonCall) - val personTwo = mocks.singleMock("personTwo", namePersonCall) - - val personOneName = (personOne.values[0] as? UtConcreteValue<*>)?.value - val personTwoName = (personTwo.values[0] as? UtConcreteValue<*>)?.value.toString() - val baseOrders = thisInstance.baseOrders - - val r1 = personOneName == "k" - val r2 = personTwoName.length > 5 - val r3 = baseOrders.isEmpty() - - r1 && r2 && r3 && (r.getOrNull() == true) - }, - - {thisInstance, mocks, r -> + {_, mocks, r -> val personOne = mocks.singleMock("personOne", namePersonCall) - val personTwo = mocks.singleMock("personTwo", namePersonCall) - - val personOneName = (personOne.values[0] as? UtConcreteValue<*>)?.value - val personTwoName = (personTwo.values[0] as? UtConcreteValue<*>)?.value.toString() - val baseOrders = thisInstance.baseOrders - - val r1 = personOneName == "k" - val r2 = personTwoName.length > 5 - val r3 = baseOrders.isNotEmpty() + val personTwo = mocks.singleMockOrNull("personTwo", namePersonCall) - r1 && r2 && r3 && (r.getOrNull() == false) - }, - coverage = DoNotCalculate, - mockStrategy = springMockStrategy, - additionalDependencies = springAdditionalDependencies, - ) - } - - /** - * In this test, we check the case when the Engine produces one model on two @Autowired variables of the same type. - * - * Therefore, we only mock one of the variables and get all `mock.values` in it - */ - @Test - fun testAgeSum(){ - checkThisMocksAndExceptions( - method = ServiceOfBeansWithSameType::ageSum, - branches = eq(3), - { _, mocks, r -> - val personOne = mocks.singleMock("personOne", agePersonCall) - - val personOneAge = (personOne.values[0] as? UtConcreteValue<*>)?.value - val isPersonOneAgeNull = personOneAge == null - - isPersonOneAgeNull && r.isException() - }, - { _, mocks, r -> - val personOne = mocks.singleMock("personOne", agePersonCall) - - val personOneAge = (personOne.values[0] as? UtConcreteValue<*>)?.value - val personTwoAge = (personOne.values[1] as? UtConcreteValue<*>)?.value - - val isPersonOneAgeNull = personOneAge != null - val isPersonTwoAgeNotNull = personTwoAge == null - - isPersonOneAgeNull && isPersonTwoAgeNotNull && r.isException() - }, - { _, mocks, r -> - val personOne = mocks.singleMock("personOne", agePersonCall) + val personOneName = personOne.value() - val personOneAge = (personOne.values[0] as? UtConcreteValue<*>)?.value.toString().toInt() - val personTwoAge = (personOne.values[1] as? UtConcreteValue<*>)?.value.toString().toInt() + val r1 = personOneName != null + val r2 = personTwo != null // `personTwo.getName()` is mocked, meaning `personOne == personTwo` - personOneAge + personTwoAge == r.getOrNull() + r1 && r2 && (r.getOrNull() == true) }, coverage = DoNotCalculate, mockStrategy = springMockStrategy,