Skip to content
37 changes: 24 additions & 13 deletions utbot-core/src/main/kotlin/org/utbot/common/ThreadUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any?>? {
if (thread?.isAlive != true) {
requestQueue = ArrayBlockingQueue<() -> Any?>(1)
responseQueue = ArrayBlockingQueue<Result<Any?>>(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 {
Expand Down Expand Up @@ -83,4 +71,27 @@ class ThreadBasedExecutor {
}
return res
}

fun invokeWithoutTimeout(action:() -> Any?) : Result<Any?> {
ensureThreadIsAlive()

requestQueue.offer(action)
return responseQueue.take()
}

private fun ensureThreadIsAlive() {
if (thread?.isAlive != true) {
requestQueue = ArrayBlockingQueue<() -> Any?>(1)
responseQueue = ArrayBlockingQueue<Result<Any?>>(1)

thread = thread(name = "executor", isDaemon = true) {
try {
while (true) {
val next = requestQueue.take()
responseQueue.offer(kotlin.runCatching { next() })
}
} catch (_: InterruptedException) {}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ object MissingState : EnvironmentModels(
)

/**
* Error happened in traverse.
* Error happened during test cases generation.
*/
data class UtError(
val description: String,
Expand Down Expand Up @@ -1328,6 +1328,7 @@ interface CodeGenerationContext
interface SpringCodeGenerationContext : CodeGenerationContext {
val springTestType: SpringTestType
val springSettings: SpringSettings
val springContextLoadingResult: SpringContextLoadingResult?
}

/**
Expand Down Expand Up @@ -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<UtError> = 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 {
Expand All @@ -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<Throwable>
)

/**
* Data we get from Spring application context
* to manage engine and code generator behaviour.
Expand All @@ -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 {}
}
Expand Down Expand Up @@ -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<UtError> =
springContextLoadingResult?.exceptions?.map { exception ->
UtError(
"Failed to load Spring application context",
exception
)
}.orEmpty() + super.getErrors()
}

enum class SpringTestType(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.utbot.framework.plugin.api.util

object IndentUtil {
const val TAB = " "
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ class ThrowableSerializer : Serializer<Throwable>() {

override fun read(kryo: Kryo, input: Input, type: Class<out Throwable>): 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
}
Expand All @@ -68,14 +68,31 @@ class ThrowableSerializer : Serializer<Throwable>() {
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
}
}
Expand Down
2 changes: 2 additions & 0 deletions utbot-framework/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,19 @@ data class CgTestMethodCluster(
data class CgMethodsCluster(
override val header: String?,
override val content: List<CgRegion<CgMethod>>
) : CgRegion<CgRegion<CgMethod>>()
) : CgRegion<CgRegion<CgMethod>>() {
companion object {
fun withoutDocs(methodsList: List<CgMethod>) = CgMethodsCluster(
header = null,
content = listOf(
CgSimpleRegion(
header = null,
content = methodsList
)
)
)
}
}

/**
* Util entity is either an instance of [CgAuxiliaryClass] or [CgUtilMethod].
Expand Down Expand Up @@ -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<CgParameterDeclaration>,
override val returnType: ClassId = voidClassId,
override val parameters: List<CgParameterDeclaration> = emptyList(),
override val statements: List<CgStatement>,
override val exceptions: Set<ClassId>,
override val exceptions: Set<ClassId> = emptySet(),
override val annotations: List<CgAnnotation>,
override val visibility: VisibilityModifier = VisibilityModifier.PUBLIC,
val type: CgTestMethodType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExecutableId, List<String>> = mutableMapOf(),
generateUtilClassFile: Boolean = false,
testFramework: TestFramework = TestFramework.defaultItem,
Expand Down Expand Up @@ -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.")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = "")
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -48,7 +51,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
}
}

methodRegions += constructAdditionalMethods()
constructAdditionalUtilMethods()?.let { methodRegions += it }

if (currentTestClass == outerMostTestClass) {
val utilEntities = collectUtilEntities()
Expand Down Expand Up @@ -81,7 +84,13 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):

abstract fun constructClassFields(testClassModel: SpringTestClassModel): List<CgFieldDeclaration>

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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -29,7 +30,11 @@ import org.utbot.framework.plugin.api.util.description

abstract class CgAbstractTestClassConstructor<T : TestClassModel>(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()
Expand Down Expand Up @@ -118,6 +123,7 @@ abstract class CgAbstractTestClassConstructor<T : TestClassModel>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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<String>()
var executableCallFound = false
Expand All @@ -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")
}
Expand Down
Loading