Skip to content

Refactor to reduce coupling of base UtBot implementation with Spring-specific features (Copy) #2424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

package org.utbot.framework.plugin.api

import mu.KotlinLogging
import org.utbot.common.FileUtil
import org.utbot.common.isDefaultValue
import org.utbot.common.withToStringThreadLocalReentrancyGuard
Expand Down Expand Up @@ -56,17 +55,8 @@ import java.io.File
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import org.utbot.common.isAbstract
import org.utbot.common.isStatic
import org.utbot.framework.isFromTrustedLibrary
import org.utbot.framework.plugin.api.TypeReplacementMode.*
import org.utbot.framework.plugin.api.util.SpringModelUtils
import org.utbot.framework.plugin.api.util.allDeclaredFieldIds
import org.utbot.framework.plugin.api.util.allSuperTypes
import org.utbot.framework.plugin.api.util.fieldId
import org.utbot.framework.plugin.api.util.isSubtypeOf
import org.utbot.framework.plugin.api.util.utContext
import org.utbot.framework.process.OpenModulesContainer
import soot.SootField
import soot.SootMethod

const val SYMBOLIC_NULL_ADDR: Int = 0
Expand Down Expand Up @@ -1332,71 +1322,6 @@ interface SpringCodeGenerationContext : CodeGenerationContext {
val springContextLoadingResult: SpringContextLoadingResult?
}

/**
* A context to use when no specific data is required.
*
* @param mockFrameworkInstalled shows if we have installed framework dependencies
* @param staticsMockingIsConfigured shows if we have installed static mocking tools
*/
open class ApplicationContext(
val mockFrameworkInstalled: Boolean = true,
staticsMockingIsConfigured: Boolean = true,
) : CodeGenerationContext {
var staticsMockingIsConfigured = staticsMockingIsConfigured
private set

init {
/**
* Situation when mock framework is not installed but static mocking is configured is semantically incorrect.
*
* However, it may be obtained in real application after this actions:
* - fully configure mocking (dependency installed + resource file created)
* - remove mockito-core dependency from project
* - forget to remove mock-maker file from resource directory
*
* Here we transform this configuration to semantically correct.
*/
if (!mockFrameworkInstalled && staticsMockingIsConfigured) {
this.staticsMockingIsConfigured = false
}
}

/**
* Shows if there are any restrictions on type implementors.
*/
open val typeReplacementMode: TypeReplacementMode = AnyImplementor

/**
* Finds a type to replace the original abstract type
* if it is guided with some additional information.
*/
open fun replaceTypeIfNeeded(type: RefType): ClassId? = null

/**
* Sets the restrictions on speculative not null
* constraints in current application context.
*
* @see docs/SpeculativeFieldNonNullability.md for more information.
*/
open fun avoidSpeculativeNotNullChecks(field: SootField): Boolean =
UtSettings.maximizeCoverageUsingReflection || !field.declaringClass.isFromTrustedLibrary()

/**
* Checks whether accessing [field] (with a method invocation or field access) speculatively
* cannot produce [NullPointerException] (according to its finality or accessibility).
*
* @see docs/SpeculativeFieldNonNullability.md for more information.
*/
open fun speculativelyCannotProduceNullPointerException(
field: SootField,
classUnderTest: ClassId,
): Boolean = field.isFinal || !field.isPublic

open fun preventsFurtherTestGeneration(): Boolean = false

open fun getErrors(): List<UtError> = emptyList()
}

