diff --git a/gradle.properties b/gradle.properties index 60025acc9c..d92ad7cf52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -81,7 +81,10 @@ openblasVersion=0.3.10-1.5.4 arpackNgVersion=3.7.0-1.5.4 commonsLoggingVersion=1.2 commonsIOVersion=2.11.0 -springBootVersion=2.7.8 + +# use latest Java 8 compaitable Spring and Spring Boot versions +springVersion=5.3.28 +springBootVersion=2.7.13 # configuration for build server # diff --git a/utbot-core/src/main/kotlin/org/utbot/common/AnnotationUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/AnnotationUtil.kt new file mode 100644 index 0000000000..e396e00d28 --- /dev/null +++ b/utbot-core/src/main/kotlin/org/utbot/common/AnnotationUtil.kt @@ -0,0 +1,33 @@ +package org.utbot.common + +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Proxy + +/** + * Assigns [newValue] to specified [property] of [annotation]. + * + * NOTE! [annotation] instance is expected to be a [Proxy] + * using [sun.reflect.annotation.AnnotationInvocationHandler] + * making this function depend on JDK vendor and version. + * + * Example: `@ImportResource -> @ImportResource(value = "classpath:shark-config.xml")` + */ +fun patchAnnotation( + annotation: Annotation, + property: String, + newValue: Any? +) { + val proxyClass = Proxy::class.java + val hField = proxyClass.getDeclaredField("h") + hField.isAccessible = true + + val invocationHandler = hField[annotation] as InvocationHandler + + val annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler") + val memberValuesField = annotationInvocationHandlerClass.getDeclaredField("memberValues") + memberValuesField.isAccessible = true + + @Suppress("UNCHECKED_CAST") // unavoidable because of reflection + val memberValues = memberValuesField[invocationHandler] as MutableMap + memberValues[property] = newValue +} diff --git a/utbot-instrumentation/build.gradle.kts b/utbot-instrumentation/build.gradle.kts index 931fdc857f..0b31fcfe03 100644 --- a/utbot-instrumentation/build.gradle.kts +++ b/utbot-instrumentation/build.gradle.kts @@ -7,7 +7,6 @@ val kotlinLoggingVersion: String by rootProject val rdVersion: String by rootProject val mockitoVersion: String by rootProject val mockitoInlineVersion: String by rootProject -val springBootVersion: String by rootProject plugins { id("com.github.johnrengelman.shadow") version "7.1.2" 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 index e792a6d385..5f68de1c07 100644 --- 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 @@ -1,18 +1,19 @@ package org.utbot.instrumentation.instrumentation.execution -import org.utbot.common.JarUtils import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info +import org.utbot.common.JarUtils +import org.utbot.common.hasOnClasspath import org.utbot.framework.plugin.api.BeanDefinitionData import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.SpringRepositoryId import org.utbot.framework.plugin.api.util.jClass 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.context.SpringInstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhaseFailingOnAnyException import org.utbot.instrumentation.process.HandlerClassesLoader -import org.utbot.spring.api.context.ContextWrapper -import org.utbot.spring.api.repositoryWrapper.RepositoryInteraction +import org.utbot.spring.api.SpringApi import java.net.URL import java.net.URLClassLoader import java.security.ProtectionDomain @@ -32,7 +33,10 @@ class SpringUtExecutionInstrumentation( private val relatedBeansCache = mutableMapOf, Set>() - private val springContext: ContextWrapper get() = instrumentationContext.springContext + private val springApi: SpringApi get() = instrumentationContext.springApi + + private object SpringBeforeTestMethodPhase : ExecutionPhaseFailingOnAnyException() + private object SpringAfterTestMethodPhase : ExecutionPhaseFailingOnAnyException() companion object { private val logger = getLogger() @@ -50,10 +54,11 @@ class SpringUtExecutionInstrumentation( ) ) - instrumentationContext = SpringInstrumentationContext(springConfig) userSourcesClassLoader = URLClassLoader(buildDirs, null) + instrumentationContext = SpringInstrumentationContext(springConfig, delegateInstrumentation.instrumentationContext) delegateInstrumentation.instrumentationContext = instrumentationContext delegateInstrumentation.init(pathsToUserClasses) + springApi.beforeTestClass() } override fun invoke( @@ -62,42 +67,36 @@ class SpringUtExecutionInstrumentation( arguments: ArgumentList, parameters: Any? ): UtConcreteExecutionResult { - RepositoryInteraction.recordedInteractions.clear() - - val beanNamesToReset: Set = getRelevantBeanNames(clazz) - val repositoryDefinitions = springContext.resolveRepositories(beanNamesToReset, userSourcesClassLoader) - - beanNamesToReset.forEach { beanName -> springContext.resetBean(beanName) } - val jdbcTemplate = getBean("jdbcTemplate") - - for (repositoryDefinition in repositoryDefinitions) { - val truncateTableCommand = "TRUNCATE TABLE ${repositoryDefinition.tableName}" - jdbcTemplate::class.java - .getMethod("execute", truncateTableCommand::class.java) - .invoke(jdbcTemplate, truncateTableCommand) - - val restartIdCommand = "ALTER TABLE ${repositoryDefinition.tableName} ALTER COLUMN id RESTART WITH 1" - jdbcTemplate::class.java - .getMethod("execute", restartIdCommand::class.java) - .invoke(jdbcTemplate, restartIdCommand) + getRelevantBeans(clazz).forEach { beanName -> springApi.resetBean(beanName) } + + return delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters) { invokeBasePhases -> + // NB! beforeTestMethod() and afterTestMethod() are intentionally called inside phases, + // so they are executed in one thread with method under test + executePhaseInTimeout(SpringBeforeTestMethodPhase) { springApi.beforeTestMethod() } + try { + invokeBasePhases() + } finally { + executePhaseInTimeout(SpringAfterTestMethodPhase) { springApi.afterTestMethod() } + } } - - return delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters) } - private fun getRelevantBeanNames(clazz: Class<*>): Set = relatedBeansCache.getOrPut(clazz) { + private fun getRelevantBeans(clazz: Class<*>): Set = relatedBeansCache.getOrPut(clazz) { beanDefinitions .filter { it.beanTypeFqn == clazz.name } - .flatMap { springContext.getDependenciesForBean(it.beanName, userSourcesClassLoader) } + // forces `getBean()` to load Spring classes, + // otherwise execution of method under test may fail with timeout + .onEach { springApi.getBean(it.beanName) } + .flatMap { springApi.getDependenciesForBean(it.beanName, userSourcesClassLoader) } .toSet() .also { logger.info { "Detected relevant beans for class ${clazz.name}: $it" } } } - fun getBean(beanName: String): Any = springContext.getBean(beanName) + fun getBean(beanName: String): Any = springApi.getBean(beanName) fun getRepositoryDescriptions(classId: ClassId): Set { - val relevantBeanNames = getRelevantBeanNames(classId.jClass) - val repositoryDescriptions = springContext.resolveRepositories(relevantBeanNames.toSet(), userSourcesClassLoader) + val relevantBeanNames = getRelevantBeans(classId.jClass) + val repositoryDescriptions = springApi.resolveRepositories(relevantBeanNames.toSet(), userSourcesClassLoader) return repositoryDescriptions.map { repositoryDescription -> SpringRepositoryId( repositoryDescription.beanName, @@ -114,19 +113,14 @@ class SpringUtExecutionInstrumentation( protectionDomain: ProtectionDomain, classfileBuffer: ByteArray ): ByteArray? = - // TODO: automatically detect which libraries we don't want to transform (by total transformation time) - if (listOf( - "org/springframework", - "com/fasterxml", - "org/hibernate", - "org/apache", - "org/h2", - "javax/", - "ch/qos", - ).any { className.startsWith(it) } - ) { - null - } else { + // we do not transform Spring classes as it takes too much time + + // maybe we should still transform classes related to data validation + // (e.g. from packages "javax/persistence" and "jakarta/persistence"), + // since traces from such classes can be particularly useful for feedback to fuzzer + if (userSourcesClassLoader.hasOnClasspath(className.replace("/", "."))) { delegateInstrumentation.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer) + } else { + null } } \ 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 9a21f86829..824393c1bb 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 @@ -9,10 +9,10 @@ import org.utbot.instrumentation.instrumentation.InvokeInstrumentation import org.utbot.instrumentation.instrumentation.et.TraceHandler import org.utbot.instrumentation.instrumentation.execution.constructors.ConstructOnlyUserClassesOrCachedObjectsStrategy import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor -import org.utbot.instrumentation.instrumentation.execution.mock.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.context.SimpleInstrumentationContext import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicClassVisitor import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicDetector -import org.utbot.instrumentation.instrumentation.execution.phases.ConstructedData import org.utbot.instrumentation.instrumentation.execution.phases.PhasesController import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor @@ -51,7 +51,7 @@ data class UtConcreteExecutionResult( object UtExecutionInstrumentation : Instrumentation { private val delegateInstrumentation = InvokeInstrumentation() - var instrumentationContext = InstrumentationContext() + var instrumentationContext: InstrumentationContext = SimpleInstrumentationContext() private val traceHandler = TraceHandler() private val ndDetector = NonDeterministicDetector() @@ -74,14 +74,14 @@ object UtExecutionInstrumentation : Instrumentation { arguments: ArgumentList, parameters: Any? ): UtConcreteExecutionResult = - invoke(clazz, methodSignature, arguments, parameters, additionalPhases = { it }) + invoke(clazz, methodSignature, arguments, parameters, phasesWrapper = { it() }) fun invoke( clazz: Class<*>, methodSignature: String, arguments: ArgumentList, parameters: Any?, - additionalPhases: PhasesController.(UtConcreteExecutionResult) -> UtConcreteExecutionResult + phasesWrapper: PhasesController.(invokeBasePhases: () -> UtConcreteExecutionResult) -> UtConcreteExecutionResult ): UtConcreteExecutionResult { if (parameters !is UtConcreteExecutionData) { throw IllegalArgumentException("Argument parameters must be of type UtConcreteExecutionData, but was: ${parameters?.javaClass}") @@ -94,65 +94,62 @@ object UtExecutionInstrumentation : Instrumentation { delegateInstrumentation, timeout ).computeConcreteExecutionResult { - try { - // some preparation actions for concrete execution - var constructedData: ConstructedData + phasesWrapper { try { - constructedData = applyPreprocessing(parameters) - } catch (t: Throwable) { - return UtConcreteExecutionResult(MissingState, UtConcreteExecutionProcessedFailure(t), Coverage()) - } - - val (params, statics, cache) = constructedData - - // invocation - val concreteResult = executePhaseInTimeout(invocationPhase) { - invoke(clazz, methodSignature, params.map { it.value }) - } + // some preparation actions for concrete execution + val constructedData = applyPreprocessing(parameters) - // statistics collection - val (coverage, ndResults) = executePhaseInTimeout(statisticsCollectionPhase) { - getCoverage(clazz) to getNonDeterministicResults() - } + val (params, statics, cache) = constructedData - // model construction - val (executionResult, stateAfter, newInstrumentation) = executePhaseInTimeout(modelConstructionPhase) { - configureConstructor { - this.cache = cache - strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy( - pathsToUserClasses, - cache - ) + // invocation + val concreteResult = executePhaseInTimeout(invocationPhase) { + invoke(clazz, methodSignature, params.map { it.value }) } - val ndStatics = constructStaticInstrumentation(ndResults.statics) - val ndNews = constructNewInstrumentation(ndResults.news, ndResults.calls) - val newInstrumentation = mergeInstrumentations(instrumentations, ndStatics, ndNews) - - val returnType = clazz.singleExecutableId(methodSignature).returnType - val executionResult = convertToExecutionResult(concreteResult,returnType) + // statistics collection + val (coverage, ndResults) = executePhaseInTimeout(statisticsCollectionPhase) { + getCoverage(clazz) to getNonDeterministicResults() + } - val stateAfterParametersWithThis = constructParameters(params) - val stateAfterStatics = constructStatics(stateBefore, statics) - val (stateAfterThis, stateAfterParameters) = if (stateBefore.thisInstance == null) { - null to stateAfterParametersWithThis - } else { - stateAfterParametersWithThis.first() to stateAfterParametersWithThis.drop(1) + // model construction + val (executionResult, stateAfter, newInstrumentation) = executePhaseInTimeout(modelConstructionPhase) { + configureConstructor { + this.cache = cache + strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy( + pathsToUserClasses, + cache + ) + } + + val ndStatics = constructStaticInstrumentation(ndResults.statics) + val ndNews = constructNewInstrumentation(ndResults.news, ndResults.calls) + val newInstrumentation = mergeInstrumentations(instrumentations, ndStatics, ndNews) + + val returnType = clazz.singleExecutableId(methodSignature).returnType + val executionResult = convertToExecutionResult(concreteResult, returnType) + + val stateAfterParametersWithThis = constructParameters(params) + val stateAfterStatics = constructStatics(stateBefore, statics) + val (stateAfterThis, stateAfterParameters) = if (stateBefore.thisInstance == null) { + null to stateAfterParametersWithThis + } else { + stateAfterParametersWithThis.first() to stateAfterParametersWithThis.drop(1) + } + val stateAfter = EnvironmentModels(stateAfterThis, stateAfterParameters, stateAfterStatics) + + Triple(executionResult, stateAfter, newInstrumentation) } - val stateAfter = EnvironmentModels(stateAfterThis, stateAfterParameters, stateAfterStatics) - Triple(executionResult, stateAfter, newInstrumentation) + UtConcreteExecutionResult( + stateAfter, + executionResult, + coverage, + newInstrumentation + ) + } finally { + // restoring data after concrete execution + applyPostprocessing() } - - additionalPhases(UtConcreteExecutionResult( - stateAfter, - executionResult, - coverage, - newInstrumentation - )) - } finally { - // restoring data after concrete execution - applyPostprocessing() } } } 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/InstrumentationContextAwareValueConstructor.kt similarity index 96% rename from utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/MockValueConstructor.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/constructors/InstrumentationContextAwareValueConstructor.kt index 59c4937135..a53df612c9 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/InstrumentationContextAwareValueConstructor.kt @@ -5,7 +5,6 @@ import org.mockito.stubbing.Answer import org.objectweb.asm.Type import org.utbot.common.Reflection import org.utbot.common.invokeCatching -import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.DirectFieldAccessId @@ -27,7 +26,6 @@ import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.UtReferenceModel -import org.utbot.framework.plugin.api.UtSpringContextModel import org.utbot.framework.plugin.api.UtStatementCallModel import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation import org.utbot.framework.plugin.api.UtVoidModel @@ -43,10 +41,9 @@ import org.utbot.framework.plugin.api.util.jField import org.utbot.framework.plugin.api.util.method import org.utbot.framework.plugin.api.util.utContext import org.utbot.instrumentation.instrumentation.execution.mock.InstanceMockController -import org.utbot.instrumentation.instrumentation.execution.mock.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.context.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.* @@ -57,13 +54,13 @@ import kotlin.reflect.KClass * * Uses model->constructed object reference-equality cache. * - * This class is based on `ValueConstructor.kt`. The main difference is the ability to create mocked objects and mock - * static methods. + * This class is based on `ValueConstructor.kt`. The main difference is the ability to create mocked objects, mock + * static methods, and [construct context dependent values][InstrumentationContext.constructContextDependentValue]. * * Note that `clearState` was deleted! */ -// TODO: JIRA:1379 -- Refactor ValueConstructor and MockValueConstructor -class MockValueConstructor( +// TODO: JIRA:1379 -- Refactor ValueConstructor and InstrumentationContextAwareValueConstructor +class InstrumentationContextAwareValueConstructor( private val instrumentationContext: InstrumentationContext ) { private val classLoader: ClassLoader @@ -111,9 +108,11 @@ class MockValueConstructor( is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model), model.classId.jClass) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) - is UtSpringContextModel -> UtConcreteValue((instrumentationContext as SpringInstrumentationContext).springContext.context) - // PythonModel, JsUtModel may be here - else -> throw UnsupportedOperationException("UtModel $model cannot construct UtConcreteValue") + else -> { + instrumentationContext.constructContextDependentValue(model) ?: + // PythonModel, JsUtModel may be here + throw UnsupportedOperationException("UtModel $model cannot construct UtConcreteValue") + } } /** 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/context/InstrumentationContext.kt similarity index 78% rename from utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstrumentationContext.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/InstrumentationContext.kt index 98928f8657..6f207bdb40 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/context/InstrumentationContext.kt @@ -1,5 +1,7 @@ -package org.utbot.instrumentation.instrumentation.execution.mock +package org.utbot.instrumentation.instrumentation.execution.context +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtModel import java.lang.reflect.Method import java.util.IdentityHashMap import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod @@ -9,11 +11,20 @@ import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod * * This information will be used later in `invoke` function to construct values. */ -open class InstrumentationContext { +interface InstrumentationContext { /** * Contains unique id for each method, which is required for this method mocking. */ - val methodSignatureToId = mutableMapOf() + val methodSignatureToId: MutableMap + + /** + * Constructs value that is dependent on the context provided by supported frameworks used in project (e.g. Spring). + * Returns `null` if no context dependent value can be constructed for specified [model]. + * + * NOTE! Doesn't attempt to construct context independent values, + * constructing such values is a responsibility of the user of this method. + */ + fun constructContextDependentValue(model: UtModel): UtConcreteValue<*>? object MockGetter { data class MockContainer(private val values: List<*>) { diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SimpleInstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SimpleInstrumentationContext.kt new file mode 100644 index 0000000000..83cc38dec8 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SimpleInstrumentationContext.kt @@ -0,0 +1,17 @@ +package org.utbot.instrumentation.instrumentation.execution.context + +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtModel + +/** + * Simple instrumentation context, that is used for pure JVM projects without + * any frameworks with special support from UTBot (like Spring) + */ +class SimpleInstrumentationContext : InstrumentationContext { + override val methodSignatureToId = mutableMapOf() + + /** + * There are no context dependent values for pure JVM projects + */ + override fun constructContextDependentValue(model: UtModel): UtConcreteValue<*>? = null +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SpringInstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SpringInstrumentationContext.kt new file mode 100644 index 0000000000..4ecac5fc69 --- /dev/null +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SpringInstrumentationContext.kt @@ -0,0 +1,36 @@ +package org.utbot.instrumentation.instrumentation.execution.context + +import org.utbot.framework.plugin.api.UtConcreteValue +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtSpringContextModel +import org.utbot.framework.plugin.api.util.utContext +import org.utbot.spring.api.SpringApi +import org.utbot.spring.api.instantiator.SpringApiProviderFacade +import org.utbot.spring.api.instantiator.InstantiationSettings + +class SpringInstrumentationContext( + private val springConfig: String, + private val delegateInstrumentationContext: InstrumentationContext, +) : InstrumentationContext by delegateInstrumentationContext { + // TODO: recreate context/app every time whenever we change method under test + val springApi: SpringApi by lazy { + val classLoader = utContext.classLoader + Thread.currentThread().contextClassLoader = classLoader + + val instantiationSettings = InstantiationSettings( + configurationClasses = arrayOf( + classLoader.loadClass(springConfig), + ), + profileExpression = null, // TODO pass profile expression here + ) + + SpringApiProviderFacade + .getInstance(classLoader) + .provideMostSpecificAvailableApi(instantiationSettings) + } + + override fun constructContextDependentValue(model: UtModel): UtConcreteValue<*>? = when (model) { + is UtSpringContextModel -> UtConcreteValue(springApi.getOrLoadSpringApplicationContext()) + else -> delegateInstrumentationContext.constructContextDependentValue(model) + } +} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstanceMockController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstanceMockController.kt index c41032c17d..e2952ab112 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstanceMockController.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/InstanceMockController.kt @@ -3,6 +3,7 @@ package org.utbot.instrumentation.instrumentation.execution.mock import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.util.jClass import org.objectweb.asm.Type +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext class InstanceMockController( clazz: ClassId, diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MethodMockController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MethodMockController.kt index 783357e8a2..73af206f5c 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MethodMockController.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/MethodMockController.kt @@ -4,6 +4,7 @@ import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.Modifier import org.utbot.common.withAccessibility +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext import org.utbot.instrumentation.instrumentation.mock.MockConfig import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod 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 deleted file mode 100644 index 6d8592c011..0000000000 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/mock/SpringInstrumentationContext.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.utbot.instrumentation.instrumentation.execution.mock - -import org.utbot.framework.plugin.api.util.utContext -import org.utbot.spring.api.context.ContextWrapper -import org.utbot.spring.api.instantiator.ApplicationInstantiatorFacade -import org.utbot.spring.api.instantiator.InstantiationSettings - -class SpringInstrumentationContext(private val springConfig: String) : InstrumentationContext() { - // TODO: recreate context/app every time whenever we change method under test - val springContext: ContextWrapper by lazy { - val classLoader = utContext.classLoader - Thread.currentThread().contextClassLoader = classLoader - - val instantiationSettings = InstantiationSettings( - configurationClasses = arrayOf( - classLoader.loadClass(springConfig), - classLoader.loadClass("org.utbot.spring.repositoryWrapper.RepositoryWrapperConfiguration") - ), - profileExpression = null, // TODO pass profile expression here - ) - - val springFacadeInstance = classLoader - .loadClass("org.utbot.spring.instantiator.SpringApplicationInstantiatorFacade") - .getConstructor() - .newInstance() as ApplicationInstantiatorFacade - - springFacadeInstance.instantiate(instantiationSettings) - } -} \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ExecutionPhase.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ExecutionPhase.kt index 7c64275b07..088ca7396f 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ExecutionPhase.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/ExecutionPhase.kt @@ -29,3 +29,8 @@ fun T.start(block: T.() -> R): R = } catch (e: Throwable) { throw this.wrapError(e) } + +abstract class ExecutionPhaseFailingOnAnyException : ExecutionPhase { + override fun wrapError(e: Throwable): ExecutionPhaseException = + ExecutionPhaseError(this::class.java.simpleName, e) +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt index 64b44aa6e2..2530da1ba5 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt @@ -13,7 +13,7 @@ import org.utbot.instrumentation.instrumentation.Instrumentation import org.utbot.instrumentation.instrumentation.et.TraceHandler import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult -import org.utbot.instrumentation.instrumentation.execution.mock.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext import java.security.AccessControlException class PhasesController( 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 458cb15cc0..98c9a74794 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 @@ -2,8 +2,8 @@ package org.utbot.instrumentation.instrumentation.execution.phases import org.utbot.framework.plugin.api.* import java.util.IdentityHashMap -import org.utbot.instrumentation.instrumentation.execution.constructors.MockValueConstructor -import org.utbot.instrumentation.instrumentation.execution.mock.InstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.constructors.InstrumentationContextAwareValueConstructor +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext import org.utbot.framework.plugin.api.util.isInaccessibleViaReflection import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult @@ -24,15 +24,19 @@ class ValueConstructionPhase( instrumentationContext: InstrumentationContext ) : ExecutionPhase { - override fun wrapError(e: Throwable): ExecutionPhaseException { - val message = this.javaClass.simpleName - return when(e) { - is TimeoutException -> ExecutionPhaseStop(message, UtConcreteExecutionResult(MissingState, UtTimeoutException(e), Coverage())) - else -> ExecutionPhaseError(message, e) - } - } + override fun wrapError(e: Throwable): ExecutionPhaseException = ExecutionPhaseStop( + phase = this.javaClass.simpleName, + result = UtConcreteExecutionResult( + stateAfter = MissingState, + result = when(e) { + is TimeoutException -> UtTimeoutException(e) + else -> UtConcreteExecutionProcessedFailure(e) + }, + coverage = Coverage() + ) + ) - private val constructor = MockValueConstructor(instrumentationContext) + private val constructor = InstrumentationContextAwareValueConstructor(instrumentationContext) fun getCache(): ConstructedCache { return constructor.objectToModelCache 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 index 75ca33ebf3..74b1ea5c66 100644 --- 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 @@ -2,7 +2,7 @@ package org.utbot.spring.analyzer import org.utbot.spring.api.instantiator.InstantiationSettings import org.utbot.spring.api.ApplicationData -import org.utbot.spring.api.instantiator.ApplicationInstantiatorFacade +import org.utbot.spring.api.instantiator.SpringApiProviderFacade import org.utbot.spring.exception.UtBotSpringShutdownException import org.utbot.spring.generated.BeanDefinitionData import org.utbot.spring.utils.SourceFinder @@ -16,17 +16,12 @@ class SpringApplicationAnalyzer { applicationData.profileExpression, ) - val springFacadeInstance = this::class.java.classLoader - .loadClass("org.utbot.spring.instantiator.SpringApplicationInstantiatorFacade") - .getConstructor() - .newInstance() - springFacadeInstance as ApplicationInstantiatorFacade - - return springFacadeInstance.instantiate(instantiationSettings) { instantiator -> - UtBotSpringShutdownException - .catch { instantiator.instantiate() } - .beanDefinitions - .toTypedArray() - } + return SpringApiProviderFacade.getInstance(this::class.java.classLoader) + .useMostSpecificNonFailingApi(instantiationSettings) { springApi -> + UtBotSpringShutdownException + .catch { springApi.getOrLoadSpringApplicationContext() } + .beanDefinitions + .toTypedArray() + } } } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/ConfigurationManager.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/ConfigurationManager.kt deleted file mode 100644 index f430795d5f..0000000000 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/ConfigurationManager.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.utbot.spring.utils - -import org.springframework.context.annotation.ImportResource -import org.springframework.context.annotation.PropertySource -import java.lang.reflect.InvocationHandler -import java.nio.file.Path -import java.util.Arrays -import kotlin.reflect.KClass - -class ConfigurationManager(private val classLoader: ClassLoader, private val userConfigurationClass: Class<*>) { - fun patchImportResourceAnnotation(userXmlFilePath: Path) = - patchAnnotation(ImportResource::class, String.format("classpath:%s", "$userXmlFilePath")) - - private fun patchAnnotation(annotationClass: KClass<*>, newValue: String?) { - val proxyClass = classLoader.loadClass("java.lang.reflect.Proxy") - val hField = proxyClass.getDeclaredField("h") - hField.isAccessible = true - - val propertySourceAnnotation = Arrays.stream( - userConfigurationClass.annotations - ) - .filter { el: Annotation -> el.annotationClass == annotationClass } - .findFirst() - - if (propertySourceAnnotation.isPresent) { - val annotationInvocationHandler = hField[propertySourceAnnotation.get()] as InvocationHandler - // TODO: https://github.com/UnitTestBot/UTBotJava/issues/2120 - // detect "file:..." resources recursively (or using bfs) and copy them without patching annotations - - val annotationInvocationHandlerClass = - classLoader.loadClass("sun.reflect.annotation.AnnotationInvocationHandler") - val memberValuesField = annotationInvocationHandlerClass.getDeclaredField("memberValues") - memberValuesField.isAccessible = true - - val memberValues = memberValuesField[annotationInvocationHandler] as MutableMap - addNewValue(memberValues, newValue) - } - } - - private fun addNewValue(memberValues: MutableMap, newValue: String?){ - if(newValue == null){ - memberValues["value"] = Array(0){""} - } - else { - val list: MutableList = (memberValues["value"] as Array).toMutableList() - list.add(newValue) - memberValues["value"] = list.toTypedArray() - } - } -} 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 e6a0ee63b9..a9601e6d6b 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 @@ -2,10 +2,13 @@ package org.utbot.spring.utils import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info +import org.springframework.context.annotation.ImportResource +import org.utbot.common.patchAnnotation import org.utbot.spring.api.ApplicationData import org.utbot.spring.config.TestApplicationConfiguration import org.utbot.spring.configurators.ApplicationConfigurationType import java.io.File +import java.nio.file.Path import kotlin.io.path.Path private val logger = getLogger() @@ -18,11 +21,13 @@ class SourceFinder( fun findSources(): Array> = when (configurationType) { ApplicationConfigurationType.XmlConfiguration -> { logger.info { "Using xml Spring configuration" } - val configurationManager = ConfigurationManager(classLoader, TestApplicationConfiguration::class.java) + // Put `applicationData.configurationFile` in `@ImportResource` of `TestApplicationConfiguration` - configurationManager.patchImportResourceAnnotation(Path(applicationData.configurationFile).fileName) + patchImportResourceAnnotation(Path(applicationData.configurationFile).fileName) + arrayOf(TestApplicationConfiguration::class.java) } + ApplicationConfigurationType.JavaConfiguration -> { logger.info { "Using java Spring configuration" } arrayOf( @@ -37,4 +42,11 @@ class SourceFinder( "xml" -> ApplicationConfigurationType.XmlConfiguration else -> ApplicationConfigurationType.JavaConfiguration } + + private fun patchImportResourceAnnotation(userXmlFilePath: Path) = + patchAnnotation( + annotation = TestApplicationConfiguration::class.java.getAnnotation(ImportResource::class.java), + property = "value", + newValue = arrayOf(String.format("classpath:%s", "$userXmlFilePath")) + ) } \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt new file mode 100644 index 0000000000..3e86627f26 --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt @@ -0,0 +1,42 @@ +package org.utbot.spring.api + +import java.net.URLClassLoader + +//TODO: `userSourcesClassLoader` must not be passed as a method argument, requires refactoring +interface SpringApi { + /** + * NOTE! [Any] return type is used here because Spring itself may not be on the classpath of the API user + */ + fun getOrLoadSpringApplicationContext(): Any + + fun getBean(beanName: String): Any + + fun getDependenciesForBean(beanName: String, userSourcesClassLoader: URLClassLoader): Set + + fun resetBean(beanName: String) + + fun resolveRepositories(beanNames: Set, userSourcesClassLoader: URLClassLoader): Set + + /** + * NOTE! Should be called once before any invocations of [beforeTestMethod] and [afterTestMethod] + */ + fun beforeTestClass() + + /** + * NOTE! Should be called on one thread with method under test and value constructor, + * because transactions are bound to threads + */ + fun beforeTestMethod() + + /** + * NOTE! Should be called on one thread with method under test and value constructor, + * because transactions are bound to threads + */ + fun afterTestMethod() +} + +data class RepositoryDescription( + val beanName: String, + val repositoryName: String, + val entityName: String, +) \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/context/ContextWrapper.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/context/ContextWrapper.kt deleted file mode 100644 index 5a1801a8c3..0000000000 --- a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/context/ContextWrapper.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.utbot.spring.api.context - -import java.net.URLClassLoader - -//TODO: `userSourcesClassLoader` must not be passed as a method argument, requires refactoring -interface ContextWrapper { - val context: Any - - fun getBean(beanName: String): Any - - fun getDependenciesForBean(beanName: String, userSourcesClassLoader: URLClassLoader): Set - - fun resetBean(beanName: String): Any - - fun resolveRepositories(beanNames: Set, userSourcesClassLoader: URLClassLoader): Set -} - -data class RepositoryDescription( - val beanName: String, - val repositoryName: String, - val entityName: String, - val tableName: String, -) \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/ApplicationInstantiatorFacade.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/ApplicationInstantiatorFacade.kt deleted file mode 100644 index 34a4018afc..0000000000 --- a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/ApplicationInstantiatorFacade.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.utbot.spring.api.instantiator - -import org.utbot.spring.api.context.ContextWrapper - - -interface ApplicationInstantiatorFacade { - fun instantiate(instantiationSettings: InstantiationSettings): ContextWrapper - - fun instantiate( - instantiationSettings: InstantiationSettings, - instantiatorRunner: (ConfiguredApplicationInstantiator) -> T - ): T -} \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/ConfiguredApplicationInstantiator.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/ConfiguredApplicationInstantiator.kt deleted file mode 100644 index 7a3032e586..0000000000 --- a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/ConfiguredApplicationInstantiator.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.utbot.spring.api.instantiator - -import org.utbot.spring.api.context.ContextWrapper - -fun interface ConfiguredApplicationInstantiator { - fun instantiate(): ContextWrapper -} \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/SpringApiProviderFacade.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/SpringApiProviderFacade.kt new file mode 100644 index 0000000000..33352fd0d9 --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/SpringApiProviderFacade.kt @@ -0,0 +1,29 @@ +package org.utbot.spring.api.instantiator + +import org.utbot.spring.api.SpringApi + +/** + * Stateless provider of independent [SpringApi] instances that do not have shared state, + * meaning each [SpringApi] instance will when needed start its own Spring Application. + */ +interface SpringApiProviderFacade { + fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): SpringApi + + /** + * [apiUser] is consequently invoked on all available (on the classpath) + * [SpringApi] types from most specific (e.g. Spring Boot) to least specific (e.g. Pure Spring) + * until it executes without throwing exception, then obtained result is returned. + */ + fun useMostSpecificNonFailingApi( + instantiationSettings: InstantiationSettings, + apiUser: (SpringApi) -> T + ): T + + companion object { + fun getInstance(classLoader: ClassLoader): SpringApiProviderFacade = + classLoader + .loadClass("org.utbot.spring.provider.SpringApiProviderFacadeImpl") + .getConstructor() + .newInstance() as SpringApiProviderFacade + } +} \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/repositoryWrapper/RepositoryInteraction.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/repositoryWrapper/RepositoryInteraction.kt deleted file mode 100644 index 2db4c28cc5..0000000000 --- a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/repositoryWrapper/RepositoryInteraction.kt +++ /dev/null @@ -1,14 +0,0 @@ -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() - } -} diff --git a/utbot-spring-commons/build.gradle.kts b/utbot-spring-commons/build.gradle.kts index 68077a5246..61131d1a9c 100644 --- a/utbot-spring-commons/build.gradle.kts +++ b/utbot-spring-commons/build.gradle.kts @@ -1,5 +1,6 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer +val springVersion: String by rootProject val springBootVersion: String by rootProject val rdVersion: String by rootProject @@ -19,6 +20,9 @@ dependencies { // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot compileOnly("org.springframework.boot:spring-boot:$springBootVersion") + compileOnly("org.springframework.boot:spring-boot-test-autoconfigure:$springBootVersion") + compileOnly("org.springframework:spring-test:$springVersion") + compileOnly("org.springframework:spring-tx:$springVersion") compileOnly("org.springframework.data:spring-data-commons:$springBootVersion") implementation("com.jetbrains.rd:rd-core:$rdVersion") { exclude(group = "org.slf4j", module = "slf4j-api") } } diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/SpringContextWrapper.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt similarity index 57% rename from utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/SpringContextWrapper.kt rename to utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt index 2256088044..58773e5e76 100644 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/SpringContextWrapper.kt +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt @@ -1,18 +1,49 @@ -package org.utbot.spring.context +package org.utbot.spring import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.warn import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.context.ConfigurableApplicationContext import org.springframework.data.repository.CrudRepository +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestContextManager import org.utbot.common.hasOnClasspath -import org.utbot.spring.api.context.ContextWrapper -import org.utbot.spring.api.context.RepositoryDescription +import org.utbot.common.patchAnnotation +import org.utbot.spring.api.SpringApi +import org.utbot.spring.api.RepositoryDescription +import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.dummy.DummySpringIntegrationTestClass +import org.utbot.spring.utils.RepositoryUtils +import java.lang.reflect.Method import java.net.URLClassLoader +import kotlin.reflect.jvm.javaMethod + +private val logger = getLogger() + +class SpringApiImpl( + instantiationSettings: InstantiationSettings, + dummyTestClass: Class, +) : SpringApi { + private lateinit var dummyTestClassInstance: DummySpringIntegrationTestClass + private val dummyTestClass = dummyTestClass.also { + patchAnnotation( + annotation = it.getAnnotation(ActiveProfiles::class.java), + property = "value", + newValue = parseProfileExpression(instantiationSettings.profileExpression) + ) + patchAnnotation( + annotation = it.getAnnotation(ContextConfiguration::class.java), + property = "classes", + newValue = instantiationSettings.configurationClasses + ) + } + private val dummyTestMethod: Method = DummySpringIntegrationTestClass::dummyTestMethod.javaMethod!! + private val testContextManager: TestContextManager = TestContextManager(this.dummyTestClass) -private val logger = getLogger() + private val context get() = testContextManager.testContext.applicationContext as ConfigurableApplicationContext -class SpringContextWrapper(override val context: ConfigurableApplicationContext) : ContextWrapper { + override fun getOrLoadSpringApplicationContext() = context private val isCrudRepositoryOnClasspath = try { CrudRepository::class.java.name @@ -87,7 +118,6 @@ class SpringContextWrapper(override val context: ConfigurableApplicationContext) beanName = repositoryBean.beanName, repositoryName = repositoryClassName, entityName = entity.name, - tableName = getTableName(entity), ) } else { logger.warn { @@ -100,6 +130,22 @@ class SpringContextWrapper(override val context: ConfigurableApplicationContext) return descriptions } + override fun beforeTestClass() { + testContextManager.beforeTestClass() + dummyTestClassInstance = dummyTestClass.getConstructor().newInstance() + testContextManager.prepareTestInstance(dummyTestClassInstance) + } + + override fun beforeTestMethod() { + testContextManager.beforeTestMethod(dummyTestClassInstance, dummyTestMethod) + testContextManager.beforeTestExecution(dummyTestClassInstance, dummyTestMethod) + } + + override fun afterTestMethod() { + testContextManager.afterTestExecution(dummyTestClassInstance, dummyTestMethod, null) + testContextManager.afterTestMethod(dummyTestClassInstance, dummyTestMethod, null) + } + private fun describesRepository(bean: Any): Boolean = try { bean is CrudRepository<*, *> @@ -107,7 +153,22 @@ class SpringContextWrapper(override val context: ConfigurableApplicationContext) false } - private fun getTableName(entity: Class<*>): String = entity.simpleName.decapitalize() + "s" + companion object { + private const val DEFAULT_PROFILE_NAME = "default" + + /** + * Transforms active profile information + * from the form of user input to a list of active profiles. + * + * Current user input form is comma-separated values, but it may be changed later. + */ + private fun parseProfileExpression(profileExpression: String?): Array = + if (profileExpression.isNullOrEmpty()) arrayOf(DEFAULT_PROFILE_NAME) + else profileExpression + .filter { !it.isWhitespace() } + .split(',') + .toTypedArray() + } data class SimpleBeanDefinition( val beanName: String, diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummyPureSpringIntegrationTestClass.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummyPureSpringIntegrationTestClass.kt new file mode 100644 index 0000000000..5a5ff81b27 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummyPureSpringIntegrationTestClass.kt @@ -0,0 +1,3 @@ +package org.utbot.spring.dummy + +class DummyPureSpringIntegrationTestClass : DummySpringIntegrationTestClass() diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringBootIntegrationTestClass.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringBootIntegrationTestClass.kt new file mode 100644 index 0000000000..28634c39ff --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringBootIntegrationTestClass.kt @@ -0,0 +1,7 @@ +package org.utbot.spring.dummy + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper +import org.springframework.test.context.BootstrapWith + +@BootstrapWith(SpringBootTestContextBootstrapper::class) +class DummySpringBootIntegrationTestClass : DummySpringIntegrationTestClass() diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringIntegrationTestClass.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringIntegrationTestClass.kt new file mode 100644 index 0000000000..e0b423a117 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/dummy/DummySpringIntegrationTestClass.kt @@ -0,0 +1,15 @@ +package org.utbot.spring.dummy + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Transactional + +@ActiveProfiles(/* fills dynamically */) +@ContextConfiguration(/* fills dynamically */) +@Transactional(isolation = Isolation.SERIALIZABLE) +@AutoConfigureTestDatabase +abstract class DummySpringIntegrationTestClass { + fun dummyTestMethod() {} +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/environment/EnvironmentFactory.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/environment/EnvironmentFactory.kt deleted file mode 100644 index 755bf537c5..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/environment/EnvironmentFactory.kt +++ /dev/null @@ -1,49 +0,0 @@ -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.instantiator.InstantiationSettings - - -private val logger = getLogger() - -class EnvironmentFactory( - private val instantiationSettings: InstantiationSettings -) { - companion object { - const val DEFAULT_PROFILE_NAME = "default" - } - - fun createEnvironment(): ConfigurableEnvironment { - val profilesToActivate = parseProfileExpression(instantiationSettings.profileExpression) - - val environment = StandardEnvironment() - - try { - environment.setActiveProfiles(*profilesToActivate) - } catch (e: Exception) { - logger.info { "Setting ${instantiationSettings.profileExpression} as active profiles failed with exception $e" } - } - - return environment - } - - /* - * Transforms active profile information - * from the form of user input to a list of active profiles. - * - * Current user input form is comma-separated values, but it may be changed later. - */ - private fun parseProfileExpression(profileExpression: String?): Array { - if (profileExpression.isNullOrEmpty()) { - return arrayOf(DEFAULT_PROFILE_NAME) - } - - return profileExpression - .filter { !it.isWhitespace() } - .split(',') - .toTypedArray() - } -} \ No newline at end of file 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 deleted file mode 100644 index e083bea457..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/PureSpringApplicationInstantiator.kt +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 4dda4efc39..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiator.kt +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 20ec63c734..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringApplicationInstantiatorFacade.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.utbot.spring.instantiator - -import org.utbot.spring.api.instantiator.InstantiationSettings -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 -import org.utbot.spring.api.context.ContextWrapper -import org.utbot.spring.api.instantiator.ApplicationInstantiatorFacade -import org.utbot.spring.api.instantiator.ConfiguredApplicationInstantiator -import org.utbot.spring.context.SpringContextWrapper - -private val logger = getLogger() - -class SpringApplicationInstantiatorFacade : ApplicationInstantiatorFacade { - - override fun instantiate(instantiationSettings: InstantiationSettings): ContextWrapper = - instantiate(instantiationSettings) { it.instantiate() } - - override fun instantiate( - instantiationSettings: InstantiationSettings, - instantiatorRunner: (ConfiguredApplicationInstantiator) -> T - ): T { - 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 environment = EnvironmentFactory(instantiationSettings).createEnvironment() - - for (instantiator in listOf(SpringBootApplicationInstantiator(), PureSpringApplicationInstantiator())) { - if (instantiator.canInstantiate()) { - logger.info { "Instantiating with $instantiator" } - try { - return instantiatorRunner(ConfiguredApplicationInstantiator { - val context = instantiator.instantiate(instantiationSettings.configurationClasses, environment) - SpringContextWrapper(context) - }) - } catch (e: Throwable) { - logger.error("Instantiating with $instantiator failed", e) - } - } - } - - error("Failed to initialize Spring context") - } -} 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 deleted file mode 100644 index 8b06a37502..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/instantiator/SpringBootApplicationInstantiator.kt +++ /dev/null @@ -1,26 +0,0 @@ -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() - - // This settings means that Spring will use any free port itself - val args = arrayOf("--server.port=0") - - return application.run(*args) - } -} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/PureSpringApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/PureSpringApiProvider.kt new file mode 100644 index 0000000000..86688e3807 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/PureSpringApiProvider.kt @@ -0,0 +1,13 @@ +package org.utbot.spring.provider + +import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.SpringApiImpl +import org.utbot.spring.dummy.DummyPureSpringIntegrationTestClass + +class PureSpringApiProvider : SpringApiProvider { + + override fun isAvailable() = true + + override fun provideAPI(instantiationSettings: InstantiationSettings) = + SpringApiImpl(instantiationSettings, DummyPureSpringIntegrationTestClass::class.java) +} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt new file mode 100644 index 0000000000..281390020a --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt @@ -0,0 +1,11 @@ +package org.utbot.spring.provider + +import org.utbot.spring.api.SpringApi +import org.utbot.spring.api.instantiator.InstantiationSettings + +interface SpringApiProvider { + + fun isAvailable(): Boolean + + fun provideAPI(instantiationSettings: InstantiationSettings): SpringApi +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt new file mode 100644 index 0000000000..95041897f6 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt @@ -0,0 +1,44 @@ +package org.utbot.spring.provider + +import com.jetbrains.rd.util.error +import org.utbot.spring.api.instantiator.InstantiationSettings + +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.SpringApi +import org.utbot.spring.api.instantiator.SpringApiProviderFacade + +private val logger = getLogger() + +class SpringApiProviderFacadeImpl : SpringApiProviderFacade { + + override fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): SpringApi = + useMostSpecificNonFailingApi(instantiationSettings) { api -> + api.getOrLoadSpringApplicationContext() + api + } + + override fun useMostSpecificNonFailingApi( + instantiationSettings: InstantiationSettings, + apiUser: (SpringApi) -> T + ): T { + 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() } + + for (apiProvider in listOf(SpringBootApiProvider(), PureSpringApiProvider())) { + if (apiProvider.isAvailable()) { + logger.info { "Getting Spring API from $apiProvider" } + try { + return apiUser(apiProvider.provideAPI(instantiationSettings)) + } catch (e: Throwable) { + logger.error("Getting Spring API from $apiProvider failed", e) + } + } + } + + error("Failed to use any Spring API") + } +} diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringBootApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringBootApiProvider.kt new file mode 100644 index 0000000000..5875dc6f9d --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringBootApiProvider.kt @@ -0,0 +1,19 @@ +package org.utbot.spring.provider + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper +import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.dummy.DummySpringBootIntegrationTestClass +import org.utbot.spring.SpringApiImpl + +class SpringBootApiProvider : SpringApiProvider { + + override fun isAvailable(): Boolean = try { + SpringBootTestContextBootstrapper::class.java.name + true + } catch (e: ClassNotFoundException) { + false + } + + override fun provideAPI(instantiationSettings: InstantiationSettings) = + SpringApiImpl(instantiationSettings, DummySpringBootIntegrationTestClass::class.java) +} \ 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 deleted file mode 100644 index 51403e9c55..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperBeanPostProcessor.kt +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 4ef82574dc..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperConfiguration.kt +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 37dea711b2..0000000000 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/repositoryWrapper/RepositoryWrapperInvocationHandler.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.utbot.spring.repositoryWrapper - -import org.utbot.spring.api.repositoryWrapper.RepositoryInteraction -import java.lang.reflect.InvocationHandler -import java.lang.reflect.InvocationTargetException -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 = try { - Result.success(method.invoke(originalRepository, *nonNullArgs)) - } catch (e: InvocationTargetException) { - Result.failure(e.targetException) - } - RepositoryInteraction.recordedInteractions.add( - RepositoryInteraction(beanName, method, nonNullArgs.toList(), result) - ) - return result.getOrThrow() - } -} \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/RepositoryUtils.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/RepositoryUtils.kt similarity index 97% rename from utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/RepositoryUtils.kt rename to utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/RepositoryUtils.kt index 2ef35a4f46..7502e06a3a 100644 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/context/RepositoryUtils.kt +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/RepositoryUtils.kt @@ -1,4 +1,4 @@ -package org.utbot.spring.context +package org.utbot.spring.utils import org.springframework.core.GenericTypeResolver import org.springframework.data.repository.CrudRepository