From 3de89cd2b7868378abf8b28365457454dbb506f2 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Mon, 18 May 2026 21:47:59 +0200 Subject: [PATCH 1/3] handling case of allOf with nested examples --- .../rest/builder/RestActionBuilderV3.kt | 50 ++++++++++++++-- .../problem/rest/RestActionBuilderV3Test.kt | 58 +++++++++++++++++++ .../constraints/multi_allof.yaml | 46 +++++++++++++++ .../constraints/single_allof.yaml | 34 +++++++++++ 4 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 core/src/test/resources/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml create mode 100644 core/src/test/resources/swagger/artificial/defaultandexamples/constraints/single_allof.yaml 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 681684915c..e5122a575d 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 @@ -1369,12 +1369,11 @@ object RestActionBuilderV3 { } /** - * * handled constraints include * - allOf * - anyOf * - oneOf - * - not (OpenAPI not support this yet) + * - not */ private fun assembleObjectGeneWithConstraints( name: String, @@ -1394,16 +1393,36 @@ object RestActionBuilderV3 { https://spec.openapis.org/oas/latest.html#discriminator-object */ - if (!options.enableConstraintHandling) + if (!options.enableConstraintHandling){ return assembleObjectGene(name, options, schema, fields, additionalFieldTemplate, referenceTypeName, examples, messages) + } + + /* + THIS IS VERY TRICKY + in theory, the use of allOf/anyOf/oneOf/not provides a rich language where many different kinds of + constraints can be specified. + this is the case when "const" is used in schemas to specify particular values, e.g., oneOf could be then + used to specify only particular combinations of field values. + the use of "const" is also quite tricky with "not", as can use to say some specific value combinations are not valid. + furthermore, those keywords are NOT mutually exclusive. + + support all possible constraint would be a GIGANTIC piece of work... yet, these kinds of constraints do + not seem so common, apart for allOf on merging fields. + TODO we should have full support, but not high priority task + */ + + /* + A further complication is how to use, and interpret, "examples" values (if any). + Eg. what to nested referenced $ref objects that have on examples? + should those be merged/hanled like the fields? + TODO possibly something to handle (but again, not high priority) + */ val allOf = schema.allOf?.map { s-> - //createObjectGene(name, s, swagger, history, null, enableConstraintHandling) getGene(name, s, schemaHolder,currentSchema, history, null, options, messages = messages, examples = examples) } val anyOf = schema.anyOf?.map { s-> - //createObjectGene(name, s, swagger, history, null, enableConstraintHandling) getGene(name, s, schemaHolder,currentSchema, history, null, options, messages = messages, examples = examples) } @@ -1413,7 +1432,6 @@ object RestActionBuilderV3 { } val oneOf = schema.oneOf?.map { s-> - //createObjectGene(name, s, swagger, history, null, enableConstraintHandling) getGene(name, s, schemaHolder,currentSchema, history, null, options = options, messages = messages) } @@ -1423,8 +1441,15 @@ object RestActionBuilderV3 { } if (!allOf.isNullOrEmpty()){ + + if(allOf.size == 1){ + //just use it as it is + return allOf[0] + } + val allFields = allOf.mapNotNull { when (it) { + is ChoiceGene<*> -> extractOriginalObject(it)?.fields is ObjectGene -> it.fields else -> null } @@ -1495,6 +1520,19 @@ object RestActionBuilderV3 { //TODO not } + private fun extractOriginalObject(choice: ChoiceGene<*>): ObjectGene? { + /* + this only makes sense if we are in this case: + Choice( Choice(examples), Object) + in which we would return Object, null otherwise + */ + val children = choice.getViewOfChildren() + if(children.size != 2 || children.none { it is UserExamplesGene && it.isUsedForExamples() }){ + return null + } + return children.find { it is ObjectGene } as ObjectGene? + } + /** * assemble ObjectGene based on [fields] and [additionalFieldTemplate] */ diff --git a/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt b/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt index a87e96af17..a5358e0ee1 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/rest/RestActionBuilderV3Test.kt @@ -2289,4 +2289,62 @@ class RestActionBuilderV3Test{ assertTrue(withFooAgain.contains("xfoo"), withFooAgain) assertTrue(withFooAgain.contains("ybar"), withFooAgain) } + + + @Test + fun testSingleAllOf() { + + val path = "/swagger/artificial/defaultandexamples/constraints/single_allof.yaml" + + val a = loadAndAssertActions(path, 1, RestActionBuilderV3.Options(probUseExamples = 1.0)) + .values.first() + + val rand = Randomness() + a.doInitialize(rand) + + val found = mutableSetOf() + + for (i in 0..100) { + a.randomize(rand, false) + val s = a.seeTopGenes().first().getValueAsRawString() + found.add(s) + if(found.size == 2){ + break + } + } + + //both examples must be there + assertEquals(2, found.size) + assertTrue(found.any{it.contains("Foo")}) + assertTrue(found.any{it.contains("Bar")}) + } + + + @Test + fun testMultiAllOf() { + + val path = "/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml" + + val a = loadAndAssertActions(path, 1, RestActionBuilderV3.Options(probUseExamples = 1.0)) + .values.first() + + val rand = Randomness() + a.doInitialize(rand) + + val found = mutableSetOf() + + for (i in 0..100) { + a.randomize(rand, false) + val s = a.seeTopGenes().first().getValueAsRawString() + found.add(s) + } + + /* + Only 1 example should be there, the one declared at top level. + TODO: maybe in future we merge examples as well... in that case, then + would need to update this test + */ + assertTrue(found.any{it.contains("Foo")}) + assertEquals(1, found.size) + } } diff --git a/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml new file mode 100644 index 0000000000..20e076c79d --- /dev/null +++ b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/multi_allof.yaml @@ -0,0 +1,46 @@ +--- +openapi: 3.1.0 +info: + title: a schema + version: 1.0.0 +servers: + - url: "/v2" +paths: + "/api": + post: + requestBody: + required: true + content: + application/json: + schema: + allOf: + - type: object + additionalProperties: false + properties: + id: + type: integer + name: + type: string + examples: + - id : 42 + name: Bar + - $ref: '#/components/schemas/Dto' + examples: + Foo: + value: + id: 123 + name: Foo + extra: some extra + responses: + '200': + description: OK +components: + schemas: + Dto: + type: object + additionalProperties: false + properties: + extra: + type: string + example: + extra: Hello \ No newline at end of file diff --git a/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/single_allof.yaml b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/single_allof.yaml new file mode 100644 index 0000000000..b229f428f2 --- /dev/null +++ b/core/src/test/resources/swagger/artificial/defaultandexamples/constraints/single_allof.yaml @@ -0,0 +1,34 @@ +--- +openapi: 3.1.0 +info: + title: a schema + version: 1.0.0 +servers: + - url: "/v2" +paths: + "/api": + post: + requestBody: + required: true + content: + application/json: + schema: + allOf: + - type: object + additionalProperties: false + properties: + id: + type: integer + name: + type: string + examples: + - id : 42 + name: Bar + examples: + Foo: + value: + id: 123 + name: Foo + responses: + '200': + description: OK \ No newline at end of file From c3e8dde7025b87839e2345511d88e2fd47839393 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Mon, 18 May 2026 21:49:03 +0200 Subject: [PATCH 2/3] removed incorrect returns --- .../core/problem/rest/builder/RestActionBuilderV3.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 e5122a575d..dae1d19e40 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 @@ -1427,8 +1427,7 @@ object RestActionBuilderV3 { } if (!allOf.isNullOrEmpty() && !anyOf.isNullOrEmpty()){ - messages.add("Cannot handle allOf and oneOf at same time for a schema with name $name") - return assembleObjectGene(name, options, schema, fields, additionalFieldTemplate, referenceTypeName, examples, messages) + messages.add("Currently cannot handle allOf and oneOf at same time for a schema with name $name") } val oneOf = schema.oneOf?.map { s-> @@ -1436,8 +1435,7 @@ object RestActionBuilderV3 { } if (!oneOf.isNullOrEmpty() && (!allOf.isNullOrEmpty() || !anyOf.isNullOrEmpty())){ - messages.add("cannot handle oneOf and allOf/oneOf at same time for a schema with name $name") - return assembleObjectGene(name, options, schema, fields, additionalFieldTemplate, referenceTypeName, examples, messages) + messages.add("Currently cannot handle oneOf and allOf/oneOf at same time for a schema with name $name") } if (!allOf.isNullOrEmpty()){ From 430b05aae86c24ebae7fb6b4f2ed7de67b6168ac Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Tue, 19 May 2026 08:28:20 +0200 Subject: [PATCH 3/3] fixed bug in wrong handling of allOf --- .../evomaster/core/problem/rest/builder/RestActionBuilderV3.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dae1d19e40..306d746182 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 @@ -1440,7 +1440,7 @@ object RestActionBuilderV3 { if (!allOf.isNullOrEmpty()){ - if(allOf.size == 1){ + if(allOf.size == 1 && fields.isEmpty()){ //just use it as it is return allOf[0] }