Skip to content

Commit eab0b84

Browse files
authored
Refactor to reduce coupling of base UtBot implementation with Spring-specific features (#2411)
* Move `ApplicationContext` and `SpringApplicationContext` to `utbot-framework` * Merge `avoidSpeculativeNotNullChecks` and `speculativelyCannotProduceNullPointerException` into one method * Extract `ApplicationContext` interface * Extract `MockerContext` interface * Configure default `ApplicationContext` for `TestCaseGenerator` * Group contexts in packages by their kind (simple and Spring) * Extract `TypeReplacer` and `NonNullSpeculator`, replace `shouldUseImplementors` with `beanDefinitions.isNotEmpty()` * Extract `ConcreteExecutionContext` * Make naming of `SpringApplicationContext` properties more consistent * Pass `Instrumentation.Factory` via Kryo instead of `Instrumentation` itself * Refactor `ConcreteExecutionContext` interface, to avoid `when`s by `ApplicationContext` type * Remove unused imports * Fix compilation after rebase * Avoid when by `projectType` when choosing `CgVariableConstructor` * Introduce `CodeGeneratorParams` data class, to avoid repeating same params in functions and constructors * Make `ApplicationContext` be responsible for creating appropriate code generator * Improve equals overrides for `Instrumentation.Factory` implementations * Move `SimpleUtExecutionInstrumentation` to a separate file * Fix compilation after rebase and decouple Spring tests * Remove unused and outdated imports left after rebase
1 parent 231d9e0 commit eab0b84

File tree

81 files changed

+1344
-1045
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+1344
-1045
lines changed

utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsAbstractCommand.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.utbot.framework.codegen.domain.ProjectType
2323
import org.utbot.framework.codegen.domain.StaticsMocking
2424
import org.utbot.framework.codegen.domain.testFrameworkByName
2525
import org.utbot.framework.codegen.generator.CodeGenerator
26+
import org.utbot.framework.codegen.generator.CodeGeneratorParams
2627
import org.utbot.framework.codegen.services.language.CgLanguageAssistant
2728
import org.utbot.framework.plugin.api.ClassId
2829
import org.utbot.framework.plugin.api.CodegenLanguage
@@ -210,15 +211,17 @@ abstract class GenerateTestsAbstractCommand(name: String, help: String) :
210211
val generateWarningsForStaticMocking =
211212
forceStaticMocking == ForceStaticMocking.FORCE && staticsMocking is NoStaticMocking
212213
return CodeGenerator(
213-
testFramework = testFrameworkByName(testFramework),
214-
classUnderTest = classUnderTest,
215-
//TODO: Support Spring projects in utbot-cli if requested
216-
projectType = ProjectType.PureJvm,
217-
codegenLanguage = codegenLanguage,
218-
cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage),
219-
staticsMocking = staticsMocking,
220-
forceStaticMocking = forceStaticMocking,
221-
generateWarningsForStaticMocking = generateWarningsForStaticMocking,
214+
CodeGeneratorParams(
215+
testFramework = testFrameworkByName(testFramework),
216+
classUnderTest = classUnderTest,
217+
//TODO: Support Spring projects in utbot-cli if requested
218+
projectType = ProjectType.PureJvm,
219+
codegenLanguage = codegenLanguage,
220+
cgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage),
221+
staticsMocking = staticsMocking,
222+
forceStaticMocking = forceStaticMocking,
223+
generateWarningsForStaticMocking = generateWarningsForStaticMocking,
224+
)
222225
)
223226
}
224227

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

Lines changed: 30 additions & 226 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,8 @@ import java.io.File
5656
import kotlin.contracts.ExperimentalContracts
5757
import kotlin.contracts.contract
5858
import org.utbot.common.isAbstract
59-
import org.utbot.common.isStatic
60-
import org.utbot.framework.isFromTrustedLibrary
61-
import org.utbot.framework.plugin.api.TypeReplacementMode.*
6259
import org.utbot.framework.plugin.api.util.SpringModelUtils
63-
import org.utbot.framework.plugin.api.util.allDeclaredFieldIds
64-
import org.utbot.framework.plugin.api.util.allSuperTypes
65-
import org.utbot.framework.plugin.api.util.fieldId
66-
import org.utbot.framework.plugin.api.util.isSubtypeOf
67-
import org.utbot.framework.plugin.api.util.utContext
6860
import org.utbot.framework.process.OpenModulesContainer
69-
import soot.SootField
7061
import soot.SootMethod
7162

