Skip to content

Commit 389fe66

Browse files
authored
Introduce and test UtBotSpringApi (#2632)
* Introduce and test `UtBotSpringApi` * Remove repeated dependency * Make `UtBotSpringApi` validate that integration tests are only used with `PresentSpringSettings` * Add `META-INF/spring` to spring-sample shadow jar * Add `UtBotSpringApi` tests for `AbsentSpringSettings` and profiles
1 parent 4bb4329 commit 389fe66

File tree

21 files changed

+421
-32
lines changed

21 files changed

+421
-32
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,10 @@ sealed class SpringConfiguration(val fullDisplayName: String) {
14301430

14311431
class JavaConfiguration(override val configBinaryName: String) : JavaBasedConfiguration(configBinaryName)
14321432

1433-
class SpringBootConfiguration(override val configBinaryName: String, val isUnique: Boolean): JavaBasedConfiguration(configBinaryName)
1433+
class SpringBootConfiguration(
1434+
override val configBinaryName: String,
1435+
val isDefinitelyUnique: Boolean
1436+
) : JavaBasedConfiguration(configBinaryName)
14341437

14351438
class XMLConfiguration(val absolutePath: String) : SpringConfiguration(absolutePath)
14361439
}

utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ private fun EngineProcessModel.setup(kryoHelper: KryoHelper, watchdog: IdleWatch
250250
}
251251
watchdog.measureTimeForActiveCall(perform, "Performing dynamic task") { params ->
252252
val task = kryoHelper.readObject<EngineProcessTask<Any?>>(params.engineProcessTask)
253-
val result = task.perform(kryoHelper)
253+
val result = task.perform()
254254
kryoHelper.writeObject(result)
255255
}
256256
}
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package org.utbot.framework.process
22

