Skip to content

Commit b8de7e7

Browse files
authored
Merge pull request #1379 from WebFuzzing/phg/supportChoiceGene
Support for ChoiceGene in DTO generated test cases
2 parents 0240a08 + 854b85e commit b8de7e7

5 files changed

Lines changed: 95 additions & 87 deletions

File tree

core-tests/e2e-tests/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertRest.kt

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,15 @@ class DtoReflectiveAssertRest {
1414
return ResponseEntity.ok("OK")
1515
}
1616

17-
// TODO: Restore when support for ChoiceGene has been added
18-
// @PostMapping(path = ["/anyof"], consumes = [MediaType.APPLICATION_JSON_VALUE])
19-
// open fun anyof(@RequestBody body: AnyOfDto) : ResponseEntity<String>{
20-
// return ResponseEntity.ok("OK")
21-
// }
22-
23-
// TODO: Restore when support for ChoiceGene has been added
24-
// @PostMapping(path = ["/oneof"], consumes = [MediaType.APPLICATION_JSON_VALUE])
25-
// open fun oneof(@RequestBody body: OneOfDto) : ResponseEntity<String>{
26-
// return ResponseEntity.ok("OK")
27-
// }
17+
@PostMapping(path = ["/anyof"], consumes = [MediaType.APPLICATION_JSON_VALUE])
18+
open fun anyof(@RequestBody body: AnyOfDto) : ResponseEntity<String>{
19+
return ResponseEntity.ok("OK")
20+
}
21+
22+
@PostMapping(path = ["/oneof"], consumes = [MediaType.APPLICATION_JSON_VALUE])
23+
open fun oneof(@RequestBody body: OneOfDto) : ResponseEntity<String>{
24+
return ResponseEntity.ok("OK")
25+
}
2826

