Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,13 @@ enum class TypeReplacementMode {
NoImplementors,
}

interface CodeGenerationContext

interface SpringCodeGenerationContext : CodeGenerationContext {
val springTestType: SpringTestType
val springSettings: SpringSettings
}

/**
* A context to use when no specific data is required.
*
Expand All @@ -1332,7 +1339,7 @@ enum class TypeReplacementMode {
open class ApplicationContext(
val mockFrameworkInstalled: Boolean = true,
staticsMockingIsConfigured: Boolean = true,
) {
) : CodeGenerationContext {
var staticsMockingIsConfigured = staticsMockingIsConfigured
private set

Expand Down Expand Up @@ -1384,21 +1391,26 @@ 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()
sealed interface SpringConfiguration {
class JavaConfiguration(val classBinaryName: String) : SpringConfiguration
class XMLConfiguration(val absolutePath: String) : SpringConfiguration
}

/**
* 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()
sealed interface SpringSettings {
class AbsentSpringSettings : SpringSettings {
// Denotes no configuration and no profile setting

// NOTICE:
// `class` should not be replaced with `object`
// in order to avoid issues caused by Kryo deserialization
// that creates new instances breaking `when` expressions
// that check reference equality instead of type equality
}

class PresentSpringSettings(
val configuration: SpringConfiguration,
val profiles: Array<String>
) : SpringSettings
}

/**
Expand All @@ -1422,9 +1434,9 @@ class SpringApplicationContext(
staticsMockingIsConfigured: Boolean,
val beanDefinitions: List<BeanDefinitionData> = emptyList(),
private val shouldUseImplementors: Boolean,
val typeReplacementApproach: TypeReplacementApproach,
val testType: SpringTestsType
): ApplicationContext(mockInstalled, staticsMockingIsConfigured) {
override val springTestType: SpringTestType,
override val springSettings: SpringSettings,
): ApplicationContext(mockInstalled, staticsMockingIsConfigured), SpringCodeGenerationContext {

companion object {
private val logger = KotlinLogging.logger {}
Expand All @@ -1436,10 +1448,9 @@ class SpringApplicationContext(
private val springInjectedClasses: Set<ClassId>
get() {
if (!areInjectedClassesInitialized) {
// TODO: use more info from SpringBeanDefinitionData than beanTypeFqn offers here
for (beanFqn in beanDefinitions.map { it.beanTypeFqn }) {
for (beanTypeName in beanDefinitions.map { it.beanTypeName }) {
try {
val beanClass = utContext.classLoader.loadClass(beanFqn)
val beanClass = utContext.classLoader.loadClass(beanTypeName)
if (!beanClass.isAbstract && !beanClass.isInterface &&
!beanClass.isLocalClass && (!beanClass.isMemberClass || beanClass.isStatic)) {
springInjectedClassesStorage += beanClass.id
Expand All @@ -1449,7 +1460,7 @@ class SpringApplicationContext(
// 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 $beanFqn (${e.message})" }
logger.warn { "Failed to load bean class for $beanTypeName (${e.message})" }

else -> throw e
}
Expand Down Expand Up @@ -1500,19 +1511,19 @@ class SpringApplicationContext(
): Boolean = field.fieldId in classUnderTest.allDeclaredFieldIds && field.declaringClass.id !in springInjectedClasses
}

enum class SpringTestsType(
enum class SpringTestType(
override val id: String,
override val displayName: String,
override val description: String,
// Integration tests generation requires spring test framework being installed
var frameworkInstalled: Boolean = false,
var testFrameworkInstalled: Boolean = false,
) : CodeGenerationSettingItem {
UNIT_TESTS(
UNIT_TEST(
"Unit tests",
"Unit tests",
"Generate unit tests mocking other classes"
),
INTEGRATION_TESTS(
INTEGRATION_TEST(
"Integration tests",
"Integration tests",
"Generate integration tests autowiring real instance"
Expand All @@ -1521,19 +1532,21 @@ enum class SpringTestsType(
override fun toString() = id

companion object : CodeGenerationSettingBox {
override val defaultItem = UNIT_TESTS
override val allItems: List<SpringTestsType> = values().toList()
override val defaultItem = UNIT_TEST
override val allItems: List<SpringTestType> = values().toList()
}
}

/**
* Describes information about beans obtained from Spring analysis process.
*
* Contains the name of the bean, its type (class or interface) and optional additional data.
*
* @param beanTypeName a name in a form obtained by [java.lang.Class.getName] method.
*/
data class BeanDefinitionData(
val beanName: String,
val beanTypeFqn: String,
val beanTypeName: String,
val additionalData: BeanAdditionalData?,
)