7263
const val SYMBOLIC_NULL_ADDR: Int = 0
@@ -1328,241 +1319,54 @@ enum class TypeReplacementMode {
13281319
NoImplementors,
13291320
}
13301321

1331-
interface CodeGenerationContext
1332-
1333-
interface SpringCodeGenerationContext : CodeGenerationContext {
1334-
val springTestType: SpringTestType
1335-
val springSettings: SpringSettings
1336-
val springContextLoadingResult: SpringContextLoadingResult?
1337-
}
1338-
1339-
/**
1340-
* A context to use when no specific data is required.
1341-
*
1342-
* @param mockFrameworkInstalled shows if we have installed framework dependencies
1343-
* @param staticsMockingIsConfigured shows if we have installed static mocking tools
1344-
*/
1345-
open class ApplicationContext(
1346-
val mockFrameworkInstalled: Boolean = true,
1347-
staticsMockingIsConfigured: Boolean = true,
1348-
) : CodeGenerationContext {
1349-
var staticsMockingIsConfigured = staticsMockingIsConfigured
1350-
private set
1351-
1352-
init {
1353-
/**
1354-
* Situation when mock framework is not installed but static mocking is configured is semantically incorrect.
1355-
*
1356-
* However, it may be obtained in real application after this actions:
1357-
* - fully configure mocking (dependency installed + resource file created)
1358-
* - remove mockito-core dependency from project
1359-
* - forget to remove mock-maker file from resource directory
1360-
*
1361-
* Here we transform this configuration to semantically correct.
1362-
*/
1363-
if (!mockFrameworkInstalled && staticsMockingIsConfigured) {
1364-
this.staticsMockingIsConfigured = false
1365-
}
1366-
}
1367-
1368-
/**
1369-
* Shows if there are any restrictions on type implementors.
1370-
*/
1371-
open val typeReplacementMode: TypeReplacementMode = AnyImplementor
1372-
1373-
/**
1374-
* Finds a type to replace the original abstract type
1375-
* if it is guided with some additional information.
1376-
*/
1377-
open fun replaceTypeIfNeeded(type: RefType): ClassId? = null
1378-
1379-
/**
1380-
* Sets the restrictions on speculative not null
1381-
* constraints in current application context.
1382-
*
1383-
* @see docs/SpeculativeFieldNonNullability.md for more information.
1384-
*/
1385-
open fun avoidSpeculativeNotNullChecks(field: SootField): Boolean =
1386-
UtSettings.maximizeCoverageUsingReflection || !field.declaringClass.isFromTrustedLibrary()
1387-
1388-
/**
1389-
* Checks whether accessing [field] (with a method invocation or field access) speculatively
1390-
* cannot produce [NullPointerException] (according to its finality or accessibility).
1391-
*
1392-
* @see docs/SpeculativeFieldNonNullability.md for more information.
1393-
*/
1394-
open fun speculativelyCannotProduceNullPointerException(
1395-
field: SootField,
1396-
classUnderTest: ClassId,
1397-
): Boolean = field.isFinal || !field.isPublic
1398-
1399-
open fun preventsFurtherTestGeneration(): Boolean = false
1400-
1401-
open fun getErrors(): List<UtError> = emptyList()
1402-
}
1403-
14041322
sealed class SpringConfiguration(val fullDisplayName: String) {
14051323
class JavaConfiguration(val classBinaryName: String) : SpringConfiguration(classBinaryName)
14061324
class XMLConfiguration(val absolutePath: String) : SpringConfiguration(absolutePath)
14071325
}
14081326

