Skip to content

Commit d514a7b

Browse files
authored
Improve detection of NPEs caused by this instance fields being null in Spring unit tests #2589 (#2617)
* Add `JavaLangObjectValueProvider` that uses class under test in place of `java.lang.Object`s * Use `RemovingConstructFailsUtExecutionInstrumentation` in all Spring tests * Replace `ConstructorAnalyzer` with `ExecutableAnalyzer` * Improve Spring-specific non-null speculation and make it work with `FieldId` * Make `InjectMockValueProvider` respect `NonNullSpeculator` * Update `SpringModelUtils` * Log "Injected fields for..." message with debug level * Fix `ExecutableAnalyzer` to avoid storing `-1` in param index to field id map * Fix `ServiceOfBeansWithSameTypeTest` * Fix typo in `JavaLangObjectValueProvider` class name
1 parent d4564dd commit d514a7b

File tree

16 files changed

+322
-237
lines changed

16 files changed

+322
-237
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.utbot.framework
22

3+
import org.utbot.framework.plugin.api.ClassId
34
import soot.SootClass
45

56
/**
@@ -10,7 +11,17 @@ private val isPackageTrusted: MutableMap<String, Boolean> = mutableMapOf()
1011
/**
1112
* Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries].
1213
*/
13-
fun SootClass.isFromTrustedLibrary(): Boolean {
14+
fun SootClass.isFromTrustedLibrary(): Boolean = isFromTrustedLibrary(packageName)
15+
16+
/**
17+
* Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries].
18+
*/
19+
fun ClassId.isFromTrustedLibrary(): Boolean = isFromTrustedLibrary(packageName)
20+
21+
/**
22+
* Determines whether [packageName] is from trusted libraries as defined in [TrustedLibraries].
23+
*/
24+
fun isFromTrustedLibrary(packageName: String): Boolean {
1425
isPackageTrusted[packageName]?.let {
1526
return it
1627
}

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt

+7
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,13 @@ val ClassId.allDeclaredFieldIds: Sequence<FieldId>
468468
val SootField.fieldId: FieldId
469469
get() = FieldId(declaringClass.id, name)
470470

471+
/**
472+
* For some lambdas class names in byte code and in Soot don't match, so we may fail
473+
* to convert some soot fields to Java fields, in such case `null` is returned.
474+
*/
475+
val SootField.jFieldOrNull: Field?
476+
get() = runCatching { fieldId.jField }.getOrNull()
477+
471478
// FieldId utils
472479
val FieldId.safeJField: Field?
473480
get() = declaringClass.jClass.declaredFields.firstOrNull { it.name == name }

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/SpringModelUtils.kt

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ private val logger = KotlinLogging.logger {}
2222

2323
object SpringModelUtils {
2424
val autowiredClassId = ClassId("org.springframework.beans.factory.annotation.Autowired")
25+
val injectClassIds = getClassIdFromEachAvailablePackage(
26+
packages = listOf("javax", "jakarta"),
27+
classNameFromPackage = "inject.Inject"
28+
)
29+
val componentClassId = ClassId("org.springframework.stereotype.Component")
2530

2631
val applicationContextClassId = ClassId("org.springframework.context.ApplicationContext")
2732
val repositoryClassId = ClassId("org.springframework.data.repository.Repository")

utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import org.utbot.engine.ResolvedModels
88
import org.utbot.framework.UtSettings
99
import org.utbot.framework.codegen.util.isAccessibleFrom
1010
import org.utbot.modifications.AnalysisMode.SettersAndDirectAccessors
11-
import org.utbot.modifications.ConstructorAnalyzer
12-
import org.utbot.modifications.ConstructorAssembleInfo
11+
import org.utbot.modifications.ExecutableAnalyzer
12+
import org.utbot.modifications.ExecutableAssembleInfo
1313
import org.utbot.modifications.UtBotFieldsModificatorsSearcher
1414
import org.utbot.framework.plugin.api.ClassId
1515
import org.utbot.framework.plugin.api.ConstructorId
@@ -77,7 +77,7 @@ class AssembleModelGenerator(private val basePackageName: String) {
7777
UtBotFieldsModificatorsSearcher(
7878
fieldInvolvementMode = FieldInvolvementMode.WriteOnly
7979
)
80-
private val constructorAnalyzer = ConstructorAnalyzer()
80+
private val executableAnalyzer = ExecutableAnalyzer()
8181

8282
/**
8383
* Clears state before and after block execution.
@@ -284,8 +284,8 @@ class AssembleModelGenerator(private val basePackageName: String) {
284284
modelsInAnalysis.add(compositeModel)
285285

286286
val constructorInfo =
287-
if (shouldAnalyzeConstructor) constructorAnalyzer.analyze(constructorId)
288-
else ConstructorAssembleInfo(constructorId)
287+
if (shouldAnalyzeConstructor) executableAnalyzer.analyze(constructorId)
288+
else ExecutableAssembleInfo(constructorId)
289289

290290
val instantiationCall = constructorCall(compositeModel, constructorInfo)
291291
return UtAssembleModel(
@@ -406,9 +406,9 @@ class AssembleModelGenerator(private val basePackageName: String) {
406406
*/
407407
private fun constructorCall(
408408
compositeModel: UtCompositeModel,
409-
constructorInfo: ConstructorAssembleInfo,
409+
constructorInfo: ExecutableAssembleInfo,
410410
): UtExecutableCallModel {
411-
val constructorParams = constructorInfo.constructorId.parameters.withIndex()
411+
val constructorParams = constructorInfo.executableId.parameters.withIndex()
412412
.map { (index, param) ->
413413
val modelOrNull = compositeModel.fields
414414
.filter { it.key == constructorInfo.params[index] }
@@ -418,7 +418,7 @@ class AssembleModelGenerator(private val basePackageName: String) {
418418
assembleModel(fieldModel)
419419
}
420420

421-
return UtExecutableCallModel(instance = null, constructorInfo.constructorId, constructorParams)
421+
return UtExecutableCallModel(instance = null, constructorInfo.executableId, constructorParams)
422422
}
423423

424424
/**
@@ -445,11 +445,11 @@ class AssembleModelGenerator(private val basePackageName: String) {
445445
val fromUtilPackage = classId.packageName.startsWith("java.util")
446446
constructorIds
447447
.sortedBy { it.parameters.size }
448-
.firstOrNull { it.parameters.isEmpty() && fromUtilPackage || constructorAnalyzer.isAppropriate(it) }
448+
.firstOrNull { it.parameters.isEmpty() && fromUtilPackage || executableAnalyzer.isAppropriate(it) }
449449
} else {
450450
constructorIds
451451
.sortedByDescending { it.parameters.size }
452-
.firstOrNull { constructorAnalyzer.isAppropriate(it) }
452+
.firstOrNull { executableAnalyzer.isAppropriate(it) }
453453
}
454454
}
455455

Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
package org.utbot.framework.context
22

33
import org.utbot.framework.plugin.api.ClassId
4+
import org.utbot.framework.plugin.api.FieldId
45
import soot.SootField
56

7+
/**
8+
* Checks whether accessing [field] (with a method invocation or field access) speculatively
9+
* cannot produce [NullPointerException] (according to its finality or accessibility).
10+
*
11+
* @see docs/SpeculativeFieldNonNullability.md for more information.
12+
*
13+
* NOTE: methods for both [FieldId] and [SootField] are provided, because for some lambdas
14+
* class names in byte code and in Soot do not match, making conversion between two field
15+
* representations not always possible, which in turn makes us to support both [FieldId]
16+
* and [SootField] to be useful for both fuzzer and symbolic engine respectively.
17+
*/
618
interface NonNullSpeculator {
7-
/**
8-
* Checks whether accessing [field] (with a method invocation or field access) speculatively
9-
* cannot produce [NullPointerException] (according to its finality or accessibility).
10-
*
11-
* @see docs/SpeculativeFieldNonNullability.md for more information.
12-
*/
1319
fun speculativelyCannotProduceNullPointerException(
1420
field: SootField,
1521
classUnderTest: ClassId,
1622
): Boolean
23+
24+
fun speculativelyCannotProduceNullPointerException(
25+
field: FieldId,
26+
classUnderTest: ClassId,
27+
): Boolean
1728
}

utbot-framework/src/main/kotlin/org/utbot/framework/context/simple/SimpleNonNullSpeculator.kt

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import org.utbot.framework.UtSettings
44
import org.utbot.framework.context.NonNullSpeculator
55
import org.utbot.framework.isFromTrustedLibrary
66
import org.utbot.framework.plugin.api.ClassId
7+
import org.utbot.framework.plugin.api.FieldId
8+
import org.utbot.framework.plugin.api.util.isFinal
9+
import org.utbot.framework.plugin.api.util.isPublic
710
import soot.SootField
811

912
class SimpleNonNullSpeculator : NonNullSpeculator {
@@ -14,4 +17,12 @@ class SimpleNonNullSpeculator : NonNullSpeculator {
1417
!UtSettings.maximizeCoverageUsingReflection &&
1518
field.declaringClass.isFromTrustedLibrary() &&
1619
(field.isFinal || !field.isPublic)
20+
21+
override fun speculativelyCannotProduceNullPointerException(
22+
field: FieldId,
23+
classUnderTest: ClassId
24+
): Boolean =
25+
!UtSettings.maximizeCoverageUsingReflection &&
26+
field.declaringClass.isFromTrustedLibrary() &&
27+
(field.isFinal || !field.isPublic)
1728
}

utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ApplicationContextUtils.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.utbot.framework.context.utils
22

33
import org.utbot.framework.context.ApplicationContext
44
import org.utbot.framework.context.ConcreteExecutionContext
5+
import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams
56
import org.utbot.fuzzing.JavaValueProvider
67

78
fun ApplicationContext.transformConcreteExecutionContext(
@@ -18,5 +19,5 @@ fun ApplicationContext.transformConcreteExecutionContext(
1819
}
1920

2021
fun ApplicationContext.transformValueProvider(
21-
transformer: (JavaValueProvider) -> JavaValueProvider
22+
transformer: FuzzingContextParams.(JavaValueProvider) -> JavaValueProvider
2223
) = transformConcreteExecutionContext { it.transformValueProvider(transformer) }

utbot-framework/src/main/kotlin/org/utbot/framework/context/utils/ConcreteExecutionContextUtils.kt

+12-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import org.utbot.framework.context.ConcreteExecutionContext
44
import org.utbot.framework.context.ConcreteExecutionContext.FuzzingContextParams
55
import org.utbot.framework.context.JavaFuzzingContext
66
import org.utbot.fuzzing.JavaValueProvider
7+
import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation
8+
9+
fun ConcreteExecutionContext.transformInstrumentationFactory(
10+
transformer: (UtExecutionInstrumentation.Factory<*>) -> UtExecutionInstrumentation.Factory<*>
11+
) = object : ConcreteExecutionContext by this {
12+
override val instrumentationFactory: UtExecutionInstrumentation.Factory<*> =
13+
transformer(this@transformInstrumentationFactory.instrumentationFactory)
14+
}
715

816
fun ConcreteExecutionContext.transformJavaFuzzingContext(
917
transformer: FuzzingContextParams.(JavaFuzzingContext) -> JavaFuzzingContext
@@ -14,5 +22,7 @@ fun ConcreteExecutionContext.transformJavaFuzzingContext(
1422
}
1523

1624
fun ConcreteExecutionContext.transformValueProvider(
17-
transformer: (JavaValueProvider) -> JavaValueProvider
18-
) = transformJavaFuzzingContext { it.transformValueProvider(transformer) }
25+
transformer: FuzzingContextParams.(JavaValueProvider) -> JavaValueProvider
26+
) = transformJavaFuzzingContext { javaFuzzingContext ->
27+
javaFuzzingContext.transformValueProvider { valueProvider -> transformer(valueProvider) }
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.utbot.fuzzing.spring
2+
3+
import org.utbot.framework.plugin.api.ClassId
4+
import org.utbot.framework.plugin.api.util.jClass
5+
import org.utbot.framework.plugin.api.util.objectClassId
6+
import org.utbot.fuzzer.FuzzedType
7+
import org.utbot.fuzzer.FuzzedValue
8+
import org.utbot.fuzzer.toTypeParametrizedByTypeVariables
9+
import org.utbot.fuzzing.FuzzedDescription
10+
import org.utbot.fuzzing.JavaValueProvider
11+
import org.utbot.fuzzing.Routine
12+
import org.utbot.fuzzing.Seed
13+
import org.utbot.fuzzing.providers.nullRoutine
14+
import org.utbot.fuzzing.toFuzzerType
15+
16+
class JavaLangObjectValueProvider(
17+
private val classesToTryUsingAsJavaLangObject: List<ClassId>,
18+
) : JavaValueProvider {
19+
override fun accept(type: FuzzedType): Boolean {
20+
return type.classId == objectClassId
21+
}
22+
23+
override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence<Seed<FuzzedType, FuzzedValue>> =
24+
classesToTryUsingAsJavaLangObject.map { classToUseAsObject ->
25+
val fuzzedType = toFuzzerType(
26+
type = classToUseAsObject.jClass.toTypeParametrizedByTypeVariables(),
27+
cache = description.typeCache
28+
)
29+
Seed.Recursive(
30+
construct = Routine.Create(listOf(fuzzedType)) { (value) -> value },
31+
modify = emptySequence(),
32+
empty = nullRoutine(type.classId)
33+
)
34+
}.asSequence()
35+
}

utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/spring/unit/InjectMocks.kt

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.utbot.fuzzing.spring.unit
22

33
import org.utbot.framework.plugin.api.ClassId
4+
import org.utbot.framework.plugin.api.FieldId
45
import org.utbot.framework.plugin.api.UtCompositeModel
56
import org.utbot.framework.plugin.api.util.allDeclaredFieldIds
67
import org.utbot.framework.plugin.api.util.isFinal
@@ -30,7 +31,8 @@ val INJECT_MOCK_FLAG = ScopeProperty<Unit>(
3031
*/
3132
class InjectMockValueProvider(
3233
private val idGenerator: IdGenerator<Int>,
33-
private val classUnderTest: ClassId
34+
private val classUnderTest: ClassId,
35+
private val isFieldNonNull: (FieldId) -> Boolean
3436
) : JavaValueProvider {
3537
override fun enrich(description: FuzzedDescription, type: FuzzedType, scope: Scope) {
3638
if (description.description.isStatic == false && scope.parameterIndex == 0 && scope.recursionDepth == 1) {
@@ -43,14 +45,24 @@ class InjectMockValueProvider(
4345
override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence<Seed<FuzzedType, FuzzedValue>> {
4446
if (description.scope?.getProperty(INJECT_MOCK_FLAG) == null) return emptySequence()
4547
val fields = type.classId.allDeclaredFieldIds.filterNot { it.isStatic && it.isFinal }.toList()
48+
val (nonNullFields, nullableFields) = fields.partition(isFieldNonNull)
4649
return sequenceOf(Seed.Recursive(
47-
construct = Routine.Create(types = fields.map { toFuzzerType(it.jField.genericType, description.typeCache) }) { values ->
50+
construct = Routine.Create(
51+
types = nonNullFields.map { toFuzzerType(it.jField.genericType, description.typeCache) }
52+
) { values ->
4853
emptyFuzzedValue(type.classId).also {
4954
(it.model as UtCompositeModel).fields.putAll(
50-
fields.zip(values).associate { (field, value) -> field to value.model }
55+
nonNullFields.zip(values).associate { (field, value) -> field to value.model }
5156
)
5257
}
5358
},
59+
modify = nullableFields.map { field ->
60+
Routine.Call<FuzzedType, FuzzedValue>(
61+
types = listOf(toFuzzerType(field.jField.genericType, description.typeCache))
62+
) { instance, (value) ->
63+
(instance.model as UtCompositeModel).fields[field] = value.model
64+
}
65+
}.asSequence(),
5466
empty = Routine.Empty { emptyFuzzedValue(type.classId) }
5567
))
5668
}

0 commit comments

Comments
 (0)