Skip to content

Add statics support in parametrized tests generation #612 #672 #671

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 1 commit into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package org.utbot.framework.codegen.model.constructor

import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.ExecutableId
import org.utbot.framework.plugin.api.FieldId
import org.utbot.framework.plugin.api.UtClusterInfo
import org.utbot.framework.plugin.api.UtExecution
import org.utbot.framework.plugin.api.UtExecutionSuccess
import org.utbot.framework.plugin.api.UtMethodTestSet
import org.utbot.framework.plugin.api.util.executableId
import org.utbot.framework.plugin.api.util.objectClassId
import org.utbot.framework.plugin.api.util.voidClassId
import soot.jimple.JimpleBody

data class CgMethodTestSet private constructor(
Expand Down Expand Up @@ -44,22 +46,46 @@ data class CgMethodTestSet private constructor(
return executionsByResult.map{ (_, executions) -> substituteExecutions(executions) }
}

/**
* Splits [CgMethodTestSet] test sets by affected static fields statics.
*
* A separate test set is created for each combination of modified statics.
*/
fun splitExecutionsByChangedStatics(): List<CgMethodTestSet> {
val successfulExecutions = executions.filter { it.result is UtExecutionSuccess }
val executionsByStaticsUsage: Map<Set<FieldId>, List<UtExecution>> =
if (successfulExecutions.isNotEmpty()) {
successfulExecutions.groupBy {
it.stateBefore.statics.keys
}
} else {
mapOf(executions.first().stateBefore.statics.keys to executions)
}

return executionsByStaticsUsage.map { (_, executions) -> substituteExecutions(executions) }
}

/**
* Finds a [ClassId] of all result models in executions.
*
* Tries to find an unique result type in testSets or
* Tries to find a unique result type in testSets or
* gets executable return type.
*/
fun resultType(): ClassId {
val successfulExecutions = executions.filter { it.result is UtExecutionSuccess }
return if (successfulExecutions.isNotEmpty()) {
successfulExecutions
.map { (it.result as UtExecutionSuccess).model.classId }
.distinct()
.singleOrNull()
?: executableId.returnType
} else {
executableId.returnType
return when (executableId.returnType) {
voidClassId -> executableId.returnType
else -> {
val successfulExecutions = executions.filter { it.result is UtExecutionSuccess }
if (successfulExecutions.isNotEmpty()) {
successfulExecutions
.map { (it.result as UtExecutionSuccess).model.classId }
.distinct()
.singleOrNull()
?: executableId.returnType
} else {
executableId.returnType
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ internal interface CgNameGenerator {
/**
* Generate a variable name given a [base] name.
* @param isMock denotes whether a variable represents a mock object or not
* @param isStatic denotes whether a variable represents a static variable or not
*/
fun variableName(base: String, isMock: Boolean = false): String
fun variableName(base: String, isMock: Boolean = false, isStatic: Boolean = false): String

/**
* Convert a given class id to a string that can serve
Expand Down Expand Up @@ -69,8 +70,12 @@ internal interface CgNameGenerator {
internal class CgNameGeneratorImpl(private val context: CgContext)
: CgNameGenerator, CgContextOwner by context {

override fun variableName(base: String, isMock: Boolean): String {
val baseName = if (isMock) base + "Mock" else base
override fun variableName(base: String, isMock: Boolean, isStatic: Boolean): String {
val baseName = when {
isMock -> base + "Mock"
isStatic -> base + "Static"
else -> base
}
return when {
baseName in existingVariableNames -> nextIndexedVarName(baseName)
isLanguageKeyword(baseName, codegenLanguage) -> createNameFromKeyword(baseName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
* Thus, this method only caches an actual initial static fields state in order to recover it
* at the end of the test, and it has nothing to do with the 'before' and 'after' caches.
*/
private fun rememberInitialStaticFields() {
for ((field, _) in currentExecution!!.stateBefore.statics.accessibleFields()) {
private fun rememberInitialStaticFields(statics: Map<FieldId, UtModel>) {
val accessibleStaticFields = statics.accessibleFields()
for ((field, _) in accessibleStaticFields) {
val declaringClass = field.declaringClass
val fieldAccessible = field.isAccessibleFrom(testClassPackageName)

Expand All @@ -236,11 +237,18 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
}
}

private fun mockStaticFields() {
for ((field, model) in currentExecution!!.stateBefore.statics.accessibleFields()) {
private fun substituteStaticFields(statics: Map<FieldId, UtModel>, isParametrized: Boolean = false) {
val accessibleStaticFields = statics.accessibleFields()
for ((field, model) in accessibleStaticFields) {
val declaringClass = field.declaringClass
val fieldAccessible = field.canBeSetIn(testClassPackageName)
val fieldValue = variableConstructor.getOrCreateVariable(model, field.name)

val fieldValue = if (isParametrized) {
currentMethodParameters[CgParameterKind.Statics(model)]
} else {
variableConstructor.getOrCreateVariable(model, field.name)
}

if (fieldAccessible) {
declaringClass[field] `=` fieldValue
} else {
Expand Down Expand Up @@ -1097,12 +1105,13 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
// TODO: remove this line when SAT-1273 is completed
execution.displayName = execution.displayName?.let { "${executableId.name}: $it" }
testMethod(testMethodName, execution.displayName) {
rememberInitialStaticFields()
val statics = currentExecution!!.stateBefore.statics
rememberInitialStaticFields(statics)
val stateAnalyzer = ExecutionStateAnalyzer(execution)
val modificationInfo = stateAnalyzer.findModifiedFields()
// TODO: move such methods to another class and leave only 2 public methods: remember initial and final states
val mainBody = {
mockStaticFields()
substituteStaticFields(statics)
setupInstrumentation()
// build this instance
thisInstance = execution.stateBefore.thisInstance?.let {
Expand All @@ -1120,7 +1129,6 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
generateFieldStateAssertions()
}

val statics = currentExecution!!.stateBefore.statics
if (statics.isNotEmpty()) {
+tryBlock {
mainBody()
Expand Down Expand Up @@ -1177,11 +1185,14 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
.firstOrNull { it.result is UtExecutionSuccess && (it.result as UtExecutionSuccess).model !is UtNullModel }
?: testSet.executions.first()

val statics = genericExecution.stateBefore.statics

return withTestMethodScope(genericExecution) {
val testName = nameGenerator.parameterizedTestMethodName(dataProviderMethodName)
withNameScope {
val testParameterDeclarations = createParameterDeclarations(testSet, genericExecution)
val mainBody = {
substituteStaticFields(statics, isParametrized = true)
// build this instance
thisInstance = genericExecution.stateBefore.thisInstance?.let { currentMethodParameters[CgParameterKind.ThisInstance] }

Expand All @@ -1203,16 +1214,30 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
parameterized = true,
dataProviderMethodName
) {
if (containsFailureExecution(testSet)) {
+tryBlock(mainBody)
.catch(Throwable::class.java.id) { e ->
rememberInitialStaticFields(statics)

if (containsFailureExecution(testSet) || statics.isNotEmpty()) {
var currentTryBlock = tryBlock {
mainBody()
}

if (containsFailureExecution(testSet)) {
currentTryBlock = currentTryBlock.catch(Throwable::class.java.id) { e ->
val pseudoExceptionVarName = when (codegenLanguage) {
CodegenLanguage.JAVA -> "${expectedErrorVarName}.isInstance(${e.name.decapitalize()})"
CodegenLanguage.KOTLIN -> "${expectedErrorVarName}!!.isInstance(${e.name.decapitalize()})"
}

testFrameworkManager.assertBoolean(CgVariable(pseudoExceptionVarName, booleanClassId))
}
}

if (statics.isNotEmpty()) {
currentTryBlock = currentTryBlock.finally {
recoverStaticFields()
}
}
+currentTryBlock
} else {
mainBody()
}
Expand Down Expand Up @@ -1265,6 +1290,22 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
currentMethodParameters[CgParameterKind.Argument(index)] = argument.parameter
}

val statics = genericExecution.stateBefore.statics
if (statics.isNotEmpty()) {
for ((fieldId, model) in statics) {
val staticType = wrapTypeIfRequired(model.classId)
val static = CgParameterDeclaration(
parameter = declareParameter(
type = staticType,
name = nameGenerator.variableName(fieldId.name, isStatic = true)
),
isReferenceType = staticType.isRefType
)
this += static
currentMethodParameters[CgParameterKind.Statics(model)] = static.parameter
}
}

val expectedResultClassId = wrapTypeIfRequired(testSet.resultType())
if (expectedResultClassId != voidClassId) {
val wrappedType = wrapIfPrimitive(expectedResultClassId)
Expand Down Expand Up @@ -1347,6 +1388,12 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
arguments += variableConstructor.getOrCreateVariable(paramModel, argumentName)
}

val statics = execution.stateBefore.statics
for ((field, model) in statics) {
arguments += variableConstructor.getOrCreateVariable(model, field.name)
}


val method = currentExecutable!!
val needsReturnValue = method.returnType != voidClassId
val containsFailureExecution = containsFailureExecution(testSet)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,15 @@ internal class CgTestClassConstructor(val context: CgContext) :
}
}
ParametrizedTestSource.PARAMETRIZE -> {
for (currentTestSet in testSet.splitExecutionsByResult()) {
createParametrizedTestAndDataProvider(currentTestSet, requiredFields, regions, methodUnderTest)
for (splitByExecutionTestSet in testSet.splitExecutionsByResult()) {
for (splitByChangedStaticsTestSet in splitByExecutionTestSet.splitExecutionsByChangedStatics()) {
createParametrizedTestAndDataProvider(
splitByChangedStaticsTestSet,
requiredFields,
regions,
methodUnderTest
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.utbot.framework.plugin.api.ExecutableId
import org.utbot.framework.plugin.api.FieldId
import org.utbot.framework.plugin.api.MethodId
import org.utbot.framework.plugin.api.TypeParameters
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.util.booleanClassId
import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.intClassId
Expand Down Expand Up @@ -630,6 +631,7 @@ data class CgParameterDeclaration(
sealed class CgParameterKind {
object ThisInstance : CgParameterKind()
data class Argument(val index: Int) : CgParameterKind()
data class Statics(val model: UtModel) : CgParameterKind()
object ExpectedResult : CgParameterKind()
object ExpectedException : CgParameterKind()
}
Expand Down