Skip to content

Commit 4110600

Browse files
authored
Unwrap NestedServletException in codegen #2586 (#2587)
* Make tests throwing `NestedServletException` fail, print cause stacktrace * Make exception collection only skip exceptions inside `assertThrows`
1 parent baea252 commit 4110600

File tree

7 files changed

+96
-36
lines changed

7 files changed

+96
-36
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ object SpringModelUtils {
173173
private val objectMapperClassId = ClassId("com.fasterxml.jackson.databind.ObjectMapper")
174174
private val cookieClassId = ClassId("javax.servlet.http.Cookie")
175175

176+
// as of Spring 6.0 `NestedServletException` is deprecated in favor of standard `ServletException` nesting
177+
val nestedServletExceptionClassIds = listOf(
178+
ClassId("org.springframework.web.util.NestedServletException"),
179+
ClassId("jakarta.servlet.ServletException")
180+
)
181+
176182
private val requestAttributesMethodId = MethodId(
177183
classId = mockHttpServletRequestBuilderClassId,
178184
name = "requestAttr",

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

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -296,26 +296,8 @@ interface CgContextOwner {
296296
return block()
297297
}
298298

299-
fun addExceptionIfNeeded(exception: ClassId) {
300-
if (exception !is BuiltinClassId) {
301-
require(exception isSubtypeOf Throwable::class.id) {
302-
"Class $exception which is not a Throwable was passed"
303-
}
304-
305-
val isUnchecked = !exception.jClass.isCheckedException
306-
val alreadyAdded =
307-
collectedExceptions.any { existingException -> exception isSubtypeOf existingException }
308-
309-
if (isUnchecked || alreadyAdded) return
310-
311-
collectedExceptions
312-
.removeIf { existingException -> existingException isSubtypeOf exception }
313-
}
314-
315-
if (collectedExceptions.add(exception)) {
316-
importIfNeeded(exception)
317-
}
318-
}
299+
fun addExceptionIfNeeded(exception: ClassId)
300+
fun <T> runWithoutCollectingExceptions(block: () -> T): T
319301

320302
fun createGetClassExpression(id: ClassId, codegenLanguage: CodegenLanguage): CgGetClass =
321303
when (codegenLanguage) {
@@ -624,6 +606,40 @@ class CgContext(
624606
mockFrameworkUsed = false
625607
}
626608

609+
// number of times collection of exceptions was suspended
610+
private var exceptionCollectionSuspensionDepth = 0
611+
612+
override fun <T> runWithoutCollectingExceptions(block: () -> T): T {
613+
exceptionCollectionSuspensionDepth++
614+
return try {
615+
block()
616+
} finally {
617+
exceptionCollectionSuspensionDepth--
618+
}
619+
}
620+
621+
override fun addExceptionIfNeeded(exception: ClassId) {
622+
if (exceptionCollectionSuspensionDepth > 0) return
623+
if (exception !is BuiltinClassId) {
624+
require(exception isSubtypeOf Throwable::class.id) {
625+
"Class $exception which is not a Throwable was passed"
626+
}
627+
628+
val isUnchecked = !exception.jClass.isCheckedException
629+
val alreadyAdded =
630+
collectedExceptions.any { existingException -> exception isSubtypeOf existingException }
631+
632+
if (isUnchecked || alreadyAdded) return
633+
634+
collectedExceptions
635+
.removeIf { existingException -> existingException isSubtypeOf exception }
636+
}
637+
638+
if (collectedExceptions.add(exception)) {
639+
importIfNeeded(exception)
640+
}
641+
}
642+
627643
override var currentTestSetId: Int = -1
628644

629645
override var currentExecutionId: Int = -1

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/access/CgCallableAccessManager.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package org.utbot.framework.codegen.services.access
22

33
import kotlinx.collections.immutable.PersistentList
4-
import org.utbot.framework.codegen.domain.Junit5
5-
import org.utbot.framework.codegen.domain.TestNg
64
import org.utbot.framework.codegen.domain.builtin.any
75
import org.utbot.framework.codegen.domain.builtin.anyOfClass
86
import org.utbot.framework.codegen.domain.builtin.getMethodId
@@ -47,7 +45,6 @@ import org.utbot.framework.plugin.api.ConstructorId
4745
import org.utbot.framework.plugin.api.ExecutableId
4846
import org.utbot.framework.plugin.api.FieldId
4947
import org.utbot.framework.plugin.api.MethodId
50-
import org.utbot.framework.plugin.api.UtExplicitlyThrownException
5148
import org.utbot.framework.plugin.api.util.exceptions
5249
import org.utbot.framework.plugin.api.util.extensionReceiverParameterIndex
5350
import org.utbot.framework.plugin.api.util.humanReadableName
@@ -160,16 +157,6 @@ class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessMana
160157
addExceptionIfNeeded(Throwable::class.id)
161158
}
162159

163-
val methodIsToCallAndThrowsExplicitly = methodId == currentExecutableToCall
164-
&& currentExecution?.result is UtExplicitlyThrownException
165-
val frameworkSupportsAssertThrows = testFramework == Junit5 || testFramework == TestNg
166-
167-
//If explicit exception is wrapped with assertThrows,
168-
// no "throws" in test method signature is required.
169-
if (methodIsToCallAndThrowsExplicitly && frameworkSupportsAssertThrows) {
170-
return
171-
}
172-
173160
methodId.method.exceptionTypes.forEach { addExceptionIfNeeded(it.id) }
174161
}
175162

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/services/framework/TestFrameworkManager.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,9 @@ internal class TestNgManager(context: CgContext) : TestFrameworkManager(context)
286286

287287
override fun expectException(exception: ClassId, block: () -> Unit) {
288288
require(testFramework is TestNg) { "According to settings, TestNg was expected, but got: $testFramework" }
289-
val lambda = statementConstructor.lambda(testFramework.throwingRunnableClassId) { block() }
289+
val lambda = statementConstructor.lambda(testFramework.throwingRunnableClassId) {
290+
runWithoutCollectingExceptions(block)
291+
}
290292
+assertions[assertThrows](exception.toExceptionClass(), lambda)
291293
}
292294

@@ -474,7 +476,9 @@ internal class Junit5Manager(context: CgContext) : TestFrameworkManager(context)
474476

475477
override fun expectException(exception: ClassId, block: () -> Unit) {
476478
require(testFramework is Junit5) { "According to settings, JUnit5 was expected, but got: $testFramework" }
477-
val lambda = statementConstructor.lambda(testFramework.executableClassId) { block() }
479+
val lambda = statementConstructor.lambda(testFramework.executableClassId) {
480+
runWithoutCollectingExceptions(block)
481+
}
478482
+assertions[assertThrows](exception.toExceptionClass(), lambda)
479483
}
480484

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,18 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte
471471
.map { it.escapeControlChars() }
472472
.toMutableList()
473473

474+
+CgMultilineComment(warningLine + collectNeededStackTraceLines(
475+
exception,
476+
executableToStartCollectingFrom = currentExecutableToCall!!
477+
))
478+
}
479+
480+
protected open fun collectNeededStackTraceLines(
481+
exception: Throwable,
482+
executableToStartCollectingFrom: ExecutableId
483+
): List<String> {
484+
val executableName = "${executableToStartCollectingFrom.classId.name}.${executableToStartCollectingFrom.name}"
485+
474486
val neededStackTraceLines = mutableListOf<String>()
475487
var executableCallFound = false
476488
exception.stackTrace.reversed().forEach { stackTraceElement ->
@@ -485,7 +497,7 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte
485497
if (!executableCallFound)
486498
logger.warn(exception) { "Failed to find executable call in stack trace" }
487499

488-
+CgMultilineComment(warningLine + neededStackTraceLines.reversed())
500+
return neededStackTraceLines.reversed()
489501
}
490502

491503
protected fun writeWarningAboutCrash() {
@@ -1819,6 +1831,9 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte
18191831
currentExecution = execution
18201832
determineExecutionType()
18211833
statesCache = EnvironmentFieldStateCache.emptyCacheFor(execution)
1834+
// modelToUsageCountInMethod = countUsages(ignoreAssembleOrigin = true) { counter ->
1835+
// execution.mapAllModels(counter)
1836+
// }
18221837
return try {
18231838
block()
18241839
} finally {

utbot-spring-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import org.utbot.framework.codegen.domain.models.CgMethodTestSet
55
import org.utbot.framework.codegen.domain.models.builders.SimpleTestClassModelBuilder
66
import org.utbot.framework.codegen.services.language.CgLanguageAssistant
77
import org.utbot.framework.codegen.tree.CgCustomAssertConstructor
8+
import org.utbot.framework.codegen.tree.CgMethodConstructor
89
import org.utbot.framework.codegen.tree.CgSpringIntegrationTestClassConstructor
10+
import org.utbot.framework.codegen.tree.CgSpringMethodConstructor
911
import org.utbot.framework.codegen.tree.CgSpringUnitTestClassConstructor
1012
import org.utbot.framework.codegen.tree.CgSpringVariableConstructor
1113
import org.utbot.framework.codegen.tree.CgVariableConstructor
@@ -30,6 +32,9 @@ class SpringCodeGenerator(
3032
// TODO decorate original `params.cgLanguageAssistant.getVariableConstructorBy(context)`
3133
CgSpringVariableConstructor(context)
3234

35+
override fun getMethodConstructorBy(context: CgContext): CgMethodConstructor =
36+
CgSpringMethodConstructor(context)
37+
3338
override fun getCustomAssertConstructorBy(context: CgContext): CgCustomAssertConstructor =
3439
params.cgLanguageAssistant.getCustomAssertConstructorBy(context)
3540
.withCustomAssertForMockMvcResultActions()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.utbot.framework.codegen.tree
2+
3+
import org.utbot.framework.codegen.domain.context.CgContext
4+
import org.utbot.framework.plugin.api.ExecutableId
5+
import org.utbot.framework.plugin.api.UtExecution
6+
import org.utbot.framework.plugin.api.util.SpringModelUtils.mockMvcPerformMethodId
7+
import org.utbot.framework.plugin.api.util.SpringModelUtils.nestedServletExceptionClassIds
8+
import org.utbot.framework.plugin.api.util.id
9+
10+
class CgSpringMethodConstructor(context: CgContext) : CgMethodConstructor(context) {
11+
override fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean =
12+
!isNestedServletException(exception) && super.shouldTestPassWithException(execution, exception)
13+
14+
override fun collectNeededStackTraceLines(
15+
exception: Throwable,
16+
executableToStartCollectingFrom: ExecutableId
17+
): List<String> =
18+
// `mockMvc.perform` wraps exceptions from user code into NestedServletException, so we unwrap them back
19+
exception.takeIf {
20+
executableToStartCollectingFrom == mockMvcPerformMethodId && isNestedServletException(it)
21+
}?.cause?.let { cause ->
22+
super.collectNeededStackTraceLines(cause, currentExecutableUnderTest!!)
23+
} ?: super.collectNeededStackTraceLines(exception, executableToStartCollectingFrom)
24+
25+
private fun isNestedServletException(exception: Throwable): Boolean =
26+
exception::class.java.id in nestedServletExceptionClassIds
27+
}

0 commit comments

Comments
 (0)