From 473da27cfd7bdfcd36e72c900b25aa252f96bc6a Mon Sep 17 00:00:00 2001 From: Romihime Date: Mon, 17 Nov 2025 21:22:30 -0300 Subject: [PATCH 01/10] New branch_xml due to changes from master --- .../rest/builder/RestActionBuilderV3.kt | 57 +++- .../evomaster/core/search/gene/ObjectGene.kt | 106 ++++++- .../search/gene/ObjectWithAttributesGene.kt | 125 ++++++++ .../core/search/gene/GeneSamplerForTests.kt | 32 ++ .../core/search/gene/ObjectGeneTest.kt | 29 ++ .../gene/uri/ObjectWithAttributesGeneTest.kt | 300 ++++++++++++++++++ 6 files changed, 633 insertions(+), 16 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index 83a2d73833..775f9b0491 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -703,7 +703,7 @@ object RestActionBuilderV3 { body } - val name = "body" + var name = "body" val description = operation.description ?: null val bodies = resolvedBody.content?.filter { @@ -742,6 +742,11 @@ object RestActionBuilderV3 { listOf() } + val deref = obj.schema.`$ref`?.let { ref -> val name = ref.substringAfterLast("/") + SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema + + name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" + var gene = getGene("body", obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) @@ -938,7 +943,38 @@ object RestActionBuilderV3 { } "object" -> { - return createObjectGene(name, schema, schemaHolder,currentSchema, history, referenceClassDef, options, examples, messages) + val properties = schema.properties ?: emptyMap() + + val attributeNames = properties + .filterValues { it.xml?.attribute == true } + .keys + + if (attributeNames.isNotEmpty()) { + val fields = properties.map { (propName, propSchema) -> + getGene( + propName, + propSchema, + schemaHolder, + currentSchema, + history, + referenceClassDef, + options, + false, + examples, + messages + ) + } + + return ObjectWithAttributesGene( + name = schema.xml?.name ?: name, + fixedFields = fields, + refType = referenceClassDef, + isFixed = true, + template = null, + additionalFields = mutableListOf(), + attributeNames = attributeNames + ) + } } //TODO file is a hack. I want to find a more elegant way of dealing with it (BMR) //FIXME is this even a standard type??? @@ -1096,6 +1132,23 @@ object RestActionBuilderV3 { valueTemplate.copy()) } + val attributeNames = schema.properties + ?.filter { (_, propSchema) -> propSchema.xml?.attribute == true } + ?.map { it.key } + ?: emptyList() + + if (attributeNames.isNotEmpty()) { + return ObjectWithAttributesGene( + name = name, + fixedFields = fields, + refType = if (schema is ObjectSchema) referenceTypeName ?: schema.title else null, + isFixed = false, + template = additionalFieldTemplate, + additionalFields = mutableListOf(), + attributeNames = attributeNames.toSet() + ) + } + return assembleObjectGeneWithConstraints( name, schema, diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt index 693a826647..ebe5b3b570 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt @@ -5,9 +5,13 @@ import org.evomaster.core.Lazy import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.OutputFormat import org.evomaster.core.problem.graphql.GqlConst +import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.collection.TupleGene +import org.evomaster.core.search.gene.numeric.DoubleGene +import org.evomaster.core.search.gene.numeric.FloatGene +import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.wrapper.FlexibleGene import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.search.gene.placeholder.CycleObjectGene @@ -37,7 +41,7 @@ import java.net.URLEncoder * - type: string * - type: integer */ -class ObjectGene( +open class ObjectGene( name: String, val fixedFields: List, val refType: String? = null, @@ -341,22 +345,96 @@ class ObjectGene( } else if (mode == GeneUtils.EscapeMode.XML) { - // TODO might have to handle here: - /* - Note: this is a very basic support, which should not really depend - much on. Problem is that we would need to access to the XSD schema - to decide when fields should be represented with tags or attributes - */ + fun escapeXmlSafe(s: String): String = + s.replace(Regex("(?", ">") + .replace("\"", """) + .replace("'", "'") + + fun singularize(n: String) = when { + n.endsWith("s") && n.length > 1 -> n.removeSuffix("s") + else -> n + }.replaceFirstChar { it.uppercase() } + + fun unwrap(v: Any?): Any? = when (v) { + is OptionalGene -> v.gene + else -> v + } + + fun cleanXmlValueString(v: String): String = + v.removeSurrounding("\"").let(::escapeXmlSafe) + + fun getPrintedValue(v: Gene): String = + cleanXmlValueString(v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat)) + + fun isPrimitiveGene(value: Any?): Boolean = when (unwrap(value)) { + is StringGene, is BooleanGene, is IntegerGene, is DoubleGene, is FloatGene, + is String, is Number, is Boolean -> true + else -> false + } + + fun serializeXml(name: String, value: Any?): String { + + if (name == "#text") { + return when (val v = unwrap(value)) { + is Gene -> getPrintedValue(v) + null -> "" + else -> escapeXmlSafe(v.toString()) + } + } + + val v = unwrap(value) ?: return "<$name>" + + return when (v) { + is ObjectWithAttributesGene -> { + v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) + } + + is ObjectGene -> { + val inner = v.fields.joinToString("") { f -> + serializeXml(f.name, unwrap(f)) + } + "<$name>$inner" + } + + is Collection<*> -> v.joinToString("", "<$name>", "") { + val itemName = singularize(name) + serializeXml(itemName, it) + } + + is Map<*, *> -> v.entries.joinToString("", "<$name>", "") { + serializeXml(it.key.toString(), it.value) + } + + is ArrayGene<*> -> { + val itemName = singularize(name) + v.getViewOfElements().joinToString("", "<$name>", "") { + serializeXml(itemName, it) + } + } + + is Gene -> "<$name>${getPrintedValue(v)}" + + else -> "<$name>${cleanXmlValueString(v.toString())}" + } + } + + val inner = includedFields.joinToString("") { f -> + serializeXml(f.name, unwrap(f)) + } + + val singleField = includedFields.singleOrNull() + val inlinePrimitive = singleField?.let { isPrimitiveGene(unwrap(it)) } == true - buffer.append(openXml(name)) - includedFields.forEach { - //FIXME put back, but then update all broken tests - //buffer.append(openXml(it.name)) - buffer.append(it.getValueAsPrintableString(previousGenes, mode, targetFormat)) - //buffer.append(closeXml(it.name)) + val xmlPayload = if (inlinePrimitive) { + val childValue = getPrintedValue(unwrap(singleField) as Gene) + "<$name>$childValue" + } else { + "<$name>$inner" } - buffer.append(closeXml(name)) + buffer.append(xmlPayload) } else if (mode == GeneUtils.EscapeMode.X_WWW_FORM_URLENCODED) { buffer.append(includedFields.map { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt new file mode 100644 index 0000000000..2ff79e1df5 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt @@ -0,0 +1,125 @@ +package org.evomaster.core.search.gene + +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.search.gene.collection.PairGene +import org.evomaster.core.search.gene.placeholder.CycleObjectGene +import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.gene.utils.GeneUtils +import org.evomaster.core.search.gene.wrapper.OptionalGene + +class ObjectWithAttributesGene( + name: String, + fixedFields: List, + refType: String? = null, + isFixed: Boolean, + template: PairGene? = null, + additionalFields: MutableList>? = null, + val attributeNames: Set = emptySet() +) : ObjectGene(name, fixedFields, refType, isFixed, template, additionalFields) { + + constructor(name: String, fields: List, refType: String? = null) : this( + name, fixedFields = fields, refType = refType, isFixed = true, template = null, additionalFields = null, attributeNames = emptySet() + ) + + override fun copyContent(): Gene { + val copiedAdditional = additionalFields + ?.map { it.copy() } + ?.filterIsInstance>() + ?.toMutableList() + + return ObjectWithAttributesGene( + name, + fixedFields.map { it.copy() }, + refType, + isFixed, + template, + copiedAdditional, + attributeNames.toMutableSet() + ) + } + + override fun getValueAsPrintableString( + previousGenes: List, + mode: GeneUtils.EscapeMode?, + targetFormat: OutputFormat?, + extraCheck: Boolean + ): String { + + if (mode != GeneUtils.EscapeMode.XML) { + return super.getValueAsPrintableString(previousGenes, mode, targetFormat, extraCheck) + } + + val includedFields = fixedFields + .filter { it !is CycleObjectGene } + .filter { it !is OptionalGene || (it.isActive && it.gene !is CycleObjectGene) } + .filter { it.isPrintable() } + + val attributeFields = includedFields.filter { attributeNames.contains(it.name) } + val childFields = includedFields.filter { !attributeNames.contains(it.name) } + + // 1) "#text" CANNOT be an attribute + if (attributeFields.any { it.name == "#text" }) { + throw IllegalStateException("#text cannot be used as an attribute in XML") + } + + // 2) Child names must be unique (XML does not allow repeated element names at this level) + val duplicated = childFields + .groupBy { it.name } + .filter { it.value.size > 1 } + .keys + + if (duplicated.isNotEmpty()) { + throw IllegalStateException("Duplicate child elements not allowed in XML: $duplicated") + } + + fun xmlEscape(s: String): String = + s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + + fun printAttribute(field: Gene): String { + val raw = field.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) + val clean = raw.removeSurrounding("\"") + return "${field.name}=\"$clean\"" + } + + val attributesString = attributeFields.joinToString(" ") { printAttribute(it) } + val sb = StringBuilder() + + if (attributesString.isEmpty()) { //No childs + sb.append("<$name>") + } else { + sb.append("<$name $attributesString>") + } + + for (child in childFields) { //Childs + + val childXml = child.getValueAsPrintableString( + previousGenes, + GeneUtils.EscapeMode.XML, + targetFormat + ) + + val isInlineValue = child.name == "#text" && !(child is ObjectWithAttributesGene) + + if (isInlineValue) { + sb.append(childXml) + continue + } + + if (child is ObjectWithAttributesGene || child is ObjectGene) { + sb.append(childXml) + continue + } + + sb.append("<${child.name}>") + sb.append(childXml) + sb.append("") + } + + sb.append("") + return sb.toString() + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index 2d1d276c76..11406e2d7d 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -133,6 +133,7 @@ object GeneSamplerForTests { PatternCharacterBlockGene::class -> samplePatternCharacterBlock(rand) as T QuantifierRxGene::class -> sampleQuantifierRxGene(rand) as T RegexGene::class -> sampleRegexGene(rand) as T + ObjectWithAttributesGene::class -> sampleObjectGeneWithAttributes(rand) as T //SQL genes SqlJSONPathGene::class -> sampleSqlJSONPathGene(rand) as T @@ -904,4 +905,35 @@ object GeneSamplerForTests { } } + fun sampleObjectGeneWithAttributes(rand: Randomness): ObjectWithAttributesGene { + + val selection = geneClasses.filter { !it.isAbstract } + val isFixed = rand.nextBoolean() + + return if (isFixed) { + ObjectWithAttributesGene( + name = "rand ObjectGeneWithAttributes ${rand.nextInt()}", + fields = listOf( + sample(rand.choose(selection), rand), + sample(rand.choose(selection), rand), + sample(rand.choose(selection), rand) + ) + ) + }else{ + ObjectWithAttributesGene( + name = "rand ObjectGeneWithAttributes ${rand.nextInt()}", + fixedFields = listOf( + sample(rand.choose(selection), rand), + sample(rand.choose(selection), rand), + sample(rand.choose(selection), rand) + ), + refType = null, + isFixed = isFixed, + template = PairGene("template", sampleStringGene(rand), samplePrintableTemplate(selection, rand)), + additionalFields = mutableListOf() + ) + } + } + + } diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt index 14150feeac..a914cf171d 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt @@ -101,4 +101,33 @@ internal class ObjectGeneTest { assertEquals("{foo,bar,nested{hello}}", actual) } + + @Test + fun testValueAsContent() { + + val root = ObjectGene( + name = "device", + listOf( + StringGene("#text", "XPhone"), + ObjectGene( + name = "location", + listOf( + StringGene("country", "AR"), + ObjectGene( + name = "gps", + listOf( + IntegerGene("#text", 12), + IntegerGene("lon", 34) + ) + ) + ) + ) + ) + ) + + val actual = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = + "XPhoneAR1234" + assertEquals(expected, actual) + } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt new file mode 100644 index 0000000000..41241e25e3 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt @@ -0,0 +1,300 @@ +package org.evomaster.core.search.gene.xml + +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.search.gene.ObjectWithAttributesGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.evomaster.core.search.gene.utils.GeneUtils + +class ObjectWithAttributesGeneTest { + + + @Test + fun testXmlPrintWithAttributesAndValue() { + + val person = ObjectWithAttributesGene( + name = "parent", + fixedFields = listOf( + StringGene("attrib1", value = "true"), + ObjectWithAttributesGene( + name = "child1", + fixedFields = listOf( + StringGene("attrib2", value = "-1"), + StringGene("attrib3", value = "bar"), + IntegerGene("#text", value = 42) + ), + isFixed = true, + attributeNames = setOf("attrib2","attrib3") + ), + StringGene("child2", value = "foo"), + ), + isFixed = true, + attributeNames = setOf("attrib1") + ) + val actual = person.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "42foo" + assertEquals(expected, actual) + } + + @Test + fun testXmlEmptyObject() { + + val obj = ObjectWithAttributesGene( + name = "empty", + fixedFields = emptyList(), + isFixed = true, + attributeNames = emptySet() + ) + + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "" + + assertEquals(expected, actual) + } + + @Test + fun testXmlEmptyAttributeValue() { + + val obj = ObjectWithAttributesGene( + name = "person", + fixedFields = listOf(StringGene("id", value = "")), + isFixed = true, + attributeNames = setOf("id") + ) + + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "" + + assertEquals(expected, actual) + } + + @Test + fun testXmlNullAttributeValue() { + + val obj = ObjectWithAttributesGene( + name = "item", + fixedFields = listOf(IntegerGene("code", value = null)), + isFixed = true, + attributeNames = setOf("code") + ) + + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "" + + assertEquals(expected, actual) + } + + @Test + fun testXmlEscaping() { + + val obj = ObjectWithAttributesGene( + name = "x", + fixedFields = listOf( + StringGene("attr", "\"<>&'"), + StringGene("#text", "\"<>&'") + ), + isFixed = true, + attributeNames = setOf("attr") + ) + + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = ""<>&'" + + assertEquals(expected, actual) + } + + @Test + fun testValueAsTextOnly() { + + val obj = ObjectWithAttributesGene( + name = "item", + fixedFields = listOf( + IntegerGene("#text", value = 42) + ), + isFixed = true, + attributeNames = emptySet() + ) + + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "42" + + assertEquals(expected, actual) + } + + @Test + fun testValueBooleanAsText() { + + val obj = ObjectWithAttributesGene( + name = "flag", + fixedFields = listOf( + BooleanGene("#text", false) + ), + isFixed = true + ) + + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "false" + + assertEquals(expected, actual) + } + + @Test + fun testValueEmptyString() { + + val obj = ObjectWithAttributesGene( + name = "node", + fixedFields = listOf( + StringGene("#text", "") + ), + isFixed = true + ) + + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "" + + assertEquals(expected, actual) + } + + @Test + fun testDeepMixedNesting() { + + val root = ObjectWithAttributesGene( + name = "root", + fixedFields = listOf( + StringGene("id", "root1"), + ObjectGene( + name = "device", + listOf( + StringGene("model", "XPhone"), + ObjectWithAttributesGene( + name = "location", + fixedFields = listOf( + StringGene("country", "AR"), + ObjectGene( + name = "gps", + listOf( + IntegerGene("lat", 12), + IntegerGene("lon", 34) + ) + ) + ), + isFixed = true, + attributeNames = setOf("country") + ) + ) + ) + ), + isFixed = true, + attributeNames = setOf("id") + ) + val actual = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = + "XPhone1234" + assertEquals(expected, actual) + } + + @Test + fun testDeepMixedNestingStartingOG() { + + val root = ObjectGene( + name = "device", + listOf( + StringGene("model", "XPhone"), + ObjectWithAttributesGene( + name = "location", + fixedFields = listOf( + StringGene("country", "AR"), + ObjectGene( + name = "gps", + listOf( + IntegerGene("lat", 12), + IntegerGene("lon", 34) + ) + ) + ), + isFixed = true, + attributeNames = setOf("country") + ) + ) + ) + + val actual = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = + "XPhone1234" + assertEquals(expected, actual) + } + + //tests from ObjectGene + @Test + fun testBooleanSelectionBase(){ + + val foo = StringGene("foo") + val bar = IntegerGene("bar") + val gene = ObjectWithAttributesGene("parent", listOf(foo, bar)) + + val selection = GeneUtils.getBooleanSelection(gene) + + val actual = selection.getValueAsPrintableString(mode = GeneUtils.EscapeMode.BOOLEAN_SELECTION_MODE) + + assertEquals("{foo,bar}", actual) + } + + @Test + fun testBooleanSelectionNested(){ + + val foo = StringGene("foo") + val bar = IntegerGene("bar") + val hello = StringGene("hello") + val nested = ObjectWithAttributesGene("nested", listOf(hello)) + val gene = ObjectWithAttributesGene("parent", listOf(foo, bar, nested)) + + val selection = GeneUtils.getBooleanSelection(gene) + + val actual = selection.getValueAsPrintableString(mode = GeneUtils.EscapeMode.BOOLEAN_SELECTION_MODE) + + assertEquals("{foo,bar,nested{hello}}", actual) + } + + @Test + fun testTextCannotBeAttribute() { + + val ex = org.junit.jupiter.api.assertThrows { + + ObjectWithAttributesGene( + name = "node", + fixedFields = listOf( + StringGene("#text", "value") + ), + isFixed = true, + attributeNames = setOf("#text") // ilegal + ).getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + } + + assertEquals("#text cannot be used as an attribute in XML", ex.message) + } + + @Test + fun testDuplicateChildNameThrowsException() { + + val ex = org.junit.jupiter.api.assertThrows { + + ObjectWithAttributesGene( + name = "node", + fixedFields = listOf( + StringGene("child", "a"), + IntegerGene("child", 123) // duplicado + ), + isFixed = true, + attributeNames = emptySet() + ).getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + } + + assertEquals( + "Duplicate child elements not allowed in XML: [child]", + ex.message + ) + } +} \ No newline at end of file From 4456f717908549bf03450b885817b651f9651f9d Mon Sep 17 00:00:00 2001 From: Romihime Date: Wed, 19 Nov 2025 19:48:08 -0300 Subject: [PATCH 02/10] Update HttpWsTestCaseWriter.kt --- .../core/output/service/HttpWsTestCaseWriter.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt index 9100678e8c..d0e152e483 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt @@ -529,8 +529,21 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { } else -> lines.add(".$send(\"$body\")") } + } else if (bodyParam.isXml()) { + + val xml = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML, targetFormat = format) + + when { + + format.isCsharp() -> { + lines.append("new StringContent($xml, Encoding.UTF8, \"${bodyParam.contentType()}\")") + } + format.isPython() -> { + lines.add("body = $xml") + } + else -> lines.add(".$send($xml)") + } } else { - //TODO XML LoggingUtil.uniqueWarn(log, "Unhandled type for body payload: " + bodyParam.contentType()) } } From 4b4bb0baf8d111c2770ca918aa57d54ecd862d29 Mon Sep 17 00:00:00 2001 From: Romihime Date: Wed, 19 Nov 2025 20:03:21 -0300 Subject: [PATCH 03/10] Fix restActionBuilderV3 --- .../evomaster/core/problem/rest/builder/RestActionBuilderV3.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index 775f9b0491..4521e4d66b 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -974,6 +974,8 @@ object RestActionBuilderV3 { additionalFields = mutableListOf(), attributeNames = attributeNames ) + }else{ + return createObjectGene(name, schema, schemaHolder,currentSchema, history, referenceClassDef, options, examples, messages) } } //TODO file is a hack. I want to find a more elegant way of dealing with it (BMR) From 664b7e4e1070b8948dc540291625de31f1fa47da Mon Sep 17 00:00:00 2001 From: Romihime Date: Wed, 19 Nov 2025 22:16:21 -0300 Subject: [PATCH 04/10] Fix cuantity of genes --- .../org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt index 298f5ed60b..c2caf6ce40 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt @@ -13,7 +13,7 @@ class GeneNumberOfGenesTest : AbstractGeneTest() { This number should not change, unless you explicitly add/remove any gene. if so, update this number accordingly */ - assertEquals(87, geneClasses.size) + assertEquals(88, geneClasses.size) } } From 20863bf190cb48a70ea890c431198149e83b251b Mon Sep 17 00:00:00 2001 From: Romihime Date: Mon, 24 Nov 2025 22:01:42 -0300 Subject: [PATCH 05/10] Changes from pr --- .../rest/builder/RestActionBuilderV3.kt | 5 +- .../evomaster/core/search/gene/ObjectGene.kt | 173 ++++++++++-------- .../search/gene/ObjectWithAttributesGene.kt | 33 ++-- .../core/search/gene/ObjectGeneTest.kt | 9 +- .../gene/uri/ObjectWithAttributesGeneTest.kt | 28 ++- 5 files changed, 149 insertions(+), 99 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index 4521e4d66b..d47357f1f8 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -703,7 +703,6 @@ object RestActionBuilderV3 { body } - var name = "body" val description = operation.description ?: null val bodies = resolvedBody.content?.filter { @@ -745,9 +744,9 @@ object RestActionBuilderV3 { val deref = obj.schema.`$ref`?.let { ref -> val name = ref.substringAfterLast("/") SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema - name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" + val name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" - var gene = getGene("body", obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) + var gene = getGene(name, obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) if (resolvedBody.required != true && gene !is OptionalGene) { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt index ebe5b3b570..9460c1fbc1 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt @@ -82,6 +82,7 @@ open class ObjectGene( private const val PROB_MODIFY_SIZE_ADDITIONAL_FIELDS = 0.1 // the default maximum size for additional fields private const val MAX_SIZE_ADDITIONAL_FIELDS = 5 + const val contentXMLTag = "#text" private val mapper = ObjectMapper() } @@ -310,6 +311,99 @@ open class ObjectGene( return mode == null || mode == GeneUtils.EscapeMode.JSON || mode == GeneUtils.EscapeMode.TEXT } + private fun escapeXmlSafe(s: String): String = + s.replace(Regex("(?", ">") + .replace("\"", """) + .replace("'", "'") + + private fun singularize(n: String): String = + when { + n.endsWith("s") && n.length > 1 -> n.removeSuffix("s") + else -> n + }.replaceFirstChar { it.uppercase() } + + private fun unwrap(v: Any?): Any? = + when (v) { + is OptionalGene -> v.gene + else -> v + } + + public fun cleanXmlValueString(v: String): String = + v.removeSurrounding("\"").let(::escapeXmlSafe) + + private fun getPrintedValue( + previousGenes: List, + v: Gene, + targetFormat: OutputFormat? + ): String = + cleanXmlValueString( + v.getValueAsPrintableString( + previousGenes, + GeneUtils.EscapeMode.XML, + targetFormat + ) + ) + + private fun isPrimitiveGene(value: Any?): Boolean = + when (unwrap(value)) { + is StringGene, is BooleanGene, is IntegerGene, is DoubleGene, is FloatGene, + is String, is Number, is Boolean -> true + else -> false + } + + private fun serializeXml( + previousGenes: List, + name: String, + value: Any?, + targetFormat: OutputFormat? + ): String { + + if (name == contentXMLTag) { + return when (val v = unwrap(value)) { + is Gene -> getPrintedValue(previousGenes, v, targetFormat) + null -> "" + else -> escapeXmlSafe(v.toString()) + } + } + + val v = unwrap(value) ?: return "<$name>" + + return when (v) { + + is ObjectWithAttributesGene -> { + v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) + } + + is ObjectGene -> { + val inner = v.fields.joinToString("") { f -> + serializeXml(previousGenes, f.name, unwrap(f), targetFormat) + } + "<$name>$inner" + } + + is Collection<*> -> v.joinToString("", "<$name>", "") { + val itemName = singularize(name) + serializeXml(previousGenes, itemName, it, targetFormat) + } + + is Map<*, *> -> v.entries.joinToString("", "<$name>", "") { + serializeXml(previousGenes, it.key.toString(), it.value, targetFormat) + } + + is ArrayGene<*> -> { + val itemName = singularize(name) + v.getViewOfElements().joinToString("", "<$name>", "") { + serializeXml(previousGenes, itemName, it, targetFormat) + } + } + + is Gene -> "<$name>${getPrintedValue(previousGenes, v, targetFormat)}" + + else -> "<$name>${cleanXmlValueString(v.toString())}" + } + } override fun getValueAsPrintableString(previousGenes: List, mode: GeneUtils.EscapeMode?, targetFormat: OutputFormat?, extraCheck: Boolean): String { @@ -345,90 +439,15 @@ open class ObjectGene( } else if (mode == GeneUtils.EscapeMode.XML) { - fun escapeXmlSafe(s: String): String = - s.replace(Regex("(?", ">") - .replace("\"", """) - .replace("'", "'") - - fun singularize(n: String) = when { - n.endsWith("s") && n.length > 1 -> n.removeSuffix("s") - else -> n - }.replaceFirstChar { it.uppercase() } - - fun unwrap(v: Any?): Any? = when (v) { - is OptionalGene -> v.gene - else -> v - } - - fun cleanXmlValueString(v: String): String = - v.removeSurrounding("\"").let(::escapeXmlSafe) - - fun getPrintedValue(v: Gene): String = - cleanXmlValueString(v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat)) - - fun isPrimitiveGene(value: Any?): Boolean = when (unwrap(value)) { - is StringGene, is BooleanGene, is IntegerGene, is DoubleGene, is FloatGene, - is String, is Number, is Boolean -> true - else -> false - } - - fun serializeXml(name: String, value: Any?): String { - - if (name == "#text") { - return when (val v = unwrap(value)) { - is Gene -> getPrintedValue(v) - null -> "" - else -> escapeXmlSafe(v.toString()) - } - } - - val v = unwrap(value) ?: return "<$name>" - - return when (v) { - is ObjectWithAttributesGene -> { - v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) - } - - is ObjectGene -> { - val inner = v.fields.joinToString("") { f -> - serializeXml(f.name, unwrap(f)) - } - "<$name>$inner" - } - - is Collection<*> -> v.joinToString("", "<$name>", "") { - val itemName = singularize(name) - serializeXml(itemName, it) - } - - is Map<*, *> -> v.entries.joinToString("", "<$name>", "") { - serializeXml(it.key.toString(), it.value) - } - - is ArrayGene<*> -> { - val itemName = singularize(name) - v.getViewOfElements().joinToString("", "<$name>", "") { - serializeXml(itemName, it) - } - } - - is Gene -> "<$name>${getPrintedValue(v)}" - - else -> "<$name>${cleanXmlValueString(v.toString())}" - } - } - val inner = includedFields.joinToString("") { f -> - serializeXml(f.name, unwrap(f)) + serializeXml(previousGenes, f.name, unwrap(f), targetFormat) } val singleField = includedFields.singleOrNull() val inlinePrimitive = singleField?.let { isPrimitiveGene(unwrap(it)) } == true val xmlPayload = if (inlinePrimitive) { - val childValue = getPrintedValue(unwrap(singleField) as Gene) + val childValue = getPrintedValue(previousGenes, unwrap(singleField) as Gene, targetFormat) "<$name>$childValue" } else { "<$name>$inner" diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt index 2ff79e1df5..1cb76618e6 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt @@ -38,6 +38,22 @@ class ObjectWithAttributesGene( ) } + + private fun printAttribute( + previousGenes: List, + targetFormat: OutputFormat?, + field: Gene + ): String { + val raw = field.getValueAsPrintableString( + previousGenes, + GeneUtils.EscapeMode.XML, + targetFormat + ) + + val clean = cleanXmlValueString(raw) + return "${field.name}=\"$clean\"" + } + override fun getValueAsPrintableString( previousGenes: List, mode: GeneUtils.EscapeMode?, @@ -72,20 +88,7 @@ class ObjectWithAttributesGene( throw IllegalStateException("Duplicate child elements not allowed in XML: $duplicated") } - fun xmlEscape(s: String): String = - s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - - fun printAttribute(field: Gene): String { - val raw = field.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) - val clean = raw.removeSurrounding("\"") - return "${field.name}=\"$clean\"" - } - - val attributesString = attributeFields.joinToString(" ") { printAttribute(it) } + val attributesString = attributeFields.joinToString(" ") { printAttribute(previousGenes, targetFormat, it) } val sb = StringBuilder() if (attributesString.isEmpty()) { //No childs @@ -102,7 +105,7 @@ class ObjectWithAttributesGene( targetFormat ) - val isInlineValue = child.name == "#text" && !(child is ObjectWithAttributesGene) + val isInlineValue = child.name == contentXMLTag && !(child is ObjectWithAttributesGene) if (isInlineValue) { sb.append(childXml) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt index a914cf171d..aa1448b7d1 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt @@ -127,7 +127,14 @@ internal class ObjectGeneTest { val actual = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = - "XPhoneAR1234" + "XPhone" + + "" + + "AR" + + "12" + + "34" + + "" + + "" + + "" assertEquals(expected, actual) } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt index 41241e25e3..fdc4120fb3 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt @@ -35,7 +35,11 @@ class ObjectWithAttributesGeneTest { attributeNames = setOf("attrib1") ) val actual = person.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) - val expected = "42foo" + val expected = + "" + + "42" + + "foo" + + "" assertEquals(expected, actual) } @@ -192,7 +196,17 @@ class ObjectWithAttributesGeneTest { ) val actual = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = - "XPhone1234" + "" + + "" + + "XPhone" + + "" + + "" + + "12" + + "34" + + "" + + "" + + "" + + "" assertEquals(expected, actual) } @@ -223,7 +237,15 @@ class ObjectWithAttributesGeneTest { val actual = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = - "XPhone1234" + "" + + "XPhone" + + "" + + "" + + "12" + + "34" + + "" + + "" + + "" assertEquals(expected, actual) } From df3f5de64aba7cf795984284190002fbb7e6eb49 Mon Sep 17 00:00:00 2001 From: Romihime Date: Tue, 25 Nov 2025 07:57:08 -0300 Subject: [PATCH 06/10] Fix --- .../evomaster/core/problem/rest/builder/RestActionBuilderV3.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index d47357f1f8..598532f51c 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -746,8 +746,7 @@ object RestActionBuilderV3 { val name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" - var gene = getGene(name, obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) - + var gene = getGene("body", obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) if (resolvedBody.required != true && gene !is OptionalGene) { gene = OptionalGene(name, gene) From 5617fa1e9288eca0826dbf83861c98170d1b17ea Mon Sep 17 00:00:00 2001 From: Romihime Date: Tue, 25 Nov 2025 19:17:37 -0300 Subject: [PATCH 07/10] Try --- .../evomaster/core/problem/rest/builder/RestActionBuilderV3.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index 598532f51c..3c6bde3bb4 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -746,7 +746,7 @@ object RestActionBuilderV3 { val name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" - var gene = getGene("body", obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) + var gene = getGene(name, obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) if (resolvedBody.required != true && gene !is OptionalGene) { gene = OptionalGene(name, gene) From 994f40e50dbc97550f9174f9753e6c42ecc41dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Mon, 2 Feb 2026 23:44:30 -0300 Subject: [PATCH 08/10] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 41050913bbed8b462dd6c794bf4b3e8dcbf55055 Merge: a2aac9d010 8fcd38035e Author: ROMINA JULIETA SUÁREZ Date: Mon Feb 2 21:32:01 2026 -0300 Merge branch 'master' into master_temp commit a2aac9d01036b3f21bd03acd456bfd44cb7425ef Author: ROMINA JULIETA SUÁREZ Date: Sun Feb 1 21:49:29 2026 -0300 Fix tests commit c2b146108415210f48919e4d9ece1660bd33c8d6 Author: ROMINA JULIETA SUÁREZ Date: Sun Feb 1 21:32:42 2026 -0300 Fixes for pr commit 1b4d1c71e3877bb20219421f047e08d1edfc17fe Merge: cc9d1497a2 7d0333f865 Author: ROMINA JULIETA SUÁREZ Date: Sun Feb 1 17:07:14 2026 -0300 Merge branch 'master' into master_temp commit cc9d1497a2b8a80fe855544d98075c8a656f39a6 Author: ROMINA JULIETA SUÁREZ Date: Thu Jan 29 19:45:12 2026 -0300 Try fixing Genes commit ee978c9d6bbfc36c49cb97bde3160c628162b214 Author: ROMINA JULIETA SUÁREZ Date: Thu Jan 29 19:39:34 2026 -0300 Try excluding problematic genes commit 875653e90f321181437c0b247ec12d8589fa4b65 Merge: fac65b47c9 39de778970 Author: ROMINA JULIETA SUÁREZ Date: Thu Jan 29 00:22:31 2026 -0300 Merge branch 'master' into master_temp commit fac65b47c905cd5cacde9d1e4fc2b2b5da705d8c Author: ROMINA JULIETA SUÁREZ Date: Wed Jan 28 23:05:46 2026 -0300 whitebox test passed commit 437766c3aae3f68c424b4603bfbea57fab8c4d78 Author: ROMINA JULIETA SUÁREZ Date: Wed Jan 28 21:16:01 2026 -0300 Fix whitebox commit a6c96668187753f5f7a11c0fcbad7cab75f4e9e2 Author: ROMINA JULIETA SUÁREZ Date: Tue Jan 27 21:47:53 2026 -0300 Change for items_item commit ccaee1a7263eb1b2da21376b6ca4bc41e6891855 Author: ROMINA JULIETA SUÁREZ Date: Tue Jan 27 00:15:10 2026 -0300 fix the faults tests for bbxml commit 2169cd9eabd5830008ebab3e0b68aa3120e130bb Merge: 4f717531bc 1354ec1283 Author: ROMINA JULIETA SUÁREZ Date: Mon Jan 26 18:29:57 2026 -0300 Merge branch 'master' into master_temp commit 4f717531bc477b986dd7a6b71e8ea144a2434c60 Author: ROMINA JULIETA SUÁREZ Date: Mon Jan 26 00:03:45 2026 -0300 Fixes for bbxmltest commit 62b2ff9ade04c4068455cf27512602928efa0d4a Author: ROMINA JULIETA SUÁREZ Date: Sun Jan 25 23:01:11 2026 -0300 try to log the error commit b931cfd9e1039d7a9b2edfc9d3612445103a708a Author: ROMINA JULIETA SUÁREZ Date: Sun Jan 25 19:58:03 2026 -0300 try again commit 550fb33e114c896019fa016485f4c1c514b1dae2 Author: ROMINA JULIETA SUÁREZ Date: Sun Jan 25 17:32:17 2026 -0300 try catch the error commit 29daedca90ea6418f5d54a47ab5cdd3ffc998700 Author: ROMINA JULIETA SUÁREZ Date: Sun Jan 25 10:15:51 2026 -0300 try only containssamevalueas commit 9f7c9676a64ecf7c67b377e256653766fd8d538a Author: ROMINA JULIETA SUÁREZ Date: Sun Jan 25 02:34:42 2026 -0300 try without those functions commit ca6674c70927f9ce97d4f489ac56791a7e266684 Merge: c4e495ecd1 e3eca4a6d9 Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 24 11:27:12 2026 -0300 Merge branch 'master' into master_temp commit c4e495ecd14727d4b6edfff77f45c881faa74b84 Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 24 00:09:29 2026 -0300 try 2nd version commit 8b3c79d5d90a6bb0f1e61dfc7c3d6587cd25d643 Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 24 00:07:16 2026 -0300 try this version commit 3b8441d6d5186187cd77143458ba7fc854d4c1a6 Author: ROMINA JULIETA SUÁREZ Date: Fri Jan 23 19:25:44 2026 -0300 try commit 74e95c5de9829b7a57070b3f74b13bd0d375f782 Author: ROMINA JULIETA SUÁREZ Date: Fri Jan 23 00:35:51 2026 -0300 try commit 1f9adef08d81369f41f01bcce2a2a0eeb25bda60 Author: ROMINA JULIETA SUÁREZ Date: Wed Jan 21 22:55:18 2026 -0300 try ci for sampler commit 2baac85b6aec4eae3bb5ed179994907435b3d561 Author: ROMINA JULIETA SUÁREZ Date: Mon Jan 19 23:19:42 2026 -0300 fix _items not child commit 516e713f12e836043525d90b5b9db626335f63a6 Author: ROMINA JULIETA SUÁREZ Date: Sun Jan 18 23:33:21 2026 -0300 Quitamos el ref, dejamos el _items por el test de stacktrace commit ccde334a673b4c29a17b51308a2cad4c90ec4b69 Author: ROMINA JULIETA SUÁREZ Date: Sun Jan 18 00:54:21 2026 -0300 update reference in restAction for _items commit a62edfa0ce3c9724f8673a3696d6ca0f26d57a98 Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 17 23:18:30 2026 -0300 Update sampler commit 804d9ebc5dacc05bb40fab5e66008ea53b945077 Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 17 20:30:40 2026 -0300 Update GeneSamplerForTests.kt commit 363835090f5dbbbc1f3b1cce46fff119f95c3025 Merge: e4ec6181f7 319e8398cc Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 17 19:11:16 2026 -0300 Merge branch 'master' into master_temp commit e4ec6181f71a3dca1f49ec7f1853adc7d91881ad Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 17 18:49:45 2026 -0300 update tests commit 4cfe9d63134b660c6c26e7737df083caf1eacdc2 Author: ROMINA JULIETA SUÁREZ Date: Sat Jan 17 13:45:57 2026 -0300 some changes commit 2f677b519cfcba853f9957ed60d07af672e02311 Merge: ba340f1539 af2b540833 Author: ROMINA JULIETA SUÁREZ Date: Fri Jan 16 10:53:17 2026 -0300 Merge branch 'master' into master_temp commit ba340f1539a40b5c1abea874e8b3bd2345281378 Author: ROMINA JULIETA SUÁREZ Date: Fri Jan 16 10:53:05 2026 -0300 Update RestActionBuilderV3.kt commit db4c99f4c561261eaed9bf5ff4678893d2028295 Author: ROMINA JULIETA SUÁREZ Date: Fri Jan 16 00:57:28 2026 -0300 try ci commit 98f04c64ede570317bfe9b1b03bf98e3fb20e894 Author: ROMINA JULIETA SUÁREZ Date: Fri Jan 16 00:24:57 2026 -0300 Quito validacion que ya realiza objectGene commit 55b5c6c67fbf61c2862a1a05fece3de30fa1f8b1 Author: ROMINA JULIETA SUÁREZ Date: Wed Jan 14 23:57:23 2026 -0300 Change in white test and sut commit 5c150c48cb074664d82ec4cc269f4bac72ea3b17 Merge: 5096e0a112 0869cd7008 Author: ROMINA JULIETA SUÁREZ Date: Wed Jan 14 21:03:25 2026 -0300 Merge branch 'master' into master_temp commit 5096e0a11257b779c2cdd01855323d4b67f9763d Author: ROMINA JULIETA SUÁREZ Date: Tue Jan 13 22:57:23 2026 -0300 Try ci without white box test commit 76fa3c79048f867639e45ea7547032954818b5a9 Author: ROMINA JULIETA SUÁREZ Date: Mon Jan 12 20:41:29 2026 -0300 Try changes from rest and objectGene commit 13b3acf8844d22d8565a406f8cfd6c164ce05812 Author: Romihime Date: Wed Jan 7 00:04:11 2026 -0300 Try CI again commit b606ef4209123ff6c45cf19e78f244bcaefa3e33 Merge: 2204d25a5f 6364863f65 Author: Romihime Date: Tue Jan 6 23:30:20 2026 -0300 Merge branch 'develop_xml2' into master_temp commit 2204d25a5f2c63af66859700e314898446142b17 Merge: 78fbc427f7 5617fa1e92 Author: Romihime Date: Tue Jan 6 23:28:55 2026 -0300 Merge branch 'master' of https://github.com/suarezrominajulieta/EvoMaster commit 6364863f653da9758e074e09c194faff526bbe07 Author: Romihime Date: Mon Jan 5 23:36:29 2026 -0300 Add whitebox simple test commit 750380ed40a4f662cab80b35d811f6d5c7e7a29c Author: Romihime Date: Sun Jan 4 17:30:48 2026 -0300 try ci commit 2b533dd8461875c89574d1a1150529aa69d6e9cb Author: Romihime Date: Thu Jan 1 12:26:11 2026 -0300 Fix: this is not a valid test commit 6b5485160fb1b68fc28b80d918187eabe6a44514 Author: Romihime Date: Thu Jan 1 11:53:52 2026 -0300 Add test to ArrayGene and check if it prints ok with xml mode commit 0f1894af715b597e88eb42fa54aa778cf1fa5104 Author: Romihime Date: Mon Dec 29 23:59:54 2025 -0300 Fixed the application, only missing the [ fix on arrays commit 72ca9e335d5ddb10c90b72e78c6f5a51de8ac8dc Author: Romihime Date: Mon Dec 29 00:25:27 2025 -0300 We need name otherwise it is body commit 170dd039d222e05a6d2c0cdb0fdbb8b22c69828a Author: Romihime Date: Sun Dec 28 23:47:44 2025 -0300 Move objectWithAttributesGeneTest and fix logs commit 18ad2c98cab2462ce393d984a603780fbd9bb816 Author: Romihime Date: Sun Dec 28 18:19:45 2025 -0300 Seven out of nine comments commit fbb63782bea7773d823056383b5db20332f76438 Author: Romihime Date: Sun Dec 28 00:01:49 2025 -0300 try ci commit 5110c21fc4510ea8ef59e87ec6d3789d4e7c34b7 Author: Romihime Date: Sun Dec 21 21:30:24 2025 -0300 try commit e502b991644b2492575aa1df04d5e4581bb9073f Author: Romihime Date: Mon Dec 15 20:06:05 2025 -0300 Somefixes commit 78fbc427f7de6639e48bb2c57e7c671cf864cacd Merge: 4a89841aaf 9204b7780c Author: Juan Pablo Galeotti Date: Wed Nov 26 09:52:54 2025 -0300 Merge pull request #2 from suarezrominajulieta/develop_xml2 support for XML attributes using ObjectGeneWithAttributes commit 9204b7780cf9a36ba36fa65655672297559a95db Merge: fb8565e4f0 4a89841aaf Author: Romihime Date: Tue Nov 25 22:47:20 2025 -0300 Merge branch 'master' into develop_xml2 commit fb8565e4f06095791d66b0c88d89a1b16712704f Author: Romihime Date: Tue Nov 25 19:17:37 2025 -0300 Try commit 0325e67bb2f4f942b8d937692d16eeffe2037ccd Author: Romihime Date: Tue Nov 25 07:57:08 2025 -0300 Fix commit 801575ad71cabdf46fbc13cf97b2564801f9ea3b Author: Romihime Date: Mon Nov 24 22:01:42 2025 -0300 Changes from pr commit 43365237611af8cfac5809237dfce841f85d07e2 Author: Romihime Date: Wed Nov 19 22:16:21 2025 -0300 Fix cuantity of genes commit efac1872085a0d7b252585fb00672cf519683169 Merge: 9303840a79 5bbad0a0f3 Author: Romihime Date: Wed Nov 19 20:24:18 2025 -0300 Merge branch 'master' into develop_xml2 commit 9303840a7906340fd70ae53735ede3def82108ef Author: Romihime Date: Wed Nov 19 20:03:21 2025 -0300 Fix restActionBuilderV3 commit c7398858f94669134c4bbad2eff247b97dfa8c6e Author: Romihime Date: Wed Nov 19 19:48:08 2025 -0300 Update HttpWsTestCaseWriter.kt commit 87e97abd3071f40dd353d335c0a5653d7ceb183f Author: Romihime Date: Mon Nov 17 21:22:30 2025 -0300 New branch_xml due to changes from master --- .../rest/examples/bb/xml/BBXMLApplication.kt | 291 +++++++++++++++++ .../rest/examples/bb/xml/BBXMLController.kt | 5 + .../e2etests/spring/rest/bb/xml/BBXMLTest.kt | 76 +++++ .../spring/openapi/v3/xml/XMLApplication.kt | 308 ++++++++++++++++++ .../spring/openapi/v3/xml/XMLController.kt | 5 + .../spring/openapi/v3/xml/XMLEMTest.kt | 87 +++++ .../output/service/HttpWsTestCaseWriter.kt | 10 +- .../rest/builder/RestActionBuilderV3.kt | 210 ++++++------ .../evomaster/core/search/gene/ObjectGene.kt | 137 +++----- .../search/gene/ObjectWithAttributesGene.kt | 64 +++- .../core/search/gene/collection/ArrayGene.kt | 13 +- .../core/search/gene/sql/SqlRangeGene.kt | 5 + .../core/search/gene/ArrayGeneTest.kt | 86 +++++ .../core/search/gene/GeneRandomizedTest.kt | 45 ++- .../core/search/gene/GeneSamplerForTests.kt | 69 ++-- .../core/search/gene/ObjectGeneTest.kt | 15 + .../{uri => }/ObjectWithAttributesGeneTest.kt | 148 ++++++--- 17 files changed, 1278 insertions(+), 296 deletions(-) create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/xml/BBXMLApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/xml/BBXMLController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt rename core/src/test/kotlin/org/evomaster/core/search/gene/{uri => }/ObjectWithAttributesGeneTest.kt (63%) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/xml/BBXMLApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/xml/BBXMLApplication.kt new file mode 100644 index 0000000000..cabfbf29d5 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/xml/BBXMLApplication.kt @@ -0,0 +1,291 @@ +package com.foo.rest.examples.bb.xml + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.MediaType +import javax.xml.bind.annotation.* + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/bbxml"]) +@RestController +open class BBXMLApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BBXMLApplication::class.java, *args) + } + } + + /* ===================== ENDPOINTS ===================== */ + + // 1. String -> XML + @PostMapping( + "/receive-string-respond-xml", + consumes = ["text/plain"], + produces = ["application/xml"] + ) + fun stringToXml(@RequestBody body: String): ResponseEntity { + if (body.isBlank()) + return ResponseEntity.status(400).build() + + return ResponseEntity.status(200) + .body(Person(name = body, age = body.length)) + } + + // 2. XML -> String + @PostMapping( + "/receive-xml-respond-string", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun xmlToString(@RequestBody person: Person): ResponseEntity { + if (!isValid(person)) + return ResponseEntity + .status(400).contentType(MediaType.TEXT_PLAIN).body("") + + return ResponseEntity.status(200).body("not ok") + } + + // 3. Employee (2-level nesting) + @PostMapping( + "/employee", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun employee(@RequestBody employee: Employee): ResponseEntity { + if (!isValid(employee)) + return ResponseEntity.status(400) + .contentType(MediaType.TEXT_PLAIN) + .body("invalid input") + + return ResponseEntity.status(200) + .body( + if (employee.role == Role.ADMIN && employee.person.age > 30) + "admin" + else + "not admin or too young" + ) + } + + // 4. Company (3-level nesting) + @PostMapping( + "/company", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun company(@RequestBody company: Company): ResponseEntity { + if (!isValid(company)) + return ResponseEntity.status(400).body("") + + return ResponseEntity.status(200) + .body(if (company.employees.isEmpty()) "small company" else "big company") + } + + // 5. Department (recursive) + @PostMapping( + "/department", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun department(@RequestBody department: Department): ResponseEntity { + if (!isValid(department)) + return ResponseEntity.status(400) + .contentType(MediaType.TEXT_PLAIN) + .body("invalid input") + + return ResponseEntity.status(200) + .body("department with ${department.employees.size + 1} employees") + } + + // 6. Organization (3 lists) + @PostMapping( + "/organization", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun organization(@RequestBody organization: Organization): ResponseEntity { + if (!isValid(organization)) + return ResponseEntity.status(400) + .contentType(MediaType.TEXT_PLAIN) + .body("invalid input") + + return ResponseEntity.status(200) + .body("organization with ${organization.people.size} people") + } + + // 7. Project (attributes) + @PostMapping( + "/project", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun project(@RequestBody project: Project): ResponseEntity { + if (!isValid(project)) + return ResponseEntity.status(400).body("") + + var adults = 0 + for (m in project.members) { + if (m.id.isNotBlank() && m.age >= 18) + adults++ + } + + return ResponseEntity.status(200).body( + if (adults > 0) + "project ${project.code} has $adults adult members" + else + "project ${project.code} has only minors" + ) + } + + // 8. Project list + @PostMapping( + "/projects", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun postProjects(@RequestBody list: ProjectList): ResponseEntity { + if (!isValid(list)) + return ResponseEntity.status(400).body("") + + var members = 0 + var hasCode = false + + for (p in list.projects) { + if (p.code.isNotBlank()) + hasCode = true + for (m in p.members) { + if (m.id.isNotBlank()) + members++ + } + } + + return ResponseEntity.status(200) + .body( + if (hasCode && members > 0) + "valid projects with $members members" + else + "invalid projects" + ) + } + + // 9. Person with attributes + @PostMapping( + "/person-with-attr", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun personWithAttr(@RequestBody person: PersonWithAttr): ResponseEntity { + + if (!isValid(person)) + return ResponseEntity.status(400).body("invalid person") + + return ResponseEntity.status(200) + .body("person ${person.id} is valid") + } + + /* ===================== SCHEMA-LIKE VALIDATION ===================== */ + + private fun isValid(p: Person): Boolean = + p.name.isNotBlank() && p.age >= 0 + + private fun isValid(e: Employee): Boolean = + isValid(e.person) + + private fun isValid(c: Company): Boolean = + c.name.isNotBlank() + + private fun isValid(d: Department): Boolean = + d.name.isNotBlank() + + private fun isValid(o: Organization): Boolean = + o.name.isNotBlank() + + private fun isValid(p: PersonWithAttr): Boolean = + p.id.isNotBlank() && + p.name.isNotBlank() && + p.age >= 0 + + private fun isValid(p: Project): Boolean = + p.code.isNotBlank() + + private fun isValid(pl: ProjectList): Boolean = + true +} + +/* ===================== MODELS (JAXB) ===================== */ + +@XmlRootElement(name = "person") +@XmlAccessorType(XmlAccessType.FIELD) +open class Person( + var name: String = "", + var age: Int = 0 +) + +@XmlRootElement(name = "employee") +@XmlAccessorType(XmlAccessType.FIELD) +open class Employee( + var person: Person = Person(), + var role: Role = Role.USER +) + +@XmlRootElement(name = "company") +@XmlAccessorType(XmlAccessType.FIELD) +open class Company( + var name: String = "", + @field:XmlElement(name = "Person", namespace = "") + var employees: MutableList = mutableListOf() +) + +enum class Role { ADMIN, USER, GUEST } + +@XmlRootElement(name = "department") +@XmlAccessorType(XmlAccessType.FIELD) +open class Department( + var name: String = "", + @field:XmlElement(name = "Employee", namespace = "") + var employees: List = emptyList(), + @field:XmlElement(name = "Department", namespace = "") + var subDepartments: MutableList = mutableListOf() +) + +@XmlRootElement(name = "organization") +@XmlAccessorType(XmlAccessType.FIELD) +open class Organization( + var name: String = "", + @field:XmlElement(name = "Person", namespace = "") + var people: MutableList = mutableListOf(), + @field:XmlElement(name = "Employee", namespace = "") + var employees: MutableList = mutableListOf(), + @field:XmlElement(name = "Company", namespace = "") + var companies: MutableList = mutableListOf() +) + +@XmlRootElement(name = "personWithAttr") +@XmlAccessorType(XmlAccessType.FIELD) +open class PersonWithAttr( + @XmlAttribute(name = "id") + var id: String = "", + var name: String = "", + var age: Int = 0 +) + +@XmlRootElement(name = "project") +@XmlAccessorType(XmlAccessType.FIELD) +open class Project( + @XmlAttribute(name = "code") + var code: String = "", + @field:XmlElement(name = "PersonWithAttr", namespace = "") + var members: MutableList = mutableListOf() +) + +@XmlRootElement(name = "projectList") +@XmlAccessorType(XmlAccessType.FIELD) +open class ProjectList( + @field:XmlElement(name = "Project", namespace = "") + var projects: MutableList = mutableListOf() +) + diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/xml/BBXMLController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/xml/BBXMLController.kt new file mode 100644 index 0000000000..10318a6a3a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/xml/BBXMLController.kt @@ -0,0 +1,5 @@ +package com.foo.rest.examples.bb.xml + +import com.foo.rest.examples.bb.SpringController + +class BBXMLController : SpringController(BBXMLApplication::class.java) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt new file mode 100644 index 0000000000..766906dd02 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt @@ -0,0 +1,76 @@ +package org.evomaster.e2etests.spring.rest.bb.xml + +import com.foo.rest.examples.bb.xml.BBXMLController +import org.evomaster.core.EMConfig +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.rest.bb.SpringTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + + +class BBXMLTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + val config = EMConfig() + initClass(BBXMLController(), config) + } + } + + @Test + fun testRunEM() { + runTestHandlingFlakyAndCompilation( + "BBXmlEM", + "org.foo.BBXmlEM", + null, // terminations + 1000, // iterations + false, // createTests - skip compilation phase + { args: MutableList -> + + addBlackBoxOptions(args, OutputFormat.JAVA_JUNIT_5) + args.add("--enableBasicAssertions") + args.add("true") + + val solution = initAndRun(args) + assertTrue(solution.individuals.size >= 1) + + /* ========= basic XML endpoints ========= */ + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/receive-string-respond-xml", null) + // 400 for receive-string-respond-xml requires blank string - hard to generate + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/receive-xml-respond-string", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/bbxml/receive-xml-respond-string", null) + + /* ========= nested XML objects ========= */ + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/employee", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/bbxml/employee", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/company", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/bbxml/company", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/department", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/bbxml/department", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/organization", null) + + /* ========= XML with attributes (main test objective) ========= */ + + // person-with-attr: simple object with @XmlAttribute + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/person-with-attr", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/bbxml/person-with-attr", null) + + // project: object with @XmlAttribute and list of PersonWithAttr + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/project", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/bbxml/project", null) + + // projects: list of Project objects + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/projects", null) + }, + 3 // timeoutMinutes + ) + } +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLApplication.kt new file mode 100644 index 0000000000..db4318f82c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLApplication.kt @@ -0,0 +1,308 @@ +package com.foo.rest.examples.spring.openapi.v3.xml + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import javax.xml.bind.annotation.XmlAccessType +import javax.xml.bind.annotation.XmlAccessorType +import javax.xml.bind.annotation.XmlAnyElement +import javax.xml.bind.annotation.XmlAttribute +import javax.xml.bind.annotation.XmlElement +import javax.xml.bind.annotation.XmlElementWrapper +import javax.xml.bind.annotation.XmlRootElement + + + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/xml"]) +@RestController +open class XMLApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(XMLApplication::class.java, *args) + } + } + + /* ===================== ENDPOINTS ===================== */ + + @PostMapping( + "/receive-string-respond-xml", + consumes = ["text/plain"], + produces = ["application/xml"] + ) + fun stringToXml(@RequestBody body: String): ResponseEntity { + + if (body.isBlank() || body.any {it.isDigit()}) + return ResponseEntity.status(400).build() + + return ResponseEntity.status(200) + .body(Person(name = body, age = body.length)) + } + + @PostMapping( + "/receive-xml-respond-string", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun xmlToString(@RequestBody person: Person): ResponseEntity { + + if (!isValid(person)) + return ResponseEntity.status(400).build() + + return ResponseEntity.status(200).body("not ok") + } + + @PostMapping( + "/employee", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun employee(@RequestBody employee: Employee): ResponseEntity { + + if (!isValid(employee)) + return ResponseEntity.status(400).build() + + return ResponseEntity.status(200) + .body( + if (employee.role == Role.ADMIN && employee.person.age > 30) + "admin" + else + "not admin or too young" + ) + } + + @PostMapping( + "/company", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun company(@RequestBody company: Company): ResponseEntity { + + if (!isValid(company)) + return ResponseEntity.status(400).build() + + return ResponseEntity.status(200) + .body(if (company.employees.isEmpty()) "small company" else "big company") + } + + @PostMapping( + "/department", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun department(@RequestBody department: Department): ResponseEntity { + + if (!isValid(department)) + return ResponseEntity.status(400).build() + + return ResponseEntity.status(200) + .body("department with ${department.employees.size + 1} employees") + } + + @PostMapping( + "/organization", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun organization(@RequestBody organization: Organization): ResponseEntity { + + if (!isValid(organization)) + return ResponseEntity.status(400).build() + + return ResponseEntity.status(200) + .body("organization with ${organization.people.size} people") + } + + @PostMapping( + "/project", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun project(@RequestBody project: Project): ResponseEntity { + + if (!isValid(project)) + return ResponseEntity.status(400).build() + + var adults = 0 + for (m in project.members) { + if (m.id.isNotBlank() && m.age >= 18) + adults++ + } + + return ResponseEntity.status(200).body( + if (adults > 0) + "project ${project.code} has $adults adult members" + else + "project ${project.code} has only minors" + ) + } + + @PostMapping( + "/projects", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun postProjects(@RequestBody list: ProjectList): ResponseEntity { + + if (!isValid(list)) + return ResponseEntity.status(400).build() + + var members = 0 + var hasCode = false + + for (p in list.projects) { + if (p.code.isNotBlank()) + hasCode = true + for (m in p.members) { + if (m.id.isNotBlank()) + members++ + } + } + + return ResponseEntity.status(200) + .body( + if (hasCode && members > 0) + "valid projects with $members members" + else + "invalid projects" + ) + } + + @PostMapping( + "/person-with-attr", + consumes = ["application/xml"], + produces = ["text/plain"] + ) + fun personWithAttr(@RequestBody person: PersonWithAttr): ResponseEntity { + + if (!isValid(person)) + return ResponseEntity.status(400).build() + + return if (person.age >= 65) + ResponseEntity.status(200).body("senior ${person.id}") + else + ResponseEntity.status(200).body("adult ${person.id}") + } + + /* ===================== WHITE-BOX VALIDATION ===================== */ + + private fun isValid(p: Person): Boolean = + p.name.isNotBlank() && + p.name.length <= 20 && + p.age in 0..120 && + !(p.name == "admin" && p.age < 18) + + private fun isValid(e: Employee): Boolean = + isValid(e.person) && + !(e.role == Role.ADMIN && e.person.age < 21) + + private fun isValid(c: Company): Boolean = + c.name.isNotBlank() && + c.name.length >= 3 && + c.employees.size <= 50 + + private fun isValid(d: Department): Boolean = + d.name.isNotBlank() && + d.name != "root" && + d.employees.size <= 10 + + private fun isValid(o: Organization): Boolean = + o.name.isNotBlank() && + (o.people.size + o.employees.size + o.companies.size) <= 100 && + !(o.companies.isNotEmpty() && o.people.isEmpty()) + + private fun isValid(p: PersonWithAttr): Boolean = + p.id.isNotBlank() && + p.id.matches(Regex("[A-Z]{2}[0-9]{2}")) && + p.name.isNotBlank() && + p.age in 0..150 + + private fun isValid(p: Project): Boolean = + p.code.length >= 3 && + p.members.size >= 0 + + private fun isValid(pl: ProjectList): Boolean = + pl.projects.isNotEmpty() && + pl.projects.size <= 5 && + pl.projects.any { it.code.isNotBlank() } +} + +/* ===================== MODELS (JAXB) ===================== */ + +@XmlRootElement(name = "person") +@XmlAccessorType(XmlAccessType.FIELD) +open class Person( + var name: String = "", + var age: Int = 0 +) + +@XmlRootElement(name = "employee") +@XmlAccessorType(XmlAccessType.FIELD) +open class Employee( + var person: Person = Person(), + var role: Role = Role.USER +) + +@XmlRootElement(name = "company") +@XmlAccessorType(XmlAccessType.FIELD) +open class Company( + var name: String = "", + @field:XmlElement(name = "Person", namespace = "") + var employees: MutableList = mutableListOf() +) + +enum class Role { ADMIN, USER, GUEST } + +@XmlRootElement(name = "department") +@XmlAccessorType(XmlAccessType.FIELD) +open class Department( + var name: String = "", + @field:XmlElement(name = "Employee", namespace = "") + var employees: MutableList = mutableListOf(), + @field:XmlElement(name = "Department", namespace = "") + var subDepartments: MutableList = mutableListOf() +) + +@XmlRootElement(name = "organization") +@XmlAccessorType(XmlAccessType.FIELD) +open class Organization( + var name: String = "", + @field:XmlElement(name = "Person", namespace = "") + var people: MutableList = mutableListOf(), + @field:XmlElement(name = "Employee", namespace = "") + var employees: MutableList = mutableListOf(), + @field:XmlElement(name = "Company", namespace = "") + var companies: MutableList = mutableListOf() +) + +@XmlRootElement(name = "PersonWithAttr") +@XmlAccessorType(XmlAccessType.FIELD) +open class PersonWithAttr( + @XmlAttribute(name = "id") + var id: String = "", + var name: String = "", + var age: Int = 0 +) + +@XmlRootElement(name = "Project") +@XmlAccessorType(XmlAccessType.FIELD) +open class Project( + @XmlAttribute(name = "code") + var code: String = "", + @field:XmlElementWrapper(name = "members") + @field:XmlElement(name = "PersonWithAttr", namespace = "") + var members: MutableList = mutableListOf() +) + +@XmlRootElement(name = "projectList") +@XmlAccessorType(XmlAccessType.FIELD) +open class ProjectList( + @field:XmlElementWrapper(name = "projects") + @field:XmlElement(name = "Project", namespace = "") + var projects: MutableList = mutableListOf() +) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLController.kt new file mode 100644 index 0000000000..51a0fd9e69 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/xml/XMLController.kt @@ -0,0 +1,5 @@ +package com.foo.rest.examples.spring.openapi.v3.xml + +import com.foo.rest.examples.spring.openapi.v3.SpringController + +class XMLController : SpringController(XMLApplication::class.java) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt new file mode 100644 index 0000000000..1d6e48fac8 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt @@ -0,0 +1,87 @@ +package org.evomaster.e2etests.spring.openapi.v3.xml + +import com.foo.rest.examples.spring.openapi.v3.xml.XMLController +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + + + +/** + * White-box E2E test for XML handling with attributes. + * + * This test verifies that EvoMaster can properly: + * 1. Handle REST APIs that accept both JSON and XML payloads on the same endpoint + * 2. Parse XML attributes (@XmlAttribute) correctly, not just elements (@XmlElement) + * 3. Generate test cases that cover different branches based on attribute values + * + * Key scenarios tested: + * - /product: Dual JSON/XML endpoint with @XmlAttribute for 'sku' field + * - /order: XML-only endpoint with nested objects containing @XmlAttribute + * - /author: JSON-only endpoint (for comparison) + * - /create-product: Endpoint that returns XML with attributes + */ +class XMLEMTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(XMLController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "XMLEM", + "org.foo.XMLEM", + 2000, + true, + { args: MutableList -> + + val solution = initAndRun(args) + assertTrue(solution.individuals.size >= 1) + + /* ========= string / person ========= */ + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/receive-string-respond-xml", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/receive-string-respond-xml", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/receive-xml-respond-string", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/receive-xml-respond-string", null) + + /* ========= nesting ========= */ + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/employee", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/employee", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/company", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/company", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/department", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/department", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/organization", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/organization", null) + + /* ========= attributes ========= */ + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/project", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/project", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/projects", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/projects", null) + + /* ========= person with attributes ========= */ + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/xml/person-with-attr", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/xml/person-with-attr", null) + + }, + 3, + ) + + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt index 479c91f076..cbf0884e70 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt @@ -604,16 +604,14 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { } else if (bodyParam.isXml()) { val xml = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML, targetFormat = format) + // Escape quotes for string literal in generated code + val escapedXml = xml.replace("\\", "\\\\").replace("\"", "\\\"") when { - - format.isCsharp() -> { - lines.append("new StringContent($xml, Encoding.UTF8, \"${bodyParam.contentType()}\")") - } format.isPython() -> { - lines.add("body = $xml") + lines.add("body = \"$escapedXml\"") } - else -> lines.add(".$send($xml)") + else -> lines.add(".$send(\"$escapedXml\")") } } else { LoggingUtil.uniqueWarn(log, "Unhandled type for body payload: " + bodyParam.contentType()) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index f0741f1432..256e108de0 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -105,9 +105,9 @@ object RestActionBuilderV3 { val probUseExamples: Double = 0.0, /** - If we are doing white-box testing, we might use advance techniques like taint analysis, - which might impact how we design the chromosome. - but, for black-box, they would not be useful + If we are doing white-box testing, we might use advance techniques like taint analysis, + which might impact how we design the chromosome. + but, for black-box, they would not be useful */ val usingWhiteBox: Boolean = true ){ @@ -172,19 +172,19 @@ object RestActionBuilderV3 { val swagger = schemaHolder.main.schemaParsed swagger.paths - .forEach { e -> - handlePathItem( - e.key, - e.value, - messages, - endpointsToSkip, - skipped, - actionCluster, - schemaHolder, - options, - errorEndpoints - ) - } + .forEach { e -> + handlePathItem( + e.key, + e.value, + messages, + endpointsToSkip, + skipped, + actionCluster, + schemaHolder, + options, + errorEndpoints + ) + } ActionBuilderUtil.verifySkipped(skipped,endpointsToSkip) ActionBuilderUtil.printActionNumberInfo("RESTful API", actionCluster.size, skipped.size, errorEndpoints.size) @@ -467,11 +467,11 @@ object RestActionBuilderV3 { val links = operation.responses ?.filter { it.value.links != null && it.value.links.isNotEmpty() } ?.flatMap { res -> res.value.links.map { - Triple( - res.key, // the status code, used as key to identify the response object - it.key, // the name of the link - it.value) // the actual link definition - } + Triple( + res.key, // the status code, used as key to identify the response object + it.key, // the name of the link + it.value) // the actual link definition + } } ?.mapNotNull { try { @@ -534,19 +534,19 @@ object RestActionBuilderV3 { val params = mutableListOf() removeDuplicatedParams(schemaHolder,currentSchema,operation,messages) - .forEach { p -> + .forEach { p -> - if(p.`$ref` != null){ - val param = SchemaUtils.getReferenceParameter(schemaHolder,currentSchema, p.`$ref`, messages) - if(param == null){ - messages.add("Failed to handle ${p.`$ref`} in $verb:$restPath") - } else { - handleParam(param, schemaHolder, currentSchema, params, options, messages) - } + if(p.`$ref` != null){ + val param = SchemaUtils.getReferenceParameter(schemaHolder,currentSchema, p.`$ref`, messages) + if(param == null){ + messages.add("Failed to handle ${p.`$ref`} in $verb:$restPath") } else { - handleParam(p, schemaHolder,currentSchema, params, options, messages) + handleParam(param, schemaHolder, currentSchema, params, options, messages) } + } else { + handleParam(p, schemaHolder,currentSchema, params, options, messages) } + } handleBodyPayload(operation, verb, restPath, schemaHolder, currentSchema, params, options, messages) @@ -626,7 +626,7 @@ object RestActionBuilderV3 { // TODO: Adding description to the parameter occurs in multiple places. This can be refactored. when (p.`in`) { "query" -> params.add(QueryParam(name, gene, p.explode ?: true, p.style ?: Parameter.StyleEnum.FORM) - .apply { this.description = description }) + .apply { this.description = description }) /* a path is inside a Disruptive Gene, because there are cases in which we want to prevent mutation. Note that 1.0 means can always be mutated @@ -741,9 +741,7 @@ object RestActionBuilderV3 { listOf() } - val deref = obj.schema.`$ref`?.let { ref -> val name = ref.substringAfterLast("/") - SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema - + val deref = obj.schema.`$ref`?.let { ref -> SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema val name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" var gene = getGene(name, obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) @@ -837,23 +835,23 @@ object RestActionBuilderV3 { "integer" -> { if (format == "int64") { val data : MutableList = schema.enum - .map{ if(it is String) it.toLong() else it as Long} - .toMutableList() + .map{ if(it is String) it.toLong() else it as Long} + .toMutableList() return EnumGene(name, (data).apply { add(42L) }) } val data : MutableList = schema.enum - .map{ if(it is String) it.toInt() else it as Int} - .toMutableList() + .map{ if(it is String) it.toInt() else it as Int} + .toMutableList() return EnumGene(name, data.apply { add(42) }) } "number" -> { //if (format == "double" || format == "float") { //TODO: Is it always casted as Double even for Float??? Need test val data : MutableList = schema.enum - .map{ if(it is String) it.toDouble() else it as Double} - .toMutableList() + .map{ if(it is String) it.toDouble() else it as Double} + .toMutableList() return EnumGene(name, data.apply { add(42.0) }) } else -> messages.add("Cannot handle enum of type: $type") @@ -923,17 +921,19 @@ object RestActionBuilderV3 { val arrayType: Schema<*> = if (schema.items == null) { LoggingUtil.uniqueWarn( log, "Array type '$name' is missing mandatory field 'items' to define its type." + - " Defaulting to 'string'") + " Defaulting to 'string'") Schema().also { it.type = "string" } } else { schema.items } - val template = getGene(name + "_item", arrayType, schemaHolder,currentSchema, history, referenceClassDef = null, options = options, messages = messages) - //Could still have an empty [] -// if (template is CycleObjectGene) { -// return CycleObjectGene(" ${template.name}") -// } + // Use the XML name from schema.xml.name (the name of the array element in XML) + // if available, otherwise fallback to name + "_item" + val itemName = schema.xml?.name ?: (name + "_item") + val template = getGene(itemName, arrayType, schemaHolder,currentSchema, history, referenceClassDef = null, options = options, messages = messages)//Could still have an empty [] + //if (template is CycleObjectGene) { + //return CycleObjectGene(" ${template.name}") + //} return createNonObjectGeneWithSchemaConstraints(schema, name, ArrayGene::class.java, options, template, isInPath, examples, messages = messages)//ArrayGene(name, template) } else { LoggingUtil.uniqueWarn(log, "Invalid 'array' definition for '$name'") @@ -969,7 +969,7 @@ object RestActionBuilderV3 { refType = referenceClassDef, isFixed = true, template = null, - additionalFields = mutableListOf(), + additionalFields = null, attributeNames = attributeNames ) }else{ @@ -993,7 +993,7 @@ object RestActionBuilderV3 { return createGeneWithUnderSpecificTypeAndSchemaConstraints( schema, name, schemaHolder,currentSchema, history, referenceClassDef, options, null, isInPath, examples, messages) - //createNonObjectGeneWithSchemaConstraints(schema, name, StringGene::class.java, enableConstraintHandling) //StringGene(name) + //createNonObjectGeneWithSchemaConstraints(schema, name, StringGene::class.java, enableConstraintHandling) //StringGene(name) } throw IllegalArgumentException("Cannot handle combination $type/$format") @@ -1015,8 +1015,8 @@ object RestActionBuilderV3 { val fields = schema.properties?.entries?.map { possiblyOptional( - getGene(it.key, it.value, schemaHolder,currentSchema, history, referenceClassDef = null, options = options, messages = messages), - schema.required?.contains(it.key) + getGene(it.key, it.value, schemaHolder,currentSchema, history, referenceClassDef = null, options = options, messages = messages), + schema.required?.contains(it.key) ) } ?: listOf() @@ -1142,9 +1142,9 @@ object RestActionBuilderV3 { name = name, fixedFields = fields, refType = if (schema is ObjectSchema) referenceTypeName ?: schema.title else null, - isFixed = false, - template = additionalFieldTemplate, - additionalFields = mutableListOf(), + isFixed = true, + template = null, + additionalFields = null, attributeNames = attributeNames.toSet() ) } @@ -1366,7 +1366,7 @@ object RestActionBuilderV3 { add refClass with title of SchemaObject Man: shall we pop history here? */ - return createGeneWithExampleAndDefault(exampleGene,defaultGene,mainGene,options,name) + return createGeneWithExampleAndDefault(exampleGene,defaultGene,mainGene,options,name) } private fun duplicateObjectWithExampleFields(name: String, mainGene: ObjectGene, exampleValue: Any): ObjectGene? { @@ -1408,7 +1408,7 @@ object RestActionBuilderV3 { mainGene.isFixed, mainGene.template?.copy() as PairGene?, mainGene.additionalFields?.map { it.copy() as PairGene}?.toMutableList() - ) + ) } /** @@ -1535,39 +1535,39 @@ object RestActionBuilderV3 { ) } LongGene::class.java -> LongGene( - name, - min = if (options.enableConstraintHandling) schema.minimum?.longValueExact() else null, - max = if (options.enableConstraintHandling) schema.maximum?.longValueExact() else null, - maxInclusive = maxInclusive, - minInclusive = minInclusive + name, + min = if (options.enableConstraintHandling) schema.minimum?.longValueExact() else null, + max = if (options.enableConstraintHandling) schema.maximum?.longValueExact() else null, + maxInclusive = maxInclusive, + minInclusive = minInclusive ) FloatGene::class.java -> FloatGene( - name, - min = if (options.enableConstraintHandling) schema.minimum?.toFloat() else null, - max = if (options.enableConstraintHandling) schema.maximum?.toFloat() else null, - maxInclusive = maxInclusive, - minInclusive = minInclusive + name, + min = if (options.enableConstraintHandling) schema.minimum?.toFloat() else null, + max = if (options.enableConstraintHandling) schema.maximum?.toFloat() else null, + maxInclusive = maxInclusive, + minInclusive = minInclusive ) DoubleGene::class.java -> DoubleGene( - name, - min = if (options.enableConstraintHandling) schema.minimum?.toDouble() else null, - max = if (options.enableConstraintHandling) schema.maximum?.toDouble() else null, - maxInclusive = maxInclusive, - minInclusive = minInclusive + name, + min = if (options.enableConstraintHandling) schema.minimum?.toDouble() else null, + max = if (options.enableConstraintHandling) schema.maximum?.toDouble() else null, + maxInclusive = maxInclusive, + minInclusive = minInclusive ) BigDecimalGene::class.java -> BigDecimalGene( - name, - min = if (options.enableConstraintHandling) schema.minimum else null, - max = if (options.enableConstraintHandling) schema.maximum else null, - maxInclusive = maxInclusive, - minInclusive = minInclusive + name, + min = if (options.enableConstraintHandling) schema.minimum else null, + max = if (options.enableConstraintHandling) schema.maximum else null, + maxInclusive = maxInclusive, + minInclusive = minInclusive ) BigIntegerGene::class.java -> BigIntegerGene( - name, - min = if (options.enableConstraintHandling) schema.minimum?.toBigIntegerExact() else null, - max = if (options.enableConstraintHandling) schema.maximum?.toBigIntegerExact() else null, - maxInclusive = maxInclusive, - minInclusive = minInclusive + name, + min = if (options.enableConstraintHandling) schema.minimum?.toBigIntegerExact() else null, + max = if (options.enableConstraintHandling) schema.maximum?.toBigIntegerExact() else null, + maxInclusive = maxInclusive, + minInclusive = minInclusive ) // string, Base64StringGene and regex gene StringGene::class.java -> buildStringGene(name, options, schema, isInPath) @@ -1578,17 +1578,17 @@ object RestActionBuilderV3 { eg, min and max also, isInPath */ - RegexHandler.createGeneForEcma262(schema.pattern).apply { this.name = name } + RegexHandler.createGeneForEcma262(schema.pattern).apply { this.name = name } } ArrayGene::class.java -> { if (collectionTemplate == null) throw IllegalArgumentException("cannot create ArrayGene when collectionTemplate is null") ArrayGene( - name, - template = collectionTemplate, - uniqueElements = if (options.enableConstraintHandling) schema.uniqueItems?:false else false, - minSize = if (options.enableConstraintHandling) schema.minItems else null, - maxSize = if (options.enableConstraintHandling) schema.maxItems else null + name, + template = collectionTemplate, + uniqueElements = if (options.enableConstraintHandling) schema.uniqueItems?:false else false, + minSize = if (options.enableConstraintHandling) schema.minItems else null, + maxSize = if (options.enableConstraintHandling) schema.maxItems else null ) } else -> throw IllegalStateException("cannot create gene with constraints for gene:${geneClass.name}") @@ -1629,7 +1629,7 @@ object RestActionBuilderV3 { "the parser would read it as an array string or simply ignore it. " else "" messages.add("The use of 'example' inside a Schema Object is deprecated in OpenAPI. Rather use 'examples'." + - " ${arrayM}Read value: $raw") + " ${arrayM}Read value: $raw") //TODO a problem here is that currently number arrays would be ignored, and so this message would not written. //however, would need to check if still the case in future in new versions of the parser } @@ -1643,12 +1643,12 @@ object RestActionBuilderV3 { val defaultGene = if(defaultValue != null){ when{ NumberGene::class.java.isAssignableFrom(geneClass) - -> EnumGene("default", listOf(defaultValue.toString()),0,true) + -> EnumGene("default", listOf(defaultValue.toString()),0,true) geneClass == StringGene::class.java || geneClass == Base64StringGene::class.java || geneClass == RegexGene::class.java - -> EnumGene("default", listOf(asRawString(defaultValue)),0,false) + -> EnumGene("default", listOf(asRawString(defaultValue)),0,false) //TODO Arrays else -> { @@ -1666,12 +1666,12 @@ object RestActionBuilderV3 { val exampleGene = if(examples.isNotEmpty()){ when{ NumberGene::class.java.isAssignableFrom(geneClass) - -> EnumGene(EXAMPLES_NAME, v,0,true, n) + -> EnumGene(EXAMPLES_NAME, v,0,true, n) geneClass == StringGene::class.java || geneClass == Base64StringGene::class.java || geneClass == RegexGene::class.java - -> EnumGene(EXAMPLES_NAME, v,0,false, n) + -> EnumGene(EXAMPLES_NAME, v,0,false, n) //TODO Arrays else -> { @@ -1869,21 +1869,21 @@ object RestActionBuilderV3 { val seen = mutableSetOf() val duplicates = mutableSetOf() - operation.parameters.forEach { + operation.parameters.forEach { val p = if(it.`$ref` != null) SchemaUtils.getReferenceParameter(schemaHolder,currentSchema, it.`$ref`, messages = messages) - else - it - if(p != null) { - val key = p.`in` + "_" + p.name - if (!seen.contains(key)) { - seen.add(key) - selection.add(p) - } else { - duplicates.add(key) - } - } + else + it + if(p != null) { + val key = p.`in` + "_" + p.name + if (!seen.contains(key)) { + seen.add(key) + selection.add(p) + } else { + duplicates.add(key) + } + } } if (duplicates.isNotEmpty()) { @@ -1897,7 +1897,7 @@ object RestActionBuilderV3 { @Deprecated("should be removed, no longer used") fun getModelsFromSwagger(swagger: OpenAPI, modelCluster: MutableMap, - options: Options + options: Options ) { // modelCluster.clear() // @@ -1988,4 +1988,4 @@ object RestActionBuilderV3 { return maps } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt index 456897297b..6ec15a5ee7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectGene.kt @@ -43,27 +43,27 @@ import java.net.URLEncoder * - type: integer */ open class ObjectGene( - name: String, - val fixedFields: List, - val refType: String? = null, - /** - * represent whether the Object is fixed - * which determinate whether it allows to have additional fields - */ - isFixed : Boolean, - /** - * a template for additionalFields - */ - val template : PairGene?, - /** - * additional fields, and its field name is mutable - * - * note that [additionalFields] is not null only if [isFixed] is true - */ - additionalFields: MutableList>? + name: String, + val fixedFields: List, + val refType: String? = null, + /** + * represent whether the Object is fixed + * which determinate whether it allows to have additional fields + */ + isFixed : Boolean, + /** + * a template for additionalFields + */ + val template : PairGene?, + /** + * additional fields, and its field name is mutable + * + * note that [additionalFields] is not null only if [isFixed] is true + */ + additionalFields: MutableList>? ): CompositeConditionalFixedGene( - name, isFixed, - mutableListOf().apply { addAll(fixedFields); if (additionalFields!=null) addAll(additionalFields) }) + name, isFixed, + mutableListOf().apply { addAll(fixedFields); if (additionalFields!=null) addAll(additionalFields) }) { init { @@ -127,7 +127,7 @@ open class ObjectGene( override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { fixedFields.filter { it.isMutable() } - .forEach { it.randomize(randomness, tryToForceNewValue) } + .forEach { it.randomize(randomness, tryToForceNewValue) } if (!isFixed){ Lazy.assert { template != null && additionalFields != null } @@ -261,7 +261,7 @@ open class ObjectGene( { thisField, otherField -> thisField.containsSameValueAs(otherField) } .all { it }) && (isFixed || this.additionalFields!!.zip(other.additionalFields!!) { thisField, otherField -> thisField.containsSameValueAs(otherField) }.all { it } - ) + ) } override fun unsafeCopyValueFrom(other: Gene): Boolean { @@ -311,7 +311,7 @@ open class ObjectGene( override fun adaptiveSelectSubsetToMutate(randomness: Randomness, internalGenes: List, mwc: MutationWeightControl, additionalGeneMutationInfo: AdditionalGeneMutationInfo): List> { if (additionalGeneMutationInfo.impact != null - && additionalGeneMutationInfo.impact is ObjectGeneImpact) { + && additionalGeneMutationInfo.impact is ObjectGeneImpact) { val impacts = internalGenes.map { /* TODO here we need to consider genes which belongs to fixedFiled or not @@ -319,7 +319,7 @@ open class ObjectGene( additionalGeneMutationInfo.impact.fixedFields.getValue(it.name) } val selected = mwc.selectSubGene( - internalGenes, true, additionalGeneMutationInfo.targets, individual = null, impacts = impacts, evi = additionalGeneMutationInfo.evi + internalGenes, true, additionalGeneMutationInfo.targets, individual = null, impacts = impacts, evi = additionalGeneMutationInfo.evi ) val map = selected.map { internalGenes.indexOf(it) } return map.map { internalGenes[it] to additionalGeneMutationInfo.copyFoInnerGene(impact = impacts[it] as? GeneImpact, gene = internalGenes[it]) } @@ -336,48 +336,6 @@ open class ObjectGene( return mode == null || mode == GeneUtils.EscapeMode.JSON || mode == GeneUtils.EscapeMode.TEXT } - private fun escapeXmlSafe(s: String): String = - s.replace(Regex("(?", ">") - .replace("\"", """) - .replace("'", "'") - - private fun singularize(n: String): String = - when { - n.endsWith("s") && n.length > 1 -> n.removeSuffix("s") - else -> n - }.replaceFirstChar { it.uppercase() } - - private fun unwrap(v: Any?): Any? = - when (v) { - is OptionalGene -> v.gene - else -> v - } - - public fun cleanXmlValueString(v: String): String = - v.removeSurrounding("\"").let(::escapeXmlSafe) - - private fun getPrintedValue( - previousGenes: List, - v: Gene, - targetFormat: OutputFormat? - ): String = - cleanXmlValueString( - v.getValueAsPrintableString( - previousGenes, - GeneUtils.EscapeMode.XML, - targetFormat - ) - ) - - private fun isPrimitiveGene(value: Any?): Boolean = - when (unwrap(value)) { - is StringGene, is BooleanGene, is IntegerGene, is DoubleGene, is FloatGene, - is String, is Number, is Boolean -> true - else -> false - } - private fun serializeXml( previousGenes: List, name: String, @@ -385,15 +343,14 @@ open class ObjectGene( targetFormat: OutputFormat? ): String { + val g = value as? Gene + val leaf = g?.getLeafGene() + if (name == contentXMLTag) { - return when (val v = unwrap(value)) { - is Gene -> getPrintedValue(previousGenes, v, targetFormat) - null -> "" - else -> escapeXmlSafe(v.toString()) - } + return leaf?.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) ?: "<$name>" } - val v = unwrap(value) ?: return "<$name>" + val v = leaf ?: return "<$name>" return when (v) { @@ -403,14 +360,20 @@ open class ObjectGene( is ObjectGene -> { val inner = v.fields.joinToString("") { f -> - serializeXml(previousGenes, f.name, unwrap(f), targetFormat) + val g = f as? Gene + val leaf = g?.getLeafGene() + serializeXml(previousGenes, f.name, leaf, targetFormat) } "<$name>$inner" } - is Collection<*> -> v.joinToString("", "<$name>", "") { - val itemName = singularize(name) - serializeXml(previousGenes, itemName, it, targetFormat) + is Collection<*> -> { + val elements = v.joinToString("") { + val leaf = (it as? Gene)?.getLeafGene() + val itemName = (leaf as? Gene)?.name ?: name + serializeXml(previousGenes, itemName, leaf, targetFormat) + } + "<$name>$elements" } is Map<*, *> -> v.entries.joinToString("", "<$name>", "") { @@ -418,15 +381,15 @@ open class ObjectGene( } is ArrayGene<*> -> { - val itemName = singularize(name) - v.getViewOfElements().joinToString("", "<$name>", "") { - serializeXml(previousGenes, itemName, it, targetFormat) + v.getViewOfElements().joinToString("", "<$name>", "") { elem -> + val leaf = (elem as? Gene)?.getLeafGene() + val itemName = (leaf as? Gene)?.name ?: name + serializeXml(previousGenes, itemName, leaf, targetFormat) } } - is Gene -> "<$name>${getPrintedValue(previousGenes, v, targetFormat)}" - - else -> "<$name>${cleanXmlValueString(v.toString())}" + //Gene + else -> "<$name>${v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat)}" } } @@ -465,14 +428,16 @@ open class ObjectGene( } else if (mode == GeneUtils.EscapeMode.XML) { val inner = includedFields.joinToString("") { f -> - serializeXml(previousGenes, f.name, unwrap(f), targetFormat) + serializeXml(previousGenes, f.name, f.getLeafGene(), targetFormat) } val singleField = includedFields.singleOrNull() - val inlinePrimitive = singleField?.let { isPrimitiveGene(unwrap(it)) } == true + val leafGene = singleField?.getLeafGene() + + val inlinePrimitive = leafGene != null && leafGene.getViewOfChildren().isEmpty() val xmlPayload = if (inlinePrimitive) { - val childValue = getPrintedValue(previousGenes, unwrap(singleField) as Gene, targetFormat) + val childValue = singleField.getLeafGene().getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML , targetFormat) "<$name>$childValue" } else { "<$name>$inner" @@ -790,4 +755,4 @@ open class ObjectGene( if (gene !is PairGene<*,*>) return false return additionalFields?.contains(gene) ?: false } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt index 1cb76618e6..4ec6013926 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt @@ -1,11 +1,15 @@ package org.evomaster.core.search.gene +import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.placeholder.CycleObjectGene import org.evomaster.core.search.gene.string.StringGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.gene.wrapper.OptionalGene +import org.evomaster.core.utils.CollectionUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory class ObjectWithAttributesGene( name: String, @@ -17,6 +21,25 @@ class ObjectWithAttributesGene( val attributeNames: Set = emptySet() ) : ObjectGene(name, fixedFields, refType, isFixed, template, additionalFields) { + companion object { + private val log: Logger = LoggerFactory.getLogger(ObjectWithAttributesGene::class.java) + } + + init { + + val includedFields = fixedFields + .filter { it !is CycleObjectGene } + .filter { it !is OptionalGene || (it.isActive && it.gene !is CycleObjectGene) } + .filter { it.isPrintable() } + + val attributeFields = includedFields.filter { attributeNames.contains(it.name) } + + // "#text" CANNOT be an attribute - warn if schema incorrectly defines it as one + if (attributeNames.contains("#text")) { + LoggingUtil.uniqueWarn(log, "Invalid XML schema: '#text' cannot be used as an attribute. It will be treated as content instead.") + } + } + constructor(name: String, fields: List, refType: String? = null) : this( name, fixedFields = fields, refType = refType, isFixed = true, template = null, additionalFields = null, attributeNames = emptySet() ) @@ -38,6 +61,28 @@ class ObjectWithAttributesGene( ) } + override fun containsSameValueAs(other: Gene): Boolean { + + if (other !is ObjectGene) { + return false + } + + // If other is also ObjectWithAttributesGene, attributeNames must match + // If other is plain ObjectGene, this.attributeNames must be empty to produce same XML + if (other is ObjectWithAttributesGene) { + if (this.attributeNames != other.attributeNames) { + return false + } + } else { + // other is ObjectGene but not ObjectWithAttributesGene + // They can only have same value if this has no attributes + if (this.attributeNames.isNotEmpty()) { + return false + } + } + + return super.containsSameValueAs(other) + } private fun printAttribute( previousGenes: List, @@ -54,13 +99,15 @@ class ObjectWithAttributesGene( return "${field.name}=\"$clean\"" } + private fun cleanXmlValueString(v: String): String = + v.removeSurrounding("\"") + override fun getValueAsPrintableString( previousGenes: List, mode: GeneUtils.EscapeMode?, targetFormat: OutputFormat?, extraCheck: Boolean ): String { - if (mode != GeneUtils.EscapeMode.XML) { return super.getValueAsPrintableString(previousGenes, mode, targetFormat, extraCheck) } @@ -73,21 +120,6 @@ class ObjectWithAttributesGene( val attributeFields = includedFields.filter { attributeNames.contains(it.name) } val childFields = includedFields.filter { !attributeNames.contains(it.name) } - // 1) "#text" CANNOT be an attribute - if (attributeFields.any { it.name == "#text" }) { - throw IllegalStateException("#text cannot be used as an attribute in XML") - } - - // 2) Child names must be unique (XML does not allow repeated element names at this level) - val duplicated = childFields - .groupBy { it.name } - .filter { it.value.size > 1 } - .keys - - if (duplicated.isNotEmpty()) { - throw IllegalStateException("Duplicate child elements not allowed in XML: $duplicated") - } - val attributesString = attributeFields.joinToString(" ") { printAttribute(previousGenes, targetFormat, it) } val sb = StringBuilder() diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/collection/ArrayGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/collection/ArrayGene.kt index 91d4c17b6b..31b564c1b7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/collection/ArrayGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/collection/ArrayGene.kt @@ -225,7 +225,14 @@ class ArrayGene( } override fun getValueAsPrintableString(previousGenes: List, mode: GeneUtils.EscapeMode?, targetFormat: OutputFormat?, extraCheck: Boolean): String { - return openingTag + + + val(open,close,sep) = if (mode == GeneUtils.EscapeMode.XML){ + Triple("","","") + }else{ + Triple(openingTag,closingTag, separatorTag) + } + + return open + elements.map { g -> if (GeneUtils.isGraphQLModes(mode)) { if ((g.getWrappedGene(EnumGene::class.java)!=null)) @@ -236,8 +243,8 @@ class ArrayGene( } else { g.getValueAsPrintableString(previousGenes, mode, targetFormat) } - }.joinToString(separatorTag) + - closingTag + }.joinToString(sep) + + close } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt index b1b3bc256b..c886443fe7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt @@ -150,6 +150,11 @@ class SqlRangeGene( return false } + // Verify templates are of the same type before copying + if (this.template.javaClass != other.template.javaClass) { + return false + } + return isLeftClosed.unsafeCopyValueFrom(other.isLeftClosed) && left.unsafeCopyValueFrom(other.left as Gene) && right.unsafeCopyValueFrom(other.right as Gene) && diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/ArrayGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/ArrayGeneTest.kt index ffb7354841..ddc764470c 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/ArrayGeneTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/ArrayGeneTest.kt @@ -5,6 +5,7 @@ import org.evomaster.core.search.gene.collection.EnumGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.numeric.LongGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.service.Randomness import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -123,4 +124,89 @@ class ArrayGeneTest { assertEquals("baz", gene.getViewOfChildren()[0].getValueAsRawString()) } + @Test + fun testJsonSerializationWithBrackets(){ + val intArray = ArrayGene( + "numbers", + template = IntegerGene("num"), + elements = mutableListOf( + IntegerGene("num1", 10), + IntegerGene("num2", 20), + IntegerGene("num3", 30) + ) + ) + + val jsonOutput = intArray.getValueAsPrintableString(mode = GeneUtils.EscapeMode.JSON) + + assertEquals("[10, 20, 30]", jsonOutput) + + assertTrue(jsonOutput.startsWith("["), "JSON debe empezar con corchete de apertura") + assertTrue(jsonOutput.endsWith("]"), "JSON debe terminar con corchete de cierre") + assertTrue(jsonOutput.contains(", "), "JSON debe contener comas separadoras") + } + + @Test + fun testXmlSerializationEmptyArray(){ + // Test para verificar que un array vacío en XML no genera corchetes + val emptyArray = ArrayGene( + "items", + template = StringGene("item"), + elements = mutableListOf() + ) + + val xmlOutput = emptyArray.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + + // Un array vacío en XML debe producir una cadena vacía (sin corchetes) + assertEquals("", xmlOutput) + } + + @Test + fun testXmlSerializationWithObjects(){ + val objectArray = ArrayGene( + "people", + template = ObjectGene("person", listOf( + StringGene("name", ""), + IntegerGene("age", 0) + )), + elements = mutableListOf( + ObjectGene("person", listOf( + StringGene("name", "Alice"), + IntegerGene("age", 30) + )), + ObjectGene("person", listOf( + StringGene("name", "Bob"), + IntegerGene("age", 25) + )) + ) + ) + + val xmlOutput = objectArray.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + + val expected = "Alice30Bob25" + assertEquals(expected, xmlOutput) + + assertTrue(!xmlOutput.contains("["), "XML con objetos no debe contener corchetes") + assertTrue(!xmlOutput.contains("]"), "XML con objetos no debe contener corchetes") + } + + @Test + fun testXmlArrayInsideObjectGene(){ + val arrayOfStrings = ArrayGene( + "items", + template = StringGene("item"), + elements = mutableListOf( + StringGene("item1", "value1"), + StringGene("item2", "value2") + ) + ) + + val root = ObjectGene("root", listOf(arrayOfStrings)) + val xmlOutput = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + + assertEquals("value1value2", xmlOutput) + + assertTrue(!xmlOutput.contains("["), "XML no debe contener corchetes") + assertTrue(!xmlOutput.contains("]"), "XML no debe contener corchetes") + assertTrue(!xmlOutput.contains(", "), "XML no debe contener comas con espacios") + } } diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt index 8dc639878e..71446e0b90 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt @@ -48,12 +48,13 @@ class GeneRandomizedTest : AbstractGeneTest(){ } } - verifyCopyValueFrom(mutable, rand) + verifyCopyValueFrom(mutable, rand,seed) } private fun verifyCopyValueFrom( mutable: List, - rand: Randomness + rand: Randomness, + seed: Long ) { val printable = mutable.filter { it.isGloballyValid() && it.isPrintable() } @@ -74,7 +75,39 @@ class GeneRandomizedTest : AbstractGeneTest(){ //assertTrue(x.containsSameValueAs(y)) } else { //they must be different. the same genotype must not lead to different phenotypes - assertFalse(x.containsSameValueAs(y)) + val containsSame = x.containsSameValueAs(y) + if (containsSame) { + // Debug info: find which child gene has the inconsistency + val debugInfo = buildString { + appendLine("=== DEBUG: containsSameValueAs returned TRUE but phenotypes differ ===") + appendLine("Seed: $seed") + appendLine("Gene class: ${root.javaClass.name}") + appendLine("Gene name: ${root.name}") + appendLine("sx (x phenotype): $sx") + appendLine("sy (y phenotype): $sy") + appendLine("x children: ${x.flatView().map { it.javaClass.simpleName + ":" + it.name }.joinToString(", ")}") + + // Check each child for inconsistency + if (x is ObjectGene && y is ObjectGene) { + for (i in x.fixedFields.indices) { + val xf = x.fixedFields[i] + val yf = y.fixedFields[i] + val xfv = try { xf.getValueAsRawString() } catch (e: Exception) { "ERROR: ${e.message}" } + val yfv = try { yf.getValueAsRawString() } catch (e: Exception) { "ERROR: ${e.message}" } + val same = try { xf.containsSameValueAs(yf) } catch (e: Exception) { false } + if (xfv != yfv && same) { + appendLine(" INCONSISTENT FIELD[$i]: ${xf.javaClass.simpleName}:${xf.name}") + appendLine(" xfv: $xfv") + appendLine(" yfv: $yfv") + appendLine(" containsSameValueAs: $same") + } + } + } + appendLine("=== END DEBUG ===") + } + System.err.println(debugInfo) + } + assertFalse(containsSame, "containsSameValueAs should be false when phenotypes differ. Gene: ${root.javaClass.simpleName}") //with same type and constraints, even "unsafe" should always work val wasCopied = x.unsafeCopyValueFrom(y) @@ -110,8 +143,8 @@ class GeneRandomizedTest : AbstractGeneTest(){ assertTrue(gene.isLocallyValid(), msg) //all tree must be valid, regardless of impact on phenotype assertTrue(gene.flatView().all { - it.isLocallyValid() - } + it.isLocallyValid() + } ) if(gene !is WrapperGene){ @@ -124,4 +157,4 @@ class GeneRandomizedTest : AbstractGeneTest(){ //TODO add more invariants here } -} +} \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index 1569166246..2ed7500025 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -81,7 +81,7 @@ object GeneSamplerForTests { genes.add(c as KClass) } } - return genes + return genes.sortedBy {it.qualifiedName} } @@ -914,33 +914,48 @@ object GeneSamplerForTests { fun sampleObjectGeneWithAttributes(rand: Randomness): ObjectWithAttributesGene { - val selection = geneClasses.filter { !it.isAbstract } - val isFixed = rand.nextBoolean() + // Use a restricted selection similar to selectionForArrayTemplate() + // to avoid problematic genes that can cause issues with containsSameValueAs + val selection = geneClasses + .filter { !it.isAbstract } + .filter { it.java != CycleObjectGene::class.java && it.java != LimitObjectGene::class.java } + .filter { it.java != SqlMultidimensionalArrayGene::class.java } + // Excluir genes con compareTo problemático + .filter { it.java != SqlRangeGene::class.java } + .filter { it.java != SqlMultiRangeGene::class.java } + + // Use samplePrintableTemplate to ensure fields are printable + // This is consistent with how sampleArrayGene works + val fields = listOf( + samplePrintableTemplate(selection, rand).apply { name += "_0" }, + samplePrintableTemplate(selection, rand).apply { name += "_1" }, + samplePrintableTemplate(selection, rand).apply { name += "_2" } + ) - return if (isFixed) { - ObjectWithAttributesGene( - name = "rand ObjectGeneWithAttributes ${rand.nextInt()}", - fields = listOf( - sample(rand.choose(selection), rand), - sample(rand.choose(selection), rand), - sample(rand.choose(selection), rand) - ) - ) - }else{ - ObjectWithAttributesGene( - name = "rand ObjectGeneWithAttributes ${rand.nextInt()}", - fixedFields = listOf( - sample(rand.choose(selection), rand), - sample(rand.choose(selection), rand), - sample(rand.choose(selection), rand) - ), - refType = null, - isFixed = isFixed, - template = PairGene("template", sampleStringGene(rand), samplePrintableTemplate(selection, rand)), - additionalFields = mutableListOf() - ) + // Only StringGene can be an XML attribute (attributes are always strings) + // #text is content, not an attribute + val stringFields = fields + .filterIsInstance() + .filter { it.name != "#text" } + + // Strategy: use a small number of fixed attribute configurations + val seed = rand.nextInt(0, 10) + val attributeNames = when { + seed < 7 || stringFields.isEmpty() -> emptySet() // 70% no attributes + seed < 9 && stringFields.isNotEmpty() -> setOf(stringFields[0].name) + stringFields.size >= 2 -> setOf(stringFields[0].name, stringFields[1].name) + stringFields.isNotEmpty() -> setOf(stringFields[0].name) + else -> emptySet() } - } - + return ObjectWithAttributesGene( + name = "rand ObjectGeneWithAttributes ${rand.nextInt()}", + fixedFields = fields, + refType = null, + isFixed = true, + template = null, + additionalFields = null, + attributeNames = attributeNames + ) + } } diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt index 20dd79c440..e0d2dfbc61 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectGeneTest.kt @@ -1,5 +1,6 @@ package org.evomaster.core.search.gene +import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.string.StringGene import org.evomaster.core.search.gene.utils.GeneUtils @@ -137,4 +138,18 @@ internal class ObjectGeneTest { "" assertEquals(expected, actual) } + + @Test + fun testXmlArrayPrinting() { + + val item1 = StringGene("sarasa1", "yC") + val item2 = StringGene("lala2", "2ctkEeIof") + + val array = ArrayGene("photoUrls", StringGene("item"), elements = mutableListOf(item1, item2)) + + val root = ObjectGene(name = "root", fields = listOf(array)) + + val xml = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + assertEquals("yC2ctkEeIof", xml) + } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGeneTest.kt similarity index 63% rename from core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt rename to core/src/test/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGeneTest.kt index fdc4120fb3..6dd63ac01c 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGeneTest.kt @@ -1,13 +1,15 @@ -package org.evomaster.core.search.gene.xml +package org.evomaster.core.search.gene -import org.evomaster.core.search.gene.BooleanGene -import org.evomaster.core.search.gene.ObjectGene -import org.evomaster.core.search.gene.ObjectWithAttributesGene +import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.string.StringGene -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test import org.evomaster.core.search.gene.utils.GeneUtils +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows class ObjectWithAttributesGeneTest { @@ -40,7 +42,7 @@ class ObjectWithAttributesGeneTest { "42" + "foo" + "" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -56,7 +58,7 @@ class ObjectWithAttributesGeneTest { val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = "" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -72,7 +74,7 @@ class ObjectWithAttributesGeneTest { val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = "" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -88,7 +90,7 @@ class ObjectWithAttributesGeneTest { val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = "" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -105,9 +107,10 @@ class ObjectWithAttributesGeneTest { ) val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + // Both attributes and content are escaped once via getValueAsPrintableString (XML mode) val expected = ""<>&'" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -125,7 +128,7 @@ class ObjectWithAttributesGeneTest { val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = "42" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -142,7 +145,7 @@ class ObjectWithAttributesGeneTest { val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = "false" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -159,7 +162,7 @@ class ObjectWithAttributesGeneTest { val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) val expected = "" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -207,7 +210,7 @@ class ObjectWithAttributesGeneTest { "" + "" + "" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } @Test @@ -246,7 +249,7 @@ class ObjectWithAttributesGeneTest { "" + "" + "" - assertEquals(expected, actual) + Assertions.assertEquals(expected, actual) } //tests from ObjectGene @@ -261,7 +264,7 @@ class ObjectWithAttributesGeneTest { val actual = selection.getValueAsPrintableString(mode = GeneUtils.EscapeMode.BOOLEAN_SELECTION_MODE) - assertEquals("{foo,bar}", actual) + Assertions.assertEquals("{foo,bar}", actual) } @Test @@ -277,46 +280,97 @@ class ObjectWithAttributesGeneTest { val actual = selection.getValueAsPrintableString(mode = GeneUtils.EscapeMode.BOOLEAN_SELECTION_MODE) - assertEquals("{foo,bar,nested{hello}}", actual) + Assertions.assertEquals("{foo,bar,nested{hello}}", actual) } @Test - fun testTextCannotBeAttribute() { + fun testTextAsAttributeLogsWarning() { + // When #text is incorrectly specified as an attribute in the schema, + // the class logs a warning but continues processing (does not throw an exception) + // This tests that the object is created successfully despite the invalid schema + val obj = ObjectWithAttributesGene( + name = "node", + fixedFields = listOf( + StringGene("#text", "value") + ), + isFixed = true, + attributeNames = setOf("#text") + ) - val ex = org.junit.jupiter.api.assertThrows { + // Object should be created successfully - no exception thrown + Assertions.assertNotNull(obj) - ObjectWithAttributesGene( - name = "node", - fixedFields = listOf( - StringGene("#text", "value") - ), - isFixed = true, - attributeNames = setOf("#text") // ilegal - ).getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) - } + // The XML output will treat #text as an attribute (even though it's invalid schema) + val actual = obj.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = "" - assertEquals("#text cannot be used as an attribute in XML", ex.message) + Assertions.assertEquals(expected, actual) } - @Test - fun testDuplicateChildNameThrowsException() { - val ex = org.junit.jupiter.api.assertThrows { + @Test + fun testArrayGeneWithAttributesInsideObjectGene() { - ObjectWithAttributesGene( - name = "node", - fixedFields = listOf( - StringGene("child", "a"), - IntegerGene("child", 123) // duplicado - ), - isFixed = true, - attributeNames = emptySet() - ).getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) - } - - assertEquals( - "Duplicate child elements not allowed in XML: [child]", - ex.message + val root = ObjectGene( + name = "project", + listOf( + StringGene("code", "PRJ-001"), + ArrayGene( + name = "members", + template = ObjectWithAttributesGene( + name = "member", + fixedFields = listOf( + StringGene("id", "M001"), + StringGene("name", "John"), + IntegerGene("age", 30) + ), + isFixed = false, + attributeNames = setOf("id"), + additionalFields = null + ), + elements = mutableListOf( + ObjectWithAttributesGene( + name = "member", + fixedFields = listOf( + StringGene("id", "M001"), + StringGene("name", "Alice"), + IntegerGene("age", 25) + ), + isFixed = false, + attributeNames = setOf("id"), + additionalFields = null + ), + ObjectWithAttributesGene( + name = "member", + fixedFields = listOf( + StringGene("id", "M002"), + StringGene("name", "Bob"), + IntegerGene("age", 35) + ), + isFixed = false, + attributeNames = setOf("id"), + additionalFields = null + ) + ) + ) + ) ) + + val actual = root.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML) + val expected = + "" + + "PRJ-001" + + "" + + "" + + "Alice" + + "25" + + "" + + "" + + "Bob" + + "35" + + "" + + "" + + "" + Assertions.assertEquals(expected, actual) } } \ No newline at end of file From 2a21eb7937281eec048f9d6f3926955b69a3d5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Tue, 3 Feb 2026 08:25:36 -0300 Subject: [PATCH 09/10] Fix, clean sqlRangeGene changes --- .../e2etests/spring/openapi/v3/xml/XMLEMTest.kt | 11 ----------- .../evomaster/core/search/gene/sql/SqlRangeGene.kt | 5 ----- 2 files changed, 16 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt index 1d6e48fac8..e25aa7da1d 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/xml/XMLEMTest.kt @@ -12,17 +12,6 @@ import org.junit.jupiter.api.Test /** * White-box E2E test for XML handling with attributes. - * - * This test verifies that EvoMaster can properly: - * 1. Handle REST APIs that accept both JSON and XML payloads on the same endpoint - * 2. Parse XML attributes (@XmlAttribute) correctly, not just elements (@XmlElement) - * 3. Generate test cases that cover different branches based on attribute values - * - * Key scenarios tested: - * - /product: Dual JSON/XML endpoint with @XmlAttribute for 'sku' field - * - /order: XML-only endpoint with nested objects containing @XmlAttribute - * - /author: JSON-only endpoint (for comparison) - * - /create-product: Endpoint that returns XML with attributes */ class XMLEMTest : SpringTestBase() { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt index c886443fe7..b1b3bc256b 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlRangeGene.kt @@ -150,11 +150,6 @@ class SqlRangeGene( return false } - // Verify templates are of the same type before copying - if (this.template.javaClass != other.template.javaClass) { - return false - } - return isLeftClosed.unsafeCopyValueFrom(other.isLeftClosed) && left.unsafeCopyValueFrom(other.left as Gene) && right.unsafeCopyValueFrom(other.right as Gene) && From 15713e0f975d2c232c3af315a440925beb3a52f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Sun, 15 Feb 2026 21:55:24 -0300 Subject: [PATCH 10/10] Remove unintended change; align file with upstream --- .../core/search/gene/GeneRandomizedTest.kt | 57 +++++-------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt index 71446e0b90..be34be367c 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt @@ -3,9 +3,7 @@ package org.evomaster.core.search.gene import org.evomaster.core.search.gene.interfaces.WrapperGene import org.evomaster.core.search.service.Randomness -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import org.junit.jupiter.api.assertThrows @@ -39,7 +37,8 @@ class GeneRandomizedTest : AbstractGeneTest(){ if (root.isPrintable()) { val x = root.getValueAsRawString() val y = copy.getValueAsRawString() - assertEquals(x, y) // the copy should result in same phenotype + // the copy should result in same phenotype + assertEquals(x, y, "Different phenotype for copy of ${root.javaClass}") } else { assertThrows("Should throw exception when trying to print ${root.javaClass}") { root.getValueAsRawString() @@ -48,13 +47,12 @@ class GeneRandomizedTest : AbstractGeneTest(){ } } - verifyCopyValueFrom(mutable, rand,seed) + verifyCopyValueFrom(mutable, rand) } private fun verifyCopyValueFrom( mutable: List, - rand: Randomness, - seed: Long + rand: Randomness ) { val printable = mutable.filter { it.isGloballyValid() && it.isPrintable() } @@ -75,50 +73,23 @@ class GeneRandomizedTest : AbstractGeneTest(){ //assertTrue(x.containsSameValueAs(y)) } else { //they must be different. the same genotype must not lead to different phenotypes - val containsSame = x.containsSameValueAs(y) - if (containsSame) { - // Debug info: find which child gene has the inconsistency - val debugInfo = buildString { - appendLine("=== DEBUG: containsSameValueAs returned TRUE but phenotypes differ ===") - appendLine("Seed: $seed") - appendLine("Gene class: ${root.javaClass.name}") - appendLine("Gene name: ${root.name}") - appendLine("sx (x phenotype): $sx") - appendLine("sy (y phenotype): $sy") - appendLine("x children: ${x.flatView().map { it.javaClass.simpleName + ":" + it.name }.joinToString(", ")}") - - // Check each child for inconsistency - if (x is ObjectGene && y is ObjectGene) { - for (i in x.fixedFields.indices) { - val xf = x.fixedFields[i] - val yf = y.fixedFields[i] - val xfv = try { xf.getValueAsRawString() } catch (e: Exception) { "ERROR: ${e.message}" } - val yfv = try { yf.getValueAsRawString() } catch (e: Exception) { "ERROR: ${e.message}" } - val same = try { xf.containsSameValueAs(yf) } catch (e: Exception) { false } - if (xfv != yfv && same) { - appendLine(" INCONSISTENT FIELD[$i]: ${xf.javaClass.simpleName}:${xf.name}") - appendLine(" xfv: $xfv") - appendLine(" yfv: $yfv") - appendLine(" containsSameValueAs: $same") - } - } - } - appendLine("=== END DEBUG ===") - } - System.err.println(debugInfo) - } - assertFalse(containsSame, "containsSameValueAs should be false when phenotypes differ. Gene: ${root.javaClass.simpleName}") + assertFalse(x.containsSameValueAs(y), "Different phenotype but same genotype for ${root.javaClass}") //with same type and constraints, even "unsafe" should always work val wasCopied = x.unsafeCopyValueFrom(y) - assertTrue(wasCopied) + assertTrue(wasCopied, "Failed to make unsafe copy for ${root.javaClass}") - assertTrue(x.containsSameValueAs(y)) + assertTrue(x.containsSameValueAs(y), "After successful copy, genotype must be the same for ${root.javaClass}") } val other = rand.choose(printable) //this should not crash, ie throw any exception - x.copyValueFrom(other) + try{ + x.copyValueFrom(other) + }catch(e: Throwable){ + throw AssertionError("Failed copy value for ${x.javaClass} (${x.getValueAsRawString()})" + + " from ${other.javaClass} (${other.getValueAsRawString()})",e) + } } }