sealed class SpringConfiguration(val fullDisplayName: String) {
class JavaConfiguration(val classBinaryName: String) : SpringConfiguration(classBinaryName)
class XMLConfiguration(val absolutePath: String) : SpringConfiguration(absolutePath)
Expand Down Expand Up @@ -1429,139 +1354,6 @@ class SpringContextLoadingResult(
val exceptions: List<Throwable>
)

/**
* Data we get from Spring application context
* to manage engine and code generator behaviour.
*
* @param beanDefinitions describes bean definitions (bean name, type, some optional additional data)
* @param shouldUseImplementors describes it we want to replace interfaces with injected types or not
*/
// TODO move this class to utbot-framework so we can use it as abstract factory
// to get rid of numerous `when`s and polymorphically create things like:
// - Instrumentation<UtConcreteExecutionResult>
// - FuzzedType (to get rid of thisInstanceFuzzedTypeWrapper)
// - JavaValueProvider
// - CgVariableConstructor
// - CodeGeneratorResult (generateForSpringClass)
// Right now this refactoring is blocked because some interfaces need to get extracted and moved to utbot-framework-api
// As an alternative we can just move ApplicationContext itself to utbot-framework
class SpringApplicationContext(
mockInstalled: Boolean,
staticsMockingIsConfigured: Boolean,
val beanDefinitions: List<BeanDefinitionData> = emptyList(),
private val shouldUseImplementors: Boolean,
override val springTestType: SpringTestType,
override val springSettings: SpringSettings,
): ApplicationContext(mockInstalled, staticsMockingIsConfigured), SpringCodeGenerationContext {

override var springContextLoadingResult: SpringContextLoadingResult? = null

companion object {
private val logger = KotlinLogging.logger {}
}

private var areInjectedClassesInitialized : Boolean = false
private var areAllInjectedTypesInitialized: Boolean = false

// Classes representing concrete types that are actually used in Spring application
private val springInjectedClasses: Set<ClassId>
get() {
if (!areInjectedClassesInitialized) {
for (beanTypeName in beanDefinitions.map { it.beanTypeName }) {
try {
val beanClass = utContext.classLoader.loadClass(beanTypeName)
if (!beanClass.isAbstract && !beanClass.isInterface &&
!beanClass.isLocalClass && (!beanClass.isMemberClass || beanClass.isStatic)) {
springInjectedClassesStorage += beanClass.id
}
} catch (e: Throwable) {
// For some Spring beans (e.g. with anonymous classes)
// it is possible to have problems with classes loading.
when (e) {
is ClassNotFoundException, is NoClassDefFoundError, is IllegalAccessError ->
logger.warn { "Failed to load bean class for $beanTypeName (${e.message})" }

else -> throw e
}
}
}

// This is done to be sure that this storage is not empty after the first class loading iteration.
// So, even if all loaded classes were filtered out, we will not try to load them again.
areInjectedClassesInitialized = true
}

return springInjectedClassesStorage
}

private val allInjectedTypes: Set<ClassId>
get() {
if (!areAllInjectedTypesInitialized) {
allInjectedTypesStorage = springInjectedClasses.flatMap { it.allSuperTypes() }.toSet()
areAllInjectedTypesInitialized = true
}

return allInjectedTypesStorage
}

// imitates `by lazy` (we can't use actual `by lazy` because communication via RD breaks it)
private var allInjectedTypesStorage: Set<ClassId> = emptySet()

// This is a service field to model the lazy behavior of [springInjectedClasses].
// Do not call it outside the getter.
//
// Actually, we should just call [springInjectedClasses] with `by lazy`, but we had problems
// with a strange `kotlin.UNINITIALIZED_VALUE` in `speculativelyCannotProduceNullPointerException` method call.
private val springInjectedClassesStorage = mutableSetOf<ClassId>()

override val typeReplacementMode: TypeReplacementMode =
if (shouldUseImplementors) KnownImplementor else NoImplementors

/**
* Replaces an interface type with its implementor type
* if there is the unique implementor in bean definitions.
*/
override fun replaceTypeIfNeeded(type: RefType): ClassId? =
if (type.isAbstractType) {
springInjectedClasses.singleOrNull { it.isSubtypeOf(type.id) }
} else {
null
}

override fun avoidSpeculativeNotNullChecks(field: SootField): Boolean = false

/**
* In Spring applications we can mark as speculatively not null
* fields if they are mocked and injecting into class under test so on.
*
* Fields are not mocked if their actual type is obtained from [springInjectedClasses].
*
*/
override fun speculativelyCannotProduceNullPointerException(
field: SootField,
classUnderTest: ClassId,
): Boolean = field.fieldId in classUnderTest.allDeclaredFieldIds && field.type.classId !in allInjectedTypes

override fun preventsFurtherTestGeneration(): Boolean =
super.preventsFurtherTestGeneration() || springContextLoadingResult?.contextLoaded == false

override fun getErrors(): List<UtError> =
springContextLoadingResult?.exceptions?.map { exception ->
UtError(
"Failed to load Spring application context",
exception
)
}.orEmpty() + super.getErrors()

fun getBeansAssignableTo(classId: ClassId): List<BeanDefinitionData> = beanDefinitions.filter { beanDef ->
// some bean classes may fail to load
runCatching {
val beanClass = ClassId(beanDef.beanTypeName).jClass
classId.jClass.isAssignableFrom(beanClass)
}.getOrElse { false }
}
}

enum class SpringTestType(
override val id: String,
override val displayName: String,
Expand Down
8 changes: 4 additions & 4 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import kotlinx.collections.immutable.persistentListOf
import org.utbot.common.nameOfPackage
import org.utbot.engine.types.OBJECT_TYPE
import org.utbot.engine.util.mockListeners.MockListenerController
import org.utbot.framework.plugin.api.ApplicationContext
import org.utbot.framework.context.MockerContext
import org.utbot.framework.plugin.api.util.isInaccessibleViaReflection
import soot.BooleanType
import soot.RefType
Expand Down Expand Up @@ -168,7 +168,7 @@ class Mocker(
private val hierarchy: Hierarchy,
chosenClassesToMockAlways: Set<ClassId>,
internal val mockListenerController: MockListenerController? = null,
private val applicationContext: ApplicationContext,
private val mockerContext: MockerContext,
) {
private val mocksAreDesired: Boolean = strategy != MockStrategy.NO_MOCKS

Expand Down Expand Up @@ -227,10 +227,10 @@ class Mocker(

val mockingIsPossible = when (mockInfo) {
is UtFieldMockInfo,
is UtObjectMockInfo -> applicationContext.mockFrameworkInstalled
is UtObjectMockInfo -> mockerContext.mockFrameworkInstalled
is UtNewInstanceMockInfo,
is UtStaticMethodMockInfo,
is UtStaticObjectMockInfo -> applicationContext.staticsMockingIsConfigured
is UtStaticObjectMockInfo -> mockerContext.staticsMockingIsConfigured
}
val mockingIsForcedAndPossible = mockAlways(mockedValue.type) && mockingIsPossible

Expand Down
21 changes: 10 additions & 11 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ import org.utbot.framework.UtSettings
import org.utbot.framework.UtSettings.preferredCexOption
import org.utbot.framework.UtSettings.substituteStaticsWithSymbolicVariable
import org.utbot.framework.isFromTrustedLibrary
import org.utbot.framework.plugin.api.ApplicationContext
import org.utbot.framework.context.ApplicationContext
import org.utbot.framework.context.NonNullSpeculator
import org.utbot.framework.context.TypeReplacer
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.ExecutableId
import org.utbot.framework.plugin.api.FieldId
Expand Down Expand Up @@ -239,7 +241,8 @@ class Traverser(
internal val typeResolver: TypeResolver,
private val globalGraph: InterProceduralUnitGraph,
private val mocker: Mocker,
private val applicationContext: ApplicationContext,
private val typeReplacer: TypeReplacer,
private val nonNullSpeculator: NonNullSpeculator,
private val taintContext: TaintContext,
) : UtContextInitializer() {

Expand Down Expand Up @@ -1393,8 +1396,8 @@ class Traverser(
// However, if we have the restriction on implementor type (it may be obtained
// from Spring bean definitions, for example), we can just create a symbolic object
// with hard constraint on the mentioned type.
val replacedClassId = when (applicationContext.typeReplacementMode) {
KnownImplementor -> applicationContext.replaceTypeIfNeeded(type)
val replacedClassId = when (typeReplacer.typeReplacementMode) {
KnownImplementor -> typeReplacer.replaceTypeIfNeeded(type)
AnyImplementor,
NoImplementors -> null
}
Expand Down Expand Up @@ -1512,7 +1515,7 @@ class Traverser(
return createMockedObject(addr, type, mockInfoGenerator, nullEqualityConstraint)
}

val concreteImplementation: Concrete? = when (applicationContext.typeReplacementMode) {
val concreteImplementation: Concrete? = when (typeReplacer.typeReplacementMode) {
AnyImplementor -> findConcreteImplementation(addr, type, typeHardConstraint)

// If our type is not abstract, both in `KnownImplementors` and `NoImplementors` mode,
Expand Down Expand Up @@ -2336,12 +2339,8 @@ class Traverser(
* See more detailed documentation in [ApplicationContext] mentioned methods.
*/
private fun checkAndMarkLibraryFieldSpeculativelyNotNull(field: SootField, createdField: SymbolicValue) {
if (applicationContext.avoidSpeculativeNotNullChecks(field) ||
!applicationContext.speculativelyCannotProduceNullPointerException(field, methodUnderTest.classId)) {
return
}

markAsSpeculativelyNotNull(createdField.addr)
if (nonNullSpeculator.speculativelyCannotProduceNullPointerException(field, methodUnderTest.classId))
markAsSpeculativelyNotNull(createdField.addr)
}

private fun createArray(pName: String, type: ArrayType): ArrayValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import org.utbot.framework.UtSettings.pathSelectorStepsLimit
import org.utbot.framework.UtSettings.pathSelectorType
import org.utbot.framework.UtSettings.processUnknownStatesDuringConcreteExecution
import org.utbot.framework.UtSettings.useDebugVisualization
import org.utbot.framework.context.ApplicationContext
import org.utbot.framework.context.spring.SpringApplicationContext
import org.utbot.framework.plugin.api.*
import org.utbot.framework.plugin.api.Step
import org.utbot.framework.plugin.api.util.*
Expand All @@ -52,6 +54,7 @@ import org.utbot.instrumentation.getRelevantSpringRepositories
import org.utbot.instrumentation.instrumentation.Instrumentation
import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData
import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult
import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation
import org.utbot.taint.*
import org.utbot.taint.model.TaintConfiguration
import soot.jimple.Stmt
Expand Down Expand Up @@ -115,10 +118,11 @@ class UtBotSymbolicEngine(
val mockStrategy: MockStrategy = NO_MOCKS,
chosenClassesToMockAlways: Set<ClassId>,
val applicationContext: ApplicationContext,
executionInstrumentation: Instrumentation<UtConcreteExecutionResult>,
executionInstrumentationFactory: UtExecutionInstrumentation.Factory<*>,
userTaintConfigurationProvider: TaintConfigurationProvider? = null,
private val solverTimeoutInMillis: Int = checkSolverTimeoutMillis,
) : UtContextInitializer() {

private val graph = methodUnderTest.sootMethod.jimpleBody().apply {
logger.trace { "JIMPLE for $methodUnderTest:\n$this" }
}.graph()
Expand All @@ -141,7 +145,7 @@ class UtBotSymbolicEngine(
hierarchy,
chosenClassesToMockAlways,
MockListenerController(controller),
applicationContext = applicationContext,
mockerContext = applicationContext.mockerContext,
)

fun attachMockListener(mockListener: MockListener) = mocker.mockListenerController?.attach(mockListener)
Expand Down Expand Up @@ -177,7 +181,8 @@ class UtBotSymbolicEngine(
typeResolver,
globalGraph,
mocker,
applicationContext,
applicationContext.typeReplacer,
applicationContext.nonNullSpeculator,
taintContext,
)

Expand All @@ -186,7 +191,7 @@ class UtBotSymbolicEngine(

private val concreteExecutor =
ConcreteExecutor(
executionInstrumentation,
executionInstrumentationFactory,
classpath,
).apply { this.classLoader = utContext.classLoader }

Expand Down
Loading