Skip to content

Commit 2472df8

Browse files
committed
add getMergedFlakyBody
1 parent 67e0032 commit 2472df8

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -843,15 +843,15 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() {
843843
lines.append("JsonConvert.DeserializeObject(await $responseVariableName.Content.ReadAsStringAsync());")
844844
}
845845

846-
handleJsonStringAssertion(bodyString, res.getFlakyBody(), lines, bodyVarName, res.getTooLargeBody())
846+
handleJsonStringAssertion(bodyString, res.getFlakyBodies()?.let { res.getMergedFlakyBody() }, lines, bodyVarName, res.getTooLargeBody())
847847

848848
} else if (type.isCompatible(MediaType.TEXT_PLAIN_TYPE)) {
849849

850850
if (format.isCsharp()) {
851851
lines.append("await $responseVariableName.Content.ReadAsStringAsync();")
852852
}
853853

854-
handleTextPlainTextAssertion(bodyString, res.getFlakyBody(), lines, bodyVarName)
854+
handleTextPlainTextAssertion(bodyString, res.getFlakyBodies()?.let { res.getMergedFlakyBody() }, lines, bodyVarName)
855855
} else {
856856
if (format.isCsharp()) {
857857
lines.append("await $responseVariableName.Content.ReadAsStringAsync();")

core/src/main/kotlin/org/evomaster/core/problem/httpws/HttpWsCallResult.kt

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package org.evomaster.core.problem.httpws
22

3+
import com.fasterxml.jackson.core.JsonProcessingException
4+
import com.fasterxml.jackson.databind.JsonNode
5+
import com.fasterxml.jackson.databind.ObjectMapper
6+
import com.fasterxml.jackson.databind.node.ArrayNode
7+
import com.fasterxml.jackson.databind.node.ObjectNode
38
import com.google.common.annotations.VisibleForTesting
49
import com.google.gson.Gson
510
import com.google.gson.JsonSyntaxException
@@ -18,6 +23,8 @@ abstract class HttpWsCallResult : EnterpriseActionResult {
1823
}
1924

2025
companion object {
26+
private val mapper = ObjectMapper()
27+
2128
const val STATUS_CODE = "STATUS_CODE"
2229
const val BODY = "BODY"
2330
const val BODY_TYPE = "BODY_TYPE"
@@ -389,6 +396,92 @@ abstract class HttpWsCallResult : EnterpriseActionResult {
389396
fun getFlakyBodies() : List<String>? = getFlakyValues(ResponseField.BODY).filterNotNull().ifEmpty { null }
390397
fun containFlakyBody(flakyBody: String) : Boolean = getFlakyBodies()?.contains(flakyBody) ?: false
391398

399+
/**
400+
* Merge all observed flaky body values into one representative body.
401+
*
402+
* For JSON objects and arrays, the original body is used as the baseline and each
403+
* observed flaky body contributes only the fields or elements that differ from it.
404+
* This allows different flaky fields observed in different executions to be handled
405+
* together when generating assertions.
406+
*
407+
* If there is no flaky body, the original body is returned. If the body is not
408+
* mergeable as JSON, the first observed flaky body is returned.
409+
*/
410+
fun getMergedFlakyBody() : String{
411+
val originalBody = getBody()
412+
val flakyBodies = getFlakyBodies()
413+
414+
if (flakyBodies.isNullOrEmpty()) {
415+
return originalBody ?: ""
416+
}
417+
418+
if (originalBody == null) {
419+
return flakyBodies.first()
420+
}
421+
422+
return try {
423+
val originalJson = mapper.readTree(originalBody)
424+
if (!originalJson.isObject && !originalJson.isArray) {
425+
return flakyBodies.first()
426+
}
427+
428+
flakyBodies
429+
.map { mapper.readTree(it) }
430+
.fold(originalJson.deepCopy<JsonNode>()) { merged, observed ->
431+
mergeJsonDiffFromOriginal(originalJson, observed, merged)
432+
}
433+
.let { mapper.writeValueAsString(it) }
434+
} catch (e: JsonProcessingException) {
435+
flakyBodies.first()
436+
} catch (e: IllegalStateException) {
437+
flakyBodies.first()
438+
}
439+
}
440+
441+
private fun mergeJsonDiffFromOriginal(
442+
original: JsonNode,
443+
observed: JsonNode,
444+
merged: JsonNode
445+
): JsonNode {
446+
if (original == observed) {
447+
return merged
448+
}
449+
450+
if (original is ObjectNode && observed is ObjectNode && merged is ObjectNode) {
451+
val observedFieldNames = observed.fieldNames().asSequence().toSet()
452+
453+
original.fieldNames().asSequence()
454+
.filter { !observedFieldNames.contains(it) }
455+
.forEach { merged.remove(it) }
456+
457+
observed.fields().asSequence().forEach { (field, observedValue) ->
458+
val originalValue = original.get(field)
459+
if (originalValue == null) {
460+
merged.set<JsonNode>(field, observedValue.deepCopy<JsonNode>())
461+
} else {
462+
val mergedValue = merged.get(field) ?: originalValue.deepCopy<JsonNode>()
463+
merged.set<JsonNode>(field, mergeJsonDiffFromOriginal(originalValue, observedValue, mergedValue))
464+
}
465+
}
466+
467+
return merged
468+
}
469+
470+
if (original is ArrayNode && observed is ArrayNode && merged is ArrayNode) {
471+
if (original.size() != observed.size()) {
472+
return observed.deepCopy<JsonNode>()
473+
}
474+
475+
for (i in 0 until observed.size()) {
476+
merged.set(i, mergeJsonDiffFromOriginal(original[i], observed[i], merged[i]))
477+
}
478+
479+
return merged
480+
}
481+
482+
return observed.deepCopy<JsonNode>()
483+
}
484+
392485
fun setFlakyBodyType(type: MediaType) = addFlakyDifference(ResponseField.BODY_TYPE, type.toString())
393486
fun getFlakyBodyType() : MediaType? = getFirstFlakyValue(ResponseField.BODY_TYPE)?.let { MediaType.valueOf(it) }
394487

0 commit comments

Comments
 (0)