Skip to content

Commit fe0f7f8

Browse files
authored
Disabled NPE checks for non-public library fields by default (#353)
1 parent d1b51fe commit fe0f7f8

File tree

9 files changed

+219
-18
lines changed

9 files changed

+219
-18
lines changed

docs/SpeculativeFieldNonNullability.md

+10-9
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ most of generated branches would be `NPE` branches, while useful paths could be
1717

1818
Beyond that, in many cases the `null` value of a field can't be generated using the public API
1919
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,
20+
it is also often true for non-public fields from standard library and third-party libraries (even setters often do not
21+
allow `null` values). Automatically generated tests assign `null` values to fields using reflection,
2122
but these tests may be uninformative as the corresponding `NPE` branches would never occur
2223
in the real code that limits itself to the public API.
2324

2425
## The solution
2526

2627
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+
do not have an explicit `@NotNull` annotation. In particular, we can use this approach to final and non-public
2829
fields of system classes, as they are usually correctly initialized and are not equal `null`.
2930

3031
At the same time, we can't always add the "not null" hard constraint for the field: it would break
@@ -38,18 +39,18 @@ no way to check whether the address corresponds to a final field, as the corresp
3839
of the global graph would refer to a local variable. The only place where we have the complete
3940
information about the field is this method.
4041

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
42+
We use the following approach. If the field belongs to a library class (according to `soot.SootClass.isLibraryClass`)
43+
and is final or non-public, we mark it as a speculatively non-nullable in the memory
4344
(see `org.utbot.engine.Memory.speculativelyNotNullAddresses`). During the NPE check
4445
we will add the `!isSpeculativelyNotNull(addr(field))` constraint
4546
to the `NPE` branch together with the usual `addr(field) == null` constraint.
4647

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.
48+
For final/non-public fields, these two conditions can't be satisfied at the same time, as we speculatively
49+
mark such fields as non-nullable. As a result, the NPE branch would be discarded. If a field
50+
is public or not final, the condition is satisfiable, so the NPE branch would stay alive.
5051

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.
52+
We limit this approach to the library classes only, because it is hard to speculatively assume
53+
something about non-nullability of final/non-public fields in the user code.
5354

5455
The same approach can be extended for other cases where we want to speculatively consider some
5556
fields as non-nullable to prevent `NPE` branch generation.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.utbot.framework
2+
3+
import mu.KotlinLogging
4+
import org.utbot.common.PathUtil.toPath
5+
import java.io.IOException
6+
7+
private val logger = KotlinLogging.logger {}
8+
9+
private val defaultUserTrustedLibrariesPath: String = "${utbotHomePath}/trustedLibraries.txt"
10+
private const val userTrustedLibrariesKey: String = "utbot.settings.trusted.libraries.path"
11+
12+
object TrustedLibraries {
13+
/**
14+
* Always "trust" JDK.
15+
*/
16+
private val defaultTrustedLibraries: List<String> = listOf(
17+
"java",
18+
"sun",
19+
"javax",
20+
"com.sun",
21+
"org.omg",
22+
"org.xml",
23+
"org.w3c.dom",
24+
)
25+
26+
private val userTrustedLibraries: List<String>
27+
get() {
28+
val userTrustedLibrariesPath = System.getProperty(userTrustedLibrariesKey) ?: defaultUserTrustedLibrariesPath
29+
val userTrustedLibrariesFile = userTrustedLibrariesPath.toPath().toFile()
30+
31+
if (!userTrustedLibrariesFile.exists()) {
32+
return emptyList()
33+
}
34+
35+
return try {
36+
userTrustedLibrariesFile.readLines()
37+
} catch (e: IOException) {
38+
logger.info { e.message }
39+
40+
emptyList()
41+
}
42+
}
43+
44+
/**
45+
* Represents prefixes of packages for trusted libraries -
46+
* as the union of [defaultTrustedLibraries] and [userTrustedLibraries].
47+
*/
48+
val trustedLibraries: Set<String> by lazy { (defaultTrustedLibraries + userTrustedLibraries).toSet() }
49+
}

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

+18-4
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ import kotlin.reflect.KProperty
1010

1111
private val logger = KotlinLogging.logger {}
1212

13+
/**
14+
* Path to the utbot home folder.
15+
*/
16+
internal val utbotHomePath = "${System.getProperty("user.home")}/.utbot"
17+
1318
/**
1419
* Default path for properties file
1520
*/
16-
internal val defaultSettingsPath = "${System.getProperty("user.home")}/.utbot/settings.properties"
17-
internal const val defaultKeyForSettingsPath = "utbot.settings.path"
21+
private val defaultSettingsPath = "$utbotHomePath/settings.properties"
22+
private const val defaultKeyForSettingsPath = "utbot.settings.path"
1823

1924
internal class SettingDelegate<T>(val initializer: () -> T) {
2025
private var value = initializer()
@@ -176,13 +181,22 @@ object UtSettings {
176181
var enableMachineLearningModule by getBooleanProperty(true)
177182

178183
/**
179-
* Options below regulate which NullPointerExceptions check should be performed.
184+
* Options below regulate which [NullPointerException] check should be performed.
180185
*
181186
* Set an option in true if you want to perform NPE check in the corresponding situations, otherwise set false.
182187
*/
183188
var checkNpeInNestedMethods by getBooleanProperty(true)
184189
var checkNpeInNestedNotPrivateMethods by getBooleanProperty(false)
185-
var checkNpeForFinalFields by getBooleanProperty(false)
190+
191+
/**
192+
* This option determines whether we should generate [NullPointerException] checks for final or non-public fields
193+
* in non-application classes. Set by true, this option highly decreases test's readability in some cases
194+
* because of using reflection API for setting final/non-public fields in non-application classes.
195+
*
196+
* NOTE: default false value loses some executions with NPE in system classes, but often most of these executions
197+
* are not expected by user.
198+
*/
199+
var maximizeCoverageUsingReflection by getBooleanProperty(false)
186200

187201
/**
188202
* Activate or deactivate substituting static fields values set in static initializer

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

+35-5
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import org.utbot.engine.symbolic.asHardConstraint
106106
import org.utbot.engine.symbolic.asSoftConstraint
107107
import org.utbot.engine.symbolic.asAssumption
108108
import org.utbot.engine.symbolic.asUpdate
109+
import org.utbot.engine.util.trusted.isFromTrustedLibrary
109110
import org.utbot.engine.util.mockListeners.MockListener
110111
import org.utbot.engine.util.mockListeners.MockListenerController
111112
import org.utbot.engine.util.statics.concrete.associateEnumSootFieldsWithConcreteValues
@@ -116,7 +117,7 @@ import org.utbot.engine.util.statics.concrete.makeEnumStaticFieldsUpdates
116117
import org.utbot.engine.util.statics.concrete.makeSymbolicValuesFromEnumConcreteValues
117118
import org.utbot.framework.PathSelectorType
118119
import org.utbot.framework.UtSettings
119-
import org.utbot.framework.UtSettings.checkNpeForFinalFields
120+
import org.utbot.framework.UtSettings.maximizeCoverageUsingReflection
120121
import org.utbot.framework.UtSettings.checkSolverTimeoutMillis
121122
import org.utbot.framework.UtSettings.enableFeatureProcess
122123
import org.utbot.framework.UtSettings.pathSelectorStepsLimit
@@ -339,7 +340,13 @@ class UtBotSymbolicEngine(
339340

340341
private val classUnderTest: ClassId = methodUnderTest.clazz.id
341342

342-
private val mocker: Mocker = Mocker(mockStrategy, classUnderTest, hierarchy, chosenClassesToMockAlways, MockListenerController(controller))
343+
private val mocker: Mocker = Mocker(
344+
mockStrategy,
345+
classUnderTest,
346+
hierarchy,
347+
chosenClassesToMockAlways,
348+
MockListenerController(controller)
349+
)
343350

344351
private val statesForConcreteExecution: MutableList<ExecutionState> = mutableListOf()
345352

@@ -2250,14 +2257,37 @@ class UtBotSymbolicEngine(
22502257
}
22512258

22522259
// See docs/SpeculativeFieldNonNullability.md for details
2253-
if (field.isFinal && field.declaringClass.isLibraryClass && !checkNpeForFinalFields) {
2254-
markAsSpeculativelyNotNull(createdField.addr)
2255-
}
2260+
checkAndMarkLibraryFieldSpeculativelyNotNull(field, createdField)
22562261
}
22572262

22582263
return createdField
22592264
}
22602265

2266+
/**
2267+
* Marks the [createdField] as speculatively not null if the [field] is considering as
2268+
* not producing [NullPointerException].
2269+
*
2270+
* @see [SootField.speculativelyCannotProduceNullPointerException], [markAsSpeculativelyNotNull], [isFromTrustedLibrary].
2271+
*/
2272+
private fun checkAndMarkLibraryFieldSpeculativelyNotNull(field: SootField, createdField: SymbolicValue) {
2273+
if (maximizeCoverageUsingReflection || !field.declaringClass.isFromTrustedLibrary()) {
2274+
return
2275+
}
2276+
2277+
if (field.speculativelyCannotProduceNullPointerException()) {
2278+
markAsSpeculativelyNotNull(createdField.addr)
2279+
}
2280+
}
2281+
2282+
/**
2283+
* Checks whether accessing [this] field (with a method invocation or field access) speculatively can produce
2284+
* [NullPointerException] (according to its finality or accessibility).
2285+
*
2286+
* @see docs/SpeculativeFieldNonNullability.md for more information.
2287+
*/
2288+
@Suppress("KDocUnresolvedReference")
2289+
private fun SootField.speculativelyCannotProduceNullPointerException(): Boolean = isFinal || !isPublic
2290+
22612291
private fun createArray(pName: String, type: ArrayType): ArrayValue {
22622292
val addr = UtAddrExpression(mkBVConst(pName, UtIntSort))
22632293
return createArray(addr, type, useConcreteType = false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.utbot.engine.util.trusted
2+
3+
import org.utbot.framework.TrustedLibraries
4+
import soot.SootClass
5+
6+
/**
7+
* Cache for already discovered trusted/untrusted packages.
8+
*/
9+
private val isPackageTrusted: MutableMap<String, Boolean> = mutableMapOf()
10+
11+
/**
12+
* Determines whether [this] class is from trusted libraries as defined in [TrustedLibraries].
13+
*/
14+
fun SootClass.isFromTrustedLibrary(): Boolean {
15+
isPackageTrusted[packageName]?.let {
16+
return it
17+
}
18+
19+
val isTrusted = TrustedLibraries.trustedLibraries.any { packageName.startsWith(it, ignoreCase = false) }
20+
21+
return isTrusted.also { isPackageTrusted[packageName] = it }
22+
}

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

+10
Original file line numberDiff line numberDiff line change
@@ -2826,3 +2826,13 @@ inline fun <reified T> withFeaturePath(featurePath: String, block: () -> T): T {
28262826
UtSettings.enableFeatureProcess = prevEnableFeatureProcess
28272827
}
28282828
}
2829+
2830+
inline fun <reified T> withUsingReflectionForMaximizingCoverage(maximizeCoverage: Boolean, block: () -> T): T {
2831+
val prev = UtSettings.maximizeCoverageUsingReflection
2832+
UtSettings.maximizeCoverageUsingReflection = maximizeCoverage
2833+
try {
2834+
return block()
2835+
} finally {
2836+
UtSettings.maximizeCoverageUsingReflection = prev
2837+
}
2838+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.utbot.examples.stdlib
2+
3+
import org.junit.jupiter.api.Tag
4+
import org.junit.jupiter.api.Test
5+
import org.utbot.examples.AbstractTestCaseGeneratorTest
6+
import org.utbot.examples.eq
7+
import org.utbot.examples.isException
8+
import org.utbot.examples.withUsingReflectionForMaximizingCoverage
9+
import java.util.Date
10+
11+
class DateExampleTest : AbstractTestCaseGeneratorTest(testClass = DateExample::class) {
12+
@Suppress("SpellCheckingInspection")
13+
@Tag("slow")
14+
@Test
15+
fun testGetTimeWithNpeChecksForNonPublicFields() {
16+
withUsingReflectionForMaximizingCoverage(maximizeCoverage = true) {
17+
checkWithException(
18+
DateExample::getTime,
19+
eq(5),
20+
*commonMatchers,
21+
{ date: Date?, r: Result<Boolean> ->
22+
val cdate = date!!.getDeclaredFieldValue("cdate")
23+
val calendarDate = cdate!!.getDeclaredFieldValue("date")
24+
25+
calendarDate == null && r.isException<NullPointerException>()
26+
},
27+
{ date: Date?, r: Result<Boolean> ->
28+
val cdate = date!!.getDeclaredFieldValue("cdate")
29+
val calendarDate = cdate!!.getDeclaredFieldValue("date")
30+
31+
val gcal = date.getDeclaredFieldValue("gcal")
32+
33+
val normalized = calendarDate!!.getDeclaredFieldValue("normalized") as Boolean
34+
val gregorianYear = calendarDate.getDeclaredFieldValue("gregorianYear") as Int
35+
36+
gcal == null && !normalized && gregorianYear >= 1582 && r.isException<NullPointerException>()
37+
}
38+
)
39+
}
40+
}
41+
42+
@Test
43+
fun testGetTimeWithoutReflection() {
44+
withUsingReflectionForMaximizingCoverage(maximizeCoverage = false) {
45+
checkWithException(
46+
DateExample::getTime,
47+
eq(3),
48+
*commonMatchers
49+
)
50+
}
51+
}
52+
53+
private val commonMatchers = arrayOf(
54+
{ date: Date?, r: Result<Boolean> -> date == null && r.isException<NullPointerException>() },
55+
{ date: Date?, r: Result<Boolean> -> date != null && date.time == 100L && r.getOrThrow() },
56+
{ date: Date?, r: Result<Boolean> -> date != null && date.time != 100L && !r.getOrThrow() }
57+
)
58+
59+
private fun Any.getDeclaredFieldValue(fieldName: String): Any? {
60+
val declaredField = javaClass.getDeclaredField(fieldName)
61+
declaredField.isAccessible = true
62+
63+
return declaredField.get(this)
64+
}
65+
}

utbot-junit-contest/src/main/kotlin/org/utbot/contest/Contest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ fun setOptions() {
164164
UtSettings.warmupConcreteExecution = true
165165
UtSettings.testMinimizationStrategyType = TestSelectionStrategyType.COVERAGE_STRATEGY
166166
UtSettings.ignoreStringLiterals = true
167+
UtSettings.maximizeCoverageUsingReflection = true
167168
}
168169

169170

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.utbot.examples.stdlib;
2+
3+
import java.util.Date;
4+
5+
public class DateExample {
6+
public boolean getTime(Date date) {
7+
return date.getTime() == 100;
8+
}
9+
}

0 commit comments

Comments
 (0)