3-
import org.utbot.framework.process.kryo.KryoHelper
4-
53
/**
64
* Implementations of this interface can be passed to engine process for execution and should
75
* be used for adding feature-specific (e.g. Spring-specific) tasks without inflating core UtBot codebase.
@@ -12,5 +10,5 @@ import org.utbot.framework.process.kryo.KryoHelper
1210
* @param R result type of the task (should be present on the classpath of both processes).
1311
*/
1412
interface EngineProcessTask<R> {
15-
fun perform(kryoHelper: KryoHelper): R
13+
fun perform(): R
1614
}

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ object UtTestsDialogProcessor {
292292
val clarifiedBeanDefinitions =
293293
clarifyBeanDefinitionReturnTypes(beanDefinitions, project)
294294

295-
SpringApplicationContextImpl(
295+
SpringApplicationContextImpl.internalCreate(
296296
simpleApplicationContext,
297297
clarifiedBeanDefinitions,
298298
model.springTestType,

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
727727
if (springBootConfigs.contains(classBinaryName)) {
728728
SpringConfiguration.SpringBootConfiguration(
729729
configBinaryName = classBinaryName,
730-
isUnique = springBootConfigs.size == 1,
730+
isDefinitelyUnique = springBootConfigs.size == 1,
731731
)
732732
} else {
733733
SpringConfiguration.JavaConfiguration(classBinaryName)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.utbot.external.api
2+
3+
import org.utbot.framework.context.ApplicationContext
4+
import org.utbot.framework.context.simple.SimpleApplicationContext
5+
import org.utbot.framework.context.spring.SpringApplicationContext
6+
import org.utbot.framework.context.spring.SpringApplicationContextImpl
7+
import org.utbot.framework.plugin.api.SpringConfiguration
8+
import org.utbot.framework.plugin.api.SpringSettings
9+
import org.utbot.framework.plugin.api.SpringTestType
10+
import org.utbot.framework.process.SpringAnalyzerTask
11+
import java.io.File
12+
13+
object UtBotSpringApi {
14+
private val springBootConfigAnnotations = setOf(
15+
"org.springframework.boot.autoconfigure.SpringBootApplication",
16+
"org.springframework.boot.SpringBootConfiguration"
17+
)
18+
19+
/**
20+
* NOTE: [classpath] should include project under test classpath (with all dependencies) as well as
21+
* `spring-test`, `spring-boot-test`, and `spring-security-test` if respectively `spring-beans`,
22+
* `spring-boot`, and `spring-security-core` are dependencies of project under test.
23+
*
24+
* UtBot doesn't add Spring test modules to classpath automatically to let API users control their versions.
25+
*/
26+
@JvmOverloads
27+
@JvmStatic
28+
fun createSpringApplicationContext(
29+
springSettings: SpringSettings,
30+
springTestType: SpringTestType,
31+
classpath: List<String>,
32+
delegateContext: ApplicationContext = SimpleApplicationContext()
33+
): SpringApplicationContext {
34+
if (springTestType == SpringTestType.INTEGRATION_TEST) {
35+
require(springSettings is SpringSettings.PresentSpringSettings) {
36+
"Integration tests can't be generated without Spring settings"
37+
}
38+
val configuration = springSettings.configuration
39+
require(configuration !is SpringConfiguration.XMLConfiguration) {
40+
"Integration tests aren't supported for XML configurations, consider using Java " +
41+
"configuration that imports your XML configuration with @ImportResource"
42+
}
43+
}
44+
return SpringApplicationContextImpl.internalCreate(
45+
delegateContext = delegateContext,
46+
beanDefinitions = when (springSettings) {
47+
SpringSettings.AbsentSpringSettings -> listOf()
48+
is SpringSettings.PresentSpringSettings -> SpringAnalyzerTask(classpath, springSettings).perform()
49+
},
50+
springTestType = springTestType,
51+
springSettings = springSettings,
52+
)
53+
}
54+
55+
@JvmStatic
56+
fun createXmlSpringConfiguration(xmlConfig: File): SpringConfiguration.XMLConfiguration =
57+
SpringConfiguration.XMLConfiguration(xmlConfig.absolutePath)
58+
59+
@JvmStatic
60+
fun createJavaSpringConfiguration(javaConfig: Class<*>): SpringConfiguration.JavaBasedConfiguration =
61+
if (javaConfig.annotations.any { it.annotationClass.java.name in springBootConfigAnnotations }) {
62+
SpringConfiguration.SpringBootConfiguration(javaConfig.name, isDefinitelyUnique = false)
63+
} else {
64+
SpringConfiguration.JavaConfiguration(javaConfig.name)
65+
}
66+
}

utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ class CgSpringIntegrationTestClassConstructor(
194194
// TODO: we support only JavaBasedConfiguration in integration tests.
195195
// Adapt for XMLConfigurations when supported.
196196
val configClass = springSettings.configuration as JavaBasedConfiguration
197-
if (configClass is JavaConfiguration || configClass is SpringBootConfiguration && !configClass.isUnique) {
197+
if (configClass is JavaConfiguration || configClass is SpringBootConfiguration && !configClass.isDefinitelyUnique) {
198198
addAnnotation(
199199
classId = contextConfigurationClassId,
200200
namedArguments = listOf(

utbot-spring-framework/src/main/kotlin/org/utbot/framework/context/spring/SpringApplicationContextImpl.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.utbot.common.dynamicPropertiesOf
55
import org.utbot.common.isAbstract
66
import org.utbot.common.isStatic
77
import org.utbot.common.withValue
8+
import org.utbot.external.api.UtBotSpringApi
89
import org.utbot.framework.UtSettings
910
import org.utbot.framework.codegen.generator.AbstractCodeGenerator
1011
import org.utbot.framework.codegen.generator.CodeGeneratorParams
@@ -39,14 +40,29 @@ import org.utbot.fuzzing.spring.unit.InjectMockValueProvider
3940
import org.utbot.fuzzing.toFuzzerType
4041
import org.utbot.instrumentation.instrumentation.execution.RemovingConstructFailsUtExecutionInstrumentation
4142

42-
class SpringApplicationContextImpl(
43+
class SpringApplicationContextImpl private constructor(
4344
private val delegateContext: ApplicationContext,
44-
override val beanDefinitions: List<BeanDefinitionData> = emptyList(),
45+
override val beanDefinitions: List<BeanDefinitionData>,
4546
private val springTestType: SpringTestType,
4647
override val springSettings: SpringSettings,
4748
): ApplicationContext by delegateContext, SpringApplicationContext {
4849
companion object {
4950
private val logger = KotlinLogging.logger {}
51+
52+
/**
53+
* Used internally by UtBot to create an instance of [SpringApplicationContextImpl]
54+
* when [beanDefinitions] are already known.
55+
*
56+
* NOTE: Bean definitions defined in config from [springSettings] are IGNORED.
57+
*
58+
* API users should use [UtBotSpringApi.createSpringApplicationContext]
59+
*/
60+
fun internalCreate(
61+
delegateContext: ApplicationContext,
62+
beanDefinitions: List<BeanDefinitionData>,
63+
springTestType: SpringTestType,
64+
springSettings: SpringSettings,
65+
) = SpringApplicationContextImpl(delegateContext, beanDefinitions, springTestType, springSettings)
5066
}
5167

5268
private object ReplacedFuzzedTypeFlag : FuzzedTypeFlag

utbot-spring-framework/src/main/kotlin/org/utbot/framework/process/SpringAnalyzerTask.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import mu.KotlinLogging
44
import org.utbot.framework.plugin.api.BeanAdditionalData
55
import org.utbot.framework.plugin.api.BeanDefinitionData
66
import org.utbot.framework.plugin.api.SpringSettings.PresentSpringSettings
7-
import org.utbot.framework.process.kryo.KryoHelper
87
import org.utbot.rd.use
98
import org.utbot.spring.process.SpringAnalyzerProcess
109

@@ -16,7 +15,7 @@ class SpringAnalyzerTask(
1615
private val logger = KotlinLogging.logger {}
1716
}
1817

19-
override fun perform(kryoHelper: KryoHelper): List<BeanDefinitionData> = try {
18+
override fun perform(): List<BeanDefinitionData> = try {
2019
SpringAnalyzerProcess.createBlocking(classpath).use {
2120
it.getBeanDefinitions(settings)
2221
}.beanDefinitions

utbot-spring-sample/build.gradle

Lines changed: 0 additions & 19 deletions
This file was deleted.

utbot-spring-sample/build.gradle.kts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
2+
import com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer
3+
4+
plugins {
5+
id("com.github.johnrengelman.shadow") version "7.1.2"
6+
id("java")
7+
}
8+
9+
val springBootVersion: String by rootProject
10+
11+
dependencies {
12+
implementation("org.projectlombok:lombok:1.18.20")
13+
annotationProcessor("org.projectlombok:lombok:1.18.20")
14+
15+
implementation(group = "org.springframework.boot", name = "spring-boot-starter-web", version = springBootVersion)
16+
implementation(group = "org.springframework.boot", name = "spring-boot-starter-data-jpa", version = springBootVersion)
17+
implementation(group = "org.springframework.boot", name = "spring-boot-starter-test", version = springBootVersion)
18+
}
19+
20+
tasks.shadowJar {
21+
isZip64 = true
22+
23+
transform(Log4j2PluginsCacheFileTransformer::class.java)
24+
archiveFileName.set("utbot-spring-sample-shadow.jar")
25+
26+
// Required for Spring to run properly when using shadowJar
27+
// More details: https://github.com/spring-projects/spring-boot/issues/1828
28+
mergeServiceFiles()
29+
append("META-INF/spring.handlers")
30+
append("META-INF/spring.schemas")
31+
append("META-INF/spring.tooling")
32+
transform(PropertiesFileTransformer().apply {
33+
paths = listOf("META-INF/spring.factories")
34+
mergeStrategy = "append"
35+
})
36+
}
37+
38+
val springSampleJar: Configuration by configurations.creating {
39+
isCanBeResolved = false
40+
isCanBeConsumed = true
41+
}
42+
43+
artifacts {
44+
add(springSampleJar.name, tasks.shadowJar)
45+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.utbot.examples.spring.config.boot;
2+
3+
import org.springframework.boot.autoconfigure.SpringBootApplication;
4+
5+
@SpringBootApplication
6+
public class ExampleSpringBootConfig {
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.utbot.examples.spring.config.boot;
2+
3+
import org.springframework.stereotype.Service;
4+
import org.utbot.examples.spring.config.utils.SafetyUtils;
5+
6+
@Service
7+
public class ExampleSpringBootService {
8+
public ExampleSpringBootService() {
9+
SafetyUtils.shouldNeverBeCalled();
10+
}
11+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.utbot.examples.spring.config.pure;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Profile;
5+
import org.utbot.examples.spring.config.utils.SafetyUtils;
6+
7+
public class ExamplePureSpringConfig {
8+
@Bean(name = "exampleService0")
9+
public ExamplePureSpringService exampleService() {
10+
SafetyUtils.shouldNeverBeCalled();
11+
return null;
12+
}
13+
14+
@Bean(name = "exampleServiceTest1")
15+
@Profile("test1")
16+
public ExamplePureSpringService exampleServiceTest1() {
17+
SafetyUtils.shouldNeverBeCalled();
18+
return null;
19+
}
20+
21+
@Bean(name = "exampleServiceTest2")
22+
@Profile("test2")
23+
public ExamplePureSpringService exampleServiceTest2() {
24+
SafetyUtils.shouldNeverBeCalled();
25+
return null;
26+
}
27+
28+
@Bean(name = "exampleServiceTest3")
29+
@Profile("test3")
30+
public ExamplePureSpringService exampleServiceTest3() {
31+
SafetyUtils.shouldNeverBeCalled();
32+
return null;
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.utbot.examples.spring.config.pure;
2+
3+
public class ExamplePureSpringService {
4+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.utbot.examples.spring.config.utils;
2+
3+
public class SafetyUtils {
4+
5+
private static final int UNEXPECTED_CALL_EXIT_STATUS = -1182;
6+
7+
/**
8+
* Bean constructors and factory methods should never be executed during bean analysis,
9+
* hence call to this method is added into them to ensure they are actually never called.
10+
*/
11+
public static void shouldNeverBeCalled() {
12+
System.err.println("shouldNeverBeCalled() is unexpectedly called");
13+
System.exit(UNEXPECTED_CALL_EXIT_STATUS);
14+
}
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.utbot.examples.spring.config.xml;
2+
3+
import org.utbot.examples.spring.config.utils.SafetyUtils;
4+
5+
public class ExampleXmlService {
6+
public ExampleXmlService() {
7+
SafetyUtils.shouldNeverBeCalled();
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
3+
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
4+
5+
<beans>
6+
7+
<bean id="xmlService" class="org.utbot.examples.spring.config.xml.ExampleXmlService"/>
8+
9+
</beans>

utbot-spring-test/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
configurations {
2+
fetchSpringSampleJar
3+
}
4+
15
dependencies {
26
testImplementation(project(":utbot-framework"))
37
testImplementation(project(":utbot-spring-framework"))
@@ -16,7 +20,11 @@ dependencies {
1620
testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockitoInlineVersion
1721
testImplementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion
1822

23+
testImplementation group: 'commons-io', name: 'commons-io', version: commonsIoVersion
24+
1925
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: springBootVersion
26+
27+
fetchSpringSampleJar project(path: ':utbot-spring-sample', configuration: 'springSampleJar')
2028
}
2129

2230
configurations {
@@ -30,3 +38,9 @@ test {
3038
jvmArgs '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9009'
3139
}
3240
}
41+
42+
processTestResources {
43+
from(configurations.fetchSpringSampleJar) {
44+
into "lib"
45+
}
46+
}

0 commit comments

Comments
 (0)