Skip to content

Commit eb6de3c

Browse files
Support Spring guided type replacements in symbolic engine (#1908)
1 parent 2ab0b0d commit eb6de3c

File tree

11 files changed

+224
-54
lines changed

11 files changed

+224
-54
lines changed

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

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ import java.io.File
5555
import kotlin.contracts.ExperimentalContracts
5656
import kotlin.contracts.contract
5757
import org.utbot.common.isAbstract
58+
import org.utbot.common.isStatic
59+
import org.utbot.framework.plugin.api.TypeReplacementMode.*
60+
import org.utbot.framework.plugin.api.util.isSubtypeOf
5861
import org.utbot.framework.plugin.api.util.utContext
5962
import org.utbot.framework.process.OpenModulesContainer
6063

@@ -1149,13 +1152,36 @@ open class TypeParameters(val parameters: List<ClassId> = emptyList())
11491152

11501153
class WildcardTypeParameter : TypeParameters(emptyList())
11511154

1155+
/**
1156+
* Describes the way to replace abstract types with concrete implementors.
1157+
*/
1158+
enum class TypeReplacementMode {
1159+
/**
1160+
* Any possible implementor (that is preferred by solver) may be used.
1161+
*/
1162+
AnyImplementor,
1163+
1164+
/**
1165+
* There is a known implementor to be used.
1166+
* For example, it is obtained from bean definitions in Spring application.
1167+
*/
1168+
KnownImplementor,
1169+
1170+
/**
1171+
* Using implementors is not allowed.
1172+
* If mocking is allowed, mock of this type will be used.
1173+
* Otherwise, branch will be pruned as unsatisfiable.
1174+
*/
1175+
NoImplementors,
1176+
}
1177+
11521178
/**
11531179
* A context to use when no specific data is required.
11541180
*
11551181
* @param mockFrameworkInstalled shows if we have installed framework dependencies
11561182
* @param staticsMockingIsConfigured shows if we have installed static mocking tools
11571183
*/
1158-
open class StandardApplicationContext(
1184+
open class ApplicationContext(
11591185
val mockFrameworkInstalled: Boolean = true,
11601186
staticsMockingIsConfigured: Boolean = true,
11611187
) {
@@ -1177,22 +1203,53 @@ open class StandardApplicationContext(
11771203
this.staticsMockingIsConfigured = false
11781204
}
11791205
}
1206+
1207+
/**
1208+
* Shows if there are any restrictions on type implementors.
1209+
*/
1210+
open val typeReplacementMode: TypeReplacementMode = AnyImplementor
1211+
1212+
/**
1213+
* Finds a type to replace the original abstract type
1214+
* if it is guided with some additional information.
1215+
*/
1216+
open fun replaceTypeIfNeeded(type: RefType): ClassId? = null
11801217
}
11811218

11821219
/**
11831220
* Data we get from Spring application context
11841221
* to manage engine and code generator behaviour.
11851222
*
1186-
* @param beanQualifiedNames describes fqn of injected classes
1223+
* @param beanQualifiedNames describes fqn of types from bean definitions
1224+
* @param shouldUseImplementors describes it we want to replace interfaces with injected types or not
11871225
*/
11881226
class SpringApplicationContext(
11891227
mockInstalled: Boolean,
11901228
staticsMockingIsConfigured: Boolean,
1191-
val beanQualifiedNames: List<String> = emptyList(),
1192-
): StandardApplicationContext(mockInstalled, staticsMockingIsConfigured) {
1229+
private val beanQualifiedNames: List<String> = emptyList(),
1230+
private val shouldUseImplementors: Boolean,
1231+
): ApplicationContext(mockInstalled, staticsMockingIsConfigured) {
1232+
11931233
private val springInjectedClasses: List<ClassId> by lazy {
1194-
beanQualifiedNames.map { fqn -> utContext.classLoader.loadClass(fqn).id }
1234+
beanQualifiedNames
1235+
.map { fqn -> utContext.classLoader.loadClass(fqn) }
1236+
.filterNot { it.isAbstract || it.isInterface || it.isLocalClass || it.isMemberClass && !it.isStatic }
1237+
.map { it.id }
11951238
}
1239+
1240+
override val typeReplacementMode: TypeReplacementMode
1241+
get() = if (shouldUseImplementors) KnownImplementor else NoImplementors
1242+
1243+
/**
1244+
* Replaces an interface type with its implementor type
1245+
* if there is the unique implementor in bean definitions.
1246+
*/
1247+
override fun replaceTypeIfNeeded(type: RefType): ClassId? =
1248+
if (type.sootClass.isInterface || type.sootClass.isAbstract) {
1249+
springInjectedClasses.singleOrNull { it.isSubtypeOf(type.id) }
1250+
} else {
1251+
null
1252+
}
11961253
}
11971254

11981255
interface CodeGenerationSettingItem {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import kotlinx.collections.immutable.persistentListOf
1919
import org.utbot.common.nameOfPackage
2020
import org.utbot.engine.types.OBJECT_TYPE
2121
import org.utbot.engine.util.mockListeners.MockListenerController
22-
import org.utbot.framework.plugin.api.StandardApplicationContext
22+
import org.utbot.framework.plugin.api.ApplicationContext
2323
import org.utbot.framework.plugin.api.util.isInaccessibleViaReflection
2424
import soot.BooleanType
2525
import soot.RefType
@@ -168,7 +168,7 @@ class Mocker(
168168
private val hierarchy: Hierarchy,
169169
chosenClassesToMockAlways: Set<ClassId>,
170170
internal val mockListenerController: MockListenerController? = null,
171-
private val applicationContext: StandardApplicationContext,
171+
private val applicationContext: ApplicationContext,
172172
) {
173173
private val mocksAreDesired: Boolean = strategy != MockStrategy.NO_MOCKS
174174

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

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,14 @@ import org.utbot.framework.UtSettings
116116
import org.utbot.framework.UtSettings.maximizeCoverageUsingReflection
117117
import org.utbot.framework.UtSettings.preferredCexOption
118118
import org.utbot.framework.UtSettings.substituteStaticsWithSymbolicVariable
119-
import org.utbot.framework.plugin.api.StandardApplicationContext
119+
import org.utbot.framework.plugin.api.ApplicationContext
120120
import org.utbot.framework.plugin.api.ClassId
121121
import org.utbot.framework.plugin.api.ExecutableId
122122
import org.utbot.framework.plugin.api.FieldId
123123
import org.utbot.framework.plugin.api.MethodId
124+
import org.utbot.framework.plugin.api.TypeReplacementMode.AnyImplementor
125+
import org.utbot.framework.plugin.api.TypeReplacementMode.KnownImplementor
126+
import org.utbot.framework.plugin.api.TypeReplacementMode.NoImplementors
124127
import org.utbot.framework.plugin.api.classId
125128
import org.utbot.framework.plugin.api.id
126129
import org.utbot.framework.plugin.api.util.executable
@@ -239,7 +242,7 @@ class Traverser(
239242
internal val typeResolver: TypeResolver,
240243
private val globalGraph: InterProceduralUnitGraph,
241244
private val mocker: Mocker,
242-
private val applicationContext: StandardApplicationContext?,
245+
private val applicationContext: ApplicationContext,
243246
) : UtContextInitializer() {
244247

245248
private val visitedStmts: MutableSet<Stmt> = mutableSetOf()
@@ -1364,7 +1367,7 @@ class Traverser(
13641367
traverseException(current, symException)
13651368
}
13661369

1367-
// TODO HACK violation of encapsulation
1370+
// TODO: HACK violation of encapsulation
13681371
fun createObject(
13691372
addr: UtAddrExpression,
13701373
type: RefType,
@@ -1373,6 +1376,27 @@ class Traverser(
13731376
): ObjectValue {
13741377
touchAddress(addr)
13751378

1379+
// Some types (e.g., interfaces) need to be mocked or replaced with the concrete implementor.
1380+
// Typically, this implementor is selected by SMT solver later.
1381+
// However, if we have the restriction on implementor type (it may be obtained
1382+
// from Spring bean definitions, for example), we can just create a symbolic object
1383+
// with hard constraint on the mentioned type.
1384+
val replacedClassId = when (applicationContext.typeReplacementMode) {
1385+
KnownImplementor -> applicationContext.replaceTypeIfNeeded(type)
1386+
AnyImplementor,
1387+
NoImplementors -> null
1388+
}
1389+
1390+
replacedClassId?.let {
1391+
val sootType = Scene.v().getRefType(it.canonicalName)
1392+
val typeStorage = typeResolver.constructTypeStorage(sootType, useConcreteType = false)
1393+
1394+
val typeHardConstraint = typeRegistry.typeConstraint(addr, typeStorage).all().asHardConstraint()
1395+
queuedSymbolicStateUpdates += typeHardConstraint
1396+
1397+
return ObjectValue(typeStorage, addr)
1398+
}
1399+
13761400
val nullEqualityConstraint = mkEq(addr, nullObjectAddr)
13771401

13781402
if (mockInfoGenerator != null) {
@@ -1475,48 +1499,79 @@ class Traverser(
14751499
"but there is no mock info generator provided to construct a mock value."
14761500
}
14771501

1478-
val mockInfo = mockInfoGenerator.generate(addr)
1479-
val mockedObjectInfo = mocker.forceMock(type, mockInfoGenerator.generate(addr))
1480-
1481-
val mockedObject: ObjectValue = when (mockedObjectInfo) {
1482-
is NoMock -> error("Value must be mocked after the force mock")
1483-
is ExpectedMock -> mockedObjectInfo.value
1484-
is UnexpectedMock -> {
1485-
// if mock occurs, but it is unexpected due to some reasons
1486-
// (e.g. we do not have mock framework installed),
1487-
// we can only generate a test that uses null value for mocked object
1488-
queuedSymbolicStateUpdates += nullEqualityConstraint.asHardConstraint()
1489-
1490-
mockedObjectInfo.value
1491-
}
1502+
return createMockedObject(addr, type, mockInfoGenerator)
1503+
}
1504+
1505+
val concreteImplementation = when (applicationContext.typeReplacementMode) {
1506+
AnyImplementor -> {
1507+
val isMockConstraint = mkEq(typeRegistry.isMock(addr), UtFalse)
1508+
1509+
queuedSymbolicStateUpdates += typeHardConstraint
1510+
queuedSymbolicStateUpdates += mkOr(isMockConstraint, nullEqualityConstraint).asHardConstraint()
1511+
1512+
// If we have this$0 with UtArrayList type, we have to create such instance.
1513+
// We should create an object with typeStorage of all possible real types and concrete implementation
1514+
// Otherwise we'd have either a wrong type in the resolver, or missing method like 'preconditionCheck'.
1515+
wrapperToClass[type]?.first()?.let { wrapper(it, addr) }?.concrete
14921516
}
1517+
// In case of `KnownImplementor` mode we should have already tried to replace type using `replaceTypeIfNeeded`.
1518+
// However, this replacement attempt might be unsuccessful even if some possible concrete types are present.
1519+
// For example, we may have two concrete implementors present in Spring bean definitions, so we do not know
1520+
// which one to use. In such case we try to mock this type, if it is possible, or prune branch as unsatisfiable.
1521+
//
1522+
// In case of `NoImplementors` mode we should try to mock this type or prune branch as unsatisfiable.
1523+
// Mocking can be impossible here as there are no guaranties that `mockInfoGenerator` is instantiated.
1524+
KnownImplementor,
1525+
NoImplementors -> {
1526+
mockInfoGenerator?.let {
1527+
return createMockedObject(addr, type, it)
1528+
}
14931529

1494-
if (mockedObjectInfo is UnexpectedMock) {
1495-
return mockedObject
1530+
queuedSymbolicStateUpdates += mkFalse().asHardConstraint()
1531+
null
14961532
}
1533+
}
14971534

1498-
queuedSymbolicStateUpdates += MemoryUpdate(mockInfos = persistentListOf(MockInfoEnriched(mockInfo)))
1535+
return ObjectValue(typeStorage, addr, concreteImplementation)
1536+
}
14991537

1500-
// add typeConstraint for mocked object. It's a declared type of the object.
1501-
val typeConstraint = typeRegistry.typeConstraint(addr, mockedObject.typeStorage).all()
1502-
val isMockConstraint = mkEq(typeRegistry.isMock(mockedObject.addr), UtTrue)
1538+
private fun createMockedObject(
1539+
addr: UtAddrExpression,
1540+
type: RefType,
1541+
mockInfoGenerator: UtMockInfoGenerator,
1542+
): ObjectValue {
1543+
val nullEqualityConstraint = mkEq(addr, nullObjectAddr)
15031544

1504-
queuedSymbolicStateUpdates += typeConstraint.asHardConstraint()
1505-
queuedSymbolicStateUpdates += mkOr(isMockConstraint, nullEqualityConstraint).asHardConstraint()
1545+
val mockInfo = mockInfoGenerator.generate(addr)
1546+
val mockedObjectInfo = mocker.forceMock(type, mockInfoGenerator.generate(addr))
1547+
1548+
val mockedObject: ObjectValue = when (mockedObjectInfo) {
1549+
is NoMock -> error("Value must be mocked after the force mock")
1550+
is ExpectedMock -> mockedObjectInfo.value
1551+
is UnexpectedMock -> {
1552+
// if mock occurs, but it is unexpected due to some reasons
1553+
// (e.g. we do not have mock framework installed),
1554+
// we can only generate a test that uses null value for mocked object
1555+
queuedSymbolicStateUpdates += nullEqualityConstraint.asHardConstraint()
15061556

1557+
mockedObjectInfo.value
1558+
}
1559+
}
1560+
1561+
if (mockedObjectInfo is UnexpectedMock) {
15071562
return mockedObject
15081563
}
15091564

1510-
// If we have this$0 with UtArrayList type, we have to create such instance.
1511-
// We should create an object with typeStorage of all possible real types and concrete implementation
1512-
// Otherwise we'd have either a wrong type in the resolver, or missing method like 'preconditionCheck'.
1513-
val concreteImplementation = wrapperToClass[type]?.first()?.let { wrapper(it, addr) }?.concrete
1514-
val isMockConstraint = mkEq(typeRegistry.isMock(addr), UtFalse)
1565+
queuedSymbolicStateUpdates += MemoryUpdate(mockInfos = persistentListOf(MockInfoEnriched(mockInfo)))
1566+
1567+
// add typeConstraint for mocked object. It's a declared type of the object.
1568+
val typeConstraint = typeRegistry.typeConstraint(addr, mockedObject.typeStorage).all()
1569+
val isMockConstraint = mkEq(typeRegistry.isMock(mockedObject.addr), UtTrue)
15151570

1516-
queuedSymbolicStateUpdates += typeHardConstraint
1571+
queuedSymbolicStateUpdates += typeConstraint.asHardConstraint()
15171572
queuedSymbolicStateUpdates += mkOr(isMockConstraint, nullEqualityConstraint).asHardConstraint()
15181573

1519-
return ObjectValue(typeStorage, addr, concreteImplementation)
1574+
return mockedObject
15201575
}
15211576

15221577
private fun TraversalContext.resolveConstant(constant: Constant): SymbolicValue =

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class UtBotSymbolicEngine(
105105
dependencyPaths: String,
106106
val mockStrategy: MockStrategy = NO_MOCKS,
107107
chosenClassesToMockAlways: Set<ClassId>,
108-
applicationContext: StandardApplicationContext,
108+
applicationContext: ApplicationContext,
109109
private val solverTimeoutInMillis: Int = checkSolverTimeoutMillis
110110
) : UtContextInitializer() {
111111
private val graph = methodUnderTest.sootMethod.jimpleBody().apply {

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,3 +711,32 @@ enum class ParametrizedTestSource(
711711
override val allItems: List<ParametrizedTestSource> = values().toList()
712712
}
713713
}
714+
715+
enum class ApplicationType {
716+
/**
717+
* Standard JVM application without DI frameworks.
718+
*/
719+
PURE_JVM,
720+
721+
/**
722+
* Spring or Spring Boot application.
723+
*/
724+
SPRING_APPLICATION,
725+
}
726+
727+
enum class TypeReplacementApproach {
728+
/**
729+
* Do not replace interfaces and abstract classes with concrete implementors.
730+
* Use mocking instead of it.
731+
*/
732+
DO_NOT_REPLACE,
733+
734+
/**
735+
* Try to replace interfaces and abstract classes with concrete implementors
736+
* obtained from bean definitions.
737+
* If it is impossible, use mocking.
738+
*
739+
* Currently used in Spring applications only.
740+
*/
741+
REPLACE_IF_POSSIBLE,
742+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ open class TestCaseGenerator(
6161
val engineActions: MutableList<(UtBotSymbolicEngine) -> Unit> = mutableListOf(),
6262
val isCanceled: () -> Boolean = { false },
6363
val forceSootReload: Boolean = true,
64-
val applicationContext: StandardApplicationContext = StandardApplicationContext(),
64+
val applicationContext: ApplicationContext = ApplicationContext(),
6565
) {
6666
private val logger: KLogger = KotlinLogging.logger {}
6767
private val timeoutLogger: KLogger = KotlinLogging.logger(logger.name + ".timeout")
@@ -257,7 +257,7 @@ open class TestCaseGenerator(
257257
method: ExecutableId,
258258
mockStrategyApi: MockStrategyApi,
259259
chosenClassesToMockAlways: Set<ClassId>,
260-
applicationContext: StandardApplicationContext,
260+
applicationContext: ApplicationContext,
261261
executionTimeEstimator: ExecutionTimeEstimator
262262
): UtBotSymbolicEngine {
263263
logger.debug("Starting symbolic execution for $method --$mockStrategyApi--")

utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ private fun EngineProcessModel.setup(kryoHelper: KryoHelper, watchdog: IdleWatch
8181
watchdog.measureTimeForActiveCall(createTestGenerator, "Creating Test Generator") { params ->
8282
AnalyticsConfigureUtil.configureML()
8383
Instrumenter.adapter = RdInstrumenter(realProtocol.rdInstrumenterAdapter)
84-
val applicationContext: StandardApplicationContext = kryoHelper.readObject(params.applicationContext)
84+
val applicationContext: ApplicationContext = kryoHelper.readObject(params.applicationContext)
8585

8686
testGenerator = TestCaseGenerator(buildDirs = params.buildDir.map { Paths.get(it) },
8787
classpath = params.classpath,
@@ -99,7 +99,7 @@ private fun EngineProcessModel.setup(kryoHelper: KryoHelper, watchdog: IdleWatch
9999
logger.debug().measureTime({ "starting generation for ${methods.size} methods, starting with ${methods.first()}" }) {
100100
val generateFlow = when (testGenerator.applicationContext) {
101101
is SpringApplicationContext -> defaultSpringFlow(params.generationTimeout)
102-
is StandardApplicationContext -> testFlow {
102+
is ApplicationContext -> testFlow {
103103
generationTimeout = params.generationTimeout
104104
isSymbolicEngineEnabled = params.isSymbolicEngineEnabled
105105
isFuzzingEnabled = params.isFuzzingEnabled

0 commit comments

Comments
 (0)