diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt index 4e325a414b..929d74815a 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt @@ -1515,8 +1515,6 @@ enum class SpringTestType( override val id: String, override val displayName: String, override val description: String, - // Integration tests generation requires spring test framework being installed - var testFrameworkInstalled: Boolean = false, ) : CodeGenerationSettingItem { UNIT_TEST( "Unit tests", diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt index 3b82df3436..e94e5c9f5e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt @@ -715,23 +715,41 @@ abstract class DependencyInjectionFramework( override val id: String, override val displayName: String, override val description: String = "Use $displayName as dependency injection framework", + val testFrameworkDisplayName: String, + /** + * Generation Spring specific tests requires special spring test framework being installed, + * so we can use `TestContextManager` from `spring-test` to configure test context in + * spring-analyzer and to run integration tests. + */ + var testFrameworkInstalled: Boolean = false ) : CodeGenerationSettingItem { var isInstalled = false companion object : CodeGenerationSettingBox { override val defaultItem: DependencyInjectionFramework get() = SpringBoot override val allItems: List get() = listOf(SpringBoot, SpringBeans) + + val installedItems get() = allItems.filter { it.isInstalled } + + /** + * Generation Spring specific tests requires special spring test framework being installed, + * so we can use `TestContextManager` from `spring-test` to configure test context in + * spring-analyzer and to run integration tests. + */ + var testFrameworkInstalled: Boolean = false } } object SpringBeans : DependencyInjectionFramework( id = "spring-beans", - displayName = "Spring Beans" + displayName = "Spring Beans", + testFrameworkDisplayName = "spring-test", ) object SpringBoot : DependencyInjectionFramework( id = "spring-boot", - displayName = "Spring Boot" + displayName = "Spring Boot", + testFrameworkDisplayName = "spring-boot-test", ) /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt index 0fd7e103d6..5ce988b963 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringIntegrationTestClassConstructor.kt @@ -17,6 +17,7 @@ import org.utbot.framework.plugin.api.util.SpringModelUtils.autoConfigureTestDbC import org.utbot.framework.plugin.api.util.SpringModelUtils.autowiredClassId import org.utbot.framework.plugin.api.util.SpringModelUtils.bootstrapWithClassId import org.utbot.framework.plugin.api.util.SpringModelUtils.contextConfigurationClassId +import org.utbot.framework.plugin.api.util.SpringModelUtils.crudRepositoryClassId import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassId import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassModeClassId import org.utbot.framework.plugin.api.util.SpringModelUtils.springBootTestContextBootstrapperClassId @@ -108,11 +109,15 @@ class CgSpringIntegrationTestClassConstructor( target = Class, ) - listOf(transactionalClassId, autoConfigureTestDbClassId) - .filter { annotationTypeIsAccessible(it) } - .forEach { annType -> addAnnotation(annType, Class) } - } + if (utContext.classLoader.tryLoadClass(transactionalClassId.name) != null) + addAnnotation(transactionalClassId, Class) - private fun annotationTypeIsAccessible(annotationType: ClassId): Boolean = - utContext.classLoader.tryLoadClass(annotationType.name) != null + // `@AutoConfigureTestDatabase` can itself be on the classpath, while spring-data + // (i.e. module containing `CrudRepository`) is not. + // + // If we add `@AutoConfigureTestDatabase` without having spring-data, + // generated tests will fail with `ClassNotFoundException: org.springframework.dao.DataAccessException`. + if (utContext.classLoader.tryLoadClass(crudRepositoryClassId.name) != null) + addAnnotation(autoConfigureTestDbClassId, Class) + } } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt index 14fbe938c6..a9b89475cc 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/UtTestsDialogProcessor.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.OrderEnumerator import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.ui.Messages @@ -148,23 +147,25 @@ object UtTestsDialogProcessor { files: Array, springConfigClass: PsiClass?, ): Promise { - // For Maven project narrow compile scope may not work, see https://github.com/UnitTestBot/UTBotJava/issues/2021. - // For Spring project classes may contain `@ComponentScan` annotations, so we need to compile the whole module. - val isMavenProject = MavenProjectsManager.getInstance(project)?.hasProjects() ?: false - val isSpringProject = springConfigClass != null - val wholeModules = isMavenProject || isSpringProject - - val buildTasks = ContainerUtil.map>, ProjectTask>( - Arrays.stream(files).collect(Collectors.groupingBy { file: VirtualFile -> - ProjectFileIndex.getInstance(project).getModuleForFile(file, false) - }).entries - ) { (key, value): Map.Entry?> -> - if (wholeModules) { - // This is a specific case, we have to compile the whole module - ModuleBuildTaskImpl(key!!, false) - } else { - // Compile only chosen classes and their dependencies before generation. - ModuleFilesBuildTaskImpl(key, false, value) + val buildTasks = runReadAction { + // For Maven project narrow compile scope may not work, see https://github.com/UnitTestBot/UTBotJava/issues/2021. + // For Spring project classes may contain `@ComponentScan` annotations, so we need to compile the whole module. + val isMavenProject = MavenProjectsManager.getInstance(project)?.hasProjects() ?: false + val isSpringProject = springConfigClass != null + val wholeModules = isMavenProject || isSpringProject + + ContainerUtil.map>, ProjectTask>( + Arrays.stream(files).collect(Collectors.groupingBy { file: VirtualFile -> + ProjectFileIndex.getInstance(project).getModuleForFile(file, false) + }).entries + ) { (key, value): Map.Entry?> -> + if (wholeModules) { + // This is a specific case, we have to compile the whole module + ModuleBuildTaskImpl(key!!, false) + } else { + // Compile only chosen classes and their dependencies before generation. + ModuleFilesBuildTaskImpl(key, false, value) + } } } return ProjectTaskManager.getInstance(project).run(ProjectTaskList(buildTasks)) @@ -191,11 +192,7 @@ object UtTestsDialogProcessor { .map { it.containingFile.virtualFile } .toTypedArray() - val compilationPromise = model.preCompilePromises - .all() - .thenAsync { compile(project, filesToCompile, springConfigClass) } - - compilationPromise.onSuccess { task -> + compile(project, filesToCompile, springConfigClass).onSuccess { task -> if (task.hasErrors() || task.isAborted) return@onSuccess @@ -220,6 +217,16 @@ object UtTestsDialogProcessor { indicator.isIndeterminate = false updateIndicator(indicator, ProgressRange.SOLVING, "Generate tests: read classes", 0.0) + // TODO sometimes preClasspathCollectionPromises get stuck, even though all + // needed dependencies get installed, we need to figure out why that happens + try { + model.preClasspathCollectionPromises + .all() + .blockingGet(10, TimeUnit.SECONDS) + } catch (e: java.util.concurrent.TimeoutException) { + logger.warn { "preClasspathCollectionPromises are stuck over 10 seconds, ignoring them" } + } + val buildPaths = ReadAction .nonBlocking { findPaths(listOf(findSrcModule(model.srcClasses)) + when (model.projectType) { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt index b0114ca4b3..bf071056e4 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt @@ -61,7 +61,7 @@ class GenerateTestsModel( lateinit var springTestType: SpringTestType val conflictTriggers: ConflictTriggers = ConflictTriggers() - val preCompilePromises: MutableList> = mutableListOf() + val preClasspathCollectionPromises: MutableList> = mutableListOf() var runGeneratedTestsWithCoverage : Boolean = false var summariesGenerationType : SummariesGenerationType = UtSettings.summaryGenerationType diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt index ef3f6a7f73..9d69c93cd3 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt @@ -200,7 +200,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m private val mockStrategies = createComboBox(MockStrategyApi.values()) private val staticsMocking = JCheckBox("Mock static methods") - private val springTestType = createComboBox(SpringTestType.values()) + private val springTestType = createComboBox(SpringTestType.values()).also { it.setMinimumAndPreferredWidth(300) } private val springConfig = createComboBoxWithSeparatorsForSpringConfigs(shortenConfigurationNames()) private val profileNames = JBTextField(23).apply { emptyText.text = DEFAULT_SPRING_PROFILE_NAME } @@ -364,15 +364,12 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m DependencyInjectionFramework.allItems.forEach { it.isInstalled = findDependencyInjectionLibrary(model.srcModule, it) != null } - val installedDiFramework = when { - SpringBoot.isInstalled -> SpringBoot - SpringBeans.isInstalled -> SpringBeans - else -> null + DependencyInjectionFramework.installedItems.forEach { + it.testFrameworkInstalled = findDependencyInjectionTestLibrary(model.testModule, it) != null } - installedDiFramework?.let { - INTEGRATION_TEST.testFrameworkInstalled = findDependencyInjectionTestLibrary(model.testModule, it) != null - } - model.projectType = if (installedDiFramework != null) ProjectType.Spring else ProjectType.PureJvm + model.projectType = + if (DependencyInjectionFramework.installedItems.isNotEmpty()) ProjectType.Spring + else ProjectType.PureJvm // Configure notification urls callbacks TestsReportNotifier.urlOpeningListener.callbacks[TestReportUrlOpeningListener.mockitoSuffix]?.plusAssign { @@ -939,15 +936,11 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } private fun configureSpringTestFrameworkIfRequired() { - if (springTestType.item == INTEGRATION_TEST) { - - val framework = when { - SpringBoot.isInstalled -> SpringBoot - SpringBeans.isInstalled -> SpringBeans - else -> error("Both Spring and Spring Boot are not installed") - } + if (springConfig.item != NO_SPRING_CONFIGURATION_OPTION) { - configureSpringTestDependency(framework) + DependencyInjectionFramework.installedItems + .filter { it.isInstalled } + .forEach { configureSpringTestDependency(it) } } } @@ -988,15 +981,15 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m !frameworkTestVersionInProject.isCompatibleWith(frameworkVersionInProject) ) { val libraryDescriptor = when (framework) { - SpringBoot -> springBootTestLibraryDescriptor(frameworkVersionInProject) + SpringBoot -> springBootTestLibraryDescriptor(frameworkVersionInProject) SpringBeans -> springTestLibraryDescriptor(frameworkVersionInProject) else -> error("Unsupported DI framework type $framework") } - model.preCompilePromises += addDependency(model.testModule, libraryDescriptor) + model.preClasspathCollectionPromises += addDependency(model.testModule, libraryDescriptor) } - INTEGRATION_TEST.testFrameworkInstalled = true + framework.testFrameworkInstalled = true } private fun configureMockFramework() { @@ -1281,14 +1274,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m index: Int, selected: Boolean, hasFocus: Boolean ) { this.append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) - if (value == INTEGRATION_TEST && !INTEGRATION_TEST.testFrameworkInstalled) { - val additionalText = when { - SpringBoot.isInstalled -> " (spring-boot-test will be installed)" - SpringBeans.isInstalled -> " (spring-test will be installed)" - else -> error("Both Spring and Spring Boot are not installed") - } - - this.append(additionalText, SimpleTextAttributes.ERROR_ATTRIBUTES) + if (springConfig.item != NO_SPRING_CONFIGURATION_OPTION) { + DependencyInjectionFramework.installedItems + // only first missing test framework is shown to avoid overflowing ComboBox + .firstOrNull { !it.testFrameworkInstalled } + ?.let { diFramework -> + val additionalText = " (${diFramework.testFrameworkDisplayName} will be installed)" + this.append(additionalText, SimpleTextAttributes.ERROR_ATTRIBUTES) + } } } } diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt index 65907f737a..7a8eb9aab9 100644 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt @@ -14,6 +14,7 @@ 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.DependencyUtils.isSpringDataOnClasspath import org.utbot.spring.utils.RepositoryUtils import java.lang.reflect.Method import java.net.URLClassLoader @@ -45,13 +46,6 @@ class SpringApiImpl( override fun getOrLoadSpringApplicationContext() = context - private val isCrudRepositoryOnClasspath = try { - CrudRepository::class.java.name - true - } catch (e: ClassNotFoundException) { - false - } - override fun getBean(beanName: String): Any = context.getBean(beanName) override fun getDependenciesForBean(beanName: String, userSourcesClassLoader: URLClassLoader): Set { @@ -94,7 +88,7 @@ class SpringApiImpl( } override fun resolveRepositories(beanNames: Set, userSourcesClassLoader: URLClassLoader): Set { - if (!isCrudRepositoryOnClasspath) return emptySet() + if (!isSpringDataOnClasspath) return emptySet() val repositoryBeans = beanNames .map { beanName -> SimpleBeanDefinition(beanName, getBean(beanName)) } .filter { beanDef -> describesRepository(beanDef.bean) } 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 index 5a5ff81b27..351472d26f 100644 --- 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 @@ -1,3 +1,8 @@ package org.utbot.spring.dummy -class DummyPureSpringIntegrationTestClass : DummySpringIntegrationTestClass() +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase + +open class DummyPureSpringIntegrationTestClass : DummySpringIntegrationTestClass() + +@AutoConfigureTestDatabase +class DummyPureSpringIntegrationTestClassAutoconfigTestDB : DummyPureSpringIntegrationTestClass() 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 index 28634c39ff..54e017e462 100644 --- 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 @@ -1,7 +1,11 @@ package org.utbot.spring.dummy +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase import org.springframework.boot.test.context.SpringBootTestContextBootstrapper import org.springframework.test.context.BootstrapWith @BootstrapWith(SpringBootTestContextBootstrapper::class) class DummySpringBootIntegrationTestClass : DummySpringIntegrationTestClass() + +@AutoConfigureTestDatabase +class DummySpringBootIntegrationTestClassAutoconfigTestDB : 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 index e0b423a117..e5dbbbf7ee 100644 --- 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 @@ -1,6 +1,5 @@ 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 @@ -9,7 +8,6 @@ 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/provider/PureSpringApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/PureSpringApiProvider.kt index 86688e3807..6129c6646b 100644 --- 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 @@ -3,11 +3,17 @@ package org.utbot.spring.provider import org.utbot.spring.api.instantiator.InstantiationSettings import org.utbot.spring.SpringApiImpl import org.utbot.spring.dummy.DummyPureSpringIntegrationTestClass +import org.utbot.spring.dummy.DummyPureSpringIntegrationTestClassAutoconfigTestDB +import org.utbot.spring.utils.DependencyUtils.isSpringDataOnClasspath class PureSpringApiProvider : SpringApiProvider { override fun isAvailable() = true override fun provideAPI(instantiationSettings: InstantiationSettings) = - SpringApiImpl(instantiationSettings, DummyPureSpringIntegrationTestClass::class.java) + SpringApiImpl( + instantiationSettings, + if (isSpringDataOnClasspath) DummyPureSpringIntegrationTestClassAutoconfigTestDB::class.java + else DummyPureSpringIntegrationTestClass::class.java + ) } \ No newline at end of file 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 index 5875dc6f9d..ab0ec17f16 100644 --- 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 @@ -1,19 +1,20 @@ 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 +import org.utbot.spring.dummy.DummySpringBootIntegrationTestClassAutoconfigTestDB +import org.utbot.spring.utils.DependencyUtils.isSpringBootTestOnClasspath +import org.utbot.spring.utils.DependencyUtils.isSpringDataOnClasspath class SpringBootApiProvider : SpringApiProvider { - override fun isAvailable(): Boolean = try { - SpringBootTestContextBootstrapper::class.java.name - true - } catch (e: ClassNotFoundException) { - false - } + override fun isAvailable(): Boolean = isSpringBootTestOnClasspath override fun provideAPI(instantiationSettings: InstantiationSettings) = - SpringApiImpl(instantiationSettings, DummySpringBootIntegrationTestClass::class.java) + SpringApiImpl( + instantiationSettings, + if (isSpringDataOnClasspath) DummySpringBootIntegrationTestClassAutoconfigTestDB::class.java + else DummySpringBootIntegrationTestClass::class.java + ) } \ No newline at end of file diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/DependencyUtils.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/DependencyUtils.kt new file mode 100644 index 0000000000..4be1dd4612 --- /dev/null +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/utils/DependencyUtils.kt @@ -0,0 +1,20 @@ +package org.utbot.spring.utils + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper +import org.springframework.data.repository.CrudRepository + +object DependencyUtils { + val isSpringDataOnClasspath = try { + CrudRepository::class.java.name + true + } catch (e: Throwable) { + false + } + + val isSpringBootTestOnClasspath = try { + SpringBootTestContextBootstrapper::class.java.name + true + } catch (e: Throwable) { + false + } +}