Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -119,18 +120,36 @@ 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")
}
}
}

// 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, "String"))
dtoClass.addField(GeneToDto.FIELD_PATH, DtoField(GeneToDto.FIELD_PATH, "String"))
dtoClass.addField(GeneToDto.FIELD_FROM, DtoField(GeneToDto.FIELD_FROM, "String"))
dtoClass.addField(GeneToDto.FIELD_VALUE, DtoField(GeneToDto.FIELD_VALUE, anyType()))
dtoCollector[dtoName] = dtoClass

gene.operations.filterIsInstance<JsonPatchPathValueGene>().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()) "Object" else "Any"
}

private fun calculateDtoFromFixedMapGene(gene: FixedMapGene<*, *>, actionName: String) {
val dtoName = TestWriterUtils.safeVariableName(actionName)
val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) }
Expand Down
89 changes: 89 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,6 +41,15 @@ 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"
}

private val log: Logger = LoggerFactory.getLogger(GeneToDto::class.java)

private var dtoOutput: DtoOutput = if (outputFormat.isJava()) {
Expand Down Expand Up @@ -67,6 +81,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")
}
}
Expand All @@ -85,10 +100,84 @@ 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<JsonPatchOperation>, one DTO per active operation.
private fun getJsonPatchDtoCall(gene: JsonPatchDocumentGene, counters: MutableList<Int>): DtoCall {
val listVarName = "list_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}"
val result = mutableListOf<String>()
result.add(dtoOutput.getNewListStatement(JSON_PATCH_OPERATION_DTO, listVarName))

var operationCounter = 1
gene.operations.forEach { operation ->
val childCounter = mutableListOf<Int>().apply {
addAll(counters)
add(operationCounter++)
}
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<Int>): DtoCall {
val varName = "dto_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}"
val result = mutableListOf<String>()
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<Int>,
result: MutableList<String>
) {
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<Int>): DtoCall {
val dtoVarName = "dto_${dtoName}_${counters.joinToString("_")}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,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<JsonPatchOperation> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ 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.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
Expand Down Expand Up @@ -1641,6 +1644,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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ObjectGene> 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<RestIndividual>(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<JsonPatchPathValueGene>()
assertTrue(pathValueOps.isNotEmpty(), "Seed 42L must produce at least one add/replace/test operation")
// All add/replace/test operations carry an ArrayGene<ObjectGene> 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")
}
}
Loading
Loading