Skip to content

Commit 84da87e

Browse files
Support running Spring integration tests in instrumentation process #2357 #2220 (#2335)
* Add basement for SpringTestContextManagerAPI * Adapt annotation patching * Refactor `patchAnnotation` * Refactor dummy test classes * Make `SpringContextWrapper` use `TestContextManager` * Refactor `InstrumentationContext` * Trigger interactions with `TestContextManager` from `SpringUtExecutionInstrumentation` * Remove unused `RepositoryWrapperBeanPostProcessor` * Remove not yet relevant comment * Make `SpringUtExecutionInstrumentation` only transform user sources * Address comments from #2335 * Remove no longer used `tableName` from `RepositoryDescription` * Rename `MockValueConstructor` to `ContextAwareValueConstructor` * Improve naming --------- Co-authored-by: IlyaMuravjov <muravjovilya@gmail.com>
1 parent 93be3c2 commit 84da87e

File tree

42 files changed

+504
-493
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+504
-493
lines changed

gradle.properties

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ openblasVersion=0.3.10-1.5.4
8181
arpackNgVersion=3.7.0-1.5.4
8282
commonsLoggingVersion=1.2
8383
commonsIOVersion=2.11.0
84-
springBootVersion=2.7.8
84+
85+
# use latest Java 8 compaitable Spring and Spring Boot versions
86+
springVersion=5.3.28
87+
springBootVersion=2.7.13
8588

8689
# configuration for build server
8790
#
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.utbot.common
2+
3+
import java.lang.reflect.InvocationHandler
4+
import java.lang.reflect.Proxy
5+
6+
/**
7+
* Assigns [newValue] to specified [property] of [annotation].
8+
*
9+
* NOTE! [annotation] instance is expected to be a [Proxy]
10+
* using [sun.reflect.annotation.AnnotationInvocationHandler]
11+
* making this function depend on JDK vendor and version.
12+
*
13+
* Example: `@ImportResource -> @ImportResource(value = "classpath:shark-config.xml")`
14+
*/
15+
fun patchAnnotation(
16+
annotation: Annotation,
17+
property: String,
18+
newValue: Any?
19+
) {
20+
val proxyClass = Proxy::class.java
21+
val hField = proxyClass.getDeclaredField("h")
22+
hField.isAccessible = true
23+
24+
val invocationHandler = hField[annotation] as InvocationHandler
25+
26+
val annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler")
27+
val memberValuesField = annotationInvocationHandlerClass.getDeclaredField("memberValues")
28+
memberValuesField.isAccessible = true
29+
30+
@Suppress("UNCHECKED_CAST") // unavoidable because of reflection
31+
val memberValues = memberValuesField[invocationHandler] as MutableMap<String, Any?>
32+
memberValues[property] = newValue
33+
}

utbot-instrumentation/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ val kotlinLoggingVersion: String by rootProject
77
val rdVersion: String by rootProject
88
val mockitoVersion: String by rootProject
99
val mockitoInlineVersion: String by rootProject
10-
val springBootVersion: String by rootProject
1110

