Skip to content

Commit de91104

Browse files
Avoid false NPE tests for Spring classes #2034 (#2052)
1 parent 0c25714 commit de91104

File tree

14 files changed

+105
-37
lines changed

14 files changed

+105
-37
lines changed

docs/SpeculativeFieldNonNullability.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,31 @@ is desirable, as it increases the coverage, but it has a downside. It is possibl
1616
most of generated branches would be `NPE` branches, while useful paths could be lost due to timeout.
1717

1818
Beyond that, in many cases the `null` value of a field can't be generated using the public API
19-
of the class. This is particularly true for final fields, especially in system classes.
19+
of the class.
20+
21+
- First of all, this is particularly true for final fields, especially in system classes.
2022
it is also often true for non-public fields from standard library and third-party libraries (even setters often do not
2123
allow `null` values). Automatically generated tests assign `null` values to fields using reflection,
2224
but these tests may be uninformative as the corresponding `NPE` branches would never occur
2325
in the real code that limits itself to the public API.
2426

27+
- After that, field may be declared with some annotation that shows that null value is actually impossible.
28+
For example, in Spring applications `@InjectMocks` and `@Mock` annotations on the fields of class under test
29+
mean that these fields always have value, so `NPE` branches for them would never occur in real code.
30+
31+
2532
## The solution
2633

2734
To discard irrelevant `NPE` branches, we can speculatively mark fields we as non-nullable even they
28-
do not have an explicit `@NotNull` annotation. In particular, we can use this approach to final and non-public
29-
fields of system classes, as they are usually correctly initialized and are not equal `null`.
35+
do not have an explicit `@NotNull` annotation.
36+
37+
- In particular, we can use this approach to final and non-public
38+
fields of system classes, as they are usually correctly initialized and are not equal `null`
39+
- For Spring application, we use this approach for the fields of class
40+
under test not obtained from Spring bean definitions
3041

31-
At the same time, we can't always add the "not null" hard constraint for the field: it would break
42+
At the same time, for non-Spring classes,
43+
we can't always add the "not null" hard constraint for the field: it would break
3244
some special cases like `Optional<T>` class, which uses the `null` value of its final field
3345
as a marker of an empty value.
3446

utbot-framework/src/main/kotlin/org/utbot/engine/util/trusted/TrustedPackages.kt renamed to utbot-framework-api/src/main/kotlin/org/utbot/framework/TrustedPackages.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
package org.utbot.engine.util.trusted
1+
package org.utbot.framework
22

3-
import org.utbot.framework.TrustedLibraries
43
import soot.SootClass
54

65
/**

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

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,15 @@ import kotlin.contracts.ExperimentalContracts
5656
import kotlin.contracts.contract
5757
import org.utbot.common.isAbstract
5858
import org.utbot.common.isStatic
59+
import org.utbot.framework.isFromTrustedLibrary
5960
import org.utbot.framework.plugin.api.TypeReplacementMode.*
61+
import org.utbot.framework.plugin.api.util.allDeclaredFieldIds
62+
import org.utbot.framework.plugin.api.util.fieldId
6063
import org.utbot.framework.plugin.api.util.isSubtypeOf
64+
import org.utbot.framework.plugin.api.util.objectClassId
6165
import org.utbot.framework.plugin.api.util.utContext
6266
import org.utbot.framework.process.OpenModulesContainer
67+
import soot.SootField
6368

6469
const val SYMBOLIC_NULL_ADDR: Int = 0
6570

@@ -1229,6 +1234,26 @@ open class ApplicationContext(
12291234
* if it is guided with some additional information.
12301235
*/
12311236
open fun replaceTypeIfNeeded(type: RefType): ClassId? = null
1237+
1238+
/**
1239+
* Sets the restrictions on speculative not null
1240+
* constraints in current application context.
1241+
*
1242+
* @see docs/SpeculativeFieldNonNullability.md for more information.
1243+
*/
1244+
open fun avoidSpeculativeNotNullChecks(field: SootField): Boolean =
1245+
UtSettings.maximizeCoverageUsingReflection || !field.declaringClass.isFromTrustedLibrary()
1246+
1247+
/**
1248+
* Checks whether accessing [field] (with a method invocation or field access) speculatively
1249+
* cannot produce [NullPointerException] (according to its finality or accessibility).
1250+
*
1251+
* @see docs/SpeculativeFieldNonNullability.md for more information.
1252+
*/
1253+
open fun speculativelyCannotProduceNullPointerException(
1254+
field: SootField,
1255+
classUnderTest: ClassId,
1256+
): Boolean = field.isFinal || !field.isPublic
12321257
}
12331258

