Skip to content

Commit 928d6ad

Browse files
authored
Add support for nullable fields in deep equals (#768)
1 parent b18f94d commit 928d6ad

File tree

7 files changed

+205
-20
lines changed

7 files changed

+205
-20
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,12 @@ sealed class UtReferenceModel(
282282
) : UtModel(classId)
283283

284284
/**
285-
* Checks if [UtModel] is a null.
285+
* Checks if [UtModel] is a [UtNullModel].
286286
*/
287287
fun UtModel.isNull() = this is UtNullModel
288288

289289
/**
290-
* Checks if [UtModel] is not a null.
290+
* Checks if [UtModel] is not a [UtNullModel].
291291
*/
292292
fun UtModel.isNotNull() = !isNull()
293293

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class CodeGenerator(
4040
testFramework = testFramework,
4141
mockFramework = mockFramework ?: MockFramework.MOCKITO,
4242
codegenLanguage = codegenLanguage,
43-
parameterizedTestSource = parameterizedTestSource,
43+
parametrizedTestSource = parameterizedTestSource,
4444
staticsMocking = staticsMocking,
4545
forceStaticMocking = forceStaticMocking,
4646
generateWarningsForStaticMocking = generateWarningsForStaticMocking,

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ internal interface CgContextOwner {
139139

140140
val codegenLanguage: CodegenLanguage
141141

142-
val parameterizedTestSource: ParametrizedTestSource
142+
val parametrizedTestSource: ParametrizedTestSource
143143

144144
// flag indicating whether a mock framework is used in the generated code
145145
var mockFrameworkUsed: Boolean
@@ -214,6 +214,8 @@ internal interface CgContextOwner {
214214

215215
var statesCache: EnvironmentFieldStateCache
216216

217+
var allExecutions: List<UtExecution>
218+
217219
fun block(init: () -> Unit): Block {
218220
val prevBlock = currentBlock
219221
return try {
@@ -407,7 +409,7 @@ internal data class CgContext(
407409
override val forceStaticMocking: ForceStaticMocking,
408410
override val generateWarningsForStaticMocking: Boolean,
409411
override val codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem,
410-
override val parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.DO_NOT_PARAMETRIZE,
412+
override val parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.DO_NOT_PARAMETRIZE,
411413
override var mockFrameworkUsed: Boolean = false,
412414
override var currentBlock: PersistentList<CgStatement> = persistentListOf(),
413415
override var existingVariableNames: PersistentSet<String> = persistentSetOf(),
@@ -427,6 +429,7 @@ internal data class CgContext(
427429
) : CgContextOwner {
428430
override lateinit var statesCache: EnvironmentFieldStateCache
429431
override lateinit var actual: CgVariable
432+
override lateinit var allExecutions: List<UtExecution>
430433

431434
/**
432435
* This property cannot be accessed outside of test class file scope

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

Lines changed: 171 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import org.utbot.framework.codegen.model.tree.CgExecutableCall
3737
import org.utbot.framework.codegen.model.tree.CgExpression
3838
import org.utbot.framework.codegen.model.tree.CgFieldAccess
3939
import org.utbot.framework.codegen.model.tree.CgGetJavaClass
40-
import org.utbot.framework.codegen.model.tree.CgIsInstance
4140
import org.utbot.framework.codegen.model.tree.CgLiteral
4241
import org.utbot.framework.codegen.model.tree.CgMethod
4342
import org.utbot.framework.codegen.model.tree.CgMethodCall
@@ -111,9 +110,10 @@ import org.utbot.framework.plugin.api.UtStaticMethodInstrumentation
111110
import org.utbot.framework.plugin.api.UtSymbolicExecution
112111
import org.utbot.framework.plugin.api.UtTimeoutException
113112
import org.utbot.framework.plugin.api.UtVoidModel
113+
import org.utbot.framework.plugin.api.isNotNull
114+
import org.utbot.framework.plugin.api.isNull
114115
import org.utbot.framework.plugin.api.onFailure
115116
import org.utbot.framework.plugin.api.onSuccess
116-
import org.utbot.framework.plugin.api.util.booleanClassId
117117
import org.utbot.framework.plugin.api.util.doubleArrayClassId
118118
import org.utbot.framework.plugin.api.util.doubleClassId
119119
import org.utbot.framework.plugin.api.util.doubleWrapperClassId
@@ -144,7 +144,6 @@ import org.utbot.summary.SummarySentenceConstants.TAB
144144
import java.lang.reflect.InvocationTargetException
145145
import java.security.AccessControlException
146146
import java.lang.reflect.ParameterizedType
147-
import kotlin.reflect.jvm.javaType
148147

149148
private const val DEEP_EQUALS_MAX_DEPTH = 5 // TODO move it to plugin settings?
150149

@@ -168,6 +167,8 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
168167

169168
private lateinit var methodType: CgTestMethodType
170169

170+
private val fieldsOfExecutionResults = mutableMapOf<Pair<FieldId, Int>, MutableList<UtModel>>()
171+
171172
private fun setupInstrumentation() {
172173
if (currentExecution is UtSymbolicExecution) {
173174
val execution = currentExecution as UtSymbolicExecution
@@ -445,7 +446,6 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
445446
val expectedExpression = CgNotNullAssertion(expectedVariable)
446447

447448
assertEquality(expectedExpression, actual)
448-
println()
449449
}
450450
}
451451
.onFailure { thisInstance[method](*methodArguments.toTypedArray()).intercepted() }
@@ -537,7 +537,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
537537
doubleDelta
538538
)
539539
expectedModel.value is Boolean -> {
540-
when (parameterizedTestSource) {
540+
when (parametrizedTestSource) {
541541
ParametrizedTestSource.DO_NOT_PARAMETRIZE ->
542542
if (expectedModel.value as Boolean) {
543543
assertions[assertTrue](actual)
@@ -842,6 +842,25 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
842842
return
843843
}
844844

845+
when (parametrizedTestSource) {
846+
ParametrizedTestSource.DO_NOT_PARAMETRIZE -> {
847+
traverseField(fieldId, fieldModel, expected, actual, depth, visitedModels)
848+
}
849+
850+
ParametrizedTestSource.PARAMETRIZE -> {
851+
traverseFieldForParametrizedTest(fieldId, fieldModel, expected, actual, depth, visitedModels)
852+
}
853+
}
854+
}
855+
856+
private fun traverseField(
857+
fieldId: FieldId,
858+
fieldModel: UtModel,
859+
expected: CgVariable,
860+
actual: CgVariable,
861+
depth: Int,
862+
visitedModels: MutableSet<UtModel>
863+
) {
845864
// fieldModel is not visited and will be marked in assertDeepEquals call
846865
val fieldName = fieldId.name
847866
var expectedVariable: CgVariable? = null
@@ -866,6 +885,140 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
866885
emptyLineIfNeeded()
867886
}
868887

888+
private fun traverseFieldForParametrizedTest(
889+
fieldId: FieldId,
890+
fieldModel: UtModel,
891+
expected: CgVariable,
892+
actual: CgVariable,
893+
depth: Int,
894+
visitedModels: MutableSet<UtModel>
895+
) {
896+
val fieldResultModels = fieldsOfExecutionResults[fieldId to depth]
897+
val nullResultModelInExecutions = fieldResultModels?.find { it.isNull() }
898+
val notNullResultModelInExecutions = fieldResultModels?.find { it.isNotNull() }
899+
900+
val hasNullResultModel = nullResultModelInExecutions != null
901+
val hasNotNullResultModel = notNullResultModelInExecutions != null
902+
903+
val needToSubstituteFieldModel = fieldModel is UtNullModel && hasNotNullResultModel
904+
905+
val fieldModelForAssert = if (needToSubstituteFieldModel) notNullResultModelInExecutions!! else fieldModel
906+
907+
// fieldModel is not visited and will be marked in assertDeepEquals call
908+
val fieldName = fieldId.name
909+
var expectedVariable: CgVariable? = null
910+
911+
val needExpectedDeclaration = needExpectedDeclaration(fieldModelForAssert)
912+
if (needExpectedDeclaration) {
913+
val expectedFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, expected, fieldName)
914+
915+
currentBlock += expectedFieldDeclaration
916+
expectedVariable = expectedFieldDeclaration.variable
917+
}
918+
919+
val actualFieldDeclaration = createDeclarationForFieldFromVariable(fieldId, actual, fieldName)
920+
currentBlock += actualFieldDeclaration
921+
922+
if (needExpectedDeclaration && hasNullResultModel) {
923+
ifStatement(
924+
CgEqualTo(expectedVariable!!, nullLiteral()),
925+
trueBranch = { +testFrameworkManager.assertions[testFramework.assertNull](actualFieldDeclaration.variable).toStatement() },
926+
falseBranch = {
927+
assertDeepEquals(
928+
fieldModelForAssert,
929+
expectedVariable,
930+
actualFieldDeclaration.variable,
931+
depth + 1,
932+
visitedModels,
933+
)
934+
}
935+
)
936+
} else {
937+
assertDeepEquals(
938+
fieldModelForAssert,
939+
expectedVariable,
940+
actualFieldDeclaration.variable,
941+
depth + 1,
942+
visitedModels,
943+
)
944+
}
945+
emptyLineIfNeeded()
946+
}
947+
948+
private fun collectExecutionsResultFields() {
949+
val successfulExecutionsModels = allExecutions
950+
.filter {
951+
it.result is UtExecutionSuccess
952+
}.map {
953+
(it.result as UtExecutionSuccess).model
954+
}
955+
956+
for (model in successfulExecutionsModels) {
957+
when (model) {
958+
is UtCompositeModel -> {
959+
for ((fieldId, fieldModel) in model.fields) {
960+
collectExecutionsResultFieldsRecursively(fieldId, fieldModel, 0)
961+
}
962+
}
963+
964+
is UtAssembleModel -> {
965+
model.origin?.let {
966+
for ((fieldId, fieldModel) in it.fields) {
967+
collectExecutionsResultFieldsRecursively(fieldId, fieldModel, 0)
968+
}
969+
}
970+
}
971+
972+
is UtNullModel,
973+
is UtPrimitiveModel,
974+
is UtArrayModel,
975+
is UtClassRefModel,
976+
is UtEnumConstantModel,
977+
is UtVoidModel -> {
978+
// only [UtCompositeModel] and [UtAssembleModel] have fields to traverse
979+
}
980+
}
981+
}
982+
}
983+
984+
private fun collectExecutionsResultFieldsRecursively(
985+
fieldId: FieldId,
986+
fieldModel: UtModel,
987+
depth: Int,
988+
) {
989+
if (depth >= DEEP_EQUALS_MAX_DEPTH) {
990+
return
991+
}
992+
993+
val fieldKey = fieldId to depth
994+
fieldsOfExecutionResults.getOrPut(fieldKey) { mutableListOf() } += fieldModel
995+
996+
when (fieldModel) {
997+
is UtCompositeModel -> {
998+
for ((id, model) in fieldModel.fields) {
999+
collectExecutionsResultFieldsRecursively(id, model, depth + 1)
1000+
}
1001+
}
1002+
1003+
is UtAssembleModel -> {
1004+
fieldModel.origin?.let {
1005+
for ((id, model) in it.fields) {
1006+
collectExecutionsResultFieldsRecursively(id, model, depth + 1)
1007+
}
1008+
}
1009+
}
1010+
1011+
is UtNullModel,
1012+
is UtPrimitiveModel,
1013+
is UtArrayModel,
1014+
is UtClassRefModel,
1015+
is UtEnumConstantModel,
1016+
is UtVoidModel -> {
1017+
// only [UtCompositeModel] and [UtAssembleModel] have fields to traverse
1018+
}
1019+
}
1020+
}
1021+
8691022
@Suppress("UNUSED_ANONYMOUS_PARAMETER")
8701023
private fun createDeclarationForFieldFromVariable(
8711024
fieldId: FieldId,
@@ -999,7 +1152,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
9991152
}
10001153
expected == nullLiteral() -> testFrameworkManager.assertNull(actual)
10011154
expected is CgLiteral && expected.value is Boolean -> {
1002-
when (parameterizedTestSource) {
1155+
when (parametrizedTestSource) {
10031156
ParametrizedTestSource.DO_NOT_PARAMETRIZE ->
10041157
testFrameworkManager.assertBoolean(expected.value, actual)
10051158
ParametrizedTestSource.PARAMETRIZE ->
@@ -1054,15 +1207,19 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c
10541207
expected: CgValue,
10551208
actual: CgVariable,
10561209
) {
1057-
when (parameterizedTestSource) {
1210+
when (parametrizedTestSource) {
10581211
ParametrizedTestSource.DO_NOT_PARAMETRIZE -> generateDeepEqualsAssertion(expected, actual)
1059-
ParametrizedTestSource.PARAMETRIZE -> when {
1060-
actual.type.isPrimitive -> generateDeepEqualsAssertion(expected, actual)
1061-
else -> ifStatement(
1062-
CgEqualTo(expected, nullLiteral()),
1063-
trueBranch = { +testFrameworkManager.assertions[testFramework.assertNull](actual).toStatement() },
1064-
falseBranch = { generateDeepEqualsAssertion(expected, actual) }
1065-
)
1212+
ParametrizedTestSource.PARAMETRIZE -> {
1213+
collectExecutionsResultFields()
1214+
1215+
when {
1216+
actual.type.isPrimitive -> generateDeepEqualsAssertion(expected, actual)
1217+
else -> ifStatement(
1218+
CgEqualTo(expected, nullLiteral()),
1219+
trueBranch = { +testFrameworkManager.assertions[testFramework.assertNull](actual).toStatement() },
1220+
falseBranch = { generateDeepEqualsAssertion(expected, actual) }
1221+
)
1222+
}
10661223
}
10671224
}
10681225
}

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,13 @@ internal class CgTestClassConstructor(val context: CgContext) :
118118
return null
119119
}
120120

121+
allExecutions = testSet.executions
122+
121123
val (methodUnderTest, _, _, clustersInfo) = testSet
122124
val regions = mutableListOf<CgRegion<CgMethod>>()
123125
val requiredFields = mutableListOf<CgParameterDeclaration>()
124126

125-
when (context.parameterizedTestSource) {
127+
when (context.parametrizedTestSource) {
126128
ParametrizedTestSource.DO_NOT_PARAMETRIZE -> {
127129
for ((clusterSummary, executionIndices) in clustersInfo) {
128130
val currentTestCaseTestMethods = mutableListOf<CgTestMethod>()

utbot-framework/src/test/kotlin/org/utbot/examples/codegen/deepequals/ClassWithNullableFieldTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,13 @@ class ClassWithNullableFieldTest : UtValueTestCaseChecker(
2323
coverage = DoNotCalculate
2424
)
2525
}
26+
27+
@Test
28+
fun testClassWithNullableField1() {
29+
check(
30+
ClassWithNullableField::returnGreatCompoundWithNullableField,
31+
eq(3),
32+
coverage = DoNotCalculate
33+
)
34+
}
2635
}

utbot-sample/src/main/java/org/utbot/examples/codegen/deepequals/ClassWithNullableField.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,23 @@ class Compound {
1212
}
1313
}
1414

15+
class GreatCompound {
16+
Compound compound;
17+
18+
GreatCompound(Compound compound) {
19+
this.compound = compound;
20+
}
21+
}
22+
1523
public class ClassWithNullableField {
1624
public Compound returnCompoundWithNullableField(int value) {
1725
if (value > 0) return new Compound(null);
1826
else return new Compound(new Component());
1927
}
28+
29+
public GreatCompound returnGreatCompoundWithNullableField(int value) {
30+
if (value > 0) return new GreatCompound(null);
31+
else if (value == 0) return new GreatCompound(new Compound(new Component()));
32+
else return new GreatCompound(new Compound(null));
33+
}
2034
}

0 commit comments

Comments
 (0)