1211
plugins {
1312
id("com.github.johnrengelman.shadow") version "7.1.2"
Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package org.utbot.instrumentation.instrumentation.execution
22

3-
import org.utbot.common.JarUtils
43
import com.jetbrains.rd.util.getLogger
54
import com.jetbrains.rd.util.info
5+
import org.utbot.common.JarUtils
6+
import org.utbot.common.hasOnClasspath
67
import org.utbot.framework.plugin.api.BeanDefinitionData
78
import org.utbot.framework.plugin.api.ClassId
89
import org.utbot.framework.plugin.api.SpringRepositoryId
910
import org.utbot.framework.plugin.api.util.jClass
1011
import org.utbot.instrumentation.instrumentation.ArgumentList
1112
import org.utbot.instrumentation.instrumentation.Instrumentation
12-
import org.utbot.instrumentation.instrumentation.execution.mock.SpringInstrumentationContext
13+
import org.utbot.instrumentation.instrumentation.execution.context.SpringInstrumentationContext
14+
import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhaseFailingOnAnyException
1315
import org.utbot.instrumentation.process.HandlerClassesLoader
14-
import org.utbot.spring.api.context.ContextWrapper
15-
import org.utbot.spring.api.repositoryWrapper.RepositoryInteraction
16+
import org.utbot.spring.api.SpringApi
1617
import java.net.URL
1718
import java.net.URLClassLoader
1819
import java.security.ProtectionDomain
@@ -32,7 +33,10 @@ class SpringUtExecutionInstrumentation(
3233

3334
private val relatedBeansCache = mutableMapOf<Class<*>, Set<String>>()
3435

35-
private val springContext: ContextWrapper get() = instrumentationContext.springContext
36+
private val springApi: SpringApi get() = instrumentationContext.springApi
37+
38+
private object SpringBeforeTestMethodPhase : ExecutionPhaseFailingOnAnyException()
39+
private object SpringAfterTestMethodPhase : ExecutionPhaseFailingOnAnyException()
3640

3741
companion object {
3842
private val logger = getLogger<SpringUtExecutionInstrumentation>()
@@ -50,10 +54,11 @@ class SpringUtExecutionInstrumentation(
5054
)
5155
)
5256

53-
instrumentationContext = SpringInstrumentationContext(springConfig)
5457
userSourcesClassLoader = URLClassLoader(buildDirs, null)
58+
instrumentationContext = SpringInstrumentationContext(springConfig, delegateInstrumentation.instrumentationContext)
5559
delegateInstrumentation.instrumentationContext = instrumentationContext
5660
delegateInstrumentation.init(pathsToUserClasses)
61+
springApi.beforeTestClass()
5762
}
5863

5964
override fun invoke(
@@ -62,42 +67,36 @@ class SpringUtExecutionInstrumentation(
6267
arguments: ArgumentList,
6368
parameters: Any?
6469
): UtConcreteExecutionResult {
65-
RepositoryInteraction.recordedInteractions.clear()
66-
67-
val beanNamesToReset: Set<String> = getRelevantBeanNames(clazz)
68-
val repositoryDefinitions = springContext.resolveRepositories(beanNamesToReset, userSourcesClassLoader)
69-
70-
beanNamesToReset.forEach { beanName -> springContext.resetBean(beanName) }
71-
val jdbcTemplate = getBean("jdbcTemplate")
72-
73-
for (repositoryDefinition in repositoryDefinitions) {
74-
val truncateTableCommand = "TRUNCATE TABLE ${repositoryDefinition.tableName}"
75-
jdbcTemplate::class.java
76-
.getMethod("execute", truncateTableCommand::class.java)
77-
.invoke(jdbcTemplate, truncateTableCommand)
78-
79-
val restartIdCommand = "ALTER TABLE ${repositoryDefinition.tableName} ALTER COLUMN id RESTART WITH 1"
80-
jdbcTemplate::class.java
81-
.getMethod("execute", restartIdCommand::class.java)
82-
.invoke(jdbcTemplate, restartIdCommand)
70+
getRelevantBeans(clazz).forEach { beanName -> springApi.resetBean(beanName) }
71+
72+
return delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters) { invokeBasePhases ->
73+
// NB! beforeTestMethod() and afterTestMethod() are intentionally called inside phases,
74+
// so they are executed in one thread with method under test
75+
executePhaseInTimeout(SpringBeforeTestMethodPhase) { springApi.beforeTestMethod() }
76+
try {
77+
invokeBasePhases()
78+
} finally {
79+
executePhaseInTimeout(SpringAfterTestMethodPhase) { springApi.afterTestMethod() }
80+
}
8381
}
84-
85-
return delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters)
8682
}
8783

88-
private fun getRelevantBeanNames(clazz: Class<*>): Set<String> = relatedBeansCache.getOrPut(clazz) {
84+
private fun getRelevantBeans(clazz: Class<*>): Set<String> = relatedBeansCache.getOrPut(clazz) {
8985
beanDefinitions
9086
.filter { it.beanTypeFqn == clazz.name }
91-
.flatMap { springContext.getDependenciesForBean(it.beanName, userSourcesClassLoader) }
87+
// forces `getBean()` to load Spring classes,
88+
// otherwise execution of method under test may fail with timeout
89+
.onEach { springApi.getBean(it.beanName) }
90+
.flatMap { springApi.getDependenciesForBean(it.beanName, userSourcesClassLoader) }
9291
.toSet()
9392
.also { logger.info { "Detected relevant beans for class ${clazz.name}: $it" } }
9493
}
9594

96-
fun getBean(beanName: String): Any = springContext.getBean(beanName)
95+
fun getBean(beanName: String): Any = springApi.getBean(beanName)
9796

9897
fun getRepositoryDescriptions(classId: ClassId): Set<SpringRepositoryId> {
99-
val relevantBeanNames = getRelevantBeanNames(classId.jClass)
100-
val repositoryDescriptions = springContext.resolveRepositories(relevantBeanNames.toSet(), userSourcesClassLoader)
98+
val relevantBeanNames = getRelevantBeans(classId.jClass)
99+
val repositoryDescriptions = springApi.resolveRepositories(relevantBeanNames.toSet(), userSourcesClassLoader)
101100
return repositoryDescriptions.map { repositoryDescription ->
102101
SpringRepositoryId(
103102
repositoryDescription.beanName,
@@ -114,19 +113,14 @@ class SpringUtExecutionInstrumentation(
114113
protectionDomain: ProtectionDomain,
115114
classfileBuffer: ByteArray
116115
): ByteArray? =
117-
// TODO: automatically detect which libraries we don't want to transform (by total transformation time)
118-
if (listOf(
119-
"org/springframework",
120-
"com/fasterxml",
121-
"org/hibernate",
122-
"org/apache",
123-
"org/h2",
124-
"javax/",
125-
"ch/qos",
126-
).any { className.startsWith(it) }
127-
) {
128-
null
129-
} else {
116+
// we do not transform Spring classes as it takes too much time
117+
118+
// maybe we should still transform classes related to data validation
119+
// (e.g. from packages "javax/persistence" and "jakarta/persistence"),
120+
// since traces from such classes can be particularly useful for feedback to fuzzer
121+
if (userSourcesClassLoader.hasOnClasspath(className.replace("/", "."))) {
130122
delegateInstrumentation.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer)
123+
} else {
124+
null
131125
}
132126
}

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/UtExecutionInstrumentation.kt

Lines changed: 52 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import org.utbot.instrumentation.instrumentation.InvokeInstrumentation
99
import org.utbot.instrumentation.instrumentation.et.TraceHandler
1010
import org.utbot.instrumentation.instrumentation.execution.constructors.ConstructOnlyUserClassesOrCachedObjectsStrategy
1111
import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor
12-
import org.utbot.instrumentation.instrumentation.execution.mock.InstrumentationContext
12+
import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext
13+
import org.utbot.instrumentation.instrumentation.execution.context.SimpleInstrumentationContext
1314
import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicClassVisitor
1415
import org.utbot.instrumentation.instrumentation.execution.ndd.NonDeterministicDetector
15-
import org.utbot.instrumentation.instrumentation.execution.phases.ConstructedData
1616
import org.utbot.instrumentation.instrumentation.execution.phases.PhasesController
1717
import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter
1818
import org.utbot.instrumentation.instrumentation.mock.MockClassVisitor
@@ -51,7 +51,7 @@ data class UtConcreteExecutionResult(
5151
object UtExecutionInstrumentation : Instrumentation<UtConcreteExecutionResult> {
5252
private val delegateInstrumentation = InvokeInstrumentation()
5353

54-
var instrumentationContext = InstrumentationContext()
54+
var instrumentationContext: InstrumentationContext = SimpleInstrumentationContext()
5555

5656
private val traceHandler = TraceHandler()
5757
private val ndDetector = NonDeterministicDetector()
@@ -74,14 +74,14 @@ object UtExecutionInstrumentation : Instrumentation<UtConcreteExecutionResult> {
7474
arguments: ArgumentList,
7575
parameters: Any?
7676
): UtConcreteExecutionResult =
77-
invoke(clazz, methodSignature, arguments, parameters, additionalPhases = { it })
77+
invoke(clazz, methodSignature, arguments, parameters, phasesWrapper = { it() })
7878

7979
fun invoke(
8080
clazz: Class<*>,
8181
methodSignature: String,
8282
arguments: ArgumentList,
8383
parameters: Any?,
84-
additionalPhases: PhasesController.(UtConcreteExecutionResult) -> UtConcreteExecutionResult
84+
phasesWrapper: PhasesController.(invokeBasePhases: () -> UtConcreteExecutionResult) -> UtConcreteExecutionResult
8585
): UtConcreteExecutionResult {
8686
if (parameters !is UtConcreteExecutionData) {
8787
throw IllegalArgumentException("Argument parameters must be of type UtConcreteExecutionData, but was: ${parameters?.javaClass}")
@@ -94,65 +94,62 @@ object UtExecutionInstrumentation : Instrumentation<UtConcreteExecutionResult> {
9494
delegateInstrumentation,
9595
timeout
9696
).computeConcreteExecutionResult {
97-
try {
98-
// some preparation actions for concrete execution
99-
var constructedData: ConstructedData
97+
phasesWrapper {
10098
try {
101-
constructedData = applyPreprocessing(parameters)
102-
} catch (t: Throwable) {
103-
return UtConcreteExecutionResult(MissingState, UtConcreteExecutionProcessedFailure(t), Coverage())
104-
}
105-
106-
val (params, statics, cache) = constructedData
107-
108-
// invocation
109-
val concreteResult = executePhaseInTimeout(invocationPhase) {
110-
invoke(clazz, methodSignature, params.map { it.value })
111-
}
99+
// some preparation actions for concrete execution
100+
val constructedData = applyPreprocessing(parameters)
112101

113-
// statistics collection
114-
val (coverage, ndResults) = executePhaseInTimeout(statisticsCollectionPhase) {
115-
getCoverage(clazz) to getNonDeterministicResults()
116-
}
102+
val (params, statics, cache) = constructedData
117103

118-
// model construction
119-
val (executionResult, stateAfter, newInstrumentation) = executePhaseInTimeout(modelConstructionPhase) {
120-
configureConstructor {
121-
this.cache = cache
122-
strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy(
123-
pathsToUserClasses,
124-
cache
125-
)
104+
// invocation
105+
val concreteResult = executePhaseInTimeout(invocationPhase) {
106+
invoke(clazz, methodSignature, params.map { it.value })
126107
}
127108

128-
val ndStatics = constructStaticInstrumentation(ndResults.statics)
129-
val ndNews = constructNewInstrumentation(ndResults.news, ndResults.calls)
130-
val newInstrumentation = mergeInstrumentations(instrumentations, ndStatics, ndNews)
131-
132-
val returnType = clazz.singleExecutableId(methodSignature).returnType
133-
val executionResult = convertToExecutionResult(concreteResult,returnType)
109+
// statistics collection
110+
val (coverage, ndResults) = executePhaseInTimeout(statisticsCollectionPhase) {
111+
getCoverage(clazz) to getNonDeterministicResults()
112+
}
134113

135-
val stateAfterParametersWithThis = constructParameters(params)
136-
val stateAfterStatics = constructStatics(stateBefore, statics)
137-
val (stateAfterThis, stateAfterParameters) = if (stateBefore.thisInstance == null) {
138-
null to stateAfterParametersWithThis
139-
} else {
140-
stateAfterParametersWithThis.first() to stateAfterParametersWithThis.drop(1)
114+
// model construction
115+
val (executionResult, stateAfter, newInstrumentation) = executePhaseInTimeout(modelConstructionPhase) {
116+
configureConstructor {
117+
this.cache = cache
118+
strategy = ConstructOnlyUserClassesOrCachedObjectsStrategy(
119+
pathsToUserClasses,
120+
cache
121+
)
122+
}
123+
124+
val ndStatics = constructStaticInstrumentation(ndResults.statics)
125+
val ndNews = constructNewInstrumentation(ndResults.news, ndResults.calls)
126+
val newInstrumentation = mergeInstrumentations(instrumentations, ndStatics, ndNews)
127+
128+
val returnType = clazz.singleExecutableId(methodSignature).returnType
129+
val executionResult = convertToExecutionResult(concreteResult, returnType)
130+
131+
val stateAfterParametersWithThis = constructParameters(params)
132+
val stateAfterStatics = constructStatics(stateBefore, statics)
133+
val (stateAfterThis, stateAfterParameters) = if (stateBefore.thisInstance == null) {
134+
null to stateAfterParametersWithThis
135+
} else {
136+
stateAfterParametersWithThis.first() to stateAfterParametersWithThis.drop(1)
137+
}
138+
val stateAfter = EnvironmentModels(stateAfterThis, stateAfterParameters, stateAfterStatics)
139+
140+
Triple(executionResult, stateAfter, newInstrumentation)
141141
}
142-
val stateAfter = EnvironmentModels(stateAfterThis, stateAfterParameters, stateAfterStatics)
143142

144-
Triple(executionResult, stateAfter, newInstrumentation)
143+
UtConcreteExecutionResult(
144+
stateAfter,
145+
executionResult,
146+
coverage,
147+
newInstrumentation
148+
)
149+
} finally {
150+
// restoring data after concrete execution
151+
applyPostprocessing()
145152
}
146-
147-
additionalPhases(UtConcreteExecutionResult(
148-
stateAfter,
149-
executionResult,
150-
coverage,
151-
newInstrumentation
152-
))
153-
} finally {
154-
// restoring data after concrete execution
155-
applyPostprocessing()
156153
}
157154
}
158155
}

0 commit comments

Comments
 (0)