12341259
/**
@@ -1244,16 +1269,35 @@ class SpringApplicationContext(
12441269
private val beanQualifiedNames: List<String> = emptyList(),
12451270
private val shouldUseImplementors: Boolean,
12461271
): ApplicationContext(mockInstalled, staticsMockingIsConfigured) {
1247-
1248-
private val springInjectedClasses: List<ClassId> by lazy {
1249-
beanQualifiedNames
1250-
.map { fqn -> utContext.classLoader.loadClass(fqn) }
1251-
.filterNot { it.isAbstract || it.isInterface || it.isLocalClass || it.isMemberClass && !it.isStatic }
1252-
.map { it.id }
1253-
}
12541272

1255-
override val typeReplacementMode: TypeReplacementMode
1256-
get() = if (shouldUseImplementors) KnownImplementor else NoImplementors
1273+
private var areInjectedClassesInitialized : Boolean = false
1274+
1275+
// Classes representing concrete types that are actually used in Spring application
1276+
private val springInjectedClasses: Set<ClassId>
1277+
get() {
1278+
if (!areInjectedClassesInitialized) {
1279+
springInjectedClassesStorage += beanQualifiedNames
1280+
.map { fqn -> utContext.classLoader.loadClass(fqn) }
1281+
.filterNot { it.isAbstract || it.isInterface || it.isLocalClass || it.isMemberClass && !it.isStatic }
1282+
.mapTo(mutableSetOf()) { it.id }
1283+
1284+
// This is done to be sure that this storage is not empty after the first class loading iteration.
1285+
// So, even if all loaded classes were filtered out, we will not try to load them again.
1286+
areInjectedClassesInitialized = true
1287+
}
1288+
1289+
return springInjectedClassesStorage
1290+
}
1291+
1292+
// This is a service field to model the lazy behavior of [springInjectedClasses].
1293+
// Do not call it outside the getter.
1294+
//
1295+
// Actually, we should just call [springInjectedClasses] with `by lazy`, but we had problems
1296+
// with a strange `kotlin.UNINITIALIZED_VALUE` in `speculativelyCannotProduceNullPointerException` method call.
1297+
private val springInjectedClassesStorage = mutableSetOf<ClassId>()
1298+
1299+
override val typeReplacementMode: TypeReplacementMode =
1300+
if (shouldUseImplementors) KnownImplementor else NoImplementors
12571301

12581302
/**
12591303
* Replaces an interface type with its implementor type
@@ -1265,6 +1309,20 @@ class SpringApplicationContext(
12651309
} else {
12661310
null
12671311
}
1312+
1313+
override fun avoidSpeculativeNotNullChecks(field: SootField): Boolean = false
1314+
1315+
/**
1316+
* In Spring applications we can mark as speculatively not null
1317+
* fields if they are mocked and injecting into class under test so on.
1318+
*
1319+
* Fields are not mocked if their actual type is obtained from [springInjectedClasses].
1320+
*
1321+
*/
1322+
override fun speculativelyCannotProduceNullPointerException(
1323+
field: SootField,
1324+
classUnderTest: ClassId,
1325+
): Boolean = field.fieldId in classUnderTest.allDeclaredFieldIds && field.declaringClass.id !in springInjectedClasses
12681326
}
12691327

12701328
val RefType.isAbstractType

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import org.utbot.framework.plugin.api.MethodId
1111
import org.utbot.framework.plugin.api.UtModel
1212
import org.utbot.framework.plugin.api.UtNullModel
1313
import org.utbot.framework.plugin.api.UtPrimitiveModel
14+
import org.utbot.framework.plugin.api.id
15+
import soot.SootField
1416
import java.lang.reflect.Constructor
1517
import java.lang.reflect.Executable
1618
import java.lang.reflect.Field
@@ -451,6 +453,9 @@ val ClassId.allDeclaredFieldIds: Sequence<FieldId>
451453
.flatMap { it.declaredFields.asSequence() }
452454
.map { it.fieldId }
453455

456+
val SootField.fieldId: FieldId
457+
get() = FieldId(declaringClass.id, name)
458+
454459
// FieldId utils
455460
val FieldId.safeJField: Field?
456461
get() = declaringClass.jClass.declaredFields.firstOrNull { it.name == name }

