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 9cf6db31a7..73e178d487 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()) } } 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 bd6f29290e..b3ded8ace9 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 @@ -709,7 +709,6 @@ object RestActionBuilderV3 { body } - val name = "body" val description = operation.description ?: null val bodies = resolvedBody.content?.filter { @@ -748,8 +747,12 @@ object RestActionBuilderV3 { listOf() } - var gene = getGene("body", obj.schema, schemaHolder,currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) + val deref = obj.schema.`$ref`?.let { ref -> val name = ref.substringAfterLast("/") + 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) if (resolvedBody.required != true && gene !is OptionalGene) { gene = OptionalGene(name, gene) @@ -944,7 +947,40 @@ 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 + ) + }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) //FIXME is this even a standard type??? @@ -1102,6 +1138,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 80101fe1db..8a727333b6 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, @@ -78,6 +82,7 @@ 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() } @@ -304,6 +309,99 @@ 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 { @@ -339,22 +437,21 @@ 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 - */ + val inner = includedFields.joinToString("") { f -> + serializeXml(previousGenes, f.name, unwrap(f), targetFormat) + } + + 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(previousGenes, unwrap(singleField) as Gene, targetFormat) + "<$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..1cb76618e6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt @@ -0,0 +1,128 @@ +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() + ) + } + + + 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?, + 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") + } + + val attributesString = attributeFields.joinToString(" ") { printAttribute(previousGenes, targetFormat, 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 == contentXMLTag && !(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/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) } } 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 3b5cefe766..1970dfe03d 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 @@ -901,4 +902,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..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 @@ -101,4 +101,40 @@ 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 = + "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 new file mode 100644 index 0000000000..fdc4120fb3 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/uri/ObjectWithAttributesGeneTest.kt @@ -0,0 +1,322 @@ +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 = + "" + + "42" + + "foo" + + "" + 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 = + "" + + "" + + "XPhone" + + "" + + "" + + "12" + + "34" + + "" + + "" + + "" + + "" + 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 = + "" + + "XPhone" + + "" + + "" + + "12" + + "34" + + "" + + "" + + "" + 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