Expand All @@ -1542,11 +1555,13 @@ data class BeanDefinitionData(
*
* Sometimes the actual type of the bean can not be obtained from bean definition.
* Then we try to recover it by method and class defining bean (e.g. using Psi elements).
*
* @param configClassName a name in a form obtained by [java.lang.Class.getName] method.
*/
data class BeanAdditionalData(
val factoryMethodName: String,
val parameterTypes: List<String>,
val configClassFqn: String,
val configClassName: String,
)

val RefType.isAbstractType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ object SpringModelUtils {
val springBootTestContextBootstrapperClassId =
ClassId("org.springframework.boot.test.context.SpringBootTestContextBootstrapper")

val activeProfilesClassId = ClassId("org.springframework.test.context.ActiveProfiles")
val contextConfigurationClassId = ClassId("org.springframework.test.context.ContextConfiguration")


// most likely only one persistent library is on the classpath, but we need to be able to work with either of them
private val persistentLibraries = listOf("javax.persistence", "jakarta.persistence")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ inline fun <T> withStaticsSubstitutionRequired(condition: Boolean, block: () ->
} finally {
UtSettings.substituteStaticsWithSymbolicVariable = standardSubstitutionSetting
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.utbot.framework.plugin.api.util.*
import org.utbot.framework.util.convertToAssemble
import org.utbot.framework.util.graph
import org.utbot.framework.util.sootMethod
import org.utbot.framework.plugin.api.SpringSettings.*
import org.utbot.fuzzer.*
import org.utbot.fuzzing.*
import org.utbot.fuzzing.providers.FieldValueProvider
Expand Down Expand Up @@ -388,7 +389,7 @@ class UtBotSymbolicEngine(
var testEmittedByFuzzer = 0
val valueProviders = ValueProvider.of(defaultValueProviders(defaultIdGenerator))
.letIf(applicationContext is SpringApplicationContext
&& applicationContext.typeReplacementApproach is TypeReplacementApproach.ReplaceIfPossible
&& applicationContext.springSettings is PresentSpringSettings
) { provider ->
val relevantRepositories = concreteExecutor.getRelevantSpringRepositories(methodUnderTest.classId)
logger.info { "Detected relevant repositories for class ${methodUnderTest.classId}: $relevantRepositories" }
Expand All @@ -412,7 +413,7 @@ class UtBotSymbolicEngine(
defaultIdGenerator,
beanNameProvider = { classId ->
(applicationContext as SpringApplicationContext).beanDefinitions
.filter { it.beanTypeFqn == classId.name }
.filter { it.beanTypeName == classId.name }
.map { it.beanName }
},
relevantRepositories = relevantRepositories
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ class SpringTestClassModel(
val springSpecificInformation: SpringSpecificInformation,
): TestClassModel(classUnderTest, methodTestSets, nestedClasses)


class SpringSpecificInformation(
val thisInstanceModels: TypedModelWrappers = mapOf(),
val thisInstanceDependentMocks: TypedModelWrappers = mapOf(),
val autowiredFromContextModels: TypedModelWrappers = mapOf(),
val thisInstanceModels: TypedModelWrappers,
val thisInstanceDependentMocks: TypedModelWrappers,
val autowiredFromContextModels: TypedModelWrappers,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.CodegenLanguage
import org.utbot.framework.plugin.api.ExecutableId
import org.utbot.framework.plugin.api.MockFramework
import org.utbot.framework.plugin.api.SpringTestsType
import org.utbot.framework.plugin.api.SpringTestType
import org.utbot.framework.plugin.api.SpringCodeGenerationContext
import org.utbot.framework.plugin.api.SpringSettings.*

class SpringCodeGenerator(
private val springTestsType: SpringTestsType = SpringTestsType.defaultItem,
val classUnderTest: ClassId,
val projectType: ProjectType,
val codeGenerationContext: SpringCodeGenerationContext,
paramNames: MutableMap<ExecutableId, List<String>> = mutableMapOf(),
generateUtilClassFile: Boolean = false,
testFramework: TestFramework = TestFramework.defaultItem,
Expand Down Expand Up @@ -59,9 +61,13 @@ class SpringCodeGenerator(
val testClassModel = SpringTestClassModelBuilder(context).createTestClassModel(classUnderTest, testSets)

logger.info { "Code generation phase started at ${now()}" }
val astConstructor = when (springTestsType) {
SpringTestsType.UNIT_TESTS -> CgSpringUnitTestClassConstructor(context)
SpringTestsType.INTEGRATION_TESTS -> CgSpringIntegrationTestClassConstructor(context)
val astConstructor = when (codeGenerationContext.springTestType) {
SpringTestType.UNIT_TEST -> CgSpringUnitTestClassConstructor(context)
SpringTestType.INTEGRATION_TEST ->
when (val settings = codeGenerationContext.springSettings) {
is PresentSpringSettings -> CgSpringIntegrationTestClassConstructor(context, settings)
is AbsentSpringSettings -> error("No Spring settings were provided for Spring integration test generation.")
}
}
val testClassFile = astConstructor.construct(testClassModel)
logger.info { "Code generation phase finished at ${now()}" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):

protected val variableConstructor: CgSpringVariableConstructor =
CgComponents.getVariableConstructorBy(context) as CgSpringVariableConstructor
protected val statementConstructor: CgStatementConstructor = CgComponents.getStatementConstructorBy(context)

override fun constructTestClassBody(testClassModel: SpringTestClassModel): CgClassBody {
return buildClassBody(currentTestClass) {
Expand Down Expand Up @@ -88,7 +87,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
annotationClassId: ClassId,
groupedModelsByClassId: TypedModelWrappers,
): List<CgFieldDeclaration> {
val annotation = statementConstructor.addAnnotation(annotationClassId, Field)
val annotation = addAnnotation(annotationClassId, Field)

val constructedDeclarations = mutableListOf<CgFieldDeclaration>()
for ((classId, listOfUtModels) in groupedModelsByClassId) {
Expand Down Expand Up @@ -133,7 +132,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
}

protected fun constructBeforeMethod(statements: List<CgStatement>): CgFrameworkUtilMethod {
val beforeAnnotation = statementConstructor.addAnnotation(context.testFramework.beforeMethodId, Method)
val beforeAnnotation = addAnnotation(context.testFramework.beforeMethodId, Method)
return CgFrameworkUtilMethod(
name = "setUp",
statements = statements,
Expand All @@ -143,7 +142,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
}

protected fun constructAfterMethod(statements: List<CgStatement>): CgFrameworkUtilMethod {
val afterAnnotation = statementConstructor.addAnnotation(context.testFramework.afterMethodId, Method)
val afterAnnotation = addAnnotation(context.testFramework.afterMethodId, Method)
return CgFrameworkUtilMethod(
name = "tearDown",
statements = statements,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,96 @@ import org.utbot.framework.codegen.domain.TestNg
import org.utbot.framework.codegen.domain.context.CgContext
import org.utbot.framework.codegen.domain.models.*
import org.utbot.framework.codegen.domain.models.AnnotationTarget.*
import org.utbot.framework.codegen.util.resolve
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.CodegenLanguage
import org.utbot.framework.plugin.api.SpringSettings.*
import org.utbot.framework.plugin.api.SpringConfiguration.*
import org.utbot.framework.plugin.api.util.SpringModelUtils
import org.utbot.framework.plugin.api.util.SpringModelUtils.activeProfilesClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.autoConfigureTestDbClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.autowiredClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.bootstrapWithClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.contextConfigurationClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassModeClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.springBootTestContextBootstrapperClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.springExtensionClassId
import org.utbot.framework.plugin.api.util.SpringModelUtils.transactionalClassId
import org.utbot.framework.plugin.api.util.utContext

class CgSpringIntegrationTestClassConstructor(context: CgContext) : CgAbstractSpringTestClassConstructor(context) {
class CgSpringIntegrationTestClassConstructor(
context: CgContext,
private val springSettings: PresentSpringSettings
) : CgAbstractSpringTestClassConstructor(context) {
override fun constructTestClass(testClassModel: SpringTestClassModel): CgClass {
collectSpringSpecificAnnotations()
addNecessarySpringSpecificAnnotations()
return super.constructTestClass(testClassModel)
}

override fun constructClassFields(testClassModel: SpringTestClassModel): List<CgFieldDeclaration> {
val autowiredFromContextModels = testClassModel.springSpecificInformation.autowiredFromContextModels
val autowiredFromContextModels =
testClassModel.springSpecificInformation.autowiredFromContextModels
return constructFieldsWithAnnotation(autowiredClassId, autowiredFromContextModels)
}

override fun constructAdditionalMethods() = CgMethodsCluster(header = null, content = emptyList())
override fun constructAdditionalMethods() =
CgMethodsCluster(header = null, content = emptyList())

private fun collectSpringSpecificAnnotations() {
private fun addNecessarySpringSpecificAnnotations() {
val springRunnerType = when (testFramework) {
Junit4 -> SpringModelUtils.runWithClassId
Junit5 -> SpringModelUtils.extendWithClassId
TestNg -> error("Spring extension is not implemented in TestNg")
else -> error("Trying to generate tests for Spring project with non-JVM framework")
}

statementConstructor.addAnnotation(
addAnnotation(
classId = springRunnerType,
argument = createGetClassExpression(springExtensionClassId, codegenLanguage),
target = Class,
)
statementConstructor.addAnnotation(
addAnnotation(
classId = bootstrapWithClassId,
argument = createGetClassExpression(springBootTestContextBootstrapperClassId, codegenLanguage),
target = Class,
)

addAnnotation(
classId = activeProfilesClassId,
namedArguments =
listOf(
CgNamedAnnotationArgument(
name = "profiles",
value =
CgArrayAnnotationArgument(
springSettings.profiles.map { profile ->
profile.resolve()
}
)
)
),
target = Class,
)
addAnnotation(
classId = contextConfigurationClassId,
namedArguments =
listOf(
CgNamedAnnotationArgument(
name = "classes",
value = CgArrayAnnotationArgument(
listOf(
createGetClassExpression(
// TODO:
// For now we support only JavaConfigurations in integration tests.
// Adapt for XMLConfigurations when supported.
ClassId((springSettings.configuration as JavaConfiguration).classBinaryName),
codegenLanguage
)
)
)
)
),
target = Class,
)
addAnnotation(
classId = dirtiesContextClassId,
namedArguments = listOf(
Expand Down
Loading