2927
@PostMapping(path = ["/primitiveTypes"], consumes = [MediaType.APPLICATION_JSON_VALUE])
3028
open fun primitiveTypes(@RequestBody body: PrimitiveTypesDto) : ResponseEntity<String>{

core-tests/e2e-tests/spring-rest-openapi-v3/src/main/resources/static/openapi-dto-reflective-assert.yaml

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
openapi: 3.0.3
22
info:
3-
title: allOf with Component Schemas
3+
title: Schema for E2E DTO Testing
44
version: 1.0.0
55

66
paths:
@@ -126,47 +126,46 @@ paths:
126126
responses:
127127
'200':
128128
description: OK
129-
# TODO: Restore when support for ChoiceGene has been added
130-
# /anyof:
131-
# post:
132-
# summary: Accepts either an email (component) or phone (inline)
133-
# requestBody:
134-
# required: true
135-
# content:
136-
# application/json:
137-
# schema:
138-
# anyOf:
139-
# - $ref: '#/components/schemas/EmailPayload'
140-
# - type: object
141-
# required: [phone]
142-
# properties:
143-
# phone:
144-
# type: string
145-
# responses:
146-
# '200':
147-
# description: OK
148-
# /oneof:
149-
# post:
150-
# summary: Accepts either cat or dog object (but not both)
151-
# requestBody:
152-
# required: true
153-
# content:
154-
# application/json:
155-
# schema:
156-
# oneOf:
157-
# - type: object
158-
# required: [cat]
159-
# properties:
160-
# cat:
161-
# type: string
162-
# - type: object
163-
# required: [mouse]
164-
# properties:
165-
# mouse:
166-
# type: string
167-
# responses:
168-
# '200':
169-
# description: OK
129+
/anyof:
130+
post:
131+
summary: Accepts either an email (component) or phone (inline)
132+
requestBody:
133+
required: true
134+
content:
135+
application/json:
136+
schema:
137+
anyOf:
138+
- $ref: '#/components/schemas/EmailPayload'
139+
- type: object
140+
required: [phone]
141+
properties:
142+
phone:
143+
type: string
144+
responses:
145+
'200':
146+
description: OK
147+
/oneof:
148+
post:
149+
summary: Accepts either cat or mouse object (but not both)
150+
requestBody:
151+
required: true
152+
content:
153+
application/json:
154+
schema:
155+
oneOf:
156+
- type: object
157+
required: [cat]
158+
properties:
159+
cat:
160+
type: string
161+
- type: object
162+
required: [mouse]
163+
properties:
164+
mouse:
165+
type: string
166+
responses:
167+
'200':
168+
description: OK
170169

171170
components:
172171
schemas:

core-tests/e2e-tests/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class DtoReflectiveAssertEMTest: SpringTestBase() {
3737

3838
Assertions.assertTrue(solution.individuals.size >= 1)
3939
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/allof", "OK")
40+
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/anyof", "OK")
41+
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/oneof", "OK")
4042
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/primitiveTypes", "OK")
4143
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/parent", "OK")
4244
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/items-inline", "OK")
@@ -47,9 +49,8 @@ class DtoReflectiveAssertEMTest: SpringTestBase() {
4749
assertParentAndChildDtosCreated()
4850
assertAllOfDtoCreated()
4951
assertItemsInlineDtoCreated()
50-
// TODO: Restore when support for ChoiceGene has been added
51-
// assertAnyOfDtoCreated()
52-
// assertOneOfDtoCreated()
52+
assertAnyOfDtoCreated()
53+
assertOneOfDtoCreated()
5354
}
5455

5556
private fun assertPrimitiveTypeDtoCreated() {

core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.evomaster.core.search.gene.placeholder.CycleObjectGene
1717
import org.evomaster.core.search.gene.regex.RegexGene
1818
import org.evomaster.core.search.gene.string.Base64StringGene
1919
import org.evomaster.core.search.gene.string.StringGene
20+
import org.evomaster.core.search.gene.wrapper.ChoiceGene
2021
import org.evomaster.core.search.gene.wrapper.OptionalGene
2122
import org.evomaster.core.utils.StringUtils
2223

@@ -37,46 +38,48 @@ class GeneToDto(
3738
}
3839

3940
/**
40-
* @param leafGene to obtain the refType if the component is defined with a name
41+
* @param gene to obtain the refType if the component is defined with a name
4142
* @param fallback to provide a fallback on the DTO named with the action if the component is defined inline
4243
* @param capitalize to determine if the DTO string name must be capitalized for test case writing
4344
*
4445
* @return the DTO name that will be used to instantiate the first variable
4546
*/
46-
fun getDtoName(leafGene: Gene, fallback: String, capitalize: Boolean): String {
47-
return when (leafGene) {
48-
is ObjectGene -> TestWriterUtils.safeVariableName(leafGene.refType?:fallback)
47+
fun getDtoName(gene: Gene, fallback: String, capitalize: Boolean): String {
48+
return when (gene) {
49+
is ObjectGene -> TestWriterUtils.safeVariableName(gene.refType?:fallback)
4950
is ArrayGene<*> -> {
50-
val template = leafGene.template
51+
val template = gene.template
5152
if (template is ObjectGene) {
5253
TestWriterUtils.safeVariableName(template.refType?:fallback)
5354
} else {
5455
// TODO handle arrays of basic data types
5556
return getListType(fallback, template, capitalize)
5657
}
5758
}
58-
else -> throw IllegalStateException("Gene $leafGene is not supported for DTO payloads for action: $fallback")
59+
is ChoiceGene<*> -> TestWriterUtils.safeVariableName(fallback)
60+
else -> throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $fallback")
5961
}
6062
}
6163

6264
/**
6365
* @param gene from which to extract the setter calls
6466
* @param dtoName that will be instantiated for payload
65-
* @param counter list to provide uniqueness under the same DTO being used in a single test case
67+
* @param counters list to provide uniqueness under the same DTO being used in a single test case
6668
* @param capitalize to determine if the DTO string name must be capitalized for test case writing
6769
*
6870
* @return a [DtoCall] object that can be written to the test case
6971
*/
70-
fun getDtoCall(gene: Gene, dtoName: String, counter: MutableList<Int>, capitalize: Boolean): DtoCall {
72+
fun getDtoCall(gene: Gene, dtoName: String, counters: MutableList<Int>, capitalize: Boolean): DtoCall {
7173
return when(gene) {
72-
is ObjectGene -> getObjectDtoCall(gene, dtoName, counter, capitalize)
73-
is ArrayGene<*> -> getArrayDtoCall(gene, dtoName, counter, null, capitalize)
74+
is ObjectGene -> getObjectDtoCall(gene, dtoName, counters)
75+
is ArrayGene<*> -> getArrayDtoCall(gene, dtoName, counters, null, capitalize)
76+
is ChoiceGene<*> -> getDtoCall(gene.activeGene(), dtoName, counters, capitalize)
7477
else -> throw RuntimeException("BUG: Gene $gene (with type ${this::class.java.simpleName}) should not be creating DTOs")
7578
}
7679
}
7780

78-
private fun getObjectDtoCall(gene: ObjectGene, dtoName: String, counter: MutableList<Int>, capitalize: Boolean): DtoCall {
79-
val dtoVarName = "dto_${dtoName}_${counter.joinToString("_")}"
81+
private fun getObjectDtoCall(gene: ObjectGene, dtoName: String, counters: MutableList<Int>): DtoCall {
82+
val dtoVarName = "dto_${dtoName}_${counters.joinToString("_")}"
8083

8184
val result = mutableListOf<String>()
8285
result.add(dtoOutput.getNewObjectStatement(dtoName, dtoVarName))
@@ -90,13 +93,13 @@ class GeneToDto(
9093
val attributeName = it.name
9194
when (leafGene) {
9295
is ObjectGene -> {
93-
val childDtoCall = getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), counter, true)
96+
val childDtoCall = getDtoCall(leafGene, getDtoName(leafGene, attributeName, true), counters, true)
9497

9598
result.addAll(childDtoCall.objectCalls)
9699
result.add(dtoOutput.getSetterStatement(dtoVarName, attributeName, childDtoCall.varName))
97100
}
98101
is ArrayGene<*> -> {
99-
val childDtoCall = getArrayDtoCall(leafGene, getDtoName(leafGene, attributeName, true), counter, attributeName, true)
102+
val childDtoCall = getArrayDtoCall(leafGene, getDtoName(leafGene, attributeName, true), counters, attributeName, true)
100103

101104
result.addAll(childDtoCall.objectCalls)
102105
result.add(dtoOutput.getSetterStatement(dtoVarName, attributeName, childDtoCall.varName))
@@ -110,20 +113,20 @@ class GeneToDto(
110113
return DtoCall(dtoVarName, result)
111114
}
112115

113-
private fun getArrayDtoCall(gene: ArrayGene<*>, dtoName: String, counter: MutableList<Int>, targetAttribute: String?, capitalize: Boolean): DtoCall {
116+
private fun getArrayDtoCall(gene: ArrayGene<*>, dtoName: String, counters: MutableList<Int>, targetAttribute: String?, capitalize: Boolean): DtoCall {
114117
val result = mutableListOf<String>()
115118
val template = gene.template
116119

117120
val listType = getListType(dtoName,template, capitalize)
118-
val listVarName = "list_${targetAttribute?:dtoName}_${counter.joinToString("_")}"
121+
val listVarName = "list_${targetAttribute?:dtoName}_${counters.joinToString("_")}"
119122
result.add(dtoOutput.getNewListStatement(listType, listVarName))
120123

121124
if (template is ObjectGene) {
122125
val childDtoName = template.refType?: if (capitalize) StringUtils.capitalization(dtoName) else dtoName
123126
var listCounter = 1
124127
gene.getViewOfElements().forEach {
125128
val childCounter = mutableListOf<Int>()
126-
childCounter.addAll(counter)
129+
childCounter.addAll(counters)
127130
childCounter.add(listCounter++)
128131
val childDtoCall = getDtoCall(it,childDtoName, childCounter, true)
129132
result.addAll(childDtoCall.objectCalls)

core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.evomaster.core.output.TestWriterUtils
99
import org.evomaster.core.output.TestWriterUtils.formatJsonWithEscapes
1010
import org.evomaster.core.output.auth.CookieWriter
1111
import org.evomaster.core.output.auth.TokenWriter
12+
import org.evomaster.core.output.dto.DtoCall
1213
import org.evomaster.core.output.dto.GeneToDto
1314
import org.evomaster.core.problem.enterprise.EnterpriseActionGroup
1415
import org.evomaster.core.problem.externalservice.httpws.HttpExternalServiceAction
@@ -23,6 +24,7 @@ import org.evomaster.core.search.FitnessValue
2324
import org.evomaster.core.search.action.Action
2425
import org.evomaster.core.search.action.ActionResult
2526
import org.evomaster.core.search.action.EvaluatedAction
27+
import org.evomaster.core.search.gene.Gene
2628
import org.evomaster.core.search.gene.ObjectGene
2729
import org.evomaster.core.search.gene.collection.ArrayGene
2830
import org.evomaster.core.search.gene.utils.GeneUtils
@@ -127,32 +129,37 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() {
127129
val choiceGene = primaryGene.getWrappedGene(ChoiceGene::class.java)
128130
val actionName = call.getName()
129131
if (choiceGene != null) {
130-
// TODO add support for payloads from choice genes
132+
// We only generate DTOs for ChoiceGene objects that contain either an ObjectGene or ArrayGene in their
133+
// genes. This check is necessary since when using `example` and `default` entries,
134+
// "primitive" genes are represented as ChoiceGene with an EnumGene and the actual
135+
// String/Integer/Number/etc gene
131136
if (hasObjectOrArrayGene(choiceGene)) {
132-
// this because when using `example` and `default` entries, "primitive" genes are represented as ChoiceGene with
133-
// an EnumGene and the actual String/Integer/Number/etc gene
134-
throw IllegalStateException("Choice genes not yet supported for dto payload")
137+
return generateDtoCall(choiceGene, actionName, lines).varName
135138
}
136139
} else {
137140
val leafGene = primaryGene.getLeafGene()
138141
if (leafGene is ObjectGene || leafGene is ArrayGene<*>) {
139-
val geneToDto = GeneToDto(format)
140-
141-
val dtoName = geneToDto.getDtoName(leafGene, actionName, false)
142-
val dtoCall = geneToDto.getDtoCall(leafGene, dtoName, mutableListOf(counter++), false)
143-
144-
dtoCall.objectCalls.forEach {
145-
lines.add(it)
146-
}
147-
lines.addEmpty()
148-
return dtoCall.varName
142+
return generateDtoCall(leafGene, actionName, lines).varName
149143
}
150144
}
151145

152146
}
153147
return ""
154148
}
155149

150+
private fun generateDtoCall(gene: Gene, actionName: String, lines: Lines): DtoCall {
151+
val geneToDto = GeneToDto(format)
152+
153+
val dtoName = geneToDto.getDtoName(gene, actionName, false)
154+
val dtoCall = geneToDto.getDtoCall(gene, dtoName, mutableListOf(counter++), false)
155+
156+
dtoCall.objectCalls.forEach {
157+
lines.add(it)
158+
}
159+
lines.addEmpty()
160+
return dtoCall
161+
}
162+
156163
private fun hasObjectOrArrayGene(gene: ChoiceGene<*>): Boolean {
157164
return gene.getViewOfChildren().any { it is ObjectGene || it is ArrayGene<*> }
158165
}

0 commit comments

Comments
 (0)