Skip to content

Introduce Spring integration tests #2205

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

Merged
merged 2 commits into from
Jun 1, 2023
Merged
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
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ if (goIde.split(",").contains(ideType)) {
}

include("utbot-spring-analyzer")
include("utbot-spring-commons")
35 changes: 35 additions & 0 deletions utbot-core/src/main/kotlin/org/utbot/common/JarUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.utbot.common

import java.io.File
import java.net.URL
import java.nio.file.Files
import java.nio.file.StandardCopyOption

object JarUtils {
private const val UNKNOWN_MODIFICATION_TIME = 0L

fun extractJarFileFromResources(jarFileName: String, jarResourcePath: String, targetDirectoryName: String): File {
val targetDirectory =
Files.createDirectories(utBotTempDirectory.toFile().resolve(targetDirectoryName).toPath()).toFile()
return targetDirectory.resolve(jarFileName).also { jarFile ->
val resource = this::class.java.classLoader.getResource(jarResourcePath)
?: error("Unable to find \"$jarResourcePath\" in resources, make sure it's on the classpath")
updateJarIfRequired(jarFile, resource)
}
}

private fun updateJarIfRequired(jarFile: File, resource: URL) {
val resourceConnection = resource.openConnection()
resourceConnection.getInputStream().use { inputStream ->
val lastResourceModification = resourceConnection.lastModified
if (
!jarFile.exists() ||
jarFile.lastModified() == UNKNOWN_MODIFICATION_TIME ||
lastResourceModification == UNKNOWN_MODIFICATION_TIME ||
jarFile.lastModified() < lastResourceModification
) {
Files.copy(inputStream, jarFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ open class EnvironmentModels(
operator fun component1(): UtModel? = thisInstance
operator fun component2(): List<UtModel> = parameters
operator fun component3(): Map<FieldId, UtModel> = statics

fun copy(
thisInstance: UtModel? = this.thisInstance,
parameters: List<UtModel> = this.parameters,
statics: Map<FieldId, UtModel> = this.statics
) = EnvironmentModels(thisInstance, parameters, statics)
}

/**
Expand Down Expand Up @@ -637,6 +643,46 @@ class UtLambdaModel(
}
}

abstract class UtAutowiredBaseModel(
override val id: Int?,
override val classId: ClassId,
val origin: UtModel,
modelName: String
) : UtReferenceModel(
id, classId, modelName
)

class UtAutowiredStateBeforeModel(
id: Int?,
classId: ClassId,
origin: UtModel,
val beanName: String,
val repositoriesContent: List<RepositoryContentModel>,
) : UtAutowiredBaseModel(
id, classId, origin, modelName = "@Autowired $beanName#$id"
)

data class RepositoryContentModel(
val repositoryBeanName: String,
val entityModels: List<UtModel>,
)

class UtAutowiredStateAfterModel(
id: Int?,
classId: ClassId,
origin: UtModel,
val repositoryInteractions: List<RepositoryInteractionModel>,
) : UtAutowiredBaseModel(
id, classId, origin, modelName = "@Autowired ${classId.name}#$id"
)

data class RepositoryInteractionModel(
val beanName: String,
val executableId: ExecutableId,
val args: List<UtModel>,
val result: UtExecutionResult
)

/**
* Model for a step to obtain [UtAssembleModel].
*/
Expand Down Expand Up @@ -1259,18 +1305,46 @@ open class ApplicationContext(
): Boolean = field.isFinal || !field.isPublic
}

sealed class TypeReplacementApproach {
/**
* Do not replace interfaces and abstract classes with concrete implementors.
* Use mocking instead of it.
*/
object DoNotReplace : TypeReplacementApproach()

/**
* Try to replace interfaces and abstract classes with concrete implementors
* obtained from bean definitions.
* If it is impossible, use mocking.
*
* Currently used in Spring applications only.
*/
class ReplaceIfPossible(val config: String) : TypeReplacementApproach()
}

/**
* 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,
private val beanDefinitions: List<BeanDefinitionData> = emptyList(),
val beanDefinitions: List<BeanDefinitionData> = emptyList(),
private val shouldUseImplementors: Boolean,
val typeReplacementApproach: TypeReplacementApproach,
val testType: SpringTestsType
): ApplicationContext(mockInstalled, staticsMockingIsConfigured) {

companion object {
Expand Down
5 changes: 5 additions & 0 deletions utbot-framework/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
configurations {
fetchSpringAnalyzerJar
fetchInstrumentationJar
}

dependencies {
Expand Down Expand Up @@ -41,10 +42,14 @@ dependencies {
implementation project(':utbot-spring-analyzer')

fetchSpringAnalyzerJar project(path: ':utbot-spring-analyzer', configuration: 'springAnalyzerJar')
fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive')
}

processResources {
from(configurations.fetchSpringAnalyzerJar) {
into "lib"
}
from(configurations.fetchInstrumentationJar) {
into "lib"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ import org.utbot.framework.util.graph
import org.utbot.framework.util.sootMethod
import org.utbot.fuzzer.*
import org.utbot.fuzzing.*
import org.utbot.fuzzing.providers.AutowiredValueProvider
import org.utbot.fuzzing.type.factories.SimpleFuzzedTypeFactory
import org.utbot.fuzzing.type.factories.SpringFuzzedTypeFactory
import org.utbot.fuzzing.utils.Trie
import org.utbot.instrumentation.ConcreteExecutor
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 soot.jimple.Stmt
import soot.tagkit.ParamNamesTag
import java.lang.reflect.Method
Expand Down Expand Up @@ -105,7 +108,8 @@ class UtBotSymbolicEngine(
dependencyPaths: String,
val mockStrategy: MockStrategy = NO_MOCKS,
chosenClassesToMockAlways: Set<ClassId>,
applicationContext: ApplicationContext,
val applicationContext: ApplicationContext,
executionInstrumentation: Instrumentation<UtConcreteExecutionResult>,
private val solverTimeoutInMillis: Int = checkSolverTimeoutMillis
) : UtContextInitializer() {
private val graph = methodUnderTest.sootMethod.jimpleBody().apply {
Expand Down Expand Up @@ -154,7 +158,7 @@ class UtBotSymbolicEngine(

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

Expand Down Expand Up @@ -355,7 +359,29 @@ class UtBotSymbolicEngine(
methodUnderTest,
collectConstantsForFuzzer(graph),
names,
listOf(transform(ValueProvider.of(defaultValueProviders(defaultIdGenerator))))
listOf(transform(ValueProvider.of(defaultValueProviders(defaultIdGenerator)))),
fuzzedTypeFactory = when (applicationContext) {
is SpringApplicationContext -> when (applicationContext.typeReplacementApproach) {
is TypeReplacementApproach.ReplaceIfPossible -> SpringFuzzedTypeFactory(
autowiredValueProvider = AutowiredValueProvider(
defaultIdGenerator,
autowiredModelOriginCreator = { beanName ->
runBlocking {
logger.info { "Getting bean: $beanName" }
concreteExecutor.withProcess { getBean(beanName) }
}
}
),
beanNamesFinder = { classId ->
applicationContext.beanDefinitions
.filter { it.beanTypeFqn == classId.name }
.map { it.beanName }
}
)
is TypeReplacementApproach.DoNotReplace -> SimpleFuzzedTypeFactory()
}
else -> SimpleFuzzedTypeFactory()
},
) { thisInstance, descr, values ->
if (thisInstance?.model is UtNullModel) {
// We should not try to run concretely any models with null-this.
Expand Down Expand Up @@ -595,7 +621,7 @@ private fun ResolvedModels.constructStateForMethod(methodUnderTest: ExecutableId
return EnvironmentModels(thisInstanceBefore, paramsBefore, statics)
}

private suspend fun ConcreteExecutor<UtConcreteExecutionResult, UtExecutionInstrumentation>.executeConcretely(
private suspend fun ConcreteExecutor<UtConcreteExecutionResult, Instrumentation<UtConcreteExecutionResult>>.executeConcretely(
methodUnderTest: ExecutableId,
stateBefore: EnvironmentModels,
instrumentation: List<UtInstrumentation>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.utbot.framework.assemble

import mu.KotlinLogging
import org.utbot.common.isPrivate
import org.utbot.common.isPublic
import org.utbot.engine.ResolvedExecution
Expand Down Expand Up @@ -55,6 +56,10 @@ import java.util.IdentityHashMap
*/
class AssembleModelGenerator(private val basePackageName: String) {

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

//Instantiated models are stored to avoid cyclic references during reference graph analysis
private val instantiatedModels: IdentityHashMap<UtModel, UtReferenceModel> =
IdentityHashMap<UtModel, UtReferenceModel>()
Expand Down Expand Up @@ -175,8 +180,13 @@ class AssembleModelGenerator(private val basePackageName: String) {
private fun assembleModel(utModel: UtModel): UtModel {
val collectedCallChain = callChain.toMutableList()

// We cannot create an assemble model for an anonymous class instance
if (utModel.classId.isAnonymous) {
try {
// We cannot create an assemble model for an anonymous class instance
if (utModel.classId.isAnonymous) {
return utModel
}
} catch (e: ClassNotFoundException) {
// happens, for example, when `utModel.classId.name` is something like "jdk.proxy3.$Proxy144"
return utModel
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,23 +688,6 @@ enum class ProjectType {
JavaScript,
}

sealed class TypeReplacementApproach {
/**
* Do not replace interfaces and abstract classes with concrete implementors.
* Use mocking instead of it.
*/
object DoNotReplace : TypeReplacementApproach()

/**
* Try to replace interfaces and abstract classes with concrete implementors
* obtained from bean definitions.
* If it is impossible, use mocking.
*
* Currently used in Spring applications only.
*/
class ReplaceIfPossible(val config: String) : TypeReplacementApproach()
}

abstract class DependencyInjectionFramework(
override val id: String,
override val displayName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte
when (methodType) {
SUCCESSFUL -> error("Unexpected successful without exception method type for execution with exception $expectedException")
PASSED_EXCEPTION -> {
// TODO consider rendering message in a comment
// expectedException.message?.let { +comment(it) }
testFrameworkManager.expectException(expectedException::class.id) {
methodInvocationBlock()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,38 @@ import org.utbot.framework.codegen.domain.context.CgContext
import org.utbot.framework.codegen.domain.models.CgValue
import org.utbot.framework.codegen.domain.models.CgVariable
import org.utbot.framework.plugin.api.UtAssembleModel
import org.utbot.framework.plugin.api.UtAutowiredBaseModel
import org.utbot.framework.plugin.api.UtAutowiredStateBeforeModel
import org.utbot.framework.plugin.api.UtCompositeModel
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.isMockModel
import org.utbot.framework.plugin.api.util.jClass

class CgSpringVariableConstructor(context: CgContext) : CgVariableConstructor(context) {
val injectedMocksModelsVariables: MutableSet<UtModelWrapper> = mutableSetOf()
val mockedModelsVariables: MutableSet<UtModelWrapper> = mutableSetOf()

override fun getOrCreateVariable(model: UtModel, name: String?): CgValue {
if (model is UtAutowiredBaseModel) {
// TODO also, properly render corresponding @Autowired field(s), and make sure name isn't taken
if (model is UtAutowiredStateBeforeModel) {
comment("begin repository fill up")
model.repositoriesContent.forEach { repositoryContent ->
repositoryContent.entityModels.forEach { entity ->
// TODO actually fill up repositories
getOrCreateVariable(entity)
emptyLine()
}
}
comment("end repository fill up")
}
emptyLine()
return CgVariable(
name ?: model.classId.jClass.simpleName.let { it[0].lowercase() + it.drop(1) },
model.classId
)
}

val alreadyCreatedInjectMocks = findCgValueByModel(model, injectedMocksModelsVariables)
if (alreadyCreatedInjectMocks != null) {
val fields: Collection<UtModel> = when (model) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.MissingState
import org.utbot.framework.plugin.api.UtArrayModel
import org.utbot.framework.plugin.api.UtAssembleModel
import org.utbot.framework.plugin.api.UtAutowiredBaseModel
import org.utbot.framework.plugin.api.UtClassRefModel
import org.utbot.framework.plugin.api.UtCompositeModel
import org.utbot.framework.plugin.api.UtEnumConstantModel
Expand Down Expand Up @@ -237,6 +238,10 @@ private class FieldStateVisitor : UtModelVisitor<FieldData>() {
recordFieldState(data, element)
}

override fun visit(element: UtAutowiredBaseModel, data: FieldData) {
element.origin.accept(this, data)
}

private fun recordFieldState(data: FieldData, model: UtModel) {
val fields = data.fields
val path = data.path
Expand Down
Loading