Skip to content

Divide Spring unit and integration testing in codegen #2230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/UnitTestBotDecomposition.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ We use an outdated approach with the [Soot](https://github.com/soot-oss/soot) fr
The current domain of code generation is specific for generating tests, though it could be reused for other purposes. Currently, the engine can be used to generate tests for different test frameworks. One can use the code generator to generate test templates inside the IntelliJ-based IDEs.

Entry and exit point:
`org.utbot.framework.codegen.CodeGenerator#generateAsStringWithTestReport`
`org.utbot.framework.codegen.generator.CodeGenerator#generateAsStringWithTestReport`

Note that for Spring projects `SpringCodeGenerator` is used. It supports both unit and integration tests generation.

## SARIF report visualizer

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import org.utbot.common.PathUtil.toURL
import org.utbot.common.toPath
import org.utbot.engine.Mocker
import org.utbot.framework.UtSettings
import org.utbot.framework.codegen.CodeGenerator
import org.utbot.framework.codegen.domain.ForceStaticMocking
import org.utbot.framework.codegen.domain.MockitoStaticMocking
import org.utbot.framework.codegen.domain.NoStaticMocking
import org.utbot.framework.codegen.domain.ProjectType
import org.utbot.framework.codegen.domain.StaticsMocking
import org.utbot.framework.codegen.domain.testFrameworkByName
import org.utbot.framework.codegen.generator.CodeGenerator
import org.utbot.framework.codegen.services.language.CgLanguageAssistant
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.CodegenLanguage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,7 @@ class SpringApplicationContext(
): Boolean = field.fieldId in classUnderTest.allDeclaredFieldIds && field.declaringClass.id !in springInjectedClasses
}