14091327
sealed interface SpringSettings {
1410-
class AbsentSpringSettings : SpringSettings {
1411-
// Denotes no configuration and no profile setting
1412-
1413-
// NOTICE:
1414-
// `class` should not be replaced with `object`
1415-
// in order to avoid issues caused by Kryo deserialization
1416-
// that creates new instances breaking `when` expressions
1417-
// that check reference equality instead of type equality
1328+
object AbsentSpringSettings : SpringSettings {
1329+
// NOTE that overriding equals is required just because without it
1330+
// we will lose equality for objects after deserialization
1331+
override fun equals(other: Any?): Boolean = other is AbsentSpringSettings
1332+
1333+
override fun hashCode(): Int = 0
14181334
}
14191335

1420-
class PresentSpringSettings(
1336+
data class PresentSpringSettings(
14211337
val configuration: SpringConfiguration,
1422-
val profiles: Array<String>
1338+
val profiles: List<String>
14231339
) : SpringSettings
14241340
}
14251341

14261342
/**
1427-
* [contextLoaded] can be `true` while [exceptions] is not empty,
1428-
* if we failed to use most specific SpringApi available (e.g. SpringBoot), but
1429-
* were able to successfully fall back to less specific SpringApi (e.g. PureSpring).
1343+
* Result of loading concrete execution context (e.g. Spring application context).
1344+
*
1345+
* [contextLoaded] can be `true` while [exceptions] is not empty. For example, we may fail
1346+
* to load context with most specific SpringApi available (e.g. SpringBoot),
1347+
* but successfully fall back to less specific SpringApi (e.g. PureSpring).
14301348
*/
1431-
class SpringContextLoadingResult(
1349+
class ConcreteContextLoadingResult(
14321350
val contextLoaded: Boolean,
14331351
val exceptions: List<Throwable>
1434-
)
1435-
1436-
/**
1437-
* Data we get from Spring application context
1438-
* to manage engine and code generator behaviour.
1439-
*
1440-
* @param beanDefinitions describes bean definitions (bean name, type, some optional additional data)
1441-
* @param shouldUseImplementors describes it we want to replace interfaces with injected types or not
1442-
*/
1443-
// TODO move this class to utbot-framework so we can use it as abstract factory
1444-
// to get rid of numerous `when`s and polymorphically create things like:
1445-
// - Instrumentation<UtConcreteExecutionResult>
1446-
// - FuzzedType (to get rid of thisInstanceFuzzedTypeWrapper)
1447-
// - JavaValueProvider
1448-
// - CgVariableConstructor
1449-
// - CodeGeneratorResult (generateForSpringClass)
1450-
// Right now this refactoring is blocked because some interfaces need to get extracted and moved to utbot-framework-api
1451-
// As an alternative we can just move ApplicationContext itself to utbot-framework
1452-
class SpringApplicationContext(
1453-
mockInstalled: Boolean,
1454-
staticsMockingIsConfigured: Boolean,
1455-
val beanDefinitions: List<BeanDefinitionData> = emptyList(),
1456-
private val shouldUseImplementors: Boolean,
1457-
override val springTestType: SpringTestType,
1458-
override val springSettings: SpringSettings,
1459-
): ApplicationContext(mockInstalled, staticsMockingIsConfigured), SpringCodeGenerationContext {
1460-
1461-
override var springContextLoadingResult: SpringContextLoadingResult? = null
1352+
) {
1353+
val utErrors: List<UtError> get() =
1354+
exceptions.map { UtError(it.message ?: "Concrete context loading failed", it) }
1355+
1356+
fun andThen(onSuccess: () -> ConcreteContextLoadingResult) =
1357+
if (contextLoaded) {
1358+
val otherResult = onSuccess()
1359+
ConcreteContextLoadingResult(
1360+
contextLoaded = otherResult.contextLoaded,
1361+
exceptions = exceptions + otherResult.exceptions
1362+
)
1363+
} else this
14621364

14631365
companion object {
1464-
private val logger = KotlinLogging.logger {}
1465-
}
1466-
1467-
private var areInjectedClassesInitialized : Boolean = false
1468-
private var areAllInjectedTypesInitialized: Boolean = false
1469-
1470-
// Classes representing concrete types that are actually used in Spring application
1471-
private val springInjectedClasses: Set<ClassId>
1472-
get() {
1473-
if (!areInjectedClassesInitialized) {
1474-
for (beanTypeName in beanDefinitions.map { it.beanTypeName }) {
1475-
try {
1476-
val beanClass = utContext.classLoader.loadClass(beanTypeName)
1477-
if (!beanClass.isAbstract && !beanClass.isInterface &&
1478-
!beanClass.isLocalClass && (!beanClass.isMemberClass || beanClass.isStatic)) {
1479-
springInjectedClassesStorage += beanClass.id
1480-
}
1481-
} catch (e: Throwable) {
1482-
// For some Spring beans (e.g. with anonymous classes)
1483-
// it is possible to have problems with classes loading.
1484-
when (e) {
1485-
is ClassNotFoundException, is NoClassDefFoundError, is IllegalAccessError ->
1486-
logger.warn { "Failed to load bean class for $beanTypeName (${e.message})" }
1487-
1488-
else -> throw e
1489-
}
1490-
}
1491-
}
1492-
1493-
// This is done to be sure that this storage is not empty after the first class loading iteration.
1494-
// So, even if all loaded classes were filtered out, we will not try to load them again.
1495-
areInjectedClassesInitialized = true
1496-
}
1497-
1498-
return springInjectedClassesStorage
1499-
}
1500-
1501-
private val allInjectedTypes: Set<ClassId>
1502-
get() {
1503-
if (!areAllInjectedTypesInitialized) {
1504-
allInjectedTypesStorage = springInjectedClasses.flatMap { it.allSuperTypes() }.toSet()
1505-
areAllInjectedTypesInitialized = true
1506-
}
1507-
1508-
return allInjectedTypesStorage
1509-
}
1510-
1511-
// imitates `by lazy` (we can't use actual `by lazy` because communication via RD breaks it)
1512-
private var allInjectedTypesStorage: Set<ClassId> = emptySet()
1513-
1514-
// This is a service field to model the lazy behavior of [springInjectedClasses].
1515-
// Do not call it outside the getter.
1516-
//
1517-
// Actually, we should just call [springInjectedClasses] with `by lazy`, but we had problems
1518-
// with a strange `kotlin.UNINITIALIZED_VALUE` in `speculativelyCannotProduceNullPointerException` method call.
1519-
private val springInjectedClassesStorage = mutableSetOf<ClassId>()
1520-
1521-
override val typeReplacementMode: TypeReplacementMode =
1522-
if (shouldUseImplementors) KnownImplementor else NoImplementors
1523-
1524-
/**
1525-
* Replaces an interface type with its implementor type
1526-
* if there is the unique implementor in bean definitions.
1527-
*/
1528-
override fun replaceTypeIfNeeded(type: RefType): ClassId? =
1529-
if (type.isAbstractType) {
1530-
springInjectedClasses.singleOrNull { it.isSubtypeOf(type.id) }
1531-
} else {
1532-
null
1533-
}
1534-
1535-
override fun avoidSpeculativeNotNullChecks(field: SootField): Boolean = false
1536-
1537-
/**
1538-
* In Spring applications we can mark as speculatively not null
1539-
* fields if they are mocked and injecting into class under test so on.
1540-
*
1541-
* Fields are not mocked if their actual type is obtained from [springInjectedClasses].
1542-
*
1543-
*/
1544-
override fun speculativelyCannotProduceNullPointerException(
1545-
field: SootField,
1546-
classUnderTest: ClassId,
1547-
): Boolean = field.fieldId in classUnderTest.allDeclaredFieldIds && field.type.classId !in allInjectedTypes
1548-
1549-
override fun preventsFurtherTestGeneration(): Boolean =
1550-
super.preventsFurtherTestGeneration() || springContextLoadingResult?.contextLoaded == false
1551-
1552-
override fun getErrors(): List<UtError> =
1553-
springContextLoadingResult?.exceptions?.map { exception ->
1554-
UtError(
1555-
"Failed to load Spring application context",
1556-
exception
1557-
)
1558-
}.orEmpty() + super.getErrors()
1559-
1560-
fun getBeansAssignableTo(classId: ClassId): List<BeanDefinitionData> = beanDefinitions.filter { beanDef ->
1561-
// some bean classes may fail to load
1562-
runCatching {
1563-
val beanClass = ClassId(beanDef.beanTypeName).jClass
1564-
classId.jClass.isAssignableFrom(beanClass)
1565-
}.getOrElse { false }
1366+
fun successWithoutExceptions() = ConcreteContextLoadingResult(
1367+
contextLoaded = true,
1368+
exceptions = emptyList()
1369+
)
15661370
}
15671371
}
15681372

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

Lines changed: 4 additions & 4 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.ApplicationContext
22+
import org.utbot.framework.context.MockerContext
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: ApplicationContext,
171+
private val mockerContext: MockerContext,
172172
) {
173173
private val mocksAreDesired: Boolean = strategy != MockStrategy.NO_MOCKS
174174

@@ -227,10 +227,10 @@ class Mocker(
227227

228228
val mockingIsPossible = when (mockInfo) {
229229
is UtFieldMockInfo,
230-
is UtObjectMockInfo -> applicationContext.mockFrameworkInstalled
230+
is UtObjectMockInfo -> mockerContext.mockFrameworkInstalled
231231
is UtNewInstanceMockInfo,
232232
is UtStaticMethodMockInfo,
233-
is UtStaticObjectMockInfo -> applicationContext.staticsMockingIsConfigured
233+
is UtStaticObjectMockInfo -> mockerContext.staticsMockingIsConfigured
234234
}
235235
val mockingIsForcedAndPossible = mockAlways(mockedValue.type) && mockingIsPossible
236236

0 commit comments

Comments
 (0)