Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DependencyInjectionFramework> 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",
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -148,23 +147,25 @@ object UtTestsDialogProcessor {
files: Array<VirtualFile>,
springConfigClass: PsiClass?,
): Promise<ProjectTaskManager.Result> {
// 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<Map.Entry<Module?, List<VirtualFile>>, ProjectTask>(
Arrays.stream(files).collect(Collectors.groupingBy { file: VirtualFile ->
ProjectFileIndex.getInstance(project).getModuleForFile(file, false)
}).entries
) { (key, value): Map.Entry<Module?, List<VirtualFile>?> ->
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<Map.Entry<Module?, List<VirtualFile>>, ProjectTask>(
Arrays.stream(files).collect(Collectors.groupingBy { file: VirtualFile ->
ProjectFileIndex.getInstance(project).getModuleForFile(file, false)
}).entries
) { (key, value): Map.Entry<Module?, List<VirtualFile>?> ->
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))
Expand All @@ -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

Expand All @@ -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<BuildPaths?> {
findPaths(listOf(findSrcModule(model.srcClasses)) + when (model.projectType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class GenerateTestsModel(
lateinit var springTestType: SpringTestType

val conflictTriggers: ConflictTriggers = ConflictTriggers()
val preCompilePromises: MutableList<Promise<*>> = mutableListOf()
val preClasspathCollectionPromises: MutableList<Promise<*>> = mutableListOf()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bue


var runGeneratedTestsWithCoverage : Boolean = false
var summariesGenerationType : SummariesGenerationType = UtSettings.summaryGenerationType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) }
}
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> {
Expand Down Expand Up @@ -94,7 +88,7 @@ class SpringApiImpl(
}

override fun resolveRepositories(beanNames: Set<String>, userSourcesClassLoader: URLClassLoader): Set<RepositoryDescription> {
if (!isCrudRepositoryOnClasspath) return emptySet()
if (!isSpringDataOnClasspath) return emptySet()
val repositoryBeans = beanNames
.map { beanName -> SimpleBeanDefinition(beanName, getBean(beanName)) }
.filter { beanDef -> describesRepository(beanDef.bean) }
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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
)
}
Loading