Skip to content

Commit 7242bb5

Browse files
committed
Test generation for Optional<T> in the plugin (#226)
Added a check for overridden classes in `shouldMock` to avoid access to engine classes that are not available in the plugin. Implemented more accurate speculative marking of final fields as not null to avoid losing paths involving `Optional.empty()`, to enable NPE checks for final fields in user code, and to avoid generating non-informative NPE tests for final fields in system classes. UtSettings.checkNpeForFinalFields is now set default (false) in `AbstractTestCaseGEneratorTest` and `SummaryTestCaseGeneratorTest`.
1 parent 576c9a6 commit 7242bb5

File tree

9 files changed

+120
-19
lines changed

9 files changed

+120
-19
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Speculative field non-nullability assumptions
2+
3+
## The problem
4+
5+
When a field is used as a base for a dot call (i.e., a method call or a field access),
6+
the symbolic engine creates a branch corresponding to the potential `NullPointerException`
7+
that can occur if the value of the field is `null`. For this path, the engine generates
8+
the hard constraint `addr(field) == null`.
9+
10+
If the field is marked as `@NotNull`, a hard constraint `addr(field) != null` is generated
11+
for it. If both constraints have been generated simultaneously, the `NPE` branch is discarded
12+
as the constraint set is unsatisfiable.
13+
14+
If a field does not have `@NotNull` annotation, the `NPE` branch will be kept. This behavior
15+
is desirable, as it increases the coverage, but it has a downside. It is possible that
16+
most of generated branches would be `NPE` branches, while useful paths could be lost due to timeout.
17+
18+
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.
20+
Automatically generated tests assign `null` values to fields in questions using reflection,
21+
but these tests may be uninformative as the corresponding `NPE` branches would never occur
22+
in the real code that limits itself to the public API.
23+
24+
## The solution
25+
26+
To discard irrelevant `NPE` branches, we can speculatively mark fields we as non-nullable even they
27+
do not have an explicit `@NotNull` annotation. In particular, we can use this approach to final
28+
fields of system classes, as they are usually correctly initialized and are not equal `null`.
29+
30+
At the same time, we can't always add the "not null" hard constraint for the field: it would break
31+
some special cases like `Optional<T>` class, which uses the `null` value of its final field
32+
as a marker of an empty value.
33+
34+
The engine checks for NPE and creates an NPE branch every time the address is used
35+
as a base of a dot call (i.e., a method call or a field access);
36+
see `UtBotSymbolicEngine.nullPointerExceptionCheck`). The problem is what at that moment, we would have
37+
no way to check whether the address corresponds to a final field, as the corresponding node
38+
of the global graph would refer to a local variable. The only place where we have the complete
39+
information about the field is this method.
40+
41+
We use the following approach. If the field is final and belongs to a system class,
42+
we mark it as a speculatively non-nullable in the memory
43+
(see `org.utbot.engine.Memory.speculativelyNotNullAddresses`). During the NPE check
44+
we will add the `!isSpeculativelyNotNull(addr(field))` constraint
45+
to the `NPE` branch together with the usual `addr(field) == null` constraint.
46+
47+
For final fields, these two conditions can't be satisfied at the same time, as we speculatively
48+
mark final fields as non-nullable. As a result, the NPE branch would be discarded. If a field
49+
is not final, the condition is satisfiable, so the NPE branch would stay alive.
50+
51+
We limit this approach to the system classes only, because it is hard to speculatively assume
52+
something about non-nullability of final fields in the user code.
53+
54+
The same approach can be extended for other cases where we want to speculatively consider some
55+
fields as non-nullable to prevent `NPE` branch generation.

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import org.utbot.engine.pc.UtSeqSort
1818
import org.utbot.engine.pc.UtShortSort
1919
import org.utbot.engine.pc.UtSolverStatusKind
2020
import org.utbot.engine.pc.UtSolverStatusSAT
21-
import org.utbot.engine.symbolic.Assumption
2221
import org.utbot.engine.pc.UtSort
2322
import org.utbot.engine.pc.mkArrayWithConst
2423
import org.utbot.engine.pc.mkBool
@@ -31,7 +30,6 @@ import org.utbot.engine.pc.mkLong
3130
import org.utbot.engine.pc.mkShort
3231
import org.utbot.engine.pc.mkString
3332
import org.utbot.engine.pc.toSort
34-
import org.utbot.framework.UtSettings.checkNpeForFinalFields
3533
import org.utbot.framework.UtSettings.checkNpeInNestedMethods
3634
import org.utbot.framework.UtSettings.checkNpeInNestedNotPrivateMethods
3735
import org.utbot.framework.plugin.api.FieldId
@@ -384,11 +382,7 @@ fun arrayTypeUpdate(addr: UtAddrExpression, type: ArrayType) =
384382
fun SootField.shouldBeNotNull(): Boolean {
385383
require(type is RefLikeType)
386384

387-
if (hasNotNullAnnotation()) return true
388-
389-
if (!checkNpeForFinalFields && isFinal) return true
390-
391-
return false
385+
return hasNotNullAnnotation()
392386
}
393387