utbot-framework/src/main/kotlin/org/utbot/engine/ArrayObjectWrappers.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.utbot.framework.plugin.api.UtModel
2828
import org.utbot.framework.plugin.api.UtNullModel
2929
import org.utbot.framework.plugin.api.UtPrimitiveModel
3030
import org.utbot.framework.plugin.api.getIdOrThrow
31+
import org.utbot.framework.plugin.api.util.fieldId
3132
import org.utbot.framework.plugin.api.util.id
3233
import org.utbot.framework.plugin.api.util.objectArrayClassId
3334
import org.utbot.framework.plugin.api.util.objectClassId

utbot-framework/src/main/kotlin/org/utbot/engine/CollectionIteratorWrappers.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.utbot.framework.plugin.api.UtAssembleModel
1111
import org.utbot.framework.plugin.api.UtExecutableCallModel
1212
import org.utbot.framework.plugin.api.UtModel
1313
import org.utbot.framework.plugin.api.UtReferenceModel
14+
import org.utbot.framework.plugin.api.util.fieldId
1415
import org.utbot.framework.plugin.api.util.id
1516
import org.utbot.framework.plugin.api.util.methodId
1617
import soot.SootClass

utbot-framework/src/main/kotlin/org/utbot/engine/CollectionWrappers.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.utbot.framework.util.graph
3131
import org.utbot.framework.plugin.api.id
3232
import org.utbot.framework.plugin.api.util.booleanClassId
3333
import org.utbot.framework.plugin.api.util.constructorId
34+
import org.utbot.framework.plugin.api.util.fieldId
3435
import org.utbot.framework.plugin.api.util.id
3536
import org.utbot.framework.plugin.api.util.methodId
3637
import org.utbot.framework.plugin.api.util.objectClassId

utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,6 @@ val JimpleLocal.variable: LocalVariable
285285
val Type.defaultSymValue: UtExpression
286286
get() = toSort().defaultValue
287287

288-
val SootField.fieldId: FieldId
289-
get() = FieldId(declaringClass.id, name)
290-
291288
val SootField.isEnumConstant: Boolean
292289
get() = name in declaringClass.id.enumConstants.orEmpty().map { enum -> enum.name }
293290

utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import org.utbot.engine.types.STRING_TYPE
4141
import org.utbot.engine.types.SeqType
4242
import org.utbot.engine.types.TypeResolver
4343
import org.utbot.framework.plugin.api.id
44+
import org.utbot.framework.plugin.api.util.fieldId
4445
import org.utbot.framework.plugin.api.util.isEnum
4546
import soot.ArrayType
4647
import soot.CharType

utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import org.utbot.framework.UtSettings
104104
import org.utbot.framework.plugin.api.visible.UtStreamConsumingException
105105
import org.utbot.framework.plugin.api.UtStreamConsumingFailure
106106
import org.utbot.framework.plugin.api.util.constructor.ValueConstructor
107+
import org.utbot.framework.plugin.api.util.fieldId
107108
import org.utbot.framework.plugin.api.util.isStatic
108109

109110
// hack

utbot-framework/src/main/kotlin/org/utbot/engine/ThreadWrappers.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.utbot.framework.plugin.api.UtNullModel
2323
import org.utbot.framework.plugin.api.id
2424
import org.utbot.framework.plugin.api.util.constructorId
2525
import org.utbot.framework.plugin.api.util.defaultValueModel
26+
import org.utbot.framework.plugin.api.util.fieldId
2627
import org.utbot.framework.plugin.api.util.intClassId
2728
import org.utbot.framework.plugin.api.util.objectClassId
2829
import org.utbot.framework.plugin.api.util.stringClassId

utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,16 @@ import org.utbot.engine.types.OBJECT_TYPE
105105
import org.utbot.engine.types.SECURITY_FIELD_SIGNATURE
106106
import org.utbot.engine.types.TypeRegistry
107107
import org.utbot.engine.types.TypeResolver
108-
import org.utbot.engine.util.trusted.isFromTrustedLibrary
109108
import org.utbot.engine.util.statics.concrete.associateEnumSootFieldsWithConcreteValues
110109
import org.utbot.engine.util.statics.concrete.isEnumAffectingExternalStatics
111110
import org.utbot.engine.util.statics.concrete.isEnumValuesFieldName
112111
import org.utbot.engine.util.statics.concrete.makeEnumNonStaticFieldsUpdates
113112
import org.utbot.engine.util.statics.concrete.makeEnumStaticFieldsUpdates
114113
import org.utbot.engine.util.statics.concrete.makeSymbolicValuesFromEnumConcreteValues
115114
import org.utbot.framework.UtSettings
116-
import org.utbot.framework.UtSettings.maximizeCoverageUsingReflection
117115
import org.utbot.framework.UtSettings.preferredCexOption
118116
import org.utbot.framework.UtSettings.substituteStaticsWithSymbolicVariable
117+
import org.utbot.framework.isFromTrustedLibrary
119118
import org.utbot.framework.plugin.api.ApplicationContext
120119
import org.utbot.framework.plugin.api.ClassId
121120
import org.utbot.framework.plugin.api.ExecutableId
@@ -128,6 +127,7 @@ import org.utbot.framework.plugin.api.classId
128127
import org.utbot.framework.plugin.api.id
129128
import org.utbot.framework.plugin.api.isAbstractType
130129
import org.utbot.framework.plugin.api.util.executable
130+
import org.utbot.framework.plugin.api.util.fieldId
131131
import org.utbot.framework.plugin.api.util.findFieldByIdOrNull
132132
import org.utbot.framework.plugin.api.util.jField
133133
import org.utbot.framework.plugin.api.util.jClass
@@ -2322,30 +2322,20 @@ class Traverser(
23222322
}
23232323

23242324
/**
2325-
* Marks the [createdField] as speculatively not null if the [field] is considering as
2326-
* not producing [NullPointerException].
2325+
* Marks the [createdField] as speculatively not null if the [field] is considering
2326+
* as not producing [NullPointerException].
23272327
*
2328-
* @see [SootField.speculativelyCannotProduceNullPointerException], [markAsSpeculativelyNotNull], [isFromTrustedLibrary].
2328+
* See more detailed documentation in [ApplicationContext] mentioned methods.
23292329
*/
23302330
private fun checkAndMarkLibraryFieldSpeculativelyNotNull(field: SootField, createdField: SymbolicValue) {
2331-
if (maximizeCoverageUsingReflection || !field.declaringClass.isFromTrustedLibrary()) {
2331+
if (applicationContext.avoidSpeculativeNotNullChecks(field) ||
2332+
!applicationContext.speculativelyCannotProduceNullPointerException(field, methodUnderTest.classId)) {
23322333
return
23332334
}
23342335

2335-
if (field.speculativelyCannotProduceNullPointerException()) {
2336-
markAsSpeculativelyNotNull(createdField.addr)
2337-
}
2336+
markAsSpeculativelyNotNull(createdField.addr)
23382337
}
23392338

2340-
/**
2341-
* Checks whether accessing [this] field (with a method invocation or field access) speculatively can produce
2342-
* [NullPointerException] (according to its finality or accessibility).
2343-
*
2344-
* @see docs/SpeculativeFieldNonNullability.md for more information.
2345-
*/
2346-
@Suppress("KDocUnresolvedReference")
2347-
private fun SootField.speculativelyCannotProduceNullPointerException(): Boolean = isFinal || !isPublic
2348-
23492339
private fun createArray(pName: String, type: ArrayType): ArrayValue {
23502340
val addr = UtAddrExpression(mkBVConst(pName, UtIntSort))
23512341
return createArray(addr, type, useConcreteType = false)

utbot-framework/src/main/kotlin/org/utbot/engine/util/statics/concrete/EnumConcreteUtils.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.utbot.engine.symbolic.SymbolicStateUpdate
1111
import org.utbot.engine.symbolic.asHardConstraint
1212
import org.utbot.engine.types.TypeResolver
1313
import org.utbot.framework.plugin.api.FieldId
14+
import org.utbot.framework.plugin.api.util.fieldId
1415
import org.utbot.framework.plugin.api.util.jField
1516
import soot.SootClass
1617
import soot.SootField

utbot-framework/src/main/kotlin/org/utbot/framework/modifications/ExecutablesAnalyzer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package org.utbot.framework.modifications
22

3-
import org.utbot.engine.fieldId
43
import org.utbot.framework.plugin.api.ClassId
54
import org.utbot.framework.plugin.api.ExecutableId
65
import org.utbot.framework.plugin.api.FieldId
76
import org.utbot.framework.plugin.api.id
7+
import org.utbot.framework.plugin.api.util.fieldId
88
import org.utbot.framework.util.executableId
99
import soot.Scene
1010
import soot.SootMethod

0 commit comments

Comments
 (0)