11package 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
38import com.google.common.annotations.VisibleForTesting
49import com.google.gson.Gson
510import 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