Skip to content

Improve detection of NPEs caused by this instance fields being null in Spring unit tests #2617

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 10 commits into from
Sep 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.utbot.framework

import org.utbot.framework.plugin.api.ClassId
import soot.SootClass

/**
Expand All @@ -10,7 +11,17 @@ private val isPackageTrusted: MutableMap<String, Boolean> = 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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,13 @@ val ClassId.allDeclaredFieldIds: Sequence<FieldId>
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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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] }
Expand All @@ -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)
}

/**
Expand All @@ -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) }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -18,5 +19,5 @@ fun ApplicationContext.transformConcreteExecutionContext(
}

fun ApplicationContext.transformValueProvider(
transformer: (JavaValueProvider) -> JavaValueProvider
transformer: FuzzingContextParams.(JavaValueProvider) -> JavaValueProvider
) = transformConcreteExecutionContext { it.transformValueProvider(transformer) }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) }
}
Original file line number Diff line number Diff line change
@@ -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<ClassId>,
) : JavaValueProvider {
override fun accept(type: FuzzedType): Boolean {
return type.classId == objectClassId
}

override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence<Seed<FuzzedType, FuzzedValue>> =
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()
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -30,7 +31,8 @@ val INJECT_MOCK_FLAG = ScopeProperty<Unit>(
*/
class InjectMockValueProvider(
private val idGenerator: IdGenerator<Int>,
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) {
Expand All @@ -43,14 +45,24 @@ class InjectMockValueProvider(
override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence<Seed<FuzzedType, FuzzedValue>> {
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<FuzzedType, FuzzedValue>(
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) }
))
}
Expand Down
Loading