diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueApplication.kt new file mode 100644 index 0000000000..02aa487b63 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueApplication.kt @@ -0,0 +1,81 @@ +package com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + * SUT for the "JSON Patch DTO with NON-PRIMITIVE value" e2e test. + * + * Goal: prove that, with [org.evomaster.core.EMConfig.dtoForRequestPayload] enabled, EvoMaster + * generates BOTH the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation` + * DTO) AND the nested DTO for the NON-PRIMITIVE `value` of those operations (the `Item` object DTO). + * + * The OpenAPI schema is the one auto-generated by Spring (springdoc, served at `/v3/api-docs`); + * no static YAML is used. springdoc names the array-element component after the Kotlin class, so the + * element type is named [Item] and EvoMaster generates a DTO called `Item`. Endpoint under test: + * [patchResource] (PATCH, `application/json-patch+json`). + * + * Key mechanic: the TYPE of the JSON Patch operation `value` is resolved from the OpenAPI schema of a + * sibling operation on the same path (GET 2xx response -> PUT body -> POST body; see + * `JsonPatchSchemaResolver`). We expose, via [getResource] (a GET, which produces no DTO of its own), + * a resource whose patchable fields are ARRAYS OF OBJECTS ([Item]). Plain nested objects would be + * flattened into primitive leaf paths by the JSON Patch builder; an array-of-objects is the case that + * yields a non-primitive value gene, which makes EvoMaster generate a nested DTO for the `Item` object. + * + * Two array fields are exposed on purpose: the builder groups value types by gene class, so both + * arrays collapse into the same value type (array of Item), making the value ALWAYS a non-primitive + * array of objects, while keeping the path enum multi-valued (i.e. mutable during search). + */ +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping("/resource") +open class JsonPatchObjectValueApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(JsonPatchObjectValueApplication::class.java, *args) + } + } + + /** Hardcoded resource whose fields are arrays of objects (exposed as the `ObjectResource` schema). */ + private val hardcoded = ObjectResource( + items = listOf( + Item(label = "first", quantity = 1), + Item(label = "second", quantity = 2) + ), + archived = listOf( + Item(label = "old", quantity = 0) + ) + ) + + @GetMapping("/{id}", produces = ["application/json"]) + fun getResource(@PathVariable id: Long): ResponseEntity { + return ResponseEntity.ok(hardcoded) + } + + @PatchMapping("/{id}", consumes = ["application/json-patch+json"]) + fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + return ResponseEntity.ok("OK") + } +} + +/** + * Response types used only to expose the resource schema (arrays of objects) to the JSON Patch + * resolver. They are response bodies (not request bodies), so they are never turned into EvoMaster + * DTOs. The EvoMaster DTO for the `value` is named after the [Item] class (its springdoc component). + */ +data class ObjectResource( + val items: List = emptyList(), + val archived: List = emptyList() +) + +data class Item(val label: String = "", val quantity: Int = 0) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueApplication.kt new file mode 100644 index 0000000000..efbefa26c1 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueApplication.kt @@ -0,0 +1,63 @@ +package com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + * SUT for the "JSON Patch DTO with STRING value" e2e test. + * + * Goal: prove that, with [org.evomaster.core.EMConfig.dtoForRequestPayload] enabled, EvoMaster + * generates the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation` + * DTO, fields op/path/from/value) and that its `value` is a primitive (string) inlined as a + * literal (i.e. no nested value DTO is produced). + * + * The OpenAPI schema is the one auto-generated by Spring (springdoc, served at `/v3/api-docs`); + * no static YAML is used. Endpoint under test: [patchResource] (PATCH, `application/json-patch+json`). + * + * Key mechanic: the TYPE of the JSON Patch operation `value` is NOT taken from what this controller + * returns at runtime; it is resolved from the OpenAPI schema of a sibling operation on the same path + * (GET 2xx response -> PUT body -> POST body; see `JsonPatchSchemaResolver`). We expose the resource + * schema through [getResource] (a GET, which has no request body and therefore produces NO DTO of + * its own). Its response object has only string fields, so every JSON Patch operation `value` is a + * primitive string. As a consequence, the ONLY DTO EvoMaster can generate for this app is the shared + * `JsonPatchOperation` DTO: if a DTO appears, it is unambiguously the JSON Patch one. + */ +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping("/resource") +open class JsonPatchStringValueApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(JsonPatchStringValueApplication::class.java, *args) + } + } + + /** Hardcoded resource whose fields are all strings (exposed as the `StringResource` schema). */ + private val hardcoded = StringResource(name = "EvoMaster", description = "JSON Patch string value") + + @GetMapping("/{id}", produces = ["application/json"]) + fun getResource(@PathVariable id: Long): ResponseEntity { + return ResponseEntity.ok(hardcoded) + } + + @PatchMapping("/{id}", consumes = ["application/json-patch+json"]) + fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + return ResponseEntity.ok("OK") + } +} + +/** + * Response type used only to expose a string-only resource schema to the JSON Patch resolver. + * It is a response body (not a request body), so it is never turned into an EvoMaster DTO. + */ +data class StringResource(val name: String = "", val description: String = "") diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueController.kt new file mode 100644 index 0000000000..0206f510dd --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueController.kt @@ -0,0 +1,9 @@ +package com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue + +import com.foo.rest.examples.spring.openapi.v3.SpringController + +/** + * Uses the OpenAPI schema auto-generated by Spring (springdoc): [SpringController.getProblemInfo] + * already points to `/v3/api-docs`, so no override and no static YAML are needed. + */ +class JsonPatchObjectValueController : SpringController(JsonPatchObjectValueApplication::class.java) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueController.kt new file mode 100644 index 0000000000..68bedb5633 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueController.kt @@ -0,0 +1,9 @@ +package com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue + +import com.foo.rest.examples.spring.openapi.v3.SpringController + +/** + * Uses the OpenAPI schema auto-generated by Spring (springdoc): [SpringController.getProblemInfo] + * already points to `/v3/api-docs`, so no override and no static YAML are needed. + */ +class JsonPatchStringValueController : SpringController(JsonPatchStringValueApplication::class.java) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt index c2a88daa27..7716891b60 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt @@ -3,8 +3,16 @@ package org.evomaster.e2etests.spring.openapi.v3 import org.evomaster.client.java.controller.InstrumentedSutStarter import org.evomaster.client.java.instrumentation.InputProperties import org.evomaster.client.java.instrumentation.InstrumentingAgent +import org.evomaster.client.java.instrumentation.shared.ClassName import org.evomaster.e2etests.utils.RestTestBase +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll +import java.util.Optional.ofNullable +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.full.createInstance +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible /** * Created by arcuri82 on 03-Mar-20. @@ -25,4 +33,23 @@ abstract class SpringTestBase : RestTestBase(){ } } + protected fun initDtoClass(name: String): Pair, Any> { + val className = ClassName("org.foo.dto.$name") + val klass = loadClass(className).kotlin + Assertions.assertNotNull(klass) + return Pair(klass, klass.createInstance()) + } + + protected fun assertProperty(klass: KClass, instance: Any, propertyName: String, propertyValue: Any?) { + val property = klass.memberProperties + .firstOrNull { it.name == propertyName } as? KMutableProperty1 + Assertions.assertNotNull(property) + + property?.let { + it.isAccessible = true + it.set(instance, ofNullable(propertyValue)) + } + Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance)) + } + } diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt index 5387959de2..2fb3076534 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt @@ -1,18 +1,13 @@ package org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert import com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertController -import org.evomaster.client.java.instrumentation.shared.ClassName import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test -import java.util.Optional.ofNullable import kotlin.reflect.KClass -import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.full.createInstance import kotlin.reflect.full.memberFunctions -import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.javaMethod @@ -161,25 +156,6 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertAdditionalPropertiesFunction(parentKlass, parentInstance, "no_root_key", childInstance) } - private fun initDtoClass(name: String): Pair, Any> { - val className = ClassName("org.foo.dto.$name") - val klass = loadClass(className).kotlin - Assertions.assertNotNull(klass) - return Pair(klass, klass.createInstance()) - } - - private fun assertProperty(klass: KClass, instance: Any, propertyName: String, propertyValue: Any?) { - val property = klass.memberProperties - .firstOrNull { it.name == propertyName } as? KMutableProperty1 - Assertions.assertNotNull(property) - - property?.let { - it.isAccessible = true - it.set(instance, ofNullable(propertyValue)) // Set the value - } - Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance)) - } - private fun assertAdditionalPropertiesFunction(klass: KClass, instance: Any, propertyName: String, propertyValue: Any?) { val setterFunction = klass.memberFunctions.firstOrNull { it.name == "addAdditionalProperty" diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt new file mode 100644 index 0000000000..786126acab --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt @@ -0,0 +1,81 @@ +package org.evomaster.e2etests.spring.openapi.v3.jsonpatchobjectvalue + +import com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue.JsonPatchObjectValueController +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +/** + * E2E test: JSON Patch DTO with a NON-PRIMITIVE value. + * + * What is being tested: + * When `dtoForRequestPayload` is enabled and the SUT has a JSON Patch endpoint whose resource + * exposes an array-of-objects field, EvoMaster must generate: + * 1. the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation` DTO), and + * 2. the nested DTO for the NON-PRIMITIVE `value` of those operations (the `Item` object DTO). + * + * Why the assertion is unambiguous: + * The SUT only exposes a GET (no request body -> no DTO) and a PATCH (json-patch). Therefore the + * only DTOs EvoMaster can generate are the JSON Patch ones. Reflectively loading + * `org.foo.dto.JsonPatchOperation` and `org.foo.dto.Item` after the generated suite is compiled + * proves that both were generated, i.e. that the value DTO of the JSON Patch operations exists. + * + * This mirrors the reflective-assert approach of + * [org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertEMTest]. + */ +class JsonPatchObjectValueDtoEMTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(JsonPatchObjectValueController()) + } + } + + @Test + fun testRunEM() { + runTestHandlingFlakyAndCompilation( + "JsonPatchObjectValueDtoEM", + "org.foo.JsonPatchObjectValueDtoEM", + 100, + ) { args: MutableList -> + + setOption(args, "dtoForRequestPayload", "true") + // This SUT intentionally has no authentication, and the PATCH returns 2xx, so the + // security oracle would flag a fault 208 (Anonymous Modifications). It is irrelevant to + // what we assert here (JSON Patch DTO generation), so we disable it to keep the suite clean. + setOption(args, "security", "false") + + val solution = initAndRun(args) + + Assertions.assertTrue(solution.individuals.size >= 1) + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/resource/{id}", "OK") + } + + assertJsonPatchOperationDtoCreated() + assertNonPrimitiveValueDtoCreated() + } + + /** + * The shared JsonPatchOperation DTO must exist with the RFC 6902 fields (op, path, from, value). + */ + private fun assertJsonPatchOperationDtoCreated() { + val (klass, instance) = initDtoClass("JsonPatchOperation") + assertProperty(klass, instance, "op", "add") + assertProperty(klass, instance, "path", "/items") + assertProperty(klass, instance, "from", "/items") + } + + /** + * The nested DTO for the non-primitive `value` of the operations must exist. Its name comes + * from the OpenAPI component `Item`, with fields label (String) and quantity (Int). + */ + private fun assertNonPrimitiveValueDtoCreated() { + val (klass, instance) = initDtoClass("Item") + assertProperty(klass, instance, "label", "a label") + assertProperty(klass, instance, "quantity", 7) + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt new file mode 100644 index 0000000000..9f7d43dd1d --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt @@ -0,0 +1,72 @@ +package org.evomaster.e2etests.spring.openapi.v3.jsonpatchstringvalue + +import com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue.JsonPatchStringValueController +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +/** + * E2E test: JSON Patch DTO with a PRIMITIVE (string) value. + * + * What is being tested: + * When `dtoForRequestPayload` is enabled and the SUT has a JSON Patch endpoint whose resource + * fields are all strings, EvoMaster must generate the DTO that represents the JSON Patch + * operations (the shared `JsonPatchOperation` DTO, with the RFC 6902 fields op/path/from/value), + * and each operation `value` must be a primitive string inlined as a literal (no nested value DTO). + * + * Why the assertion is unambiguous: + * The SUT only exposes a GET (no request body -> no DTO) and a PATCH (json-patch). Therefore, if + * EvoMaster generates any DTO at all, it can only be the JSON Patch one. Reflectively loading + * `org.foo.dto.JsonPatchOperation` after the generated suite is compiled proves it was generated. + * + * This mirrors the reflective-assert approach of + * [org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertEMTest]. + */ +class JsonPatchStringValueDtoEMTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(JsonPatchStringValueController()) + } + } + + @Test + fun testRunEM() { + runTestHandlingFlakyAndCompilation( + "JsonPatchStringValueDtoEM", + "org.foo.JsonPatchStringValueDtoEM", + 100, + ) { args: MutableList -> + + setOption(args, "dtoForRequestPayload", "true") + // This SUT intentionally has no authentication, and the PATCH returns 2xx, so the + // security oracle would flag a fault 208 (Anonymous Modifications). It is irrelevant to + // what we assert here (JSON Patch DTO generation), so we disable it to keep the suite clean. + setOption(args, "security", "false") + + val solution = initAndRun(args) + + Assertions.assertTrue(solution.individuals.size >= 1) + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/resource/{id}", "OK") + } + + assertJsonPatchOperationDtoCreatedWithStringValue() + } + + /** + * The shared JsonPatchOperation DTO must exist with the RFC 6902 fields (op, path, from, value). + * Because the resource only has string fields, `value` is a primitive: we prove it can hold a String. + */ + private fun assertJsonPatchOperationDtoCreatedWithStringValue() { + val (klass, instance) = initDtoClass("JsonPatchOperation") + assertProperty(klass, instance, "op", "replace") + assertProperty(klass, instance, "path", "/name") + assertProperty(klass, instance, "from", "/description") + // A JSON Patch value can be any JSON value; here it is a primitive string inlined as a literal. + assertProperty(klass, instance, "value", "EvoMaster") + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index 7a0351a2eb..dda00a5848 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -23,6 +23,7 @@ import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.numeric.LongGene import org.evomaster.core.search.gene.placeholder.CycleObjectGene import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene import org.evomaster.core.search.gene.string.StringGene @@ -119,11 +120,7 @@ class DtoWriter( gene is ObjectGene -> calculateDtoFromObject(gene, actionName) gene is ArrayGene<*> -> calculateDtoFromArray(gene, actionName) gene is FixedMapGene<*, *> -> calculateDtoFromFixedMapGene(gene, actionName) - // TODO: a JsonPatchDocumentGene is currently skipped from DTO collection. Once we decide - // how a JSON Patch document should be rendered when a test case is written (it is not a - // regular object/array DTO but an RFC 6902 array of operations), this should build and - // emit the corresponding DTO instead of returning. - gene is JsonPatchDocumentGene -> return + gene is JsonPatchDocumentGene -> calculateDtoFromJsonPatch(gene) isPrimitiveGene(gene) -> return else -> { throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $actionName") @@ -131,6 +128,29 @@ class DtoWriter( } } + /** + * Registers the shared JsonPatchOperation DTO and collects nested DTOs for object/array values. + */ + private fun calculateDtoFromJsonPatch(gene: JsonPatchDocumentGene) { + val dtoName = GeneToDto.JSON_PATCH_OPERATION_DTO + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(it) } + dtoClass.addField(GeneToDto.FIELD_OP, DtoField(GeneToDto.FIELD_OP, GeneToDto.TYPE_STRING)) + dtoClass.addField(GeneToDto.FIELD_PATH, DtoField(GeneToDto.FIELD_PATH, GeneToDto.TYPE_STRING)) + dtoClass.addField(GeneToDto.FIELD_FROM, DtoField(GeneToDto.FIELD_FROM, GeneToDto.TYPE_STRING)) + dtoClass.addField(GeneToDto.FIELD_VALUE, DtoField(GeneToDto.FIELD_VALUE, anyType())) + + gene.operations.filterIsInstance().forEach { operation -> + when (val valueGene = operation.pathValueChoice.activeGene().second.getLeafGene()) { + is ObjectGene -> calculateDtoFromObject(valueGene, GeneToDto.FIELD_VALUE) + is ArrayGene<*> -> calculateDtoFromArray(valueGene, GeneToDto.FIELD_VALUE) + } + } + } + + private fun anyType(): String { + return if (outputFormat.isJava()) GeneToDto.TYPE_JAVA_OBJECT else GeneToDto.TYPE_KOTLIN_ANY + } + private fun calculateDtoFromFixedMapGene(gene: FixedMapGene<*, *>, actionName: String) { val dtoName = TestWriterUtils.safeVariableName(actionName) val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index bcd2c2299a..9f39a1e946 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -12,6 +12,11 @@ import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchFromPathGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchOperationGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathOnlyGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene import org.evomaster.core.search.gene.numeric.DoubleGene import org.evomaster.core.search.gene.numeric.FloatGene import org.evomaster.core.search.gene.numeric.IntegerGene @@ -36,6 +41,18 @@ class GeneToDto( val outputFormat: OutputFormat ) { + companion object { + // Shared DTO class name for all JSON Patch operations (RFC 6902). + const val JSON_PATCH_OPERATION_DTO = "JsonPatchOperation" + const val FIELD_OP = "op" + const val FIELD_PATH = "path" + const val FIELD_FROM = "from" + const val FIELD_VALUE = "value" + const val TYPE_STRING = "String" + const val TYPE_JAVA_OBJECT = "Object" + const val TYPE_KOTLIN_ANY = "Any" + } + private val log: Logger = LoggerFactory.getLogger(GeneToDto::class.java) private var dtoOutput: DtoOutput = if (outputFormat.isJava()) { @@ -67,6 +84,7 @@ class GeneToDto( } is ChoiceGene<*> -> TestWriterUtils.safeVariableName(fallback) is FixedMapGene<*,*> -> TestWriterUtils.safeVariableName(fallback) + is JsonPatchDocumentGene -> JSON_PATCH_OPERATION_DTO else -> throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $fallback") } } @@ -85,10 +103,89 @@ class GeneToDto( is ArrayGene<*> -> getArrayDtoCall(gene, dtoName, counters, null, capitalize) is ChoiceGene<*> -> getDtoCall(gene.activeGene(), dtoName, counters, capitalize) is FixedMapGene<*,*> -> getFixedMapGeneDtoCall(gene, dtoName, counters) + is JsonPatchDocumentGene -> getJsonPatchDtoCall(gene, counters) else -> throw RuntimeException("BUG: Gene $gene (with type ${this::class.java.simpleName}) should not be creating DTOs") } } + /** + * Renders a JSON Patch document as a List, one DTO per active operation. + */ + private fun getJsonPatchDtoCall(gene: JsonPatchDocumentGene, counters: MutableList): DtoCall { + val listVarName = "list_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" + val result = mutableListOf() + result.add(dtoOutput.getNewListStatement(JSON_PATCH_OPERATION_DTO, listVarName)) + + gene.operations.forEachIndexed { index, operation -> + val childCounter = mutableListOf().apply { + addAll(counters) + add(index + 1) + } + val operationCall = getJsonPatchOperationCall(operation, childCounter) + result.addAll(operationCall.objectCalls) + result.add(dtoOutput.getAddElementToListStatement(listVarName, operationCall.varName)) + } + + return DtoCall(listVarName, result) + } + + /** + * Renders a single RFC 6902 operation as a JsonPatchOperation DTO with only its relevant fields set. + */ + private fun getJsonPatchOperationCall(operation: JsonPatchOperationGene, counters: MutableList): DtoCall { + val varName = "dto_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" + val result = mutableListOf() + result.add(dtoOutput.getNewObjectStatement(JSON_PATCH_OPERATION_DTO, varName)) + result.add(dtoOutput.getSetterStatement(varName, FIELD_OP, "\"${operation.operationName}\"")) + + when (operation) { + is JsonPatchPathOnlyGene -> { + result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(operation.pathGene))) + } + is JsonPatchFromPathGene -> { + result.add(dtoOutput.getSetterStatement(varName, FIELD_FROM, renderLeafValue(operation.fromGene))) + result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(operation.pathGene))) + } + is JsonPatchPathValueGene -> { + val pair = operation.pathValueChoice.activeGene() + result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(pair.first))) + setJsonPatchValue(varName, pair.second.getLeafGene(), counters, result) + } + } + + return DtoCall(varName, result) + } + + /** + * Sets the "value" field: primitives are inlined as literals, objects/arrays delegate to DTO generation. + */ + private fun setJsonPatchValue( + varName: String, + valueGene: Gene, + counters: MutableList, + result: MutableList + ) { + when (valueGene) { + is ObjectGene -> { + val childCall = getDtoCall(valueGene, getDtoName(valueGene, FIELD_VALUE, true), counters, true) + result.addAll(childCall.objectCalls) + result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, childCall.varName)) + } + is ArrayGene<*> -> { + val childCall = getArrayDtoCall(valueGene, getDtoName(valueGene, FIELD_VALUE, true), counters, FIELD_VALUE, true) + result.addAll(childCall.objectCalls) + result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, childCall.varName)) + } + else -> result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, renderLeafValue(valueGene))) + } + } + + // Returns the printable string representation of a gene's leaf value with its language-specific suffix. + private fun renderLeafValue(gene: Gene): String { + val leafGene = gene.getLeafGene() + return "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(leafGene)}" + } + private fun getObjectDtoCall(gene: ObjectGene, dtoName: String, counters: MutableList): DtoCall { val dtoVarName = "dto_${dtoName}_${counters.joinToString("_")}" 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 1bad17932d..8da5baaefd 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 @@ -148,11 +148,13 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { val bodyParam = call.parameters.find { p -> p is BodyParam } as BodyParam? if (bodyParam != null && bodyParam.isJson() && payloadIsValidJson(bodyParam)) { val primaryGene = bodyParam.primaryGene() - if (primaryGene.getWrappedGene(JsonPatchDocumentGene::class.java) != null) { - return "" + val actionName = call.getName() + val jsonPatchGene = primaryGene.getWrappedGene(JsonPatchDocumentGene::class.java) + if (jsonPatchGene != null) { + // A JSON Patch document is rendered as a List DTO (RFC 6902). + return generateDtoCall(jsonPatchGene, actionName, lines).varName } val choiceGene = primaryGene.getWrappedGene(ChoiceGene::class.java) - val actionName = call.getName() if (choiceGene != null) { // We only generate DTOs for ChoiceGene objects that contain either an ObjectGene or ArrayGene in their // genes. This check is necessary since when using `example` and `default` entries, diff --git a/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt index 6d210bf112..5d3111bfa1 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt @@ -29,9 +29,12 @@ import org.evomaster.core.search.gene.UUIDGene import org.evomaster.core.search.gene.collection.EnumGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.output.dto.GeneToDto +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.gene.wrapper.CustomMutationRateGene import org.evomaster.core.search.gene.wrapper.OptionalGene +import org.evomaster.core.search.service.Randomness import org.evomaster.core.sql.schema.TableId import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -1711,6 +1714,40 @@ public void test() throws Exception { } } + @Test + fun testJsonPatchBodyRenderedAsDto() { + val format = OutputFormat.KOTLIN_JUNIT_5 + val baseUrlOfSut = "baseUrlOfSut" + + val schema = ObjectGene("body", listOf(StringGene("name"), IntegerGene("age"))) + val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } + val bodyParam = BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) + + val action = RestCallAction("1", HttpVerb.PATCH, RestPath("/items/1"), mutableListOf(bodyParam)) + val individual = RestIndividual(mutableListOf(action), SampleType.RANDOM) + TestUtils.doInitializeIndividualForTesting(individual, Randomness().apply { updateSeed(42L) }) + + val fitnessVal = FitnessValue(0.0) + val result = RestCallResult(action.getLocalId()).apply { setStatusCode(200) } + val ei = EvaluatedIndividual(fitnessVal, individual, listOf(result)) + + val config = getConfig(format) + config.dtoForRequestPayload = true + config.problemType = EMConfig.ProblemType.REST + + val test = TestCase(test = ei, name = "test") + val writer = RestTestCaseWriter(config, PartialOracles()) + val output = writer.convertToCompilableTestCode(test, baseUrlOfSut).toString() + + // The JSON Patch body must be rendered as a DTO list, not as a raw JSON string. + assertTrue(output.contains("list_${GeneToDto.JSON_PATCH_OPERATION_DTO}_"), + "Expected DTO list variable in generated output") + assertTrue(output.contains(".body(list_${GeneToDto.JSON_PATCH_OPERATION_DTO}_"), + "Expected DTO variable passed as body argument") + assertFalse(output.contains("{\"op\":"), + "Body must not contain raw JSON string representation of a patch operation") + } + @Test fun testInActiveBodyParamInTest(){ val stringGene = StringGene("stringGene") diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt new file mode 100644 index 0000000000..73594607ea --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt @@ -0,0 +1,107 @@ +package org.evomaster.core.output.dto + +import org.evomaster.core.TestUtils +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.output.Termination +import org.evomaster.core.output.naming.RestActionTestCaseUtils.getEvaluatedIndividualWith +import org.evomaster.core.output.naming.RestActionTestCaseUtils.getRestCallAction +import org.evomaster.core.problem.api.param.Param +import org.evomaster.core.problem.enterprise.SampleType +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.core.problem.rest.data.RestCallResult +import org.evomaster.core.problem.rest.data.RestIndividual +import org.evomaster.core.problem.rest.param.BodyParam +import org.evomaster.core.search.EvaluatedIndividual +import org.evomaster.core.search.FitnessValue +import org.evomaster.core.search.Solution +import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.service.Randomness +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.nio.file.Paths +import java.util.Collections.singletonList + +class DtoWriterJsonPatchTest { + + private val outputTestSuitePath = Paths.get("./target/dto-writer-json-patch-test") + private val testPackage = "test.package" + + private fun jsonPatchBodyParam(): Param { + val schema = ObjectGene("body", listOf(StringGene("name"), IntegerGene("age"))) + val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } + return BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) + } + + private fun jsonPatchSolution(): Solution<*> { + val action = getRestCallAction("/items/{id}", HttpVerb.PATCH, mutableListOf(jsonPatchBodyParam())) + val eIndividual = getEvaluatedIndividualWith(action) + return Solution(singletonList(eIndividual), "", "", Termination.NONE, emptyList(), emptyList()) + } + + @Test + fun collectsJsonPatchOperationDtoWithAllFields() { + val dtoWriter = DtoWriter(OutputFormat.KOTLIN_JUNIT_5) + dtoWriter.write(outputTestSuitePath, testPackage, jsonPatchSolution()) + + val dtos = dtoWriter.getCollectedDtos() + val operationDto = dtos[GeneToDto.JSON_PATCH_OPERATION_DTO] + assertNotNull(operationDto, "Expected a ${GeneToDto.JSON_PATCH_OPERATION_DTO} DTO to be collected") + + val fields = operationDto!!.fieldsMap + assertEquals(DtoField(GeneToDto.FIELD_OP, "String"), fields[GeneToDto.FIELD_OP]) + assertEquals(DtoField(GeneToDto.FIELD_PATH, "String"), fields[GeneToDto.FIELD_PATH]) + assertEquals(DtoField(GeneToDto.FIELD_FROM, "String"), fields[GeneToDto.FIELD_FROM]) + // value is the generic object type since a JSON Patch value can be any JSON value + assertEquals(DtoField(GeneToDto.FIELD_VALUE, "Any"), fields[GeneToDto.FIELD_VALUE]) + } + + @Test + fun valueFieldIsObjectForJavaOutput() { + val dtoWriter = DtoWriter(OutputFormat.JAVA_JUNIT_5) + dtoWriter.write(outputTestSuitePath, testPackage, jsonPatchSolution()) + + val operationDto = dtoWriter.getCollectedDtos()[GeneToDto.JSON_PATCH_OPERATION_DTO] + assertNotNull(operationDto) + assertEquals(DtoField(GeneToDto.FIELD_VALUE, "Object"), operationDto!!.fieldsMap[GeneToDto.FIELD_VALUE]) + } + + @Test + fun collectsNestedObjectDtoWhenValueIsArrayOfObjects() { + // Schema where the only patchable field is an array of objects: + // every add/replace/test operation will have an ArrayGene as its value gene, + // so calculateDtoFromJsonPatch must visit calculateDtoFromArray for those operations. + val tagSchema = ObjectGene("Tag", listOf(StringGene("label")), refType = "Tag") + val schema = ObjectGene("body", listOf(ArrayGene("tags", tagSchema))) + val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } + val bodyParam = BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) + + val action = getRestCallAction("/items/{id}", HttpVerb.PATCH, mutableListOf(bodyParam)) + val individual = RestIndividual(mutableListOf(action), SampleType.RANDOM) + TestUtils.doInitializeIndividualForTesting(individual, Randomness().apply { updateSeed(42L) }) + + val result = RestCallResult(action.getLocalId()).apply { setStatusCode(200) } + val ei = EvaluatedIndividual(FitnessValue(0.0), individual, listOf(result)) + val solution = Solution(singletonList(ei), "", "", Termination.NONE, emptyList(), emptyList()) + + val dtoWriter = DtoWriter(OutputFormat.KOTLIN_JUNIT_5) + dtoWriter.write(outputTestSuitePath, testPackage, solution) + val dtos = dtoWriter.getCollectedDtos() + + assertNotNull(dtos[GeneToDto.JSON_PATCH_OPERATION_DTO]) + + val patchGene = bodyParam.primaryGene() as JsonPatchDocumentGene + val pathValueOps = patchGene.operations.filterIsInstance() + assertTrue(pathValueOps.isNotEmpty(), "Seed 42L must produce at least one add/replace/test operation") + // All add/replace/test operations carry an ArrayGene value in this schema; + // the nested Tag DTO must therefore be collected. + assertNotNull(dtos["Tag"], "Nested Tag DTO must be collected for operations with array-of-objects value") + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/GeneToDtoJsonPatchTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/GeneToDtoJsonPatchTest.kt new file mode 100644 index 0000000000..c09c75612e --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/GeneToDtoJsonPatchTest.kt @@ -0,0 +1,88 @@ +package org.evomaster.core.output.dto + +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchFromPathGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.service.Randomness +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** + * Verifies that a [JsonPatchDocumentGene] (RFC 6902) is rendered by [GeneToDto] as a + * List instead of stringified JSON. + */ +class GeneToDtoJsonPatchTest { + + private val dtoName = GeneToDto.JSON_PATCH_OPERATION_DTO + + private fun patchDoc(seed: Long = 42L): JsonPatchDocumentGene { + val schema = ObjectGene("body", listOf(StringGene("name"), IntegerGene("age"))) + val doc = JsonPatchDocumentGene("patch", schema) + doc.doInitialize(Randomness().apply { updateSeed(seed) }) + return doc + } + + private fun setOpValue(line: String): String = + Regex("""\.setOp\("(.*?)"\)""").find(line)!!.groupValues[1] + + @Test + fun rendersAsListOfJsonPatchOperationDtosKotlin() { + val doc = patchDoc() + val dtoCall = GeneToDto(OutputFormat.KOTLIN_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false) + val calls = dtoCall.objectCalls + + assertEquals("list_${dtoName}_0", dtoCall.varName) + assertEquals("val list_${dtoName}_0 = mutableListOf<$dtoName>()", calls.first()) + + val ops = doc.operations + // One object instantiation and one add-to-list per operation + assertEquals(ops.size, calls.count { it.contains("= $dtoName()") }) + assertEquals(ops.size, calls.count { it.startsWith("list_${dtoName}_0.add(dto_${dtoName}_") }) + + // Every operation sets its "op", and the multiset of op names matches the document + val opNamesInCode = calls.filter { it.contains(".setOp(") }.map { setOpValue(it) } + assertEquals(ops.map { it.operationName }.sorted(), opNamesInCode.sorted()) + } + + @Test + fun setsOnlyTheFieldsRelevantToEachOperation() { + val doc = patchDoc() + val calls = GeneToDto(OutputFormat.KOTLIN_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false).objectCalls + + val ops = doc.operations + // "from" only for move/copy, "value" only for add/replace/test + assertEquals(ops.count { it is JsonPatchFromPathGene }, calls.count { it.contains(".setFrom(") }) + assertEquals(ops.count { it is JsonPatchPathValueGene }, calls.count { it.contains(".setValue(") }) + // "op" and "path" are present in every operation + assertEquals(ops.size, calls.count { it.contains(".setOp(") }) + assertEquals(ops.size, calls.count { it.contains(".setPath(") }) + } + + @Test + fun primitiveValuesAreInlinedAsLiterals() { + // Force every operation type to render; assert string values are quoted and int values are bare + val doc = patchDoc(seed = 7L) + val calls = GeneToDto(OutputFormat.KOTLIN_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false).objectCalls + + calls.filter { it.contains(".setValue(") }.forEach { line -> + val value = Regex("""\.setValue\((.*)\)""").find(line)!!.groupValues[1] + val isQuotedString = value.startsWith("\"") && value.endsWith("\"") + val isInt = value.toIntOrNull() != null + assertTrue(isQuotedString || isInt, "Unexpected non-literal value rendering: $line") + } + } + + @Test + fun rendersWithJavaSyntax() { + val doc = patchDoc() + val calls = GeneToDto(OutputFormat.JAVA_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false).objectCalls + + assertEquals("List<$dtoName> list_${dtoName}_0 = new ArrayList<$dtoName>();", calls.first()) + assertTrue(calls.any { it.contains("$dtoName dto_${dtoName}_0_1 = new $dtoName();") }) + } +}