diff --git a/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt index 453f2d2468..05ed3d7fdb 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt @@ -36,19 +36,7 @@ class ThreadBasedExecutor { * [stopWatch] is used to respect specific situations (such as class loading and transforming) while invoking. */ fun invokeWithTimeout(timeoutMillis: Long, stopWatch: StopWatch? = null, action:() -> Any?) : Result? { - if (thread?.isAlive != true) { - requestQueue = ArrayBlockingQueue<() -> Any?>(1) - responseQueue = ArrayBlockingQueue>(1) - - thread = thread(name = "executor", isDaemon = true) { - try { - while (true) { - val next = requestQueue.take() - responseQueue.offer(kotlin.runCatching { next() }) - } - } catch (_: InterruptedException) {} - } - } + ensureThreadIsAlive() requestQueue.offer { try { @@ -83,4 +71,27 @@ class ThreadBasedExecutor { } return res } + + fun invokeWithoutTimeout(action:() -> Any?) : Result { + ensureThreadIsAlive() + + requestQueue.offer(action) + return responseQueue.take() + } + + private fun ensureThreadIsAlive() { + if (thread?.isAlive != true) { + requestQueue = ArrayBlockingQueue<() -> Any?>(1) + responseQueue = ArrayBlockingQueue>(1) + + thread = thread(name = "executor", isDaemon = true) { + try { + while (true) { + val next = requestQueue.take() + responseQueue.offer(kotlin.runCatching { next() }) + } + } catch (_: InterruptedException) {} + } + } + } } \ No newline at end of file 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 929d74815a..2968d75a8f 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 @@ -327,7 +327,7 @@ object MissingState : EnvironmentModels( ) /** - * Error happened in traverse. + * Error happened during test cases generation. */ data class UtError( val description: String, @@ -1328,6 +1328,7 @@ interface CodeGenerationContext interface SpringCodeGenerationContext : CodeGenerationContext { val springTestType: SpringTestType val springSettings: SpringSettings + val springContextLoadingResult: SpringContextLoadingResult? } /** @@ -1389,11 +1390,15 @@ open class ApplicationContext( field: SootField, classUnderTest: ClassId, ): Boolean = field.isFinal || !field.isPublic + + open fun preventsFurtherTestGeneration(): Boolean = false + + open fun getErrors(): List = emptyList() } -sealed interface SpringConfiguration { - class JavaConfiguration(val classBinaryName: String) : SpringConfiguration - class XMLConfiguration(val absolutePath: String) : SpringConfiguration +sealed class SpringConfiguration(val fullDisplayName: String) { + class JavaConfiguration(val classBinaryName: String) : SpringConfiguration(classBinaryName) + class XMLConfiguration(val absolutePath: String) : SpringConfiguration(absolutePath) } sealed interface SpringSettings { @@ -1413,6 +1418,16 @@ sealed interface SpringSettings { ) : SpringSettings } +/** + * [contextLoaded] can be `true` while [exceptions] is not empty, + * if we failed to use most specific SpringApi available (e.g. SpringBoot), but + * were able to successfully fall back to less specific SpringApi (e.g. PureSpring). + */ +class SpringContextLoadingResult( + val contextLoaded: Boolean, + val exceptions: List +) + /** * Data we get from Spring application context * to manage engine and code generator behaviour. @@ -1438,6 +1453,8 @@ class SpringApplicationContext( override val springSettings: SpringSettings, ): ApplicationContext(mockInstalled, staticsMockingIsConfigured), SpringCodeGenerationContext { + override var springContextLoadingResult: SpringContextLoadingResult? = null + companion object { private val logger = KotlinLogging.logger {} } @@ -1509,6 +1526,17 @@ class SpringApplicationContext( field: SootField, classUnderTest: ClassId, ): Boolean = field.fieldId in classUnderTest.allDeclaredFieldIds && field.declaringClass.id !in springInjectedClasses + + override fun preventsFurtherTestGeneration(): Boolean = + super.preventsFurtherTestGeneration() || springContextLoadingResult?.contextLoaded == false + + override fun getErrors(): List = + springContextLoadingResult?.exceptions?.map { exception -> + UtError( + "Failed to load Spring application context", + exception + ) + }.orEmpty() + super.getErrors() } enum class SpringTestType( diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IndentUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IndentUtil.kt new file mode 100644 index 0000000000..5e6bcc57a0 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IndentUtil.kt @@ -0,0 +1,5 @@ +package org.utbot.framework.plugin.api.util + +object IndentUtil { + const val TAB = " " +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ThrowableSerializer.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ThrowableSerializer.kt index a645d454fe..821ed28563 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ThrowableSerializer.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/process/kryo/ThrowableSerializer.kt @@ -57,9 +57,9 @@ class ThrowableSerializer : Serializer() { override fun read(kryo: Kryo, input: Input, type: Class): Throwable? { fun ThrowableModel.toThrowable(): Throwable { - val throwableFromBytes = this.serializedExceptionBytes?.let { bytes -> + this.serializedExceptionBytes?.let { bytes -> try { - ByteArrayInputStream(bytes).use { byteInputStream -> + return@toThrowable ByteArrayInputStream(bytes).use { byteInputStream -> val objectInputStream = IgnoringUidWrappingObjectInputStream(byteInputStream, kryo.classLoader) objectInputStream.readObject() as Throwable } @@ -68,14 +68,31 @@ class ThrowableSerializer : Serializer() { logger.warn { "Failed to deserialize ${this.classId} from bytes, cause: $e" } logger.warn { "Falling back to constructing throwable instance from ThrowableModel" } } - null } } - return throwableFromBytes ?: when { - RuntimeException::class.java.isAssignableFrom(classId.jClass) -> RuntimeException(message, cause?.toThrowable()) - Error::class.java.isAssignableFrom(classId.jClass) -> Error(message, cause?.toThrowable()) - else -> Exception(message, cause?.toThrowable()) - }.also { + + val cause = cause?.toThrowable() + + val messageCauseConstructor = runCatching { classId.jClass.getConstructor(String::class.java, Throwable::class.java) }.getOrNull() + val causeOnlyConstructor = runCatching { classId.jClass.getConstructor(Throwable::class.java) }.getOrNull() + val messageOnlyConstructor = runCatching { classId.jClass.getConstructor(String::class.java) }.getOrNull() + + val throwableFromConstructor = runCatching { + when { + messageCauseConstructor != null && message != null && cause != null -> + messageCauseConstructor.newInstance(message, cause) + + causeOnlyConstructor != null && cause != null -> causeOnlyConstructor.newInstance(cause) + messageOnlyConstructor != null && message != null -> messageOnlyConstructor.newInstance(message) + else -> null + } + }.getOrNull() as Throwable? + + return (throwableFromConstructor ?: when { + RuntimeException::class.java.isAssignableFrom(classId.jClass) -> RuntimeException(message, cause) + Error::class.java.isAssignableFrom(classId.jClass) -> Error(message, cause) + else -> Exception(message, cause) + }).also { it.stackTrace = stackTrace } } diff --git a/utbot-framework/build.gradle b/utbot-framework/build.gradle index d8b7a5f335..649a06b4ef 100644 --- a/utbot-framework/build.gradle +++ b/utbot-framework/build.gradle @@ -15,6 +15,8 @@ dependencies { api project(':utbot-framework-api') api project(':utbot-rd') + implementation project(':utbot-spring-commons-api') + implementation group: 'com.jetbrains.rd', name: 'rd-framework', version: rdVersion implementation group: 'com.jetbrains.rd', name: 'rd-core', version: rdVersion diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/SpringBuiltins.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/SpringBuiltins.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgElement.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgElement.kt index 522dffd3eb..a054d4fe1d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgElement.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/CgElement.kt @@ -234,7 +234,19 @@ data class CgTestMethodCluster( data class CgMethodsCluster( override val header: String?, override val content: List> -) : CgRegion>() +) : CgRegion>() { + companion object { + fun withoutDocs(methodsList: List) = CgMethodsCluster( + header = null, + content = listOf( + CgSimpleRegion( + header = null, + content = methodsList + ) + ) + ) + } +} /** * Util entity is either an instance of [CgAuxiliaryClass] or [CgUtilMethod]. @@ -293,10 +305,10 @@ sealed class CgMethod(open val isStatic: Boolean) : CgElement { class CgTestMethod( override val name: String, - override val returnType: ClassId, - override val parameters: List, + override val returnType: ClassId = voidClassId, + override val parameters: List = emptyList(), override val statements: List, - override val exceptions: Set, + override val exceptions: Set = emptySet(), override val annotations: List, override val visibility: VisibilityModifier = VisibilityModifier.PUBLIC, val type: CgTestMethodType, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt index 3dbcedf3b7..0fa4c449b1 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt @@ -24,7 +24,7 @@ import org.utbot.framework.plugin.api.SpringSettings.* class SpringCodeGenerator( val classUnderTest: ClassId, val projectType: ProjectType, - val codeGenerationContext: SpringCodeGenerationContext, + val springCodeGenerationContext: SpringCodeGenerationContext, paramNames: MutableMap> = mutableMapOf(), generateUtilClassFile: Boolean = false, testFramework: TestFramework = TestFramework.defaultItem, @@ -61,11 +61,11 @@ class SpringCodeGenerator( val testClassModel = SpringTestClassModelBuilder(context).createTestClassModel(classUnderTest, testSets) logger.info { "Code generation phase started at ${now()}" } - val astConstructor = when (codeGenerationContext.springTestType) { + val astConstructor = when (springCodeGenerationContext.springTestType) { SpringTestType.UNIT_TEST -> CgSpringUnitTestClassConstructor(context) SpringTestType.INTEGRATION_TEST -> - when (val settings = codeGenerationContext.springSettings) { - is PresentSpringSettings -> CgSpringIntegrationTestClassConstructor(context, settings) + when (val settings = springCodeGenerationContext.springSettings) { + is PresentSpringSettings -> CgSpringIntegrationTestClassConstructor(context, springCodeGenerationContext, settings) is AbsentSpringSettings -> error("No Spring settings were provided for Spring integration test generation.") } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgPrinter.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgPrinter.kt index 6d6062d502..62daa3b6b4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgPrinter.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/renderer/CgPrinter.kt @@ -1,5 +1,7 @@ package org.utbot.framework.codegen.renderer +import org.utbot.framework.plugin.api.util.IndentUtil + interface CgPrinter { fun print(text: String) fun println(text: String = "") @@ -58,6 +60,6 @@ class CgPrinterImpl( private operator fun String.times(n: Int): String = repeat(n) companion object { - private const val TAB = " " + private const val TAB = IndentUtil.TAB } } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt index da57f174bc..337454db66 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt @@ -17,6 +17,7 @@ import org.utbot.framework.codegen.domain.models.CgVariable import org.utbot.framework.codegen.domain.models.SpringTestClassModel import org.utbot.framework.codegen.domain.models.builders.TypedModelWrappers import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtSpringContextModel import org.utbot.framework.plugin.api.util.SpringModelUtils.getBeanNameOrNull import org.utbot.framework.plugin.api.util.id @@ -36,6 +37,8 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext): fields += constructClassFields(testClassModel) clearUnwantedVariableModels() + constructAdditionalTestMethods()?.let { methodRegions += it } + for ((testSetIndex, testSet) in testClassModel.methodTestSets.withIndex()) { updateCurrentExecutable(testSet.executableId) withTestSetIdScope(testSetIndex) { @@ -48,7 +51,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext): } } - methodRegions += constructAdditionalMethods() + constructAdditionalUtilMethods()?.let { methodRegions += it } if (currentTestClass == outerMostTestClass) { val utilEntities = collectUtilEntities() @@ -81,7 +84,13 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext): abstract fun constructClassFields(testClassModel: SpringTestClassModel): List - abstract fun constructAdditionalMethods(): CgMethodsCluster + /** + * Here "additional" means that these tests are not obtained from + * [UtExecution]s generated by engine or fuzzer, but have another origin. + */ + open fun constructAdditionalTestMethods(): CgMethodsCluster? = null + + open fun constructAdditionalUtilMethods(): CgMethodsCluster? = null protected fun constructFieldsWithAnnotation( annotationClassId: ClassId, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractTestClassConstructor.kt index 68805c3496..16ef9aff27 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractTestClassConstructor.kt @@ -1,5 +1,6 @@ package org.utbot.framework.codegen.tree +import mu.KotlinLogging import org.utbot.framework.UtSettings import org.utbot.framework.codegen.domain.builtin.TestClassUtilMethodProvider import org.utbot.framework.codegen.domain.context.CgContext @@ -29,7 +30,11 @@ import org.utbot.framework.plugin.api.util.description abstract class CgAbstractTestClassConstructor(val context: CgContext): CgContextOwner by context, - CgStatementConstructor by CgComponents.getStatementConstructorBy(context){ + CgStatementConstructor by CgComponents.getStatementConstructorBy(context) { + + companion object { + private val logger = KotlinLogging.logger {} + } init { CgComponents.clearContextRelatedStorage() @@ -118,6 +123,7 @@ abstract class CgAbstractTestClassConstructor(val context: C } protected fun processFailure(testSet: CgMethodTestSet, failure: Throwable) { + logger.warn(failure) { "Code generation error" } codeGenerationErrors .getOrPut(testSet) { mutableMapOf() } .merge(failure.description, 1, Int::plus) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt index 1be284604c..d9e1832b8e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgMethodConstructor.kt @@ -65,6 +65,7 @@ import org.utbot.framework.codegen.tree.CgComponents.getVariableConstructorBy import org.utbot.framework.codegen.util.canBeReadFrom import org.utbot.framework.codegen.util.canBeSetFrom import org.utbot.framework.codegen.util.equalTo +import org.utbot.framework.codegen.util.escapeControlChars import org.utbot.framework.codegen.util.inc import org.utbot.framework.codegen.util.length import org.utbot.framework.codegen.util.lessThan @@ -113,6 +114,7 @@ import org.utbot.framework.plugin.api.isNotNull import org.utbot.framework.plugin.api.isNull import org.utbot.framework.plugin.api.onFailure import org.utbot.framework.plugin.api.onSuccess +import org.utbot.framework.plugin.api.util.IndentUtil.TAB import org.utbot.framework.plugin.api.util.allSuperTypes import org.utbot.framework.plugin.api.util.baseStreamClassId import org.utbot.framework.plugin.api.util.doubleArrayClassId @@ -153,7 +155,6 @@ import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.framework.plugin.api.util.wrapIfPrimitive import org.utbot.framework.util.isUnit import org.utbot.fuzzer.UtFuzzedExecution -import org.utbot.summary.SummarySentenceConstants.TAB import java.lang.reflect.InvocationTargetException import java.lang.reflect.ParameterizedType import java.security.AccessControlException @@ -338,7 +339,7 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte SUCCESSFUL -> error("Unexpected successful without exception method type for execution with exception $expectedException") PASSED_EXCEPTION -> { // TODO consider rendering message in a comment - // expectedException.message?.let { +comment(it) } + // expectedException.message?.let { +comment(it.escapeControlChars()) } testFrameworkManager.expectException(expectedException::class.id) { methodInvocationBlock() } @@ -461,9 +462,10 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte require(currentExecutable is ExecutableId) val executableName = "${currentExecutable!!.classId.name}.${currentExecutable!!.name}" - val warningLine = mutableListOf( - "This test fails because method [$executableName] produces [$exception]".escapeControlChars() - ) + val warningLine = "This test fails because method [$executableName] produces [$exception]" + .lines() + .map { it.escapeControlChars() } + .toMutableList() val neededStackTraceLines = mutableListOf() var executableCallFound = false @@ -482,10 +484,6 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte +CgMultilineComment(warningLine + neededStackTraceLines.reversed()) } - private fun String.escapeControlChars() : String { - return this.replace("\b", "\\b").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r").replace("\\u","\\\\u") - } - protected fun writeWarningAboutCrash() { +CgSingleLineComment("This invocation possibly crashes JVM") } 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 5ce988b963..a5d4299847 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 @@ -1,5 +1,6 @@ package org.utbot.framework.codegen.tree +import mu.KotlinLogging import org.utbot.common.tryLoadClass import org.utbot.framework.codegen.domain.Junit4 import org.utbot.framework.codegen.domain.Junit5 @@ -7,10 +8,15 @@ import org.utbot.framework.codegen.domain.TestNg import org.utbot.framework.codegen.domain.context.CgContext import org.utbot.framework.codegen.domain.models.* import org.utbot.framework.codegen.domain.models.AnnotationTarget.* +import org.utbot.framework.codegen.domain.models.CgTestMethodType.FAILING +import org.utbot.framework.codegen.domain.models.CgTestMethodType.SUCCESSFUL +import org.utbot.framework.codegen.util.escapeControlChars import org.utbot.framework.codegen.util.resolve import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.SpringCodeGenerationContext import org.utbot.framework.plugin.api.SpringSettings.* import org.utbot.framework.plugin.api.SpringConfiguration.* +import org.utbot.framework.plugin.api.util.IndentUtil.TAB import org.utbot.framework.plugin.api.util.SpringModelUtils import org.utbot.framework.plugin.api.util.SpringModelUtils.activeProfilesClassId import org.utbot.framework.plugin.api.util.SpringModelUtils.autoConfigureTestDbClassId @@ -24,11 +30,17 @@ import org.utbot.framework.plugin.api.util.SpringModelUtils.springBootTestContex import org.utbot.framework.plugin.api.util.SpringModelUtils.springExtensionClassId import org.utbot.framework.plugin.api.util.SpringModelUtils.transactionalClassId import org.utbot.framework.plugin.api.util.utContext +import org.utbot.spring.api.UTSpringContextLoadingException class CgSpringIntegrationTestClassConstructor( context: CgContext, - private val springSettings: PresentSpringSettings + private val springCodeGenerationContext: SpringCodeGenerationContext, + private val springSettings: PresentSpringSettings, ) : CgAbstractSpringTestClassConstructor(context) { + companion object { + private val logger = KotlinLogging.logger {} + } + override fun constructTestClass(testClassModel: SpringTestClassModel): CgClass { addNecessarySpringSpecificAnnotations() return super.constructTestClass(testClassModel) @@ -40,8 +52,51 @@ class CgSpringIntegrationTestClassConstructor( return constructFieldsWithAnnotation(autowiredClassId, autowiredFromContextModels) } - override fun constructAdditionalMethods() = - CgMethodsCluster(header = null, content = emptyList()) + override fun constructAdditionalTestMethods() = + CgMethodsCluster.withoutDocs( + listOfNotNull(constructContextLoadsMethod()) + ) + + private fun constructContextLoadsMethod() : CgTestMethod { + val contextLoadingResult = springCodeGenerationContext.springContextLoadingResult + if (contextLoadingResult == null) + logger.error { "Missing contextLoadingResult" } + val exception = contextLoadingResult?.exceptions?.firstOrNull() + return CgTestMethod( + name = "contextLoads", + statements = listOfNotNull( + exception?.let { e -> constructFailedContextLoadingTraceComment(e) }, + if (contextLoadingResult == null) CgSingleLineComment("Error: context loading result from concrete execution is missing") else null + ), + annotations = listOf(addAnnotation(context.testFramework.testAnnotationId, Method)), + documentation = CgDocumentationComment(listOf( + CgDocRegularLineStmt("This sanity check test fails if the application context cannot start.") + ) + exception?.let { constructFailedContextLoadingDocComment() }.orEmpty()), + type = if (contextLoadingResult != null && exception == null) SUCCESSFUL else FAILING + ) + } + + private fun constructFailedContextLoadingDocComment() = listOf( + CgDocRegularLineStmt("

"), + CgDocRegularLineStmt("Context loading throws an exception."), + CgDocRegularLineStmt("Please try to fix your context or environment configuration."), + CgDocRegularLineStmt("Spring configuration applied: ${springSettings.configuration.fullDisplayName}."), + ) + + private fun constructFailedContextLoadingTraceComment(exception: Throwable) = CgMultilineComment( + exception + .stackTraceToString() + .lines() + .let { lines -> + if (exception is UTSpringContextLoadingException) lines.dropWhile { !it.contains("Caused") } + else lines + } + .mapIndexed { i, line -> + if (i == 0) "Failure ${line.replace("Caused", "caused")}" + else TAB + line + } + .map { it.escapeControlChars() } + ) private fun addNecessarySpringSpecificAnnotations() { val springRunnerType = when (testFramework) { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringUnitTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringUnitTestClassConstructor.kt index 09e54b0c82..431ec3815b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringUnitTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgSpringUnitTestClassConstructor.kt @@ -44,10 +44,8 @@ class CgSpringUnitTestClassConstructor(context: CgContext) : CgAbstractSpringTes return fields } - override fun constructAdditionalMethods(): CgMethodsCluster { - if (!additionalMethodsRequired) { - return CgMethodsCluster(header = null, content = emptyList(),) - } + override fun constructAdditionalUtilMethods(): CgMethodsCluster? { + if (!additionalMethodsRequired) return null importIfNeeded(openMocksMethodId) @@ -67,16 +65,10 @@ class CgSpringUnitTestClassConstructor(context: CgContext) : CgAbstractSpringTes val openMocksStatement = CgAssignment(mockitoCloseableVariable, openMocksCall) val closeStatement = CgStatementExecutableCall(closeCall) - return CgMethodsCluster( - header = null, + return CgMethodsCluster.withoutDocs( listOf( - CgSimpleRegion( - header = null, - listOf( - constructBeforeMethod(listOf(openMocksStatement)), - constructAfterMethod(listOf(closeStatement)), - ) - ) + constructBeforeMethod(listOf(openMocksStatement)), + constructAfterMethod(listOf(closeStatement)), ) ) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/CommentUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/CommentUtil.kt new file mode 100644 index 0000000000..7c22717f15 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/util/CommentUtil.kt @@ -0,0 +1,10 @@ +package org.utbot.framework.codegen.util + +import org.utbot.framework.plugin.api.util.IndentUtil.TAB + +fun String.escapeControlChars() = + replace("\b", "\\b") + .replace("\n", "\\n") + .replace("\t", TAB) + .replace("\r", "\\r") + .replace("\\u","\\\\u") \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt index e7e2e5a65c..ba78166b30 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/TestCaseGenerator.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.yield @@ -37,8 +39,9 @@ import org.utbot.framework.util.toModel import org.utbot.framework.plugin.api.SpringSettings.* import org.utbot.framework.plugin.api.SpringTestType.* import org.utbot.instrumentation.ConcreteExecutor -import org.utbot.instrumentation.instrumentation.execution.SpringUtExecutionInstrumentation +import org.utbot.instrumentation.instrumentation.spring.SpringUtExecutionInstrumentation import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation +import org.utbot.instrumentation.tryLoadingSpringContext import org.utbot.instrumentation.warmup import org.utbot.taint.TaintConfigurationProvider import java.io.File @@ -109,6 +112,7 @@ open class TestCaseGenerator( //warmup if (warmupConcreteExecution) { // force pool to create an appropriate executor + // TODO ensure that instrumented process that starts here is properly terminated ConcreteExecutor( executionInstrumentation, classpathForEngine, @@ -140,7 +144,15 @@ open class TestCaseGenerator( chosenClassesToMockAlways: Set = Mocker.javaDefaultClasses.mapTo(mutableSetOf()) { it.id }, executionTimeEstimator: ExecutionTimeEstimator = ExecutionTimeEstimator(utBotGenerationTimeoutInMillis, 1), userTaintConfigurationProvider: TaintConfigurationProvider? = null, - ): Flow { + ): Flow = flow { + if (isCanceled()) + return@flow + + doContextDependentPreparationForTestGeneration() + applicationContext.getErrors().forEach { emit(it) } + if (applicationContext.preventsFurtherTestGeneration()) + return@flow + try { val engine = createSymbolicEngine( controller, @@ -153,7 +165,7 @@ open class TestCaseGenerator( ) engineActions.map { engine.apply(it) } engineActions.clear() - return defaultTestFlow(engine, executionTimeEstimator.userTimeout) + emitAll(defaultTestFlow(engine, executionTimeEstimator.userTimeout)) } catch (e: Exception) { logger.error(e) {"Generate async failed"} throw e @@ -167,8 +179,17 @@ open class TestCaseGenerator( methodsGenerationTimeout: Long = utBotGenerationTimeoutInMillis, userTaintConfigurationProvider: TaintConfigurationProvider? = null, generate: (engine: UtBotSymbolicEngine) -> Flow = defaultTestFlow(methodsGenerationTimeout) - ): List { - if (isCanceled()) return methods.map { UtMethodTestSet(it) } + ): List = ConcreteExecutor.defaultPool.use { _ -> // TODO: think on appropriate way to close instrumented processes + if (isCanceled()) return@use methods.map { UtMethodTestSet(it) } + + doContextDependentPreparationForTestGeneration() + + val method2errors: Map> = methods.associateWith { + applicationContext.getErrors().associateTo(mutableMapOf()) { it.description to 1 } + } + + if (applicationContext.preventsFurtherTestGeneration()) + return@use methods.map { method -> UtMethodTestSet(method, errors = method2errors.getValue(method)) } val executionStartInMillis = System.currentTimeMillis() val executionTimeEstimator = ExecutionTimeEstimator(methodsGenerationTimeout, methods.size) @@ -177,7 +198,6 @@ open class TestCaseGenerator( val method2controller = methods.associateWith { EngineController() } val method2executions = methods.associateWith { mutableListOf() } - val method2errors = methods.associateWith { mutableMapOf() } val conflictTriggers = ConflictTriggers() val forceMockListener = ForceMockListener.create(this, conflictTriggers) @@ -269,12 +289,11 @@ open class TestCaseGenerator( } } } - ConcreteExecutor.defaultPool.close() // TODO: think on appropriate way to close instrumented processes forceMockListener.detach(this, forceMockListener) forceStaticMockListener.detach(this, forceStaticMockListener) - return methods.map { method -> + return@use methods.map { method -> UtMethodTestSet( method, minimizeExecutions(method.classId, method2executions.getValue(method)), @@ -429,6 +448,22 @@ open class TestCaseGenerator( annotation.isInstance(existingAnnotation) } } + + private fun doContextDependentPreparationForTestGeneration() { + when (applicationContext) { + is SpringApplicationContext -> when (applicationContext.springTestType) { + UNIT_TEST -> Unit + INTEGRATION_TEST -> + if (applicationContext.springContextLoadingResult == null) + // force pool to create an appropriate executor + applicationContext.springContextLoadingResult = ConcreteExecutor( + executionInstrumentation, + classpathForEngine + ).tryLoadingSpringContext() + } + else -> Unit + } + } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt index 0fe1b94720..ead8820553 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/process/EngineProcessMain.kt @@ -303,7 +303,7 @@ private fun createCodeGenerator(kryoHelper: KryoHelper, params: RenderParams, co SpringCodeGenerator( classUnderTest = classUnderTest, projectType = projectType, - codeGenerationContext = codeGenerationContext, + springCodeGenerationContext = codeGenerationContext, generateUtilClassFile = generateUtilClassFile, paramNames = paramNames, testFramework = testFramework, diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt index f4a12d005f..90539abbf9 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/ConcreteExecutor.kt @@ -20,6 +20,7 @@ import org.utbot.framework.plugin.api.InstrumentedProcessDeathException import org.utbot.common.logException import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.SpringContextLoadingResult import org.utbot.framework.plugin.api.SpringRepositoryId import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.signature @@ -292,6 +293,13 @@ fun ConcreteExecutor<*, *>.getRelevantSpringRepositories(classId: ClassId): Set< } } +fun ConcreteExecutor<*, *>.tryLoadingSpringContext(): SpringContextLoadingResult = runBlocking { + withProcess { + val result = instrumentedProcessModel.tryLoadingSpringContext.startSuspending(lifetime, Unit) + kryoHelper.readObject(result.springContextLoadingResult) + } +} + /** * Extension function for the [ConcreteExecutor], which allows to collect static field value of [fieldId]. */ diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt index 2530da1ba5..7f3d100c56 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt @@ -69,6 +69,12 @@ class PhasesController( return@start result.getOrThrow() as T } + fun executePhaseWithoutTimeout(phase: R, block: R.() -> T): T = phase.start { + return@start ThreadBasedExecutor.threadLocal.invokeWithoutTimeout { + phase.block() + }.getOrThrow() as T + } + fun applyPreprocessing(parameters: UtConcreteExecutionData): ConstructedData { val constructedData = executePhaseInTimeout(valueConstructionPhase) { diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SpringInstrumentationContext.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringInstrumentationContext.kt similarity index 81% rename from utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SpringInstrumentationContext.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringInstrumentationContext.kt index 4a6625949b..9a43204246 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SpringInstrumentationContext.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringInstrumentationContext.kt @@ -1,4 +1,4 @@ -package org.utbot.instrumentation.instrumentation.execution.context +package org.utbot.instrumentation.instrumentation.spring import org.utbot.framework.plugin.api.SpringSettings.* import org.utbot.framework.plugin.api.SpringConfiguration.* @@ -6,16 +6,17 @@ import org.utbot.framework.plugin.api.UtConcreteValue import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtSpringContextModel import org.utbot.framework.plugin.api.util.utContext +import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext import org.utbot.spring.api.SpringApi -import org.utbot.spring.api.instantiator.SpringApiProviderFacade -import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.api.provider.SpringApiProviderFacade +import org.utbot.spring.api.provider.InstantiationSettings class SpringInstrumentationContext( private val springSettings: PresentSpringSettings, private val delegateInstrumentationContext: InstrumentationContext, ) : InstrumentationContext by delegateInstrumentationContext { // TODO: recreate context/app every time whenever we change method under test - val springApi: SpringApi by lazy { + val springApiProviderResult: SpringApiProviderFacade.ProviderResult by lazy { val classLoader = utContext.classLoader Thread.currentThread().contextClassLoader = classLoader @@ -37,6 +38,8 @@ class SpringInstrumentationContext( .provideMostSpecificAvailableApi(instantiationSettings) } + val springApi get() = springApiProviderResult.result.getOrThrow() + override fun constructContextDependentValue(model: UtModel): UtConcreteValue<*>? = when (model) { is UtSpringContextModel -> UtConcreteValue(springApi.getOrLoadSpringApplicationContext()) else -> delegateInstrumentationContext.constructContextDependentValue(model) diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SpringUtExecutionInstrumentation.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringUtExecutionInstrumentation.kt similarity index 82% rename from utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SpringUtExecutionInstrumentation.kt rename to utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringUtExecutionInstrumentation.kt index 0e6df14e34..a2f55b089d 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/SpringUtExecutionInstrumentation.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringUtExecutionInstrumentation.kt @@ -1,4 +1,4 @@ -package org.utbot.instrumentation.instrumentation.execution +package org.utbot.instrumentation.instrumentation.spring import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info @@ -6,12 +6,14 @@ import org.utbot.common.JarUtils import org.utbot.common.hasOnClasspath import org.utbot.framework.plugin.api.BeanDefinitionData import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.SpringContextLoadingResult import org.utbot.framework.plugin.api.SpringRepositoryId import org.utbot.framework.plugin.api.SpringSettings.* import org.utbot.framework.plugin.api.util.jClass import org.utbot.instrumentation.instrumentation.ArgumentList import org.utbot.instrumentation.instrumentation.Instrumentation -import org.utbot.instrumentation.instrumentation.execution.context.SpringInstrumentationContext +import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult +import org.utbot.instrumentation.instrumentation.execution.UtExecutionInstrumentation import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhaseFailingOnAnyException import org.utbot.instrumentation.process.HandlerClassesLoader import org.utbot.spring.api.SpringApi @@ -59,7 +61,14 @@ class SpringUtExecutionInstrumentation( instrumentationContext = SpringInstrumentationContext(springSettings, delegateInstrumentation.instrumentationContext) delegateInstrumentation.instrumentationContext = instrumentationContext delegateInstrumentation.init(pathsToUserClasses) - springApi.beforeTestClass() + } + + fun tryLoadingSpringContext(): SpringContextLoadingResult { + val apiProviderResult = instrumentationContext.springApiProviderResult + return SpringContextLoadingResult( + contextLoaded = apiProviderResult.result.isSuccess, + exceptions = apiProviderResult.exceptions + ) } override fun invoke( @@ -73,11 +82,14 @@ class SpringUtExecutionInstrumentation( return delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters) { invokeBasePhases -> // NB! beforeTestMethod() and afterTestMethod() are intentionally called inside phases, // so they are executed in one thread with method under test - executePhaseInTimeout(SpringBeforeTestMethodPhase) { springApi.beforeTestMethod() } + // NB! beforeTestMethod() and afterTestMethod() are executed without timeout, because: + // - if the invokeBasePhases() times out, we still want to execute afterTestMethod() + // - first call to beforeTestMethod() can take significant amount of time due to class loading & transformation + executePhaseWithoutTimeout(SpringBeforeTestMethodPhase) { springApi.beforeTestMethod() } try { invokeBasePhases() } finally { - executePhaseInTimeout(SpringAfterTestMethodPhase) { springApi.afterTestMethod() } + executePhaseWithoutTimeout(SpringAfterTestMethodPhase) { springApi.afterTestMethod() } } } } diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt index a7cba18234..70b64339c0 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/InstrumentedProcessMain.kt @@ -12,13 +12,14 @@ import org.utbot.framework.process.kryo.KryoHelper import org.utbot.instrumentation.agent.Agent import org.utbot.instrumentation.instrumentation.Instrumentation import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation -import org.utbot.instrumentation.instrumentation.execution.SpringUtExecutionInstrumentation +import org.utbot.instrumentation.instrumentation.spring.SpringUtExecutionInstrumentation import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelConstructor import org.utbot.instrumentation.process.generated.CollectCoverageResult import org.utbot.instrumentation.process.generated.GetSpringBeanResult import org.utbot.instrumentation.process.generated.GetSpringRepositoriesResult import org.utbot.instrumentation.process.generated.InstrumentedProcessModel import org.utbot.instrumentation.process.generated.InvokeMethodCommandResult +import org.utbot.instrumentation.process.generated.TryLoadingSpringContextResult import org.utbot.instrumentation.process.generated.instrumentedProcessModel import org.utbot.rd.IdleWatchdog import org.utbot.rd.ClientProtocolBuilder @@ -173,4 +174,8 @@ private fun InstrumentedProcessModel.setup(kryoHelper: KryoHelper, watchdog: Idl val repositoryDescriptions = (instrumentation as SpringUtExecutionInstrumentation).getRepositoryDescriptions(classId) GetSpringRepositoriesResult(kryoHelper.writeObject(repositoryDescriptions)) } + watchdog.measureTimeForActiveCall(tryLoadingSpringContext, "Trying to load Spring application context") { params -> + val contextLoadingResult = (instrumentation as SpringUtExecutionInstrumentation).tryLoadingSpringContext() + TryLoadingSpringContextResult(kryoHelper.writeObject(contextLoadingResult)) + } } \ No newline at end of file diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt index 175bbff366..e08ed48ba6 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/rd/generated/InstrumentedProcessModel.Generated.kt @@ -26,7 +26,8 @@ class InstrumentedProcessModel private constructor( private val _collectCoverage: RdCall, private val _computeStaticField: RdCall, private val _getSpringBean: RdCall, - private val _getRelevantSpringRepositories: RdCall + private val _getRelevantSpringRepositories: RdCall, + private val _tryLoadingSpringContext: RdCall ) : RdExtBase() { //companion @@ -45,6 +46,7 @@ class InstrumentedProcessModel private constructor( serializers.register(GetSpringBeanResult) serializers.register(GetSpringRepositoriesParams) serializers.register(GetSpringRepositoriesResult) + serializers.register(TryLoadingSpringContextResult) } @@ -65,7 +67,7 @@ class InstrumentedProcessModel private constructor( } - const val serializationHash = -6973752778611891323L + const val serializationHash = -3572666434834334555L } override val serializersOwner: ISerializersOwner get() = InstrumentedProcessModel @@ -116,6 +118,12 @@ class InstrumentedProcessModel private constructor( * Gets a list of [SpringRepositoryId]s that class specified by the [ClassId] (possibly indirectly) depends on (requires Spring instrumentation) */ val getRelevantSpringRepositories: RdCall get() = _getRelevantSpringRepositories + + /** + * This command is sent to the instrumented process from the [ConcreteExecutor] + if the user wants to determine whether or not Spring application context can load + */ + val tryLoadingSpringContext: RdCall get() = _tryLoadingSpringContext //methods //initializer init { @@ -127,6 +135,7 @@ class InstrumentedProcessModel private constructor( _computeStaticField.async = true _getSpringBean.async = true _getRelevantSpringRepositories.async = true + _tryLoadingSpringContext.async = true } init { @@ -138,6 +147,7 @@ class InstrumentedProcessModel private constructor( bindableChildren.add("computeStaticField" to _computeStaticField) bindableChildren.add("getSpringBean" to _getSpringBean) bindableChildren.add("getRelevantSpringRepositories" to _getRelevantSpringRepositories) + bindableChildren.add("tryLoadingSpringContext" to _tryLoadingSpringContext) } //secondary constructor @@ -150,7 +160,8 @@ class InstrumentedProcessModel private constructor( RdCall(CollectCoverageParams, CollectCoverageResult), RdCall(ComputeStaticFieldParams, ComputeStaticFieldResult), RdCall(GetSpringBeanParams, GetSpringBeanResult), - RdCall(GetSpringRepositoriesParams, GetSpringRepositoriesResult) + RdCall(GetSpringRepositoriesParams, GetSpringRepositoriesResult), + RdCall(FrameworkMarshallers.Void, TryLoadingSpringContextResult) ) //equals trait @@ -167,6 +178,7 @@ class InstrumentedProcessModel private constructor( print("computeStaticField = "); _computeStaticField.print(printer); println() print("getSpringBean = "); _getSpringBean.print(printer); println() print("getRelevantSpringRepositories = "); _getRelevantSpringRepositories.print(printer); println() + print("tryLoadingSpringContext = "); _tryLoadingSpringContext.print(printer); println() } printer.print(")") } @@ -180,7 +192,8 @@ class InstrumentedProcessModel private constructor( _collectCoverage.deepClonePolymorphic(), _computeStaticField.deepClonePolymorphic(), _getSpringBean.deepClonePolymorphic(), - _getRelevantSpringRepositories.deepClonePolymorphic() + _getRelevantSpringRepositories.deepClonePolymorphic(), + _tryLoadingSpringContext.deepClonePolymorphic() ) } //contexts @@ -889,3 +902,60 @@ data class SetInstrumentationParams ( //deepClone //contexts } + + +/** + * #### Generated from [InstrumentedProcessModel.kt:60] + */ +data class TryLoadingSpringContextResult ( + val springContextLoadingResult: ByteArray +) : IPrintable { + //companion + + companion object : IMarshaller { + override val _type: KClass = TryLoadingSpringContextResult::class + + @Suppress("UNCHECKED_CAST") + override fun read(ctx: SerializationCtx, buffer: AbstractBuffer): TryLoadingSpringContextResult { + val springContextLoadingResult = buffer.readByteArray() + return TryLoadingSpringContextResult(springContextLoadingResult) + } + + override fun write(ctx: SerializationCtx, buffer: AbstractBuffer, value: TryLoadingSpringContextResult) { + buffer.writeByteArray(value.springContextLoadingResult) + } + + + } + //fields + //methods + //initializer + //secondary constructor + //equals trait + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other::class != this::class) return false + + other as TryLoadingSpringContextResult + + if (!(springContextLoadingResult contentEquals other.springContextLoadingResult)) return false + + return true + } + //hash code trait + override fun hashCode(): Int { + var __r = 0 + __r = __r*31 + springContextLoadingResult.contentHashCode() + return __r + } + //pretty print + override fun print(printer: PrettyPrinter) { + printer.println("TryLoadingSpringContextResult (") + printer.indent { + print("springContextLoadingResult = "); springContextLoadingResult.print(printer); println() + } + printer.print(")") + } + //deepClone + //contexts +} diff --git a/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt b/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt index 9fdfefcab2..cdefe5c103 100644 --- a/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt +++ b/utbot-rd/src/main/rdgen/org/utbot/rd/models/InstrumentedProcessModel.kt @@ -57,6 +57,10 @@ object InstrumentedProcessModel : Ext(InstrumentedProcessRoot) { field("springRepositoryIds", array(PredefinedType.byte)) } + val TryLoadingSpringContextResult = structdef { + field("springContextLoadingResult", array(PredefinedType.byte)) + } + init { call("AddPaths", AddPathsParams, PredefinedType.void).apply { async @@ -101,5 +105,10 @@ object InstrumentedProcessModel : Ext(InstrumentedProcessRoot) { documentation = "Gets a list of [SpringRepositoryId]s that class specified by the [ClassId]" + " (possibly indirectly) depends on (requires Spring instrumentation)" } + call("tryLoadingSpringContext", PredefinedType.void, TryLoadingSpringContextResult).apply { + async + documentation = "This command is sent to the instrumented process from the [ConcreteExecutor]\n" + + "if the user wants to determine whether or not Spring application context can load" + } } } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt index e1a07a7c57..706ad79efd 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzer/SpringApplicationAnalyzer.kt @@ -1,8 +1,8 @@ package org.utbot.spring.analyzer -import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.api.provider.InstantiationSettings import org.utbot.spring.api.ApplicationData -import org.utbot.spring.api.instantiator.SpringApiProviderFacade +import org.utbot.spring.api.provider.SpringApiProviderFacade import org.utbot.spring.exception.UtBotSpringShutdownException import org.utbot.spring.generated.BeanDefinitionData import org.utbot.spring.utils.SourceFinder @@ -23,6 +23,6 @@ class SpringApplicationAnalyzer { .catch { springApi.getOrLoadSpringApplicationContext() } .beanDefinitions .toTypedArray() - } + }.result.getOrThrow() } } \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt index 3e86627f26..3457204c5e 100644 --- a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/SpringApi.kt @@ -6,6 +6,8 @@ import java.net.URLClassLoader interface SpringApi { /** * NOTE! [Any] return type is used here because Spring itself may not be on the classpath of the API user + * + * @throws [UTSpringContextLoadingException] as a wrapper for all runtime exceptions */ fun getOrLoadSpringApplicationContext(): Any @@ -17,11 +19,6 @@ interface SpringApi { fun resolveRepositories(beanNames: Set, userSourcesClassLoader: URLClassLoader): Set - /** - * NOTE! Should be called once before any invocations of [beforeTestMethod] and [afterTestMethod] - */ - fun beforeTestClass() - /** * NOTE! Should be called on one thread with method under test and value constructor, * because transactions are bound to threads diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/UTSpringContextLoadingException.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/UTSpringContextLoadingException.kt new file mode 100644 index 0000000000..471c706497 --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/UTSpringContextLoadingException.kt @@ -0,0 +1,11 @@ +package org.utbot.spring.api + +/** + * Used primarily to let code generation distinguish between + * parts of stack trace inside UTBot (including RD, etc.) + * and parts of stack trace inside Spring and user application. + */ +class UTSpringContextLoadingException(override val cause: Throwable) : Exception( + "UTBot failed to load Spring application context", + cause +) diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/InstantiationSettings.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/InstantiationSettings.kt deleted file mode 100644 index 3a783a1b7b..0000000000 --- a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/InstantiationSettings.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.utbot.spring.api.instantiator - -class InstantiationSettings( - val configurationClasses: Array>, - val profiles: Array, -) \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/InstantiationSettings.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/InstantiationSettings.kt new file mode 100644 index 0000000000..ea2146a327 --- /dev/null +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/InstantiationSettings.kt @@ -0,0 +1,9 @@ +package org.utbot.spring.api.provider + +class InstantiationSettings( + val configurationClasses: Array>, + val profiles: Array, +) { + override fun toString(): String = + "InstantiationSettings(configurationClasses=${configurationClasses.contentToString()}, profiles=${profiles.contentToString()})" +} \ No newline at end of file diff --git a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/SpringApiProviderFacade.kt b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/SpringApiProviderFacade.kt similarity index 64% rename from utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/SpringApiProviderFacade.kt rename to utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/SpringApiProviderFacade.kt index 33352fd0d9..49b90d2aef 100644 --- a/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/instantiator/SpringApiProviderFacade.kt +++ b/utbot-spring-commons-api/src/main/kotlin/org/utbot/spring/api/provider/SpringApiProviderFacade.kt @@ -1,4 +1,4 @@ -package org.utbot.spring.api.instantiator +package org.utbot.spring.api.provider import org.utbot.spring.api.SpringApi @@ -7,17 +7,19 @@ import org.utbot.spring.api.SpringApi * meaning each [SpringApi] instance will when needed start its own Spring Application. */ interface SpringApiProviderFacade { - fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): SpringApi + fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): ProviderResult /** * [apiUser] is consequently invoked on all available (on the classpath) * [SpringApi] types from most specific (e.g. Spring Boot) to least specific (e.g. Pure Spring) * until it executes without throwing exception, then obtained result is returned. + * + * All exceptions are collected into [ProviderResult.exceptions]. */ fun useMostSpecificNonFailingApi( instantiationSettings: InstantiationSettings, apiUser: (SpringApi) -> T - ): T + ): ProviderResult companion object { fun getInstance(classLoader: ClassLoader): SpringApiProviderFacade = @@ -26,4 +28,14 @@ interface SpringApiProviderFacade { .getConstructor() .newInstance() as SpringApiProviderFacade } + + /** + * [result] can be a [Result.success] while [exceptions] is not empty, + * if we failed to use most specific [SpringApi] available (e.g. SpringBoot), but + * were able to successfully fall back to less specific [SpringApi] (e.g. PureSpring). + */ + class ProviderResult( + val result: Result, + val exceptions: List + ) } \ No newline at end of file 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 7a8eb9aab9..687654cbae 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 @@ -12,7 +12,8 @@ import org.utbot.common.hasOnClasspath import org.utbot.common.patchAnnotation import org.utbot.spring.api.SpringApi import org.utbot.spring.api.RepositoryDescription -import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.api.UTSpringContextLoadingException +import org.utbot.spring.api.provider.InstantiationSettings import org.utbot.spring.dummy.DummySpringIntegrationTestClass import org.utbot.spring.utils.DependencyUtils.isSpringDataOnClasspath import org.utbot.spring.utils.RepositoryUtils @@ -42,9 +43,13 @@ class SpringApiImpl( private val dummyTestMethod: Method = DummySpringIntegrationTestClass::dummyTestMethod.javaMethod!! private val testContextManager: TestContextManager = TestContextManager(this.dummyTestClass) - private val context get() = testContextManager.testContext.applicationContext as ConfigurableApplicationContext + private val context get() = getOrLoadSpringApplicationContext() - override fun getOrLoadSpringApplicationContext() = context + override fun getOrLoadSpringApplicationContext() = try { + testContextManager.testContext.applicationContext as ConfigurableApplicationContext + } catch (e: Throwable) { + throw UTSpringContextLoadingException(e) + } override fun getBean(beanName: String): Any = context.getBean(beanName) @@ -124,20 +129,32 @@ class SpringApiImpl( return descriptions } - override fun beforeTestClass() { + private var beforeTestClassCalled = false + private var isInsideTestMethod = false + + private fun beforeTestClass() { + beforeTestClassCalled = true testContextManager.beforeTestClass() dummyTestClassInstance = dummyTestClass.getConstructor().newInstance() testContextManager.prepareTestInstance(dummyTestClassInstance) } override fun beforeTestMethod() { + if (!beforeTestClassCalled) + beforeTestClass() + if (isInsideTestMethod) { + logger.warn { "afterTestMethod() wasn't called for previous test method, calling it from beforeTestMethod()" } + afterTestMethod() + } testContextManager.beforeTestMethod(dummyTestClassInstance, dummyTestMethod) testContextManager.beforeTestExecution(dummyTestClassInstance, dummyTestMethod) + isInsideTestMethod = true } override fun afterTestMethod() { testContextManager.afterTestExecution(dummyTestClassInstance, dummyTestMethod, null) testContextManager.afterTestMethod(dummyTestClassInstance, dummyTestMethod, null) + isInsideTestMethod = false } private fun describesRepository(bean: Any): Boolean = 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 6129c6646b..371bb93a93 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 @@ -1,6 +1,6 @@ package org.utbot.spring.provider -import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.api.provider.InstantiationSettings import org.utbot.spring.SpringApiImpl import org.utbot.spring.dummy.DummyPureSpringIntegrationTestClass import org.utbot.spring.dummy.DummyPureSpringIntegrationTestClassAutoconfigTestDB diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt index 281390020a..66b30109a0 100644 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProvider.kt @@ -1,7 +1,7 @@ package org.utbot.spring.provider import org.utbot.spring.api.SpringApi -import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.api.provider.InstantiationSettings interface SpringApiProvider { diff --git a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt index 95041897f6..519115a310 100644 --- a/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt +++ b/utbot-spring-commons/src/main/kotlin/org/utbot/spring/provider/SpringApiProviderFacadeImpl.kt @@ -1,20 +1,21 @@ package org.utbot.spring.provider import com.jetbrains.rd.util.error -import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.api.provider.InstantiationSettings import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info import org.springframework.boot.SpringBootVersion import org.springframework.core.SpringVersion import org.utbot.spring.api.SpringApi -import org.utbot.spring.api.instantiator.SpringApiProviderFacade +import org.utbot.spring.api.provider.SpringApiProviderFacade +import org.utbot.spring.api.provider.SpringApiProviderFacade.ProviderResult private val logger = getLogger() class SpringApiProviderFacadeImpl : SpringApiProviderFacade { - override fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): SpringApi = + override fun provideMostSpecificAvailableApi(instantiationSettings: InstantiationSettings): ProviderResult = useMostSpecificNonFailingApi(instantiationSettings) { api -> api.getOrLoadSpringApplicationContext() api @@ -23,22 +24,30 @@ class SpringApiProviderFacadeImpl : SpringApiProviderFacade { override fun useMostSpecificNonFailingApi( instantiationSettings: InstantiationSettings, apiUser: (SpringApi) -> T - ): T { + ): ProviderResult { logger.info { "Current Java version is: " + System.getProperty("java.version") } logger.info { "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrNull() } logger.info { "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrNull() } + logger.info { "InstantiationSettings: $instantiationSettings" } - for (apiProvider in listOf(SpringBootApiProvider(), PureSpringApiProvider())) { - if (apiProvider.isAvailable()) { - logger.info { "Getting Spring API from $apiProvider" } - try { - return apiUser(apiProvider.provideAPI(instantiationSettings)) - } catch (e: Throwable) { - logger.error("Getting Spring API from $apiProvider failed", e) + val exceptions = mutableListOf() + + val apiProviders = sequenceOf(SpringBootApiProvider(), PureSpringApiProvider()) + + val result = apiProviders + .filter { apiProvider -> apiProvider.isAvailable() } + .map { apiProvider -> + logger.info { "Using Spring API from $apiProvider" } + val result = runCatching { apiUser(apiProvider.provideAPI(instantiationSettings)) } + result.onFailure { e -> + exceptions.add(e) + logger.error("Using Spring API from $apiProvider failed", e) } + result } - } + .firstOrNull { it.isSuccess } + ?: Result.failure(exceptions.first()) - error("Failed to use any Spring API") + return ProviderResult(result, exceptions) } } 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 ab0ec17f16..42166664c5 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,6 +1,6 @@ package org.utbot.spring.provider -import org.utbot.spring.api.instantiator.InstantiationSettings +import org.utbot.spring.api.provider.InstantiationSettings import org.utbot.spring.dummy.DummySpringBootIntegrationTestClass import org.utbot.spring.SpringApiImpl import org.utbot.spring.dummy.DummySpringBootIntegrationTestClassAutoconfigTestDB diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/DBSCANClusteringConstants.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/DBSCANClusteringConstants.kt index 81929cf56f..29b4a52d70 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/DBSCANClusteringConstants.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/DBSCANClusteringConstants.kt @@ -1,5 +1,7 @@ package org.utbot.summary +import org.utbot.framework.plugin.api.util.IndentUtil + object DBSCANClusteringConstants { /** * Sets minimum number of successful execution @@ -24,7 +26,7 @@ object DBSCANClusteringConstants { object SummarySentenceConstants { const val SENTENCE_SEPARATION = ",\n" - const val TAB = " " + const val TAB = IndentUtil.TAB const val NEW_LINE = "\n" const val DOT_SYMBOL = '.' const val COMMA_SYMBOL = ',' diff --git a/utbot-testing/src/main/kotlin/org/utbot/testing/TestCodeGeneratorPipeline.kt b/utbot-testing/src/main/kotlin/org/utbot/testing/TestCodeGeneratorPipeline.kt index b43a4c5453..c48c7f6f24 100644 --- a/utbot-testing/src/main/kotlin/org/utbot/testing/TestCodeGeneratorPipeline.kt +++ b/utbot-testing/src/main/kotlin/org/utbot/testing/TestCodeGeneratorPipeline.kt @@ -268,7 +268,7 @@ class TestCodeGeneratorPipeline(private val testInfrastructureConfiguration: Tes parameterizedTestSource = parametrizedTestSource, runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, enableTestsTimeout = enableTestsTimeout, - codeGenerationContext = SpringApplicationContext( + springCodeGenerationContext = SpringApplicationContext( mockInstalled = true, staticsMockingIsConfigured = true, shouldUseImplementors = false,