enum class SpringTestType(
enum class SpringTestsType(
override val id: String,
override val displayName: String,
override val description: String,
Expand All @@ -1363,7 +1363,7 @@ enum class SpringTestType(

companion object : CodeGenerationSettingBox {
override val defaultItem = UNIT_TESTS
override val allItems: List<SpringTestType> = values().toList()
override val allItems: List<SpringTestsType> = values().toList()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package org.utbot.external.api
import org.utbot.common.FileUtil
import org.utbot.common.nameOfPackage
import org.utbot.framework.UtSettings
import org.utbot.framework.codegen.CodeGenerator
import org.utbot.framework.codegen.domain.ForceStaticMocking
import org.utbot.framework.codegen.domain.Junit5
import org.utbot.framework.codegen.domain.NoStaticMocking
import org.utbot.framework.codegen.domain.ProjectType
import org.utbot.framework.codegen.domain.StaticsMocking
import org.utbot.framework.codegen.domain.TestFramework
import org.utbot.framework.codegen.generator.CodeGenerator
import org.utbot.framework.codegen.services.language.CgLanguageAssistant
import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionData
import org.utbot.instrumentation.instrumentation.execution.UtConcreteExecutionResult
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.utbot.framework.codegen.domain.builtin

import org.utbot.framework.plugin.api.BuiltinClassId

internal val autowiredClassId = BuiltinClassId(
canonicalName = "org.springframework.beans.factory.annotation",
simpleName = "Autowired",
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.utbot.framework.codegen.domain.models

import org.utbot.framework.codegen.domain.UtModelWrapper
import org.utbot.framework.codegen.domain.models.builders.TypedModelWrappers
import org.utbot.framework.plugin.api.ClassId

/**
Expand Down Expand Up @@ -30,7 +30,7 @@ class SpringTestClassModel(
classUnderTest: ClassId,
methodTestSets: List<CgMethodTestSet>,
nestedClasses: List<SimpleTestClassModel>,
val injectedMockModels: Map<ClassId, Set<UtModelWrapper>> = mapOf(),
val mockedModels: Map<ClassId, Set<UtModelWrapper>> = mapOf(),
val thisInstanceModels: TypedModelWrappers = mapOf(),
val thisInstanceDependentMocks: TypedModelWrappers = mapOf(),
): TestClassModel(classUnderTest, methodTestSets, nestedClasses)

Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,25 @@ import org.utbot.framework.plugin.api.UtPrimitiveModel
import org.utbot.framework.plugin.api.UtVoidModel
import org.utbot.framework.plugin.api.isMockModel

typealias TypedModelWrappers = Map<ClassId, Set<UtModelWrapper>>

class SpringTestClassModelBuilder(val context: CgContext): TestClassModelBuilder() {

override fun createTestClassModel(classUnderTest: ClassId, testSets: List<CgMethodTestSet>): SpringTestClassModel {
val baseModel = SimpleTestClassModelBuilder(context).createTestClassModel(classUnderTest, testSets)
val (injectedModels, mockedModels) = collectInjectedAndMockedModels(testSets)
val (thisInstanceModels, dependentMockModels) = collectThisInstanceAndDependentModels(testSets)

return SpringTestClassModel(
classUnderTest = baseModel.classUnderTest,
methodTestSets = baseModel.methodTestSets,
nestedClasses = baseModel.nestedClasses,
injectedMockModels = injectedModels,
mockedModels = mockedModels
thisInstanceModels = thisInstanceModels,
thisInstanceDependentMocks = dependentMockModels
)
}

private fun collectInjectedAndMockedModels(testSets: List<CgMethodTestSet>): Pair<Map<ClassId, Set<UtModelWrapper>>, Map<ClassId, Set<UtModelWrapper>>> {
val thisInstances = mutableSetOf<UtModelWrapper>()
private fun collectThisInstanceAndDependentModels(testSets: List<CgMethodTestSet>): Pair<TypedModelWrappers, TypedModelWrappers> {
val thisInstanceModels = mutableSetOf<UtModelWrapper>()
val thisInstancesDependentModels = mutableSetOf<UtModelWrapper>()

with(context) {
Expand All @@ -46,7 +48,7 @@ class SpringTestClassModelBuilder(val context: CgContext): TestClassModelBuilder
setOf(execution.stateBefore.thisInstance, execution.stateAfter.thisInstance)
.filterNotNull()
.forEach { model ->
thisInstances += model.wrap()
thisInstanceModels += model.wrap()
thisInstancesDependentModels += collectByThisInstanceModel(model)
}
}
Expand All @@ -58,10 +60,10 @@ class SpringTestClassModelBuilder(val context: CgContext): TestClassModelBuilder
val dependentMockModels =
thisInstancesDependentModels
.filterTo(mutableSetOf()) { cgModel ->
cgModel.model.isMockModel() && cgModel !in thisInstances
cgModel.model.isMockModel() && cgModel !in thisInstanceModels
}

return thisInstances.groupByClassId() to dependentMockModels.groupByClassId()
return thisInstanceModels.groupByClassId() to dependentMockModels.groupByClassId()
}

private fun collectByThisInstanceModel(model: UtModel): Set<UtModelWrapper> {
Expand All @@ -71,7 +73,7 @@ class SpringTestClassModelBuilder(val context: CgContext): TestClassModelBuilder
return dependentModels
}

private fun Set<UtModelWrapper>.groupByClassId(): Map<ClassId, Set<UtModelWrapper>> {
private fun Set<UtModelWrapper>.groupByClassId(): TypedModelWrappers {
val classModels = mutableMapOf<ClassId, Set<UtModelWrapper>>()

for (modelGroup in this.groupBy { it.model.classId }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
package org.utbot.framework.codegen
package org.utbot.framework.codegen.generator

import mu.KotlinLogging
import org.utbot.framework.codegen.domain.ForceStaticMocking
import org.utbot.framework.codegen.domain.HangingTestsTimeout
import org.utbot.framework.codegen.domain.ParametrizedTestSource
import org.utbot.framework.codegen.domain.ProjectType
import org.utbot.framework.codegen.domain.ProjectType.*
import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour
import org.utbot.framework.codegen.domain.StaticsMocking
import org.utbot.framework.codegen.domain.TestFramework
import org.utbot.framework.codegen.domain.models.CgMethodTestSet
import org.utbot.framework.codegen.domain.context.CgContext
import org.utbot.framework.codegen.domain.models.CgClassFile
import org.utbot.framework.codegen.domain.models.builders.SimpleTestClassModelBuilder
import org.utbot.framework.codegen.domain.models.builders.SpringTestClassModelBuilder
import org.utbot.framework.codegen.domain.models.CgMethodTestSet
import org.utbot.framework.codegen.renderer.CgAbstractRenderer
import org.utbot.framework.codegen.reports.TestsGenerationReport
import org.utbot.framework.codegen.tree.CgSimpleTestClassConstructor
import org.utbot.framework.codegen.tree.ututils.UtilClassKind
import org.utbot.framework.codegen.services.language.CgLanguageAssistant
import org.utbot.framework.codegen.tree.CgSpringTestClassConstructor
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.CodegenLanguage
import org.utbot.framework.plugin.api.ExecutableId
Expand All @@ -28,9 +21,9 @@ import org.utbot.framework.plugin.api.UtMethodTestSet
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

open class CodeGenerator(
val classUnderTest: ClassId,
val projectType: ProjectType,
abstract class AbstractCodeGenerator(
classUnderTest: ClassId,
projectType: ProjectType,
paramNames: MutableMap<ExecutableId, List<String>> = mutableMapOf(),
generateUtilClassFile: Boolean = false,
testFramework: TestFramework = TestFramework.defaultItem,
Expand All @@ -46,8 +39,7 @@ open class CodeGenerator(
enableTestsTimeout: Boolean = true,
testClassPackageName: String = classUnderTest.packageName,
) {

private val logger = KotlinLogging.logger {}
protected val logger = KotlinLogging.logger {}

open var context: CgContext = CgContext(
classUnderTest = classUnderTest,
Expand Down Expand Up @@ -80,49 +72,14 @@ open class CodeGenerator(
val cgTestSets = testSets.map { CgMethodTestSet(it) }.toList()
return withCustomContext(testClassCustomName) {
context.withTestClassFileScope {
when (context.projectType) {
Spring -> generateForSpringClass(cgTestSets)
else -> generateForSimpleClass(cgTestSets)
}
generate(cgTestSets)
}
}
}

private fun generateForSimpleClass(testSets: List<CgMethodTestSet>): CodeGeneratorResult {
val astConstructor = CgSimpleTestClassConstructor(context)
val testClassModel = SimpleTestClassModelBuilder(context).createTestClassModel(classUnderTest, testSets)

logger.info { "Code generation phase started at ${now()}" }
val testClassFile = astConstructor.construct(testClassModel)
logger.info { "Code generation phase finished at ${now()}" }

val generatedCode = renderToString(testClassFile)

return CodeGeneratorResult(
generatedCode = generatedCode,
utilClassKind = UtilClassKind.fromCgContextOrNull(context),
testsGenerationReport = astConstructor.testsGenerationReport
)
}

private fun generateForSpringClass(testSets: List<CgMethodTestSet>): CodeGeneratorResult {
val astConstructor = CgSpringTestClassConstructor(context)
val testClassModel = SpringTestClassModelBuilder(context).createTestClassModel(classUnderTest, testSets)

logger.info { "Code generation phase started at ${now()}" }
val testClassFile = astConstructor.construct(testClassModel)
logger.info { "Code generation phase finished at ${now()}" }
protected abstract fun generate(testSets: List<CgMethodTestSet>): CodeGeneratorResult

val generatedCode = renderToString(testClassFile)

return CodeGeneratorResult(
generatedCode = generatedCode,
utilClassKind = UtilClassKind.fromCgContextOrNull(context),
testsGenerationReport = astConstructor.testsGenerationReport
)
}

private fun renderToString(testClassFile: CgClassFile): String {
protected fun renderToString(testClassFile: CgClassFile): String {
logger.info { "Rendering phase started at ${now()}" }
val renderer = CgAbstractRenderer.makeRenderer(context)
testClassFile.accept(renderer)
Expand All @@ -131,7 +88,7 @@ open class CodeGenerator(
return renderer.toString()
}

private fun now() = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS"))
protected fun now(): String = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS"))

/**
* Wrapper function that configures context as needed for utbot-online:
Expand All @@ -147,17 +104,4 @@ open class CodeGenerator(
context = prevContext
}
}
}

/**
* @property generatedCode the source code of the test class
* @property testsGenerationReport some info about test generation process
* @property utilClassKind the kind of util class if it is required, otherwise - null
*/
data class CodeGeneratorResult(
val generatedCode: String,
val testsGenerationReport: TestsGenerationReport,
// null if no util class needed, e.g. when we are generating utils directly into test class
val utilClassKind: UtilClassKind? = null,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.utbot.framework.codegen.generator

import org.utbot.framework.codegen.domain.ForceStaticMocking
import org.utbot.framework.codegen.domain.HangingTestsTimeout
import org.utbot.framework.codegen.domain.ParametrizedTestSource
import org.utbot.framework.codegen.domain.ProjectType
import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour
import org.utbot.framework.codegen.domain.StaticsMocking
import org.utbot.framework.codegen.domain.TestFramework
import org.utbot.framework.codegen.domain.models.CgMethodTestSet
import org.utbot.framework.codegen.domain.models.builders.SimpleTestClassModelBuilder
import org.utbot.framework.codegen.services.language.CgLanguageAssistant
import org.utbot.framework.codegen.tree.CgSimpleTestClassConstructor
import org.utbot.framework.codegen.tree.ututils.UtilClassKind
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.CodegenLanguage
import org.utbot.framework.plugin.api.ExecutableId
import org.utbot.framework.plugin.api.MockFramework

open class CodeGenerator(
val classUnderTest: ClassId,
val projectType: ProjectType,
paramNames: MutableMap<ExecutableId, List<String>> = mutableMapOf(),
generateUtilClassFile: Boolean = false,
testFramework: TestFramework = TestFramework.defaultItem,
mockFramework: MockFramework = MockFramework.defaultItem,
staticsMocking: StaticsMocking = StaticsMocking.defaultItem,
forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem,
generateWarningsForStaticMocking: Boolean = true,
codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem,
cgLanguageAssistant: CgLanguageAssistant = CgLanguageAssistant.getByCodegenLanguage(codegenLanguage),
parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem,
runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem,
hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(),
enableTestsTimeout: Boolean = true,
testClassPackageName: String = classUnderTest.packageName,
): AbstractCodeGenerator(
classUnderTest,
projectType,
paramNames,
generateUtilClassFile,
testFramework,
mockFramework,
staticsMocking,
forceStaticMocking,
generateWarningsForStaticMocking,
codegenLanguage,
cgLanguageAssistant,
parameterizedTestSource,
runtimeExceptionTestsBehaviour,
hangingTestsTimeout,
enableTestsTimeout,
testClassPackageName,
) {

override fun generate(testSets: List<CgMethodTestSet>): CodeGeneratorResult {
val testClassModel = SimpleTestClassModelBuilder(context).createTestClassModel(classUnderTest, testSets)

logger.info { "Code generation phase started at ${now()}" }
val astConstructor = CgSimpleTestClassConstructor(context)
val testClassFile = astConstructor.construct(testClassModel)
logger.info { "Code generation phase finished at ${now()}" }

val generatedCode = renderToString(testClassFile)

return CodeGeneratorResult(
generatedCode = generatedCode,
utilClassKind = UtilClassKind.fromCgContextOrNull(context),
testsGenerationReport = astConstructor.testsGenerationReport,
)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.utbot.framework.codegen.generator

import org.utbot.framework.codegen.reports.TestsGenerationReport
import org.utbot.framework.codegen.tree.ututils.UtilClassKind

/**
* @property generatedCode the source code of the test class
* @property testsGenerationReport some info about test generation process
* @property utilClassKind the kind of util class if it is required, otherwise - null
*/
data class CodeGeneratorResult(
val generatedCode: String,
val testsGenerationReport: TestsGenerationReport,
// null if no util class needed, e.g. when we are generating utils directly into test class
val utilClassKind: UtilClassKind? = null,
)
Loading