Skip to content

Commit 0107fac

Browse files
authored
Only add @AutoConfigureTestDatabase when spring-data is installed; install spring-test with spring-boot-test (#2391)
* Only add `@AutoConfigureTestDatabase` to dummy test class when `spring-data` is installed * Only add `@AutoConfigureTestDatabase` to generated test class when `spring-data` is installed * Install both `spring-boot-test` and `spring-test` for Spring Boot projects * Limit wait time for `preClasspathCollectionPromises`
1 parent 66573e0 commit 0107fac

File tree

13 files changed

+131
-82
lines changed

13 files changed

+131
-82
lines changed

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,8 +1515,6 @@ enum class SpringTestType(
15151515
override val id: String,
15161516
override val displayName: String,
15171517
override val description: String,
1518-
// Integration tests generation requires spring test framework being installed
1519-
var testFrameworkInstalled: Boolean = false,
15201518
) : CodeGenerationSettingItem {
15211519
UNIT_TEST(
15221520
"Unit tests",

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -715,23 +715,41 @@ abstract class DependencyInjectionFramework(
715715
override val id: String,
716716
override val displayName: String,
717717
override val description: String = "Use $displayName as dependency injection framework",
718+
val testFrameworkDisplayName: String,
719+
/**
720+
* Generation Spring specific tests requires special spring test framework being installed,
721+
* so we can use `TestContextManager` from `spring-test` to configure test context in
722+
* spring-analyzer and to run integration tests.
723+
*/
724+
var testFrameworkInstalled: Boolean = false
718725
) : CodeGenerationSettingItem {
719726
var isInstalled = false
720727

721728
companion object : CodeGenerationSettingBox {
722729
override val defaultItem: DependencyInjectionFramework get() = SpringBoot
723730
override val allItems: List<DependencyInjectionFramework> get() = listOf(SpringBoot, SpringBeans)
731+
732+
val installedItems get() = allItems.filter { it.isInstalled }
733+
734+
/**
735+
* Generation Spring specific tests requires special spring test framework being installed,
736+
* so we can use `TestContextManager` from `spring-test` to configure test context in
737+
* spring-analyzer and to run integration tests.
738+
*/
739+
var testFrameworkInstalled: Boolean = false
724740
}
725741
}
726742

727743
object SpringBeans : DependencyInjectionFramework(
728744
id = "spring-beans",
729-
displayName = "Spring Beans"
745+
displayName = "Spring Beans",
746+
testFrameworkDisplayName = "spring-test",
730747
)
731748

732749
object SpringBoot : DependencyInjectionFramework(
733750
id = "spring-boot",
734-
displayName = "Spring Boot"
751+
displayName = "Spring Boot",
752+
testFrameworkDisplayName = "spring-boot-test",
735753
)
736754

737755
/**

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.utbot.framework.plugin.api.util.SpringModelUtils.autoConfigureTestDbC
1717
import org.utbot.framework.plugin.api.util.SpringModelUtils.autowiredClassId
1818
import org.utbot.framework.plugin.api.util.SpringModelUtils.bootstrapWithClassId
1919
import org.utbot.framework.plugin.api.util.SpringModelUtils.contextConfigurationClassId
20+
import org.utbot.framework.plugin.api.util.SpringModelUtils.crudRepositoryClassId
2021
import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassId
2122
import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassModeClassId
2223
import org.utbot.framework.plugin.api.util.SpringModelUtils.springBootTestContextBootstrapperClassId
@@ -108,11 +109,15 @@ class CgSpringIntegrationTestClassConstructor(
108109
target = Class,
109110
)
110111

111-
listOf(transactionalClassId, autoConfigureTestDbClassId)
112-
.filter { annotationTypeIsAccessible(it) }
113-
.forEach { annType -> addAnnotation(annType, Class) }
114-
}
112+
if (utContext.classLoader.tryLoadClass(transactionalClassId.name) != null)
113+
addAnnotation(transactionalClassId, Class)
115114

116-
private fun annotationTypeIsAccessible(annotationType: ClassId): Boolean =
117-
utContext.classLoader.tryLoadClass(annotationType.name) != null
115+
// `@AutoConfigureTestDatabase` can itself be on the classpath, while spring-data
116+
// (i.e. module containing `CrudRepository`) is not.
117+
//
118+
// If we add `@AutoConfigureTestDatabase` without having spring-data,
119+
// generated tests will fail with `ClassNotFoundException: org.springframework.dao.DataAccessException`.
120+
if (utContext.classLoader.tryLoadClass(crudRepositoryClassId.name) != null)
121+
addAnnotation(autoConfigureTestDbClassId, Class)
122+
}
118123
}

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

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.intellij.openapi.progress.ProgressIndicator
88
import com.intellij.openapi.progress.Task
99
import com.intellij.openapi.project.DumbService
1010
import com.intellij.openapi.project.Project
11-
import com.intellij.openapi.roots.ModuleRootManager
1211
import com.intellij.openapi.roots.OrderEnumerator
1312
import com.intellij.openapi.roots.ProjectFileIndex
1413
import com.intellij.openapi.ui.Messages
@@ -148,23 +147,25 @@ object UtTestsDialogProcessor {
148147
files: Array<VirtualFile>,
149148
springConfigClass: PsiClass?,
150149
): Promise<ProjectTaskManager.Result> {
151-
// For Maven project narrow compile scope may not work, see https://github.com/UnitTestBot/UTBotJava/issues/2021.
152-
// For Spring project classes may contain `@ComponentScan` annotations, so we need to compile the whole module.
153-
val isMavenProject = MavenProjectsManager.getInstance(project)?.hasProjects() ?: false
154-
val isSpringProject = springConfigClass != null
155-
val wholeModules = isMavenProject || isSpringProject
156-
157-
val buildTasks = ContainerUtil.map<Map.Entry<Module?, List<VirtualFile>>, ProjectTask>(
158-
Arrays.stream(files).collect(Collectors.groupingBy { file: VirtualFile ->
159-
ProjectFileIndex.getInstance(project).getModuleForFile(file, false)
160-
}).entries
161-
) { (key, value): Map.Entry<Module?, List<VirtualFile>?> ->
162-
if (wholeModules) {
163-
// This is a specific case, we have to compile the whole module
164-
ModuleBuildTaskImpl(key!!, false)
165-
} else {
166-
// Compile only chosen classes and their dependencies before generation.
167-
ModuleFilesBuildTaskImpl(key, false, value)
150+
val buildTasks = runReadAction {
151+
// For Maven project narrow compile scope may not work, see https://github.com/UnitTestBot/UTBotJava/issues/2021.
152+
// For Spring project classes may contain `@ComponentScan` annotations, so we need to compile the whole module.
153+
val isMavenProject = MavenProjectsManager.getInstance(project)?.hasProjects() ?: false
154+
val isSpringProject = springConfigClass != null
155+
val wholeModules = isMavenProject || isSpringProject
156+
157+
ContainerUtil.map<Map.Entry<Module?, List<VirtualFile>>, ProjectTask>(
158+
Arrays.stream(files).collect(Collectors.groupingBy { file: VirtualFile ->
159+
ProjectFileIndex.getInstance(project).getModuleForFile(file, false)
160+
}).entries
161+
) { (key, value): Map.Entry<Module?, List<VirtualFile>?> ->
162+
if (wholeModules) {
163+
// This is a specific case, we have to compile the whole module
164+
ModuleBuildTaskImpl(key!!, false)
165+
} else {
166+
// Compile only chosen classes and their dependencies before generation.
167+
ModuleFilesBuildTaskImpl(key, false, value)
168+
}
168169
}
169170
}
170171
return ProjectTaskManager.getInstance(project).run(ProjectTaskList(buildTasks))
@@ -191,11 +192,7 @@ object UtTestsDialogProcessor {
191192
.map { it.containingFile.virtualFile }
192193
.toTypedArray()
193194

194-
val compilationPromise = model.preCompilePromises
195-
.all()
196-
.thenAsync { compile(project, filesToCompile, springConfigClass) }
197-
198-
compilationPromise.onSuccess { task ->
195+
compile(project, filesToCompile, springConfigClass).onSuccess { task ->
199196
if (task.hasErrors() || task.isAborted)
200197
return@onSuccess
201198

@@ -220,6 +217,16 @@ object UtTestsDialogProcessor {
220217
indicator.isIndeterminate = false
221218
updateIndicator(indicator, ProgressRange.SOLVING, "Generate tests: read classes", 0.0)
222219

220+
// TODO sometimes preClasspathCollectionPromises get stuck, even though all
221+
// needed dependencies get installed, we need to figure out why that happens
222+
try {
223+
model.preClasspathCollectionPromises
224+
.all()
225+
.blockingGet(10, TimeUnit.SECONDS)
226+
} catch (e: java.util.concurrent.TimeoutException) {
227+
logger.warn { "preClasspathCollectionPromises are stuck over 10 seconds, ignoring them" }
228+
}
229+
223230
val buildPaths = ReadAction
224231
.nonBlocking<BuildPaths?> {
225232
findPaths(listOf(findSrcModule(model.srcClasses)) + when (model.projectType) {

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class GenerateTestsModel(
6161
lateinit var springTestType: SpringTestType
6262

6363
val conflictTriggers: ConflictTriggers = ConflictTriggers()
64-
val preCompilePromises: MutableList<Promise<*>> = mutableListOf()
64+
val preClasspathCollectionPromises: MutableList<Promise<*>> = mutableListOf()
6565

6666
var runGeneratedTestsWithCoverage : Boolean = false
6767
var summariesGenerationType : SummariesGenerationType = UtSettings.summaryGenerationType

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

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
200200
private val mockStrategies = createComboBox(MockStrategyApi.values())
201201
private val staticsMocking = JCheckBox("Mock static methods")
202202

203-
private val springTestType = createComboBox(SpringTestType.values())
203+
private val springTestType = createComboBox(SpringTestType.values()).also { it.setMinimumAndPreferredWidth(300) }
204204
private val springConfig = createComboBoxWithSeparatorsForSpringConfigs(shortenConfigurationNames())
205205
private val profileNames = JBTextField(23).apply { emptyText.text = DEFAULT_SPRING_PROFILE_NAME }
206206

@@ -364,15 +364,12 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
364364
DependencyInjectionFramework.allItems.forEach {
365365
it.isInstalled = findDependencyInjectionLibrary(model.srcModule, it) != null
366366
}
367-
val installedDiFramework = when {
368-
SpringBoot.isInstalled -> SpringBoot
369-
SpringBeans.isInstalled -> SpringBeans
370-
else -> null
367+
DependencyInjectionFramework.installedItems.forEach {
368+
it.testFrameworkInstalled = findDependencyInjectionTestLibrary(model.testModule, it) != null
371369
}
372-
installedDiFramework?.let {
373-
INTEGRATION_TEST.testFrameworkInstalled = findDependencyInjectionTestLibrary(model.testModule, it) != null
374-
}
375-
model.projectType = if (installedDiFramework != null) ProjectType.Spring else ProjectType.PureJvm
370+
model.projectType =
371+
if (DependencyInjectionFramework.installedItems.isNotEmpty()) ProjectType.Spring
372+
else ProjectType.PureJvm
376373

377374
// Configure notification urls callbacks
378375
TestsReportNotifier.urlOpeningListener.callbacks[TestReportUrlOpeningListener.mockitoSuffix]?.plusAssign {
@@ -939,15 +936,11 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
939936
}
940937

941938
private fun configureSpringTestFrameworkIfRequired() {
942-
if (springTestType.item == INTEGRATION_TEST) {
943-
944-
val framework = when {
945-
SpringBoot.isInstalled -> SpringBoot
946-
SpringBeans.isInstalled -> SpringBeans
947-
else -> error("Both Spring and Spring Boot are not installed")
948-
}
939+
if (springConfig.item != NO_SPRING_CONFIGURATION_OPTION) {
949940

950-
configureSpringTestDependency(framework)
941+
DependencyInjectionFramework.installedItems
942+
.filter { it.isInstalled }
943+
.forEach { configureSpringTestDependency(it) }
951944
}
952945
}
953946

@@ -988,15 +981,15 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
988981
!frameworkTestVersionInProject.isCompatibleWith(frameworkVersionInProject)
989982
) {
990983
val libraryDescriptor = when (framework) {
991-
SpringBoot -> springBootTestLibraryDescriptor(frameworkVersionInProject)
984+
SpringBoot -> springBootTestLibraryDescriptor(frameworkVersionInProject)
992985
SpringBeans -> springTestLibraryDescriptor(frameworkVersionInProject)
993986
else -> error("Unsupported DI framework type $framework")
994987
}
995988

996-
model.preCompilePromises += addDependency(model.testModule, libraryDescriptor)
989+
model.preClasspathCollectionPromises += addDependency(model.testModule, libraryDescriptor)
997990
}
998991

999-
INTEGRATION_TEST.testFrameworkInstalled = true
992+
framework.testFrameworkInstalled = true
1000993
}
1001994

1002995
private fun configureMockFramework() {
@@ -1281,14 +1274,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m
12811274
index: Int, selected: Boolean, hasFocus: Boolean
12821275
) {
12831276
this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES)
1284-
if (value == INTEGRATION_TEST && !INTEGRATION_TEST.testFrameworkInstalled) {
1285-
val additionalText = when {
1286-
SpringBoot.isInstalled -> " (spring-boot-test will be installed)"
1287-
SpringBeans.isInstalled -> " (spring-test will be installed)"
1288-
else -> error("Both Spring and Spring Boot are not installed")
1289-
}
1290-
1291-
this.append(additionalText, SimpleTextAttributes.ERROR_ATTRIBUTES)
1277+
if (springConfig.item != NO_SPRING_CONFIGURATION_OPTION) {
1278+
DependencyInjectionFramework.installedItems
1279+
// only first missing test framework is shown to avoid overflowing ComboBox
1280+
.firstOrNull { !it.testFrameworkInstalled }
1281+
?.let { diFramework ->
1282+
val additionalText = " (${diFramework.testFrameworkDisplayName} will be installed)"
1283+
this.append(additionalText, SimpleTextAttributes.ERROR_ATTRIBUTES)
1284+
}
12921285
}
12931286
}
12941287
}

utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.utbot.spring.api.SpringApi
1414
import org.utbot.spring.api.RepositoryDescription
1515
import org.utbot.spring.api.instantiator.InstantiationSettings
1616
import org.utbot.spring.dummy.DummySpringIntegrationTestClass
17+
import org.utbot.spring.utils.DependencyUtils.isSpringDataOnClasspath
1718
import org.utbot.spring.utils.RepositoryUtils
1819
import java.lang.reflect.Method
1920
import java.net.URLClassLoader
@@ -45,13 +46,6 @@ class SpringApiImpl(
4546

4647
override fun getOrLoadSpringApplicationContext() = context
4748

48-
private val isCrudRepositoryOnClasspath = try {
49-
CrudRepository::class.java.name
50-
true
51-
} catch (e: ClassNotFoundException) {
52-
false
53-
}
54-
5549
override fun getBean(beanName: String): Any = context.getBean(beanName)
5650

5751
override fun getDependenciesForBean(beanName: String, userSourcesClassLoader: URLClassLoader): Set<String> {
@@ -94,7 +88,7 @@ class SpringApiImpl(
9488
}
9589

9690
override fun resolveRepositories(beanNames: Set<String>, userSourcesClassLoader: URLClassLoader): Set<RepositoryDescription> {
97-
if (!isCrudRepositoryOnClasspath) return emptySet()
91+
if (!isSpringDataOnClasspath) return emptySet()
9892
val repositoryBeans = beanNames
9993
.map { beanName -> SimpleBeanDefinition(beanName, getBean(beanName)) }
10094
.filter { beanDef -> describesRepository(beanDef.bean) }
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
package org.utbot.spring.dummy
22

3-
class DummyPureSpringIntegrationTestClass : DummySpringIntegrationTestClass()
3+
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
4+
5+
open class DummyPureSpringIntegrationTestClass : DummySpringIntegrationTestClass()
6+
7+
@AutoConfigureTestDatabase
8+
class DummyPureSpringIntegrationTestClassAutoconfigTestDB : DummyPureSpringIntegrationTestClass()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package org.utbot.spring.dummy
22

3+
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
34
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper
45
import org.springframework.test.context.BootstrapWith
56

67
@BootstrapWith(SpringBootTestContextBootstrapper::class)
78
class DummySpringBootIntegrationTestClass : DummySpringIntegrationTestClass()
9+
10+
@AutoConfigureTestDatabase
11+
class DummySpringBootIntegrationTestClassAutoconfigTestDB : DummySpringIntegrationTestClass()
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.utbot.spring.dummy
22

3-
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
43
import org.springframework.test.context.ActiveProfiles
54
import org.springframework.test.context.ContextConfiguration
65
import org.springframework.transaction.annotation.Isolation
@@ -9,7 +8,6 @@ import org.springframework.transaction.annotation.Transactional
98
@ActiveProfiles(/* fills dynamically */)
109
@ContextConfiguration(/* fills dynamically */)
1110
@Transactional(isolation = Isolation.SERIALIZABLE)
12-
@AutoConfigureTestDatabase
1311
abstract class DummySpringIntegrationTestClass {
1412
fun dummyTestMethod() {}
1513
}

0 commit comments

Comments
 (0)