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..e25aa7da1d --- /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,76 @@ +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. + */ +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 15f09faa5b..260ed3e894 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 @@ -623,8 +623,19 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { } else -> lines.add(".$send(\"$body\")") } + } 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.isPython() -> { + lines.add("body = \"$escapedXml\"") + } + else -> lines.add(".$send(\"$escapedXml\")") + } } 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 59694867ca..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 @@ -703,7 +703,6 @@ object RestActionBuilderV3 { body } - val name = "body" val description = operation.description ?: null val bodies = resolvedBody.content?.filter { @@ -742,8 +741,10 @@ 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 -> 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) @@ -834,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") @@ -920,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'") @@ -938,7 +941,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 = null, + 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??? @@ -957,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") @@ -979,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() @@ -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 = true, + template = null, + additionalFields = null, + attributeNames = attributeNames.toSet() + ) + } + return assembleObjectGeneWithConstraints( name, schema, @@ -1313,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? { @@ -1355,7 +1408,7 @@ object RestActionBuilderV3 { mainGene.isFixed, mainGene.template?.copy() as PairGene?, mainGene.additionalFields?.map { it.copy() as PairGene}?.toMutableList() - ) + ) } /** @@ -1482,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) @@ -1525,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}") @@ -1576,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 } @@ -1590,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 -> { @@ -1613,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 -> { @@ -1816,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()) { @@ -1844,7 +1897,7 @@ object RestActionBuilderV3 { @Deprecated("should be removed, no longer used") fun getModelsFromSwagger(swagger: OpenAPI, modelCluster: MutableMap, - options: Options + options: Options ) { // modelCluster.clear() // @@ -1935,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 1dc0cc883d..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 @@ -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 @@ -38,28 +42,28 @@ import java.net.URLEncoder * - type: string * - type: integer */ -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>? +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>? ): CompositeConditionalFixedGene( - name, isFixed, - mutableListOf().apply { addAll(fixedFields); if (additionalFields!=null) addAll(additionalFields) }) + name, isFixed, + mutableListOf().apply { addAll(fixedFields); if (additionalFields!=null) addAll(additionalFields) }) { init { @@ -84,6 +88,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() } @@ -122,7 +127,7 @@ 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 } @@ -256,7 +261,7 @@ 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 { @@ -306,7 +311,7 @@ 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 @@ -314,7 +319,7 @@ 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]) } @@ -331,6 +336,62 @@ class ObjectGene( return mode == null || mode == GeneUtils.EscapeMode.JSON || mode == GeneUtils.EscapeMode.TEXT } + private fun serializeXml( + previousGenes: List, + name: String, + value: Any?, + targetFormat: OutputFormat? + ): String { + + val g = value as? Gene + val leaf = g?.getLeafGene() + + if (name == contentXMLTag) { + return leaf?.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) ?: "<$name>" + } + + val v = leaf ?: return "<$name>" + + return when (v) { + + is ObjectWithAttributesGene -> { + v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat) + } + + is ObjectGene -> { + val inner = v.fields.joinToString("") { f -> + val g = f as? Gene + val leaf = g?.getLeafGene() + serializeXml(previousGenes, f.name, leaf, targetFormat) + } + "<$name>$inner" + } + + 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>", "") { + serializeXml(previousGenes, it.key.toString(), it.value, targetFormat) + } + + is ArrayGene<*> -> { + v.getViewOfElements().joinToString("", "<$name>", "") { elem -> + val leaf = (elem as? Gene)?.getLeafGene() + val itemName = (leaf as? Gene)?.name ?: name + serializeXml(previousGenes, itemName, leaf, targetFormat) + } + } + + //Gene + else -> "<$name>${v.getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML, targetFormat)}" + } + } override fun getValueAsPrintableString(previousGenes: List, mode: GeneUtils.EscapeMode?, targetFormat: OutputFormat?, extraCheck: Boolean): String { @@ -366,22 +427,23 @@ 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, f.getLeafGene(), targetFormat) + } + + val singleField = includedFields.singleOrNull() + val leafGene = singleField?.getLeafGene() - 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 inlinePrimitive = leafGene != null && leafGene.getViewOfChildren().isEmpty() + + val xmlPayload = if (inlinePrimitive) { + val childValue = singleField.getLeafGene().getValueAsPrintableString(previousGenes, GeneUtils.EscapeMode.XML , 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 { @@ -693,4 +755,4 @@ 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 new file mode 100644 index 0000000000..4ec6013926 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGene.kt @@ -0,0 +1,160 @@ +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, + fixedFields: List, + refType: String? = null, + isFixed: Boolean, + template: PairGene? = null, + additionalFields: MutableList>? = null, + 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() + ) + + 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 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, + targetFormat: OutputFormat?, + field: Gene + ): String { + val raw = field.getValueAsPrintableString( + previousGenes, + GeneUtils.EscapeMode.XML, + targetFormat + ) + + val clean = cleanXmlValueString(raw) + 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) + } + + 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) } + + 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/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/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/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/GeneRandomizedTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneRandomizedTest.kt index 49c0497438..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 @@ -114,8 +114,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){ @@ -128,4 +128,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 a43cf15710..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} } @@ -134,6 +134,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 @@ -911,4 +912,50 @@ object GeneSamplerForTests { } } + fun sampleObjectGeneWithAttributes(rand: Randomness): ObjectWithAttributesGene { + + // 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" } + ) + + // 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 f5d04fdb30..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 @@ -101,4 +102,54 @@ 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) + } + + @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/ObjectWithAttributesGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGeneTest.kt new file mode 100644 index 0000000000..6dd63ac01c --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/ObjectWithAttributesGeneTest.kt @@ -0,0 +1,376 @@ +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 +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 { + + + @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" + + "" + Assertions.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 = "" + + Assertions.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 = "" + + Assertions.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 = "" + + Assertions.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) + // Both attributes and content are escaped once via getValueAsPrintableString (XML mode) + val expected = ""<>&'" + + Assertions.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" + + Assertions.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" + + Assertions.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 = "" + + Assertions.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" + + "" + + "" + + "" + + "" + Assertions.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" + + "" + + "" + + "" + Assertions.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) + + Assertions.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) + + Assertions.assertEquals("{foo,bar,nested{hello}}", actual) + } + + @Test + 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") + ) + + // Object should be created successfully - no exception thrown + Assertions.assertNotNull(obj) + + // 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 = "" + + Assertions.assertEquals(expected, actual) + } + + + @Test + fun testArrayGeneWithAttributesInsideObjectGene() { + + 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