394388
/**

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,17 @@ data class Memory( // TODO: split purely symbolic memory and information about s
137137
UtFalse,
138138
UtArraySort(UtAddrSort, UtBoolSort)
139139
),
140-
private val instanceFieldReadOperations: PersistentSet<InstanceFieldReadOperation> = persistentHashSetOf()
140+
private val instanceFieldReadOperations: PersistentSet<InstanceFieldReadOperation> = persistentHashSetOf(),
141+
142+
/**
143+
* Storage for addresses that we speculatively consider non-nullable (e.g., final fields of system classes).
144+
* See [org.utbot.engine.UtBotSymbolicEngine.createFieldOrMock] for usage,
145+
* and [docs/SpeculativeFieldNonNullability.md] for details.
146+
*/
147+
private val speculativelyNotNullAddresses: UtArrayExpressionBase = UtConstArrayExpression(
148+
UtFalse,
149+
UtArraySort(UtAddrSort, UtBoolSort)
150+
)
141151
) {
142152
val chunkIds: Set<ChunkId>
143153
get() = initial.keys
@@ -167,6 +177,11 @@ data class Memory( // TODO: split purely symbolic memory and information about s
167177
*/
168178
fun isTouched(addr: UtAddrExpression): UtArraySelectExpression = touchedAddresses.select(addr)
169179

180+
/**
181+
* Returns symbolic information about whether [addr] corresponds to a final field known to be not null.
182+
*/
183+
fun isSpeculativelyNotNull(addr: UtAddrExpression): UtArraySelectExpression = speculativelyNotNullAddresses.select(addr)
184+
170185
/**
171186
* @return ImmutableCollection of the initial values for all the arrays we touched during the execution
172187
*/
@@ -260,6 +275,10 @@ data class Memory( // TODO: split purely symbolic memory and information about s
260275
acc.store(addr, UtTrue)
261276
}
262277

278+
val updSpeculativelyNotNullAddresses = update.speculativelyNotNullAddresses.fold(speculativelyNotNullAddresses) { acc, addr ->
279+
acc.store(addr, UtTrue)
280+
}
281+
263282
return this.copy(
264283
initial = updInitial,
265284
current = updCurrent,
@@ -275,7 +294,8 @@ data class Memory( // TODO: split purely symbolic memory and information about s
275294
updates = updates + update,
276295
visitedValues = updVisitedValues,
277296
touchedAddresses = updTouchedAddresses,
278-
instanceFieldReadOperations = instanceFieldReadOperations.addAll(update.instanceFieldReads)
297+
instanceFieldReadOperations = instanceFieldReadOperations.addAll(update.instanceFieldReads),
298+
speculativelyNotNullAddresses = updSpeculativelyNotNullAddresses
279299
)
280300
}
281301

@@ -955,7 +975,8 @@ data class MemoryUpdate(
955975
val visitedValues: PersistentList<UtAddrExpression> = persistentListOf(),
956976
val touchedAddresses: PersistentList<UtAddrExpression> = persistentListOf(),
957977
val classIdToClearStatics: ClassId? = null,
958-
val instanceFieldReads: PersistentSet<InstanceFieldReadOperation> = persistentHashSetOf()
978+
val instanceFieldReads: PersistentSet<InstanceFieldReadOperation> = persistentHashSetOf(),
979+
val speculativelyNotNullAddresses: PersistentList<UtAddrExpression> = persistentListOf()
959980
) {
960981
operator fun plus(other: MemoryUpdate) =
961982
this.copy(
@@ -972,7 +993,8 @@ data class MemoryUpdate(
972993
visitedValues = visitedValues.addAll(other.visitedValues),
973994
touchedAddresses = touchedAddresses.addAll(other.touchedAddresses),
974995
classIdToClearStatics = other.classIdToClearStatics,
975-
instanceFieldReads = instanceFieldReads.addAll(other.instanceFieldReads)
996+
instanceFieldReads = instanceFieldReads.addAll(other.instanceFieldReads),
997+
speculativelyNotNullAddresses = speculativelyNotNullAddresses.addAll(other.speculativelyNotNullAddresses),
976998
)
977999
}
9781000

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,13 @@ class Mocker(
184184
if (mockAlways(type)) return true // always mock randoms and loggers
185185
if (mockInfo is UtFieldMockInfo) {
186186
val declaringClass = mockInfo.fieldId.declaringClass
187+
val sootDeclaringClass = Scene.v().getSootClass(declaringClass.name)
187188

188-
if (Scene.v().getSootClass(declaringClass.name).isArtificialEntity) {
189-
return false // see BaseStreamExample::minExample for an example; cannot load java class for such class
189+
if (sootDeclaringClass.isArtificialEntity || sootDeclaringClass.isOverridden) {
190+
// Cannot load Java class for artificial classes, see BaseStreamExample::minExample for an example.
191+
// Wrapper classes that override system classes ([org.utbot.engine.overrides] package) are also
192+
// unavailable to the [UtContext] class loader used by the plugin.
193+
return false
190194
}
191195

192196
return when {

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import org.utbot.engine.pc.mkBVConst
7777
import org.utbot.engine.pc.mkBoolConst
7878
import org.utbot.engine.pc.mkChar
7979
import org.utbot.engine.pc.mkEq
80+
import org.utbot.engine.pc.mkFalse
8081
import org.utbot.engine.pc.mkFpConst
8182
import org.utbot.engine.pc.mkInt
8283
import org.utbot.engine.pc.mkNot
@@ -115,6 +116,7 @@ import org.utbot.engine.util.statics.concrete.makeEnumStaticFieldsUpdates
115116
import org.utbot.engine.util.statics.concrete.makeSymbolicValuesFromEnumConcreteValues
116117
import org.utbot.framework.PathSelectorType
117118
import org.utbot.framework.UtSettings
119+
import org.utbot.framework.UtSettings.checkNpeForFinalFields
118120
import org.utbot.framework.UtSettings.checkSolverTimeoutMillis
119121
import org.utbot.framework.UtSettings.enableFeatureProcess
120122
import org.utbot.framework.UtSettings.pathSelectorStepsLimit
@@ -2199,8 +2201,15 @@ class UtBotSymbolicEngine(
21992201
val chunkId = hierarchy.chunkIdForField(objectType, field)
22002202
val createdField = createField(objectType, addr, field.type, chunkId, mockInfoGenerator)
22012203

2202-
if (field.type is RefLikeType && field.shouldBeNotNull()) {
2203-
queuedSymbolicStateUpdates += mkNot(mkEq(createdField.addr, nullObjectAddr)).asHardConstraint()
2204+
if (field.type is RefLikeType) {
2205+
if (field.shouldBeNotNull()) {
2206+
queuedSymbolicStateUpdates += mkNot(mkEq(createdField.addr, nullObjectAddr)).asHardConstraint()
2207+
}
2208+
2209+
// See docs/SpeculativeFieldNonNullability.md for details
2210+
if (field.isFinal && field.declaringClass.isLibraryClass && !checkNpeForFinalFields) {
2211+
markAsSpeculativelyNotNull(createdField.addr)
2212+
}
22042213
}
22052214

22062215
return createdField
@@ -2370,6 +2379,10 @@ class UtBotSymbolicEngine(
23702379
queuedSymbolicStateUpdates += MemoryUpdate(touchedAddresses = persistentListOf(addr))
23712380
}
23722381

2382+
private fun markAsSpeculativelyNotNull(addr: UtAddrExpression) {
2383+
queuedSymbolicStateUpdates += MemoryUpdate(speculativelyNotNullAddresses = persistentListOf(addr))
2384+
}
2385+
23732386
/**
23742387
* Add a memory update to reflect that a field was read.
23752388
*
@@ -3290,9 +3303,11 @@ class UtBotSymbolicEngine(
32903303
private fun nullPointerExceptionCheck(addr: UtAddrExpression) {
32913304
val canBeNull = addrEq(addr, nullObjectAddr)
32923305
val canNotBeNull = mkNot(canBeNull)
3306+
val notMarked = mkEq(memory.isSpeculativelyNotNull(addr), mkFalse())
3307+
val notMarkedAndNull = mkAnd(notMarked, canBeNull)
32933308

32943309
if (environment.method.checkForNPE(environment.state.executionStack.size)) {
3295-
implicitlyThrowException(NullPointerException(), setOf(canBeNull))
3310+
implicitlyThrowException(NullPointerException(), setOf(notMarkedAndNull))
32963311
}
32973312

32983313
queuedSymbolicStateUpdates += canNotBeNull.asHardConstraint()

utbot-framework/src/test/kotlin/org/utbot/examples/AbstractTestCaseGeneratorTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ abstract class AbstractTestCaseGeneratorTest(
8888
UtSettings.checkSolverTimeoutMillis = 0
8989
UtSettings.checkNpeInNestedMethods = true
9090
UtSettings.checkNpeInNestedNotPrivateMethods = true
91-
UtSettings.checkNpeForFinalFields = true
9291
UtSettings.substituteStaticsWithSymbolicVariable = true
9392
UtSettings.useAssembleModelGenerator = true
9493
UtSettings.saveRemainingStatesForConcreteExecution = false

utbot-framework/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,4 +482,14 @@ class OptionalsTest : AbstractTestCaseGeneratorTest(
482482
coverage = DoNotCalculate
483483
)
484484
}
485+
486+
@Test
487+
fun testOptionalOfPositive() {
488+
check(
489+
Optionals::optionalOfPositive,
490+
eq(2),
491+
{ value, result -> value > 0 && result != null && result.isPresent && result.get() == value },
492+
{ value, result -> value <= 0 && result != null && !result.isPresent }
493+
)
494+
}
485495
}

utbot-sample/src/main/java/org/utbot/examples/collections/Optionals.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,8 @@ public boolean equalOptionalsDouble(OptionalDouble left, OptionalDouble right) {
314314
return false;
315315
}
316316
}
317+
318+
public Optional<Integer> optionalOfPositive(int value) {
319+
return value > 0 ? Optional.of(value) : Optional.empty();
320+
}
317321
}

utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import org.utbot.common.workaround
66
import org.utbot.examples.AbstractTestCaseGeneratorTest
77
import org.utbot.examples.CoverageMatcher
88
import org.utbot.examples.DoNotCalculate
9-
import org.utbot.framework.UtSettings.checkNpeForFinalFields
109
import org.utbot.framework.UtSettings.checkNpeInNestedMethods
1110
import org.utbot.framework.UtSettings.checkNpeInNestedNotPrivateMethods
1211
import org.utbot.framework.UtSettings.checkSolverTimeoutMillis
@@ -96,7 +95,6 @@ open class SummaryTestCaseGeneratorTest(
9695
checkSolverTimeoutMillis = 0
9796
checkNpeInNestedMethods = true
9897
checkNpeInNestedNotPrivateMethods = true
99-
checkNpeForFinalFields = true
10098
}
10199
val utMethod = UtMethod.from(method)
102100
val testCase = executionsModel(utMethod, mockStrategy)

0 commit comments

Comments
 (0)