diff --git a/settings.gradle.kts b/settings.gradle.kts index f5a36b11a5..ac97ba4b05 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -71,3 +71,4 @@ if (goIde.split(",").contains(ideType)) { } include("utbot-spring-analyzer") +include("utbot-spring-commons") diff --git a/utbot-core/src/main/kotlin/org/utbot/common/JarUtils.kt b/utbot-core/src/main/kotlin/org/utbot/common/JarUtils.kt new file mode 100644 index 0000000000..ac7a9e630c --- /dev/null +++ b/utbot-core/src/main/kotlin/org/utbot/common/JarUtils.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt index 483ac079b7..16704e4e00 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt @@ -241,6 +241,12 @@ open class EnvironmentModels( operator fun component1(): UtModel? = thisInstance operator fun component2(): List = parameters operator fun component3(): Map = statics + + fun copy( + thisInstance: UtModel? = this.thisInstance, + parameters: List = this.parameters, + statics: Map = this.statics + ) = EnvironmentModels(thisInstance, parameters, statics) } /** @@ -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, +) : UtAutowiredBaseModel( + id, classId, origin, modelName = "@Autowired $beanName#$id" +) + +data class RepositoryContentModel( + val repositoryBeanName: String, + val entityModels: List, +) + +class UtAutowiredStateAfterModel( + id: Int?, + classId: ClassId, + origin: UtModel, + val repositoryInteractions: List, +) : UtAutowiredBaseModel( + id, classId, origin, modelName = "@Autowired ${classId.name}#$id" +) + +data class RepositoryInteractionModel( + val beanName: String, + val executableId: ExecutableId, + val args: List, + val result: UtExecutionResult +) + /** * Model for a step to obtain [UtAssembleModel]. */ @@ -1259,6 +1305,23 @@ 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. @@ -1266,11 +1329,22 @@ open class ApplicationContext( * @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 +// - 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 = emptyList(), + val beanDefinitions: List = emptyList(), private val shouldUseImplementors: Boolean, + val typeReplacementApproach: TypeReplacementApproach, + val testType: SpringTestsType ): ApplicationContext(mockInstalled, staticsMockingIsConfigured) { companion object { diff --git a/utbot-framework/build.gradle b/utbot-framework/build.gradle index a028d51d67..5d40d4effb 100644 --- a/utbot-framework/build.gradle +++ b/utbot-framework/build.gradle @@ -1,5 +1,6 @@ configurations { fetchSpringAnalyzerJar + fetchInstrumentationJar } dependencies { @@ -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" + } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt index 7033434b5a..0290680a09 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -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 @@ -105,7 +108,8 @@ class UtBotSymbolicEngine( dependencyPaths: String, val mockStrategy: MockStrategy = NO_MOCKS, chosenClassesToMockAlways: Set, - applicationContext: ApplicationContext, + val applicationContext: ApplicationContext, + executionInstrumentation: Instrumentation, private val solverTimeoutInMillis: Int = checkSolverTimeoutMillis ) : UtContextInitializer() { private val graph = methodUnderTest.sootMethod.jimpleBody().apply { @@ -154,7 +158,7 @@ class UtBotSymbolicEngine( private val concreteExecutor = ConcreteExecutor( - UtExecutionInstrumentation, + executionInstrumentation, classpath, ).apply { this.classLoader = utContext.classLoader } @@ -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. @@ -595,7 +621,7 @@ private fun ResolvedModels.constructStateForMethod(methodUnderTest: ExecutableId return EnvironmentModels(thisInstanceBefore, paramsBefore, statics) } -private suspend fun ConcreteExecutor.executeConcretely( +private suspend fun ConcreteExecutor>.executeConcretely( methodUnderTest: ExecutableId, stateBefore: EnvironmentModels, instrumentation: List, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt index 19965f2fb9..f601df46aa 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt @@ -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 @@ -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 = IdentityHashMap() @@ -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 } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt index 11eeefdd95..836f26faba 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt @@ -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, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt index 4d177cc573..725ed1a1dc 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt @@ -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() } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringVariableConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringVariableConstructor.kt index f0497328fc..9501b37234 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringVariableConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringVariableConstructor.kt @@ -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 = mutableSetOf() val mockedModelsVariables: MutableSet = 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 = when (model) { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt index 149261926d..99cc8c889c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt @@ -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 @@ -237,6 +238,10 @@ private class FieldStateVisitor : UtModelVisitor() { 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 diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt index a7346a26c0..02ffcbf4fd 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt @@ -36,6 +36,7 @@ import org.utbot.framework.util.SootUtils import org.utbot.framework.util.jimpleBody import org.utbot.framework.util.toModel import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.execution.SpringUtExecutionInstrumentation import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation import org.utbot.instrumentation.warmup import java.io.File @@ -65,6 +66,19 @@ open class TestCaseGenerator( ) { private val logger: KLogger = KotlinLogging.logger {} private val timeoutLogger: KLogger = KotlinLogging.logger(logger.name + ".timeout") + private val executionInstrumentation by lazy { + when (applicationContext) { + is SpringApplicationContext -> when (val approach = applicationContext.typeReplacementApproach) { + is TypeReplacementApproach.ReplaceIfPossible -> + when (applicationContext.testType) { + SpringTestsType.UNIT_TESTS -> UtExecutionInstrumentation + SpringTestsType.INTEGRATION_TESTS -> SpringUtExecutionInstrumentation(UtExecutionInstrumentation, approach.config) + } + is TypeReplacementApproach.DoNotReplace -> UtExecutionInstrumentation + } + else -> UtExecutionInstrumentation + } + } private val classpathForEngine: String get() = (buildDirs + listOfNotNull(classpath)).joinToString(File.pathSeparator) @@ -88,7 +102,7 @@ open class TestCaseGenerator( if (warmupConcreteExecution) { // force pool to create an appropriate executor ConcreteExecutor( - UtExecutionInstrumentation, + executionInstrumentation, classpathForEngine, ).apply { warmup() @@ -269,6 +283,7 @@ open class TestCaseGenerator( mockStrategy = mockStrategyApi.toModel(), chosenClassesToMockAlways = chosenClassesToMockAlways, applicationContext = applicationContext, + executionInstrumentation = executionInstrumentation, solverTimeoutInMillis = executionTimeEstimator.updatedSolverCheckTimeoutMillis ) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt index e54ad58153..b62ecace4d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestFlow.kt @@ -25,16 +25,6 @@ fun defaultTestFlow(timeout: Long) = testFlow { } } -/** - * Creates default flow for Spring application. - */ -fun defaultSpringFlow(params: GenerateParams) = testFlow { - generationTimeout = params.generationTimeout - isSymbolicEngineEnabled = true - isFuzzingEnabled = params.isFuzzingEnabled - fuzzingValue = params.fuzzingValue -} - /** * Creates default flow that uses [UtSettings] for customization. */ diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt index 9d72c31721..34d24030ad 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt @@ -120,16 +120,11 @@ private fun EngineProcessModel.setup(kryoHelper: KryoHelper, watchdog: IdleWatch val methods: List = kryoHelper.readObject(params.methods) logger.debug() .measureTime({ "starting generation for ${methods.size} methods, starting with ${methods.first()}" }) { - val generateFlow = when (testGenerator.applicationContext) { - is SpringApplicationContext -> defaultSpringFlow(params) - is ApplicationContext -> testFlow { - generationTimeout = params.generationTimeout - isSymbolicEngineEnabled = params.isSymbolicEngineEnabled - isFuzzingEnabled = params.isFuzzingEnabled - fuzzingValue = params.fuzzingValue - } - - else -> error("Unknown application context ${testGenerator.applicationContext}") + val generateFlow = testFlow { + generationTimeout = params.generationTimeout + isSymbolicEngineEnabled = params.isSymbolicEngineEnabled + isFuzzingEnabled = params.isFuzzingEnabled + fuzzingValue = params.fuzzingValue } val result = testGenerator.generate( diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt index 093992d1b7..e3ec8a1862 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/UtModelVisitor.kt @@ -2,6 +2,7 @@ package org.utbot.framework.util 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 @@ -35,6 +36,7 @@ abstract class UtModelVisitor { is UtAssembleModel -> visit(element, data) is UtCompositeModel -> visit(element, data) is UtLambdaModel -> visit(element, data) + is UtAutowiredBaseModel -> visit(element, data) } } @@ -44,6 +46,7 @@ abstract class UtModelVisitor { protected abstract fun visit(element: UtAssembleModel, data: D) protected abstract fun visit(element: UtCompositeModel, data: D) protected abstract fun visit(element: UtLambdaModel, data: D) + protected abstract fun visit(element: UtAutowiredBaseModel, data: D) /** * Returns true when we can traverse the given model. diff --git a/utbot-instrumentation/build.gradle b/utbot-instrumentation/build.gradle deleted file mode 100644 index 21d1e22510..0000000000 --- a/utbot-instrumentation/build.gradle +++ /dev/null @@ -1,64 +0,0 @@ -compileKotlin { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 - } -} - -compileTestKotlin { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 - } -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -dependencies { - implementation project(':utbot-framework-api') - implementation project(':utbot-rd') - - implementation group: 'org.ow2.asm', name: 'asm', version: asmVersion - implementation group: 'org.ow2.asm', name: 'asm-commons', version: asmVersion - implementation group: 'com.esotericsoftware.kryo', name: 'kryo5', version: kryoVersion - // this is necessary for serialization of some collections - implementation group: 'de.javakaffee', name: 'kryo-serializers', version: kryoSerializersVersion - implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion - - implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: rdVersion - implementation group: 'com.jetbrains.rd', name: 'rd-core', version: rdVersion - implementation group: 'net.java.dev.jna', name: 'jna-platform', version: '5.5.0' - - - // TODO: this is necessary for inline classes mocking in UtExecutionInstrumentation - implementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' - implementation group: 'org.mockito', name: 'mockito-inline', version: '4.2.0' -} - -jar { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - - manifest { - attributes ( - 'Main-Class': 'org.utbot.instrumentation.process.InstrumentedProcessMainKt', - 'Premain-Class': 'org.utbot.instrumentation.agent.Agent', - ) - } - - // we need only classes from implementation and utbot to execute instrumented process - dependsOn configurations.compileClasspath - from { - configurations.compileClasspath - .findAll { it.isDirectory() || it.name.endsWith('jar') } - .collect { it.isDirectory() ? it : zipTree(it) } - } -} - -configurations { - instrumentationArchive -} - -artifacts { - instrumentationArchive jar -} \ No newline at end of file diff --git a/utbot-instrumentation/build.gradle.kts b/utbot-instrumentation/build.gradle.kts new file mode 100644 index 0000000000..b7b5def7d0 --- /dev/null +++ b/utbot-instrumentation/build.gradle.kts @@ -0,0 +1,102 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer + +val asmVersion: String by rootProject +val kryoVersion: String by rootProject +val kryoSerializersVersion: String by rootProject +val kotlinLoggingVersion: String by rootProject +val rdVersion: String by rootProject +val springBootVersion: String by rootProject + +plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" + id("java") + application +} + +tasks.compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +tasks.compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +application { + mainClass.set("org.utbot.instrumentation.process.InstrumentedProcessMainKt") +} + +val fetchSpringCommonsJar: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false +} + +dependencies { + implementation(project(":utbot-framework-api")) + implementation(project(":utbot-rd")) + implementation(project(":utbot-spring-commons")) + + implementation("org.ow2.asm:asm:$asmVersion") + implementation("org.ow2.asm:asm-commons:$asmVersion") + implementation("com.esotericsoftware.kryo:kryo5:$kryoVersion") + // this is necessary for serialization of some collections + implementation("de.javakaffee:kryo-serializers:$kryoSerializersVersion") + implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") + + implementation("com.jetbrains.rd:rd-framework:$rdVersion") + implementation("com.jetbrains.rd:rd-core:$rdVersion") + implementation("net.java.dev.jna:jna-platform:5.5.0") + + // TODO: this is necessary for inline classes mocking in UtExecutionInstrumentation + implementation("org.mockito:mockito-core:4.2.0") + implementation("org.mockito:mockito-inline:4.2.0") + + compileOnly("org.springframework.boot:spring-boot:$springBootVersion") + fetchSpringCommonsJar(project(":utbot-spring-commons", configuration = "springCommonsJar")) +} + +/** + * Shadow plugin unpacks the nested `utbot-spring-commons-shadow.jar`. + * But we need it to be packed. Workaround: double-nest the jar. + */ +val shadowJarUnpackWorkaround by tasks.register("shadowBugWorkaround") { + destinationDirectory.set(layout.buildDirectory.dir("build/shadow-bug-workaround")) + from(fetchSpringCommonsJar) { + into("lib") + } +} + +tasks.shadowJar { + dependsOn(shadowJarUnpackWorkaround) + + from(shadowJarUnpackWorkaround) { + into("lib") + } + + manifest { + attributes( + "Main-Class" to "org.utbot.instrumentation.process.InstrumentedProcessMainKt", + "Premain-Class" to "org.utbot.instrumentation.agent.Agent", + ) + } + + transform(Log4j2PluginsCacheFileTransformer::class.java) + archiveFileName.set("utbot-instrumentation-shadow.jar") +} + +val instrumentationArchive: Configuration by configurations.creating { + isCanBeResolved = false + isCanBeConsumed = true +} + +artifacts { + add(instrumentationArchive.name, tasks.shadowJar) +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt index 28607220e3..f89b09cade 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/agent/DynamicClassTransformer.kt @@ -38,8 +38,9 @@ class DynamicClassTransformer : ClassFileTransformer { return if (pathToClassfile in pathsToUserClasses || packsToAlwaysTransform.any(className::startsWith) ) { - logger.info { "Transforming: $className" } - transformer.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer) + transformer.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer)?.also { + logger.info { "Transformed: $className" } + } } else { null } diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt index b2fbe6e0a6..d6bd839734 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/Instrumentation.kt @@ -29,6 +29,8 @@ interface Instrumentation : ClassFileTransformer /** * Will be called in the very beginning in the instrumented process. + * + * Do not call from engine process to avoid unwanted side effects (e.g. Spring context initialization) */ fun init(pathsToUserClasses: Set) {} } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SpringUtExecutionInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SpringUtExecutionInstrumentation.kt new file mode 100644 index 0000000000..786ae3697f --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SpringUtExecutionInstrumentation.kt @@ -0,0 +1,176 @@ +package org.utbot.instrumentation.instrumentation.execution + +import org.utbot.common.JarUtils +import com.jetbrains.rd.util.getLogger +import org.utbot.framework.plugin.api.RepositoryInteractionModel +import org.utbot.framework.plugin.api.UtAutowiredStateAfterModel +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.idOrNull +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.instrumentation.instrumentation.ArgumentList +import org.utbot.instrumentation.instrumentation.Instrumentation +import org.utbot.instrumentation.instrumentation.execution.mock.SpringInstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.phases.ModelConstructionPhase +import org.utbot.instrumentation.process.HandlerClassesLoader +import org.utbot.spring.api.repositoryWrapper.RepositoryInteraction +import java.security.ProtectionDomain +import kotlin.random.Random + +/** + * UtExecutionInstrumentation wrapper that is aware of Spring config and initialises Spring context + */ +class SpringUtExecutionInstrumentation( + private val instrumentation: UtExecutionInstrumentation, + private val springConfig: String +) : Instrumentation by instrumentation { + private lateinit var springContext: Any + + companion object { + private val logger = getLogger() + private const val SPRING_COMMONS_JAR_FILENAME = "utbot-spring-commons-shadow.jar" + } + + override fun init(pathsToUserClasses: Set) { + HandlerClassesLoader.addUrls(listOf(JarUtils.extractJarFileFromResources( + jarFileName = SPRING_COMMONS_JAR_FILENAME, + jarResourcePath = "lib/$SPRING_COMMONS_JAR_FILENAME", + targetDirectoryName = "spring-commons" + ).path)) + + instrumentation.instrumentationContext = object : SpringInstrumentationContext() { + override fun getBean(beanName: String) = + this@SpringUtExecutionInstrumentation.getBean(beanName) + + override fun saveToRepository(repository: Any, entity: Any) = + this@SpringUtExecutionInstrumentation.saveToRepository(repository, entity) + } + + instrumentation.init(pathsToUserClasses) + + val classLoader = utContext.classLoader + Thread.currentThread().contextClassLoader = classLoader + + val primarySources = arrayOf( + classLoader.loadClass(springConfig), + classLoader.loadClass("org.utbot.spring.repositoryWrapper.RepositoryWrapperConfiguration") + ) + + // Setting server.port value to 0 means given Spring to select any appropriate port itself. + // See https://stackoverflow.com/questions/21083170/how-to-configure-port-for-a-spring-boot-application + val args = arrayOf("--server.port=0") + + // TODO if we don't have SpringBoot just create ApplicationContext here, reuse code from utbot-spring-analyzer + // TODO recreate context/app every time whenever we change method under test + val springAppClass = + classLoader.loadClass("org.springframework.boot.SpringApplication") + springContext = springAppClass + .getMethod("run", primarySources::class.java, args::class.java) + .invoke(null, primarySources, args) + } + + override fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any? + ): UtConcreteExecutionResult { + RepositoryInteraction.recordedInteractions.clear() + // TODO properly detect which beans need to be reset, right now "orderRepository" and "orderService" are hardcoded + val beanNamesToReset = listOf("orderRepository", "orderService") + beanNamesToReset.forEach { beanNameToReset -> + val beanDefToReset = springContext::class.java + .getMethod("getBeanDefinition", String::class.java) + .invoke(springContext, beanNameToReset) + springContext::class.java + .getMethod("removeBeanDefinition", String::class.java) + .invoke(springContext, beanNameToReset) + springContext::class.java + .getMethod( + "registerBeanDefinition", + String::class.java, + utContext.classLoader.loadClass("org.springframework.beans.factory.config.BeanDefinition") + ) + .invoke(springContext, beanNameToReset, beanDefToReset) + } + + val jdbcTemplate = getBean("jdbcTemplate") + // TODO properly detect which repositories need to be cleared, right now "orders" is hardcoded + val sql = "TRUNCATE TABLE orders" + jdbcTemplate::class.java + .getMethod("execute", sql::class.java) + .invoke(jdbcTemplate, sql) + val sql2 = "ALTER TABLE orders ALTER COLUMN id RESTART WITH 1" + jdbcTemplate::class.java + .getMethod("execute", sql::class.java) + .invoke(jdbcTemplate, sql2) + + return instrumentation.invoke(clazz, methodSignature, arguments, parameters) { executionResult -> + executePhaseInTimeout(modelConstructionPhase) { + executionResult.copy( + stateAfter = executionResult.stateAfter.copy( + thisInstance = executionResult.stateAfter.thisInstance?.let { thisInstance -> + UtAutowiredStateAfterModel( + id = thisInstance.idOrNull(), + classId = thisInstance.classId, + origin = thisInstance, + repositoryInteractions = constructRepositoryInteractionModels() + ) + } + ) + ) + } + } + } + + fun getBean(beanName: String): Any = + springContext::class.java + .getMethod("getBean", String::class.java) + .invoke(springContext, beanName) + + fun saveToRepository(repository: Any, entity: Any) { + // ignore repository interactions done during repository fill up + val savedRecordedRepositoryResponses = RepositoryInteraction.recordedInteractions.toList() + repository::class.java + .getMethod("save", Any::class.java) + .invoke(repository, entity) + RepositoryInteraction.recordedInteractions.clear() + RepositoryInteraction.recordedInteractions.addAll(savedRecordedRepositoryResponses) + } + + private fun ModelConstructionPhase.constructRepositoryInteractionModels(): List { + return RepositoryInteraction.recordedInteractions.map { interaction -> + RepositoryInteractionModel( + beanName = interaction.beanName, + executableId = interaction.method.executableId, + args = constructParameters(interaction.args.zip(interaction.method.parameters).map { (arg, param) -> + UtConcreteValue(arg, param.type) + }), + result = convertToExecutionResult(interaction.result, interaction.method.returnType.id) + ) + } + } + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray + ): ByteArray? = + // TODO automatically detect which libraries we don't want to transform (by total transformation time) + // transforming Spring takes too long + if (listOf( + "org/springframework", + "com/fasterxml", + "org/hibernate", + "org/apache", + "org/h2" + ).any { className.startsWith(it) } + ) { + null + } else { + instrumentation.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt index b1bc40e565..3f39833330 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt @@ -17,8 +17,8 @@ import org.utbot.instrumentation.instrumentation.execution.phases.start import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor import java.security.ProtectionDomain -import java.util.* import kotlin.reflect.jvm.javaMethod +import java.util.* /** * Consists of the data needed to execute the method concretely. Also includes method arguments stored in models. @@ -34,7 +34,7 @@ data class UtConcreteExecutionData( val timeout: Long ) -class UtConcreteExecutionResult( +data class UtConcreteExecutionResult( val stateAfter: EnvironmentModels, val result: UtExecutionResult, val coverage: Coverage, @@ -48,10 +48,11 @@ class UtConcreteExecutionResult( } } +// TODO if possible make it non singleton object UtExecutionInstrumentation : Instrumentation { private val delegateInstrumentation = InvokeInstrumentation() - private val instrumentationContext = InstrumentationContext() + var instrumentationContext = InstrumentationContext() private val traceHandler = TraceHandler() private val ndDetector = NonDeterministicDetector() @@ -73,6 +74,15 @@ object UtExecutionInstrumentation : Instrumentation { methodSignature: String, arguments: ArgumentList, parameters: Any? + ): UtConcreteExecutionResult = + invoke(clazz, methodSignature, arguments, parameters, additionalPhases = { it }) + + fun invoke( + clazz: Class<*>, + methodSignature: String, + arguments: ArgumentList, + parameters: Any?, + additionalPhases: PhasesController.(UtConcreteExecutionResult) -> UtConcreteExecutionResult ): UtConcreteExecutionResult { if (parameters !is UtConcreteExecutionData) { throw IllegalArgumentException("Argument parameters must be of type UtConcreteExecutionData, but was: ${parameters?.javaClass}") @@ -147,12 +157,12 @@ object UtExecutionInstrumentation : Instrumentation { Triple(executionResult, stateAfter, newInstrumentation) } - UtConcreteExecutionResult( + additionalPhases(UtConcreteExecutionResult( stateAfter, executionResult, coverage, newInstrumentation - ) + )) } finally { postprocessingPhase.start { resetStaticFields() @@ -164,13 +174,8 @@ object UtExecutionInstrumentation : Instrumentation { override fun getStaticField(fieldId: FieldId): Result = delegateInstrumentation.getStaticField(fieldId).map { value -> - val cache = IdentityHashMap() - val strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy( - pathsToUserClasses, cache - ) - UtModelConstructor(cache, strategy).run { - construct(value, fieldId.type) - } + UtModelConstructor.createOnlyUserClassesConstructor(pathsToUserClasses) + .construct(value, fieldId.type) } override fun transform( diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MockValueConstructor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MockValueConstructor.kt index 1dfb235cec..a72a0df7ad 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MockValueConstructor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MockValueConstructor.kt @@ -12,6 +12,7 @@ import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtAutowiredStateBeforeModel import org.utbot.framework.plugin.api.UtClassRefModel import org.utbot.framework.plugin.api.UtCompositeModel import org.utbot.framework.plugin.api.UtConcreteValue @@ -41,6 +42,7 @@ import org.utbot.instrumentation.instrumentation.execution.mock.InstanceMockCont import org.utbot.instrumentation.instrumentation.execution.mock.InstrumentationContext import org.utbot.instrumentation.instrumentation.execution.mock.MethodMockController import org.utbot.instrumentation.instrumentation.execution.mock.MockController +import org.utbot.instrumentation.instrumentation.execution.mock.SpringInstrumentationContext import org.utbot.instrumentation.process.runSandbox import java.lang.reflect.Modifier import java.util.* @@ -105,8 +107,9 @@ class MockValueConstructor( is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model), model.classId.jClass) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) + is UtAutowiredStateBeforeModel -> UtConcreteValue(constructFromAutowiredModel(model)) // PythonModel, JsUtModel may be here - else -> throw UnsupportedOperationException() + else -> throw UnsupportedOperationException("UtModel $model cannot construct UtConcreteValue") } /** @@ -360,6 +363,19 @@ class MockValueConstructor( return lambda } + private fun constructFromAutowiredModel(autowiredModel: UtAutowiredStateBeforeModel): Any { + val springInstrumentationContext = instrumentationContext as SpringInstrumentationContext + autowiredModel.repositoriesContent.forEach { repositoryContent -> + val repository = springInstrumentationContext.getBean(repositoryContent.repositoryBeanName) + repositoryContent.entityModels.forEach { entityModel -> + construct(entityModel).value?.let { entity -> + springInstrumentationContext.saveToRepository(repository, entity) + } + } + } + return springInstrumentationContext.getBean(autowiredModel.beanName) + } + /** * Updates instance state with [callModel] invocation. * diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelConstructor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelConstructor.kt index f49a523f5f..2f6218f936 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelConstructor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/UtModelConstructor.kt @@ -41,6 +41,16 @@ class UtModelConstructor( .mapNotNull { it.id } .toMutableSet() + companion object { + fun createOnlyUserClassesConstructor(pathsToUserClasses: Set): UtModelConstructor { + val cache = IdentityHashMap() + val strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy( + pathsToUserClasses, cache + ) + return UtModelConstructor(cache, strategy) + } + } + private fun computeUnusedIdAndUpdate(): Int { while (unusedId in usedIds) { unusedId++ diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstrumentationContext.kt index dd80a52651..98928f8657 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstrumentationContext.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstrumentationContext.kt @@ -5,11 +5,11 @@ import java.util.IdentityHashMap import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod /** - * Some information, which is computed after classes instrumentation. + * Some information, which is fully computed after classes instrumentation. * - * This information will be used later in `invoke` function. + * This information will be used later in `invoke` function to construct values. */ -class InstrumentationContext { +open class InstrumentationContext { /** * Contains unique id for each method, which is required for this method mocking. */ diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/SpringInstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/SpringInstrumentationContext.kt new file mode 100644 index 0000000000..0a8e248a34 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/SpringInstrumentationContext.kt @@ -0,0 +1,6 @@ +package org.utbot.instrumentation.instrumentation.execution.mock + +abstract class SpringInstrumentationContext : InstrumentationContext() { + abstract fun getBean(beanName: String): Any + abstract fun saveToRepository(repository: Any, entity: Any) +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ValueConstructionPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ValueConstructionPhase.kt index 7cfe3e19c4..512cf4fa3b 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ValueConstructionPhase.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ValueConstructionPhase.kt @@ -1,7 +1,6 @@ package org.utbot.instrumentation.instrumentation.execution.phases import org.utbot.framework.plugin.api.* -import java.io.Closeable import java.util.IdentityHashMap import org.utbot.instrumentation.instrumentation.execution.constructors.MockValueConstructor import org.utbot.instrumentation.instrumentation.execution.mock.InstrumentationContext diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt index 486e282537..8b8bde4433 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt @@ -6,11 +6,15 @@ import com.jetbrains.rd.util.reactive.adviseOnce import kotlinx.coroutines.* import org.mockito.Mockito import org.utbot.common.* +import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.util.UtContext import org.utbot.instrumentation.agent.Agent import org.utbot.instrumentation.instrumentation.Instrumentation import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.execution.SpringUtExecutionInstrumentation +import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor import org.utbot.instrumentation.process.generated.CollectCoverageResult +import org.utbot.instrumentation.process.generated.GetSpringBeanResult import org.utbot.instrumentation.process.generated.InstrumentedProcessModel import org.utbot.instrumentation.process.generated.InvokeMethodCommandResult import org.utbot.instrumentation.process.generated.instrumentedProcessModel @@ -37,11 +41,14 @@ internal object HandlerClassesLoader : URLClassLoader(emptyArray()) { } /** - * System classloader can find org.slf4j thus when we want to mock something from org.slf4j - * we also want this class will be loaded by [HandlerClassesLoader] + * System classloader can find org.slf4j and org.utbot.spring thus + * - when we want to mock something from org.slf4j we also want this class will be loaded by [HandlerClassesLoader] + * - we want org.utbot.spring to be loaded by [HandlerClassesLoader] so it can use Spring directly */ override fun loadClass(name: String, resolve: Boolean): Class<*> { - if (name.startsWith("org.slf4j")) { + // TODO extract `utbot-spring-commons-api` into a separate module to not mess around with class loader + if (name.startsWith("org.slf4j") || + (name.startsWith("org.utbot.spring") && !name.startsWith("org.utbot.spring.api"))) { return (findLoadedClass(name) ?: findClass(name)).apply { if (resolve) resolveClass(this) } @@ -139,7 +146,7 @@ private fun InstrumentedProcessModel.setup(kryoHelper: KryoHelper, watchdog: Idl logger.debug { "setInstrumentation request" } instrumentation = kryoHelper.readObject(params.instrumentation) logger.debug { "instrumentation - ${instrumentation.javaClass.name} " } - Agent.dynamicClassTransformer.transformer = instrumentation // classTransformer is set + Agent.dynamicClassTransformer.transformer = instrumentation Agent.dynamicClassTransformer.addUserPaths(pathsToUserClasses) instrumentation.init(pathsToUserClasses) } @@ -155,4 +162,11 @@ private fun InstrumentedProcessModel.setup(kryoHelper: KryoHelper, watchdog: Idl val result = (instrumentation as CoverageInstrumentation).collectCoverageInfo(anyClass) CollectCoverageResult(kryoHelper.writeObject(result)) } + watchdog.measureTimeForActiveCall(getSpringBean, "Getting Spring bean") { params -> + val bean = (instrumentation as SpringUtExecutionInstrumentation).getBean(params.beanName) + val model = UtModelConstructor.createOnlyUserClassesConstructor(pathsToUserClasses).construct( + bean, ClassId(bean.javaClass.name) + ) + GetSpringBeanResult(kryoHelper.writeObject(model)) + } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt index 6bf0bc2d12..7b1460d4c1 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/InstrumentedProcess.kt @@ -11,12 +11,14 @@ import org.utbot.common.nameOfPackage import org.utbot.common.scanForResourcesContaining import org.utbot.common.utBotTempDirectory import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.services.WorkingDirService import org.utbot.framework.process.AbstractRDProcessCompanion import org.utbot.instrumentation.agent.DynamicClassTransformer import org.utbot.instrumentation.instrumentation.Instrumentation import org.utbot.instrumentation.process.DISABLE_SANDBOX_OPTION import org.utbot.instrumentation.process.generated.AddPathsParams +import org.utbot.instrumentation.process.generated.GetSpringBeanParams import org.utbot.instrumentation.process.generated.InstrumentedProcessModel import org.utbot.instrumentation.process.generated.SetInstrumentationParams import org.utbot.instrumentation.process.generated.instrumentedProcessModel @@ -28,13 +30,14 @@ import org.utbot.rd.generated.loggerModel import org.utbot.rd.loggers.UtRdKLogger import org.utbot.rd.loggers.setup import org.utbot.rd.onSchedulerBlocking +import org.utbot.rd.startBlocking import org.utbot.rd.startUtProcessWithRdServer import org.utbot.rd.terminateOnException import java.io.File private val logger = KotlinLogging.logger { } -private const val UTBOT_INSTRUMENTATION = "utbot-instrumentation" +private const val UTBOT_INSTRUMENTATION = "utbot-instrumentation-shadow" private const val INSTRUMENTATION_LIB = "lib" private fun tryFindInstrumentationJarInResources(): File? { @@ -68,8 +71,8 @@ private val instrumentationJarFile: File = logger.debug("Failed to find jar in the resources.") tryFindInstrumentationJarOnClasspath() } ?: error(""" - Can't find file: $UTBOT_INSTRUMENTATION-.jar. - Make sure you added $UTBOT_INSTRUMENTATION-.jar to the resources folder from gradle. + Can't find file: $UTBOT_INSTRUMENTATION.jar. + Make sure you added $UTBOT_INSTRUMENTATION.jar to the resources folder from gradle. """.trimIndent()) } @@ -159,4 +162,7 @@ class InstrumentedProcess private constructor( return proc } } + + fun getBean(beanName: String): UtModel = + kryoHelper.readObject(instrumentedProcessModel.getSpringBean.startBlocking(GetSpringBeanParams(beanName)).beanModel) } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt index 1d84b58d5f..d7a2494e0d 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt @@ -24,7 +24,8 @@ class InstrumentedProcessModel private constructor( private val _setInstrumentation: RdCall, private val _invokeMethodCommand: RdCall, private val _collectCoverage: RdCall, - private val _computeStaticField: RdCall + private val _computeStaticField: RdCall, + private val _getSpringBean: RdCall ) : RdExtBase() { //companion @@ -39,6 +40,8 @@ class InstrumentedProcessModel private constructor( serializers.register(CollectCoverageResult) serializers.register(ComputeStaticFieldParams) serializers.register(ComputeStaticFieldResult) + serializers.register(GetSpringBeanParams) + serializers.register(GetSpringBeanResult) } @@ -59,7 +62,7 @@ class InstrumentedProcessModel private constructor( } - const val serializationHash = 2443784041000581664L + const val serializationHash = 821530494958270551L } override val serializersOwner: ISerializersOwner get() = InstrumentedProcessModel @@ -100,6 +103,11 @@ class InstrumentedProcessModel private constructor( [fieldId] */ val computeStaticField: RdCall get() = _computeStaticField + + /** + * Gets Spring bean by name (requires Spring instrumentation) + */ + val getSpringBean: RdCall get() = _getSpringBean //methods //initializer init { @@ -109,6 +117,7 @@ class InstrumentedProcessModel private constructor( _invokeMethodCommand.async = true _collectCoverage.async = true _computeStaticField.async = true + _getSpringBean.async = true } init { @@ -118,6 +127,7 @@ class InstrumentedProcessModel private constructor( bindableChildren.add("invokeMethodCommand" to _invokeMethodCommand) bindableChildren.add("collectCoverage" to _collectCoverage) bindableChildren.add("computeStaticField" to _computeStaticField) + bindableChildren.add("getSpringBean" to _getSpringBean) } //secondary constructor @@ -128,7 +138,8 @@ class InstrumentedProcessModel private constructor( RdCall(SetInstrumentationParams, FrameworkMarshallers.Void), RdCall(InvokeMethodCommandParams, InvokeMethodCommandResult), RdCall(CollectCoverageParams, CollectCoverageResult), - RdCall(ComputeStaticFieldParams, ComputeStaticFieldResult) + RdCall(ComputeStaticFieldParams, ComputeStaticFieldResult), + RdCall(GetSpringBeanParams, GetSpringBeanResult) ) //equals trait @@ -143,6 +154,7 @@ class InstrumentedProcessModel private constructor( print("invokeMethodCommand = "); _invokeMethodCommand.print(printer); println() print("collectCoverage = "); _collectCoverage.print(printer); println() print("computeStaticField = "); _computeStaticField.print(printer); println() + print("getSpringBean = "); _getSpringBean.print(printer); println() } printer.print(")") } @@ -154,7 +166,8 @@ class InstrumentedProcessModel private constructor( _setInstrumentation.deepClonePolymorphic(), _invokeMethodCommand.deepClonePolymorphic(), _collectCoverage.deepClonePolymorphic(), - _computeStaticField.deepClonePolymorphic() + _computeStaticField.deepClonePolymorphic(), + _getSpringBean.deepClonePolymorphic() ) } //contexts @@ -448,6 +461,120 @@ data class ComputeStaticFieldResult ( } +/** + * #### Generated from [InstrumentedProcessModel.kt:44] + */ +data class GetSpringBeanParams ( + val beanName: String +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = GetSpringBeanParams::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): GetSpringBeanParams { + val beanName = buffer.readString() + return GetSpringBeanParams(beanName) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: GetSpringBeanParams) { + buffer.writeString(value.beanName) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as GetSpringBeanParams + + if (beanName != other.beanName) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + beanName.hashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("GetSpringBeanParams (") + printer.indent { + print("beanName = "); beanName.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + +/** + * #### Generated from [InstrumentedProcessModel.kt:48] + */ +data class GetSpringBeanResult ( + val beanModel: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = GetSpringBeanResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): GetSpringBeanResult { + val beanModel = buffer.readByteArray() + return GetSpringBeanResult(beanModel) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: GetSpringBeanResult) { + buffer.writeByteArray(value.beanModel) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as GetSpringBeanResult + + if (!(beanModel contentEquals other.beanModel)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + beanModel.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("GetSpringBeanResult (") + printer.indent { + print("beanModel = "); beanModel.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} + + /** * #### Generated from [InstrumentedProcessModel.kt:17] */ diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt index 59ee8793bf..933700c3aa 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt @@ -47,12 +47,13 @@ import org.utbot.framework.CancellationStrategyType.NONE import org.utbot.framework.CancellationStrategyType.SAVE_PROCESSED_RESULTS import org.utbot.framework.UtSettings import org.utbot.framework.codegen.domain.ProjectType.* -import org.utbot.framework.codegen.domain.TypeReplacementApproach.* +import org.utbot.framework.plugin.api.TypeReplacementApproach.* import org.utbot.framework.plugin.api.ApplicationContext import org.utbot.framework.plugin.api.BeanDefinitionData import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.JavaDocCommentStyle import org.utbot.framework.plugin.api.SpringApplicationContext +import org.utbot.framework.plugin.api.SpringTestsType import org.utbot.framework.plugin.api.util.LockFile import org.utbot.framework.plugin.api.util.withStaticsSubstitutionRequired import org.utbot.framework.plugin.services.JdkInfoService @@ -271,6 +272,8 @@ object UtTestsDialogProcessor { staticMockingConfigured, clarifiedBeanDefinitions, shouldUseImplementors, + model.typeReplacementApproach, + model.springTestsType ) } else -> ApplicationContext(mockFrameworkInstalled, staticMockingConfigured) @@ -360,21 +363,34 @@ object UtTestsDialogProcessor { ) }, 0, 500, TimeUnit.MILLISECONDS) try { + val useEngine = when (model.projectType) { + Spring -> when (model.springTestsType) { + SpringTestsType.UNIT_TESTS -> true + SpringTestsType.INTEGRATION_TESTS -> false + } + else -> true + } val useFuzzing = when (model.projectType) { - Spring -> model.typeReplacementApproach == DoNotReplace + Spring -> when (model.springTestsType) { + SpringTestsType.UNIT_TESTS -> when (model.typeReplacementApproach) { + DoNotReplace -> true + is ReplaceIfPossible -> false + } + SpringTestsType.INTEGRATION_TESTS -> true + } else -> UtSettings.useFuzzing } val rdGenerateResult = process.generate( - model.conflictTriggers, - methods, - model.mockStrategy, - model.chosenClassesToMockAlways, - model.timeout, - model.timeout, - true, - useFuzzing, - project.service().fuzzingValue, - searchDirectory.pathString + conflictTriggers = model.conflictTriggers, + methods = methods, + mockStrategyApi = model.mockStrategy, + chosenClassesToMockAlways = model.chosenClassesToMockAlways, + timeout = model.timeout, + generationTimeout = model.timeout, + isSymbolicEngineEnabled = useEngine, + isFuzzingEnabled = useFuzzing, + fuzzingValue = project.service().fuzzingValue, + searchDirectory = searchDirectory.pathString ) if (rdGenerateResult.notEmptyCases == 0) { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt index d9299e1dec..cd7e96bd4c 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt @@ -19,7 +19,7 @@ import org.jetbrains.concurrency.Promise import org.jetbrains.kotlin.psi.KtFile import org.utbot.framework.SummariesGenerationType import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.domain.TypeReplacementApproach +import org.utbot.framework.plugin.api.TypeReplacementApproach import org.utbot.framework.plugin.api.JavaDocCommentStyle import org.utbot.framework.plugin.api.SpringTestsType import org.utbot.framework.util.ConflictTriggers diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt index 6c594b9302..185824a927 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt @@ -90,7 +90,6 @@ import org.utbot.framework.codegen.domain.SpringBoot import org.utbot.framework.codegen.domain.StaticsMocking import org.utbot.framework.codegen.domain.TestFramework import org.utbot.framework.codegen.domain.TestNg -import org.utbot.framework.codegen.domain.TypeReplacementApproach import org.utbot.framework.plugin.api.CodeGenerationSettingItem import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockFramework @@ -99,6 +98,7 @@ import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.SpringTestsType import org.utbot.framework.plugin.api.SpringTestsType.* import org.utbot.framework.plugin.api.TreatOverflowAsError +import org.utbot.framework.plugin.api.TypeReplacementApproach import org.utbot.framework.plugin.api.utils.MOCKITO_EXTENSIONS_FILE_CONTENT import org.utbot.framework.plugin.api.utils.MOCKITO_EXTENSIONS_FOLDER import org.utbot.framework.plugin.api.utils.MOCKITO_MOCKMAKER_FILE_NAME diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/AutowiredFuzzedType.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/AutowiredFuzzedType.kt new file mode 100644 index 0000000000..1e0c0c39ff --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/AutowiredFuzzedType.kt @@ -0,0 +1,14 @@ +package org.utbot.fuzzer + +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.providers.CustomJavaValueProviderHolder + +class AutowiredFuzzedType( + fuzzedType: FuzzedType, + val beanNames: List, + override val javaValueProvider: JavaValueProvider +) : FuzzedType(fuzzedType.classId, fuzzedType.generics), CustomJavaValueProviderHolder { + override fun toString(): String { + return "AutowiredFuzzedType(classId=$classId, generics=${generics.map { it.classId }}, beanNames=$beanNames)" + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt index a11cc0dd0a..407777bb96 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzer/FuzzedType.kt @@ -10,7 +10,7 @@ import org.utbot.framework.plugin.api.ClassId * * @see ClassId.typeParameters */ -class FuzzedType( +open class FuzzedType( val classId: ClassId, val generics: List = emptyList(), ) { diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt index 4e06efe1ea..98c1a82a7c 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/JavaLanguage.kt @@ -7,6 +7,8 @@ import org.utbot.framework.plugin.api.Instruction import org.utbot.framework.plugin.api.util.* import org.utbot.fuzzer.* import org.utbot.fuzzing.providers.* +import org.utbot.fuzzing.type.factories.FuzzedTypeFactory +import org.utbot.fuzzing.type.factories.SimpleFuzzedTypeFactory import org.utbot.fuzzing.utils.Trie import java.lang.reflect.* import java.util.concurrent.CancellationException @@ -20,7 +22,7 @@ typealias JavaValueProvider = ValueProvider, - val typeCache: MutableMap, + val fuzzedTypeFactory: FuzzedTypeFactory, val random: Random, ) : Description( description.parameters.mapIndexed { index, classId -> @@ -45,6 +47,7 @@ fun defaultValueProviders(idGenerator: IdentityPreservingIdGenerator) = lis IteratorValueProvider(idGenerator), EmptyCollectionValueProvider(idGenerator), DateValueProvider(idGenerator), + DelegatingToCustomJavaValueProvider, // NullValueProvider, ) @@ -54,6 +57,7 @@ suspend fun runJavaFuzzing( constants: Collection, names: List, providers: List> = defaultValueProviders(idGenerator), + fuzzedTypeFactory: FuzzedTypeFactory = SimpleFuzzedTypeFactory(), exec: suspend (thisInstance: FuzzedValue?, description: FuzzedDescription, values: List) -> BaseFeedback, FuzzedType, FuzzedValue> ) { val random = Random(0) @@ -62,10 +66,6 @@ suspend fun runJavaFuzzing( val returnType = methodUnderTest.returnType val parameters = methodUnderTest.parameters - // For a concrete fuzzing run we need to track types we create. - // Because of generics can be declared as recursive structures like `>`, - // we should track them by reference and do not call `equals` and `hashCode` recursively. - val typeCache = hashMapOf() /** * To fuzz this instance, the class of it is added into head of parameters list. * Done for compatibility with old fuzzer logic and should be reworked more robust way. @@ -88,9 +88,9 @@ suspend fun runJavaFuzzing( fuzzerType = { try { when { - self != null && it == 0 -> toFuzzerType(methodUnderTest.executable.declaringClass, typeCache) - self != null -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it - 1], typeCache) - else -> toFuzzerType(methodUnderTest.executable.genericParameterTypes[it], typeCache) + self != null && it == 0 -> fuzzedTypeFactory.createFuzzedType(methodUnderTest.executable.declaringClass, isThisInstance = true) + self != null -> fuzzedTypeFactory.createFuzzedType(methodUnderTest.executable.genericParameterTypes[it - 1], isThisInstance = false) + else -> fuzzedTypeFactory.createFuzzedType(methodUnderTest.executable.genericParameterTypes[it], isThisInstance = false) } } catch (_: Throwable) { null @@ -103,8 +103,8 @@ suspend fun runJavaFuzzing( if (!isStatic && !isConstructor) { classUnderTest } else { null } } val tracer = Trie(Instruction::id) - val descriptionWithOptionalThisInstance = FuzzedDescription(createFuzzedMethodDescription(thisInstance), tracer, typeCache, random) - val descriptionWithOnlyParameters = FuzzedDescription(createFuzzedMethodDescription(null), tracer, typeCache, random) + val descriptionWithOptionalThisInstance = FuzzedDescription(createFuzzedMethodDescription(thisInstance), tracer, fuzzedTypeFactory, random) + val descriptionWithOnlyParameters = FuzzedDescription(createFuzzedMethodDescription(null), tracer, fuzzedTypeFactory, random) val start = System.nanoTime() try { logger.info { "Starting fuzzing for method: $methodUnderTest" } diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Autowired.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Autowired.kt new file mode 100644 index 0000000000..a7a48655c5 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Autowired.kt @@ -0,0 +1,58 @@ +package org.utbot.fuzzing.providers + +import org.utbot.framework.plugin.api.* +import org.utbot.fuzzer.* +import org.utbot.fuzzing.* + +class AutowiredValueProvider( + private val idGenerator: IdGenerator, + private val autowiredModelOriginCreator: (beanName: String) -> UtModel +) : ValueProvider { + override fun accept(type: FuzzedType) = type is AutowiredFuzzedType + + override fun generate( + description: FuzzedDescription, + type: FuzzedType + ) = sequence { + (type as AutowiredFuzzedType).beanNames.forEach { beanName -> + yield( + Seed.Recursive( + construct = Routine.Create(types = emptyList()) { + UtAutowiredStateBeforeModel( + id = idGenerator.createId(), + classId = type.classId, + beanName = beanName, + origin = autowiredModelOriginCreator(beanName), // TODO think about setting origin id and its fields ids + // TODO properly detect which repositories need to be filled up (right now orderRepository is hardcoded) + repositoriesContent = listOf( + RepositoryContentModel( + repositoryBeanName = "orderRepository", + entityModels = mutableListOf() + ) + ), + ).fuzzed { "@Autowired ${type.classId.simpleName} $beanName" } + }, + modify = sequence { + // TODO mutate model itself (not just repositories) + // TODO properly detect which repositories need to be filled up (right now orderRepository is hardcoded) + yield(Routine.Call( + listOf(FuzzedType(ClassId("com.rest.order.models.Order"))), + ) { self, values -> + val entityValue = values[0] + val model = self.model as UtAutowiredStateBeforeModel + // TODO maybe use `entityValue.summary` to update `model.summary` + model.repositoriesContent + .first { it.repositoryBeanName == "orderRepository" } + .entityModels as MutableList += entityValue.model + }) + }, + empty = Routine.Empty { + UtNullModel(type.classId).fuzzed { + summary = "%var% = null" + } + } + ) + ) + } + } +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Custom.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Custom.kt new file mode 100644 index 0000000000..cd51c0a6f7 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Custom.kt @@ -0,0 +1,18 @@ +package org.utbot.fuzzing.providers + +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzing.FuzzedDescription +import org.utbot.fuzzing.JavaValueProvider +import org.utbot.fuzzing.Seed + +interface CustomJavaValueProviderHolder { + val javaValueProvider: JavaValueProvider +} + +object DelegatingToCustomJavaValueProvider : JavaValueProvider { + override fun accept(type: FuzzedType): Boolean = type is CustomJavaValueProviderHolder + + override fun generate(description: FuzzedDescription, type: FuzzedType): Sequence> = + (type as CustomJavaValueProviderHolder).javaValueProvider.generate(description, type) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt index b314314311..3bb11151bb 100644 --- a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/providers/Objects.kt @@ -24,7 +24,7 @@ class ObjectValueProvider( NumberValueProvider.classId ) - override fun accept(type: FuzzedType) = !isIgnored(type.classId) + override fun accept(type: FuzzedType) = !isIgnored(type.classId) && type !is CustomJavaValueProviderHolder override fun generate( description: FuzzedDescription, @@ -45,7 +45,7 @@ class ObjectValueProvider( private fun createValue(classId: ClassId, constructorId: ConstructorId, description: FuzzedDescription): Seed.Recursive { return Seed.Recursive( construct = Routine.Create(constructorId.executable.genericParameterTypes.map { - toFuzzerType(it, description.typeCache) + description.fuzzedTypeFactory.createFuzzedType(it, isThisInstance = false) }) { values -> val id = idGenerator.createId() UtAssembleModel( @@ -146,7 +146,7 @@ internal fun findAccessibleModifiableFields(description: FuzzedDescription?, cla val setterAndGetter = jClass.findPublicSetterGetterIfHasPublicGetter(field, packageName) FieldDescription( name = field.name, - type = if (description != null) toFuzzerType(field.type, description.typeCache) else FuzzedType(field.type.id), + type = description?.fuzzedTypeFactory?.createFuzzedType(field.type, isThisInstance = false) ?: FuzzedType(field.type.id), canBeSetDirectly = isAccessible( field, packageName diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/FuzzedTypeFactory.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/FuzzedTypeFactory.kt new file mode 100644 index 0000000000..3b1a1cd237 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/FuzzedTypeFactory.kt @@ -0,0 +1,8 @@ +package org.utbot.fuzzing.type.factories + +import org.utbot.fuzzer.FuzzedType +import java.lang.reflect.Type + +interface FuzzedTypeFactory { + fun createFuzzedType(type: Type, isThisInstance: Boolean): FuzzedType +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/SimpleFuzzedTypeFactory.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/SimpleFuzzedTypeFactory.kt new file mode 100644 index 0000000000..4ba5b06e3e --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/SimpleFuzzedTypeFactory.kt @@ -0,0 +1,15 @@ +package org.utbot.fuzzing.type.factories + +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzing.toFuzzerType +import java.lang.reflect.Type + +class SimpleFuzzedTypeFactory : FuzzedTypeFactory { + // For a concrete fuzzing run we need to track types we create. + // Because of generics can be declared as recursive structures like `>`, + // we should track them by reference and do not call `equals` and `hashCode` recursively. + private val cache: MutableMap = mutableMapOf() + + override fun createFuzzedType(type: Type, isThisInstance: Boolean): FuzzedType = + toFuzzerType(type, cache) +} \ No newline at end of file diff --git a/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/SpringFuzzedTypeFactory.kt b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/SpringFuzzedTypeFactory.kt new file mode 100644 index 0000000000..6d83429735 --- /dev/null +++ b/utbot-java-fuzzing/src/main/kotlin/org/utbot/fuzzing/type/factories/SpringFuzzedTypeFactory.kt @@ -0,0 +1,21 @@ +package org.utbot.fuzzing.type.factories + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.fuzzer.AutowiredFuzzedType +import org.utbot.fuzzer.FuzzedType +import org.utbot.fuzzing.providers.AutowiredValueProvider +import java.lang.reflect.Type + +class SpringFuzzedTypeFactory( + private val delegate: FuzzedTypeFactory = SimpleFuzzedTypeFactory(), + private val autowiredValueProvider: AutowiredValueProvider, + private val beanNamesFinder: (ClassId) -> List +) : FuzzedTypeFactory { + override fun createFuzzedType(type: Type, isThisInstance: Boolean): FuzzedType { + val fuzzedType = delegate.createFuzzedType(type, isThisInstance) + if (!isThisInstance) return fuzzedType + val beanNames = beanNamesFinder(fuzzedType.classId) + return if (beanNames.isEmpty()) fuzzedType else + AutowiredFuzzedType(fuzzedType, beanNames, autowiredValueProvider) + } +} \ No newline at end of file diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt index f66a2a990b..797c4e9985 100644 --- a/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt @@ -41,6 +41,14 @@ object InstrumentedProcessModel : Ext(InstrumentedProcessRoot) { field("result", array(PredefinedType.byte)) } + val GetSpringBeanParams = structdef { + field("beanName", PredefinedType.string) + } + + val GetSpringBeanResult = structdef { + field("beanModel", array(PredefinedType.byte)) + } + init { call("AddPaths", AddPathsParams, PredefinedType.void).apply { async @@ -76,5 +84,9 @@ object InstrumentedProcessModel : Ext(InstrumentedProcessRoot) { "This command is sent to the instrumented process from the [ConcreteExecutor] if user wants to get value of static field\n" + "[fieldId]" } + call("GetSpringBean", GetSpringBeanParams, GetSpringBeanResult).apply { + async + documentation = "Gets Spring bean by name (requires Spring instrumentation)" + } } } \ No newline at end of file diff --git a/utbot-spring-analyzer/build.gradle.kts b/utbot-spring-analyzer/build.gradle.kts index 641e064318..011b7dde40 100644 --- a/utbot-spring-analyzer/build.gradle.kts +++ b/utbot-spring-analyzer/build.gradle.kts @@ -1,5 +1,4 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer -import com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer val springBootVersion: String by rootProject val rdVersion: String by rootProject @@ -23,39 +22,27 @@ configurations.implementation.get().extendsFrom(shadowJarConfiguration) dependencies { // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot - implementation("org.springframework.boot:spring-boot:$springBootVersion") - implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") + compileOnly("org.springframework.boot:spring-boot:$springBootVersion") + compileOnly("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") fun ModuleDependency.excludeSlf4jApi() = exclude(group = "org.slf4j", module = "slf4j-api") // TODO stop putting dependencies that are only used in SpringAnalyzerProcess into shadow jar - shadowJarConfiguration(project(":utbot-rd")) { excludeSlf4jApi() } - shadowJarConfiguration(project(":utbot-core")) { excludeSlf4jApi() } - shadowJarConfiguration(project(":utbot-framework-api")) { excludeSlf4jApi() } - shadowJarConfiguration("com.jetbrains.rd:rd-framework:$rdVersion") { excludeSlf4jApi() } - shadowJarConfiguration("com.jetbrains.rd:rd-core:$rdVersion") { excludeSlf4jApi() } - shadowJarConfiguration("commons-logging:commons-logging:$commonsLoggingVersion") { excludeSlf4jApi() } - shadowJarConfiguration("commons-io:commons-io:$commonsIOVersion") { excludeSlf4jApi() } + implementation(project(":utbot-rd")) { excludeSlf4jApi() } + implementation(project(":utbot-core")) { excludeSlf4jApi() } + implementation(project(":utbot-framework-api")) { excludeSlf4jApi() } + implementation(project(":utbot-spring-commons")) { excludeSlf4jApi() } + implementation("com.jetbrains.rd:rd-framework:$rdVersion") { excludeSlf4jApi() } + implementation("com.jetbrains.rd:rd-core:$rdVersion") { excludeSlf4jApi() } + implementation("commons-logging:commons-logging:$commonsLoggingVersion") { excludeSlf4jApi() } } application { mainClass.set("org.utbot.spring.process.SpringAnalyzerProcessMainKt") } -// see more details -- https://github.com/spring-projects/spring-boot/issues/1828 tasks.shadowJar { - configurations = listOf(shadowJarConfiguration) - isZip64 = true - // Required for Spring - mergeServiceFiles() - append("META-INF/spring.handlers") - append("META-INF/spring.schemas") - append("META-INF/spring.tooling") - transform(PropertiesFileTransformer().apply { - paths = listOf("META-INF/spring.factories") - mergeStrategy = "append" - }) transform(Log4j2PluginsCacheFileTransformer::class.java) archiveFileName.set("utbot-spring-analyzer-shadow.jar") diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt new file mode 100644 index 0000000000..b22d751781 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt @@ -0,0 +1,24 @@ +package org.utbot.spring.analyzer + +import org.utbot.spring.context.InstantiationContext +import org.utbot.spring.instantiator.SpringApplicationInstantiatorFacade +import org.utbot.spring.api.ApplicationData +import org.utbot.spring.exception.UtBotSpringShutdownException +import org.utbot.spring.generated.BeanDefinitionData +import org.utbot.spring.utils.SourceFinder + +class SpringApplicationAnalyzer { + + fun getBeanDefinitions(applicationData: ApplicationData): Array { + val configurationClasses = SourceFinder(applicationData).findSources() + val instantiationContext = InstantiationContext( + configurationClasses, + applicationData.profileExpression, + ) + + return UtBotSpringShutdownException + .catch { SpringApplicationInstantiatorFacade(instantiationContext).instantiate() } + .beanDefinitions + .toTypedArray() + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt deleted file mode 100644 index fcbf0004d8..0000000000 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.utbot.spring.analyzers - -import org.springframework.context.annotation.AnnotationConfigApplicationContext -import org.springframework.core.env.ConfigurableEnvironment -import org.utbot.spring.exception.UtBotSpringShutdownException -import org.utbot.spring.generated.BeanDefinitionData - -class PureSpringApplicationAnalyzer : SpringApplicationAnalyzer { - override fun analyze(sources: Array>, environment: ConfigurableEnvironment): List { - val applicationContext = AnnotationConfigApplicationContext() - applicationContext.register(*sources) - applicationContext.environment = environment - return UtBotSpringShutdownException.catch { applicationContext.refresh() }.beanDefinitions - } - - override fun canAnalyze() = true -} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt deleted file mode 100644 index 3e36a8d80b..0000000000 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.utbot.spring.analyzers - -import org.springframework.core.env.ConfigurableEnvironment -import org.utbot.spring.generated.BeanDefinitionData - -interface SpringApplicationAnalyzer { - - fun canAnalyze(): Boolean - - fun analyze(sources: Array>, environment: ConfigurableEnvironment): List -} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt deleted file mode 100644 index d06aeea5e0..0000000000 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.utbot.spring.analyzers - -import com.jetbrains.rd.util.error -import com.jetbrains.rd.util.getLogger -import com.jetbrains.rd.util.info -import org.springframework.boot.SpringBootVersion -import org.springframework.core.SpringVersion -import org.utbot.spring.api.ApplicationData -import org.utbot.spring.generated.BeanDefinitionData -import org.utbot.spring.utils.EnvironmentFactory -import org.utbot.spring.utils.SourceFinder - -private val logger = getLogger() - -class SpringApplicationAnalyzerFacade(private val applicationData: ApplicationData) { - - fun analyze(): List { - logger.info { "Current Java version is: " + System.getProperty("java.version") } - logger.info { "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrNull() } - logger.info { "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrNull() } - - val sources = SourceFinder(applicationData).findSources() - val environmentFactory = EnvironmentFactory(applicationData) - - for (analyzer in listOf(SpringBootApplicationAnalyzer(), PureSpringApplicationAnalyzer())) { - if (analyzer.canAnalyze()) { - logger.info { "Analyzing with $analyzer" } - try { - return analyzer.analyze(sources, environmentFactory.createEnvironment()) - } catch (e: Throwable) { - logger.error("Analyzer $analyzer failed", e) - } - } - } - logger.error { "All Spring analyzers failed, using empty bean list" } - return emptyList() - } -} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt deleted file mode 100644 index 9227967f13..0000000000 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.utbot.spring.analyzers - -import org.springframework.boot.builder.SpringApplicationBuilder -import org.springframework.core.env.ConfigurableEnvironment -import org.utbot.spring.exception.UtBotSpringShutdownException -import org.utbot.spring.generated.BeanDefinitionData - -class SpringBootApplicationAnalyzer : SpringApplicationAnalyzer { - override fun analyze(sources: Array>, environment: ConfigurableEnvironment): List { - val app = SpringApplicationBuilder(*sources) - .environment(environment) - .build() - return UtBotSpringShutdownException.catch { app.run() }.beanDefinitions - } - - override fun canAnalyze(): Boolean = try { - this::class.java.classLoader.loadClass("org.springframework.boot.SpringApplication") - true - } catch (e: ClassNotFoundException) { - false - } -} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/config/TestApplicationConfiguration.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/config/TestApplicationConfiguration.kt index b783fdc3b0..5fdee855f6 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/config/TestApplicationConfiguration.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/config/TestApplicationConfiguration.kt @@ -11,6 +11,5 @@ import org.utbot.spring.postProcessors.UtBotBeanFactoryPostProcessor open class TestApplicationConfiguration { @Bean - open fun utBotBeanFactoryPostProcessor(): BeanFactoryPostProcessor = - UtBotBeanFactoryPostProcessor + open fun utBotBeanFactoryPostProcessor(): BeanFactoryPostProcessor = UtBotBeanFactoryPostProcessor } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt index 0ec38e33ef..f38474f7e9 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt @@ -3,7 +3,7 @@ package org.utbot.spring.process import com.jetbrains.rd.util.lifetime.LifetimeDefinition import kotlinx.coroutines.runBlocking import mu.KotlinLogging -import org.apache.commons.io.FileUtils +import org.utbot.common.JarUtils import org.utbot.common.getPid import org.utbot.common.utBotTempDirectory import org.utbot.framework.UtSettings @@ -22,7 +22,6 @@ import org.utbot.spring.generated.SpringAnalyzerProcessModel import org.utbot.spring.generated.SpringAnalyzerResult import org.utbot.spring.generated.springAnalyzerProcessModel import java.io.File -import java.net.URL import java.nio.file.Files class SpringAnalyzerProcessInstantDeathException : @@ -31,42 +30,18 @@ class SpringAnalyzerProcessInstantDeathException : UtSettings.runSpringAnalyzerProcessWithDebug ) -private const val SPRING_ANALYZER_JAR_FILENAME = "utbot-spring-analyzer-shadow.jar" -private const val SPRING_ANALYZER_JAR_PATH = "lib/$SPRING_ANALYZER_JAR_FILENAME" - -private const val UNKNOWN_MODIFICATION_TIME = 0L - private val logger = KotlinLogging.logger {} private var classpathArgs = listOf() -private val springAnalyzerDirectory = - Files.createDirectories(utBotTempDirectory.toFile().resolve("spring-analyzer").toPath()).toFile() +private const val SPRING_ANALYZER_JAR_FILENAME = "utbot-spring-analyzer-shadow.jar" private val springAnalyzerJarFile = - springAnalyzerDirectory - .resolve(SPRING_ANALYZER_JAR_FILENAME).also { jarFile -> - val resource = SpringAnalyzerProcess::class.java.classLoader.getResource(SPRING_ANALYZER_JAR_PATH) - ?: error("Unable to find \"$SPRING_ANALYZER_JAR_PATH\" in resources, make sure it's on the classpath") - updateJarIfRequired(jarFile, resource) - } - -private fun updateJarIfRequired(jarFile: File, resource: URL) { - val resourceConnection = resource.openConnection() - val lastResourceModification = try { - resourceConnection.lastModified - } finally { - resourceConnection.getInputStream().close() - } - if ( - !jarFile.exists() || - jarFile.lastModified() == UNKNOWN_MODIFICATION_TIME || - lastResourceModification == UNKNOWN_MODIFICATION_TIME || - jarFile.lastModified() < lastResourceModification - ) { - FileUtils.copyURLToFile(resource, jarFile) - } -} + JarUtils.extractJarFileFromResources( + jarFileName = SPRING_ANALYZER_JAR_FILENAME, + jarResourcePath = "lib/$SPRING_ANALYZER_JAR_FILENAME", + targetDirectoryName = "spring-analyzer" + ) class SpringAnalyzerProcess private constructor( rdProcess: ProcessWithRdServer diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt index a8129ab204..2475780f42 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt @@ -13,7 +13,7 @@ import org.utbot.rd.RdSettingsContainerFactory import org.utbot.rd.generated.loggerModel import org.utbot.rd.generated.settingsModel import org.utbot.rd.loggers.UtRdRemoteLoggerFactory -import org.utbot.spring.analyzers.SpringApplicationAnalyzerFacade +import org.utbot.spring.analyzer.SpringApplicationAnalyzer import org.utbot.spring.api.ApplicationData import org.utbot.spring.generated.SpringAnalyzerProcessModel import org.utbot.spring.generated.SpringAnalyzerResult @@ -48,7 +48,7 @@ private fun SpringAnalyzerProcessModel.setup(watchdog: IdleWatchdog, realProtoco ) SpringAnalyzerResult( - SpringApplicationAnalyzerFacade(applicationData).analyze().toTypedArray() + SpringApplicationAnalyzer().getBeanDefinitions(applicationData) ) } } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt index 1db43f435d..e6a0ee63b9 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt @@ -10,7 +10,7 @@ import kotlin.io.path.Path private val logger = getLogger() -open class SourceFinder( +class SourceFinder( private val applicationData: ApplicationData ) { private val classLoader: ClassLoader = this::class.java.classLoader @@ -37,4 +37,4 @@ open class SourceFinder( "xml" -> ApplicationConfigurationType.XmlConfiguration else -> ApplicationConfigurationType.JavaConfiguration } -} +} \ No newline at end of file diff --git a/utbot-spring-commons/build.gradle.kts b/utbot-spring-commons/build.gradle.kts new file mode 100644 index 0000000000..74d7a7eb43 --- /dev/null +++ b/utbot-spring-commons/build.gradle.kts @@ -0,0 +1,37 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer + +val springBootVersion: String by rootProject +val rdVersion: String by rootProject + +plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" + id("java") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot + compileOnly("org.springframework.boot:spring-boot:$springBootVersion") + + implementation("com.jetbrains.rd:rd-core:$rdVersion") { exclude(group = "org.slf4j", module = "slf4j-api") } +} + +tasks.shadowJar { + isZip64 = true + + transform(Log4j2PluginsCacheFileTransformer::class.java) + archiveFileName.set("utbot-spring-commons-shadow.jar") +} + +val springCommonsJar: Configuration by configurations.creating { + isCanBeResolved = false + isCanBeConsumed = true +} + +artifacts { + add(springCommonsJar.name, tasks.shadowJar) +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/api/repositoryWrapper/RepositoryInteraction.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/api/repositoryWrapper/RepositoryInteraction.kt new file mode 100644 index 0000000000..baeb39dc5e --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/api/repositoryWrapper/RepositoryInteraction.kt @@ -0,0 +1,14 @@ +package org.utbot.spring.api.repositoryWrapper + +import java.lang.reflect.Method + +data class RepositoryInteraction( + val beanName: String, + val method: Method, + val args: List, + val result: Result +) { + companion object { + val recordedInteractions = mutableListOf() + } +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/InstantiationContext.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/InstantiationContext.kt new file mode 100644 index 0000000000..92a4554f8b --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/InstantiationContext.kt @@ -0,0 +1,6 @@ +package org.utbot.spring.context + +class InstantiationContext( + val configurationClasses: Array>, + val profileExpression: String?, +) diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/EnvironmentFactory.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/environment/EnvironmentFactory.kt similarity index 75% rename from utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/EnvironmentFactory.kt rename to utbot-spring-commons/src/main/kotlin/org/utbot/spring/environment/EnvironmentFactory.kt index 65ee7dda37..54f7098cab 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/EnvironmentFactory.kt +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/environment/EnvironmentFactory.kt @@ -1,29 +1,30 @@ -package org.utbot.spring.utils +package org.utbot.spring.environment import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.StandardEnvironment -import org.utbot.spring.api.ApplicationData +import org.utbot.spring.context.InstantiationContext + private val logger = getLogger() class EnvironmentFactory( - private val applicationData: ApplicationData + private val instantiationContext: InstantiationContext ) { companion object { const val DEFAULT_PROFILE_NAME = "default" } fun createEnvironment(): ConfigurableEnvironment { - val profilesToActivate = parseProfileExpression(applicationData.profileExpression) + val profilesToActivate = parseProfileExpression(instantiationContext.profileExpression) val environment = StandardEnvironment() try { environment.setActiveProfiles(*profilesToActivate) } catch (e: Exception) { - logger.info { "Setting ${applicationData.profileExpression} as active profiles failed with exception $e" } + logger.info { "Setting ${instantiationContext.profileExpression} as active profiles failed with exception $e" } } return environment diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/PureSpringApplicationInstantiator.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/PureSpringApplicationInstantiator.kt new file mode 100644 index 0000000000..e083bea457 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/PureSpringApplicationInstantiator.kt @@ -0,0 +1,19 @@ +package org.utbot.spring.instantiator + +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.core.env.ConfigurableEnvironment + +class PureSpringApplicationInstantiator : SpringApplicationInstantiator { + + override fun canInstantiate() = true + + override fun instantiate(sources: Array>, environment: ConfigurableEnvironment): ConfigurableApplicationContext { + val applicationContext = AnnotationConfigApplicationContext() + applicationContext.register(*sources) + applicationContext.environment = environment + + applicationContext.refresh() + return applicationContext + } +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiator.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiator.kt new file mode 100644 index 0000000000..4dda4efc39 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiator.kt @@ -0,0 +1,11 @@ +package org.utbot.spring.instantiator + +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.core.env.ConfigurableEnvironment + +interface SpringApplicationInstantiator { + + fun canInstantiate(): Boolean + + fun instantiate(sources: Array>, environment: ConfigurableEnvironment): ConfigurableApplicationContext +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiatorFacade.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiatorFacade.kt new file mode 100644 index 0000000000..d439861552 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiatorFacade.kt @@ -0,0 +1,36 @@ +package org.utbot.spring.instantiator + +import org.springframework.context.ConfigurableApplicationContext +import org.utbot.spring.context.InstantiationContext +import org.utbot.spring.environment.EnvironmentFactory + +import com.jetbrains.rd.util.error +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.springframework.boot.SpringBootVersion +import org.springframework.core.SpringVersion + +private val logger = getLogger() + +class SpringApplicationInstantiatorFacade(private val instantiationContext: InstantiationContext) { + + fun instantiate(): ConfigurableApplicationContext? { + logger.info { "Current Java version is: " + System.getProperty("java.version") } + logger.info { "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrNull() } + logger.info { "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrNull() } + + val environmentFactory = EnvironmentFactory(instantiationContext) + + val suitableInstantiator = + listOf(SpringBootApplicationInstantiator(), PureSpringApplicationInstantiator()) + .firstOrNull { it.canInstantiate() } + ?: null.also { logger.error { "All Spring analyzers failed, using empty bean list" } } + + logger.info { "Instantiating Spring application with $suitableInstantiator" } + + return suitableInstantiator?.instantiate( + instantiationContext.configurationClasses, + environmentFactory.createEnvironment(), + ) + } +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringBootApplicationInstantiator.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringBootApplicationInstantiator.kt new file mode 100644 index 0000000000..b72578ae2b --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringBootApplicationInstantiator.kt @@ -0,0 +1,23 @@ +package org.utbot.spring.instantiator + +import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.core.env.ConfigurableEnvironment + +class SpringBootApplicationInstantiator : SpringApplicationInstantiator { + + override fun canInstantiate(): Boolean = try { + this::class.java.classLoader.loadClass("org.springframework.boot.SpringApplication") + true + } catch (e: ClassNotFoundException) { + false + } + + override fun instantiate(sources: Array>, environment: ConfigurableEnvironment): ConfigurableApplicationContext { + val application = SpringApplicationBuilder(*sources) + .environment(environment) + .build() + + return application.run() + } +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperBeanPostProcessor.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperBeanPostProcessor.kt new file mode 100644 index 0000000000..51403e9c55 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperBeanPostProcessor.kt @@ -0,0 +1,18 @@ +package org.utbot.spring.repositoryWrapper + +import org.springframework.beans.factory.config.BeanPostProcessor +import java.lang.reflect.Proxy + +object RepositoryWrapperBeanPostProcessor : BeanPostProcessor { + // see https://github.com/spring-projects/spring-boot/issues/7033 for reason why we post process AFTER initialization + override fun postProcessAfterInitialization(bean: Any, beanName: String): Any = + if (bean::class.java.interfaces.any { + it.name == "org.springframework.data.repository.Repository" + }) { + Proxy.newProxyInstance( + this::class.java.classLoader, + bean::class.java.interfaces, + RepositoryWrapperInvocationHandler(bean, beanName) + ) + } else bean +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperConfiguration.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperConfiguration.kt new file mode 100644 index 0000000000..4ef82574dc --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperConfiguration.kt @@ -0,0 +1,12 @@ +package org.utbot.spring.repositoryWrapper + +import org.springframework.beans.factory.config.BeanPostProcessor +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +open class RepositoryWrapperConfiguration { + @Bean + open fun utBotRepositoryWrapper(): BeanPostProcessor = + RepositoryWrapperBeanPostProcessor +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperInvocationHandler.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperInvocationHandler.kt new file mode 100644 index 0000000000..4b47e94216 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperInvocationHandler.kt @@ -0,0 +1,19 @@ +package org.utbot.spring.repositoryWrapper + +import org.utbot.spring.api.repositoryWrapper.RepositoryInteraction +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method + +class RepositoryWrapperInvocationHandler( + private val originalRepository: Any, + private val beanName: String +) : InvocationHandler { + override fun invoke(proxy: Any, method: Method, args: Array?): Any? { + val nonNullArgs = args ?: emptyArray() + val result = runCatching { method.invoke(originalRepository, *nonNullArgs) } + RepositoryInteraction.recordedInteractions.add( + RepositoryInteraction(beanName, method, nonNullArgs.toList(), result) + ) + return result.getOrThrow() + } +} \ No newline at end of file