From 0a6b6a3b93406770309c90bdbead3fa451ebfe8e Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Mon, 27 Oct 2025 13:13:45 +0100 Subject: [PATCH 1/3] collecting info on example names --- .../rest/builder/RestActionBuilderV3.kt | 74 ++++++++++++------- .../core/search/gene/collection/EnumGene.kt | 17 ++++- .../core/search/gene/wrapper/ChoiceGene.kt | 19 ++++- 3 files changed, 80 insertions(+), 30 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 49b3f4feec..bd6f29290e 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 @@ -559,21 +559,26 @@ object RestActionBuilderV3 { example: Any?, examples: Map?, messages: MutableList - ) : List{ + ) : List>{ + + /** + * List of pairs value/name. + * the name if optional, as only defined for "examples" + */ + val data = mutableListOf>() - val data = mutableListOf() if(example != null){ - data.add(example) + data.add(Pair(example,null)) } if(!examples.isNullOrEmpty()){ - examples.values.forEach { - val exm = if(it.`$ref` != null){ - SchemaUtils.getReferenceExample(schemaHolder, currentSchema, it.`$ref`, messages) + examples.entries.forEach { + val exm = if(it.value.`$ref` != null){ + SchemaUtils.getReferenceExample(schemaHolder, currentSchema, it.value.`$ref`, messages) } else { - it + it.value } if(exm != null) { - data.add(exm.value) + data.add(Pair(exm.value, it.key)) } } } @@ -780,7 +785,7 @@ object RestActionBuilderV3 { referenceClassDef: String?, options: Options, isInPath: Boolean = false, - examples: List = listOf(), + examples: List> = listOf(), messages: MutableList ): Gene { @@ -974,7 +979,7 @@ object RestActionBuilderV3 { history: Deque, referenceTypeName: String?, options: Options, - examples: List, + examples: List>, messages: MutableList ): Gene { @@ -1131,7 +1136,7 @@ object RestActionBuilderV3 { history: Deque, referenceTypeName: String?, options: Options, - examples: List, + examples: List>, messages: MutableList ) : Gene{ /* @@ -1237,7 +1242,7 @@ object RestActionBuilderV3 { fields: List, additionalFieldTemplate: PairGene?, referenceTypeName: String?, - otherExampleValues: List, + otherExampleValues: List>, messages: MutableList ) : Gene{ if (fields.isEmpty()) { @@ -1269,19 +1274,29 @@ object RestActionBuilderV3 { val exampleValue = if(options.probUseExamples > 0) schema.example else null val multiExampleValues = if(options.probUseExamples > 0) schema.examples else null - val examples = mutableListOf() + val examples = mutableListOf>() if(exampleValue != null){ duplicateObjectWithExampleFields(name,mainGene, exampleValue)?.let { - examples.add(it) + examples.add(Pair(it,null)) } } if(multiExampleValues != null ){ - examples.addAll(multiExampleValues.mapNotNull { duplicateObjectWithExampleFields(name,mainGene, it) }) + examples.addAll(multiExampleValues + .mapNotNull { duplicateObjectWithExampleFields(name,mainGene, it) } + .map { Pair(it, null) } + ) } - examples.addAll(otherExampleValues.mapNotNull { duplicateObjectWithExampleFields(name,mainGene, it) }) + examples.addAll(otherExampleValues + .mapNotNull { duplicateObjectWithExampleFields(name,mainGene, it.first) + ?.let { obj -> Pair(obj,it.second) } + } + ) + + val v = examples.map { it.first } //values + val n = examples.map{it.second} // names val exampleGene = if(examples.isNotEmpty()){ - ChoiceGene(EXAMPLES_NAME, examples) + ChoiceGene(EXAMPLES_NAME, v, valueNames = n) } else null val defaultGene = if(defaultValue != null){ duplicateObjectWithExampleFields("default", mainGene, defaultValue) @@ -1372,7 +1387,7 @@ object RestActionBuilderV3 { options: Options, collectionTemplate: Gene?, isInPath: Boolean, - examples: List, + examples: List>, messages: MutableList ) : Gene{ @@ -1412,7 +1427,7 @@ object RestActionBuilderV3 { collectionTemplate: Gene? = null, //might need to add extra constraints if in path isInPath: Boolean, - exampleObjects: List, + exampleObjects: List>, format: String? = null, messages: MutableList ) : Gene{ @@ -1544,10 +1559,12 @@ object RestActionBuilderV3 { val exampleValue = if(options.probUseExamples > 0) schema.example else null val multiExampleValues = if(options.probUseExamples > 0) schema.examples else null - val examples = mutableListOf() + //value and optional name + val examples = mutableListOf>() + if(exampleValue != null) { val raw = asRawString(exampleValue) - examples.add(raw) + examples.add(Pair(raw,null)) val arrayM = if(raw.startsWith("[")) "If you are wrongly passing to it an array of values, " + "the parser would read it as an array string or simply ignore it. " else "" @@ -1558,9 +1575,9 @@ object RestActionBuilderV3 { } if(multiExampleValues != null && multiExampleValues.isNotEmpty()){ //possibly bug in parser, but it was reading strings values double-quoted in this case - examples.addAll(multiExampleValues.map { asRawString(it) }) + examples.addAll(multiExampleValues.map { Pair(asRawString(it), null) }) } - examples.addAll( exampleObjects.map { asRawString(it) }) + examples.addAll( exampleObjects.map { Pair(asRawString(it.first), it.second) }) val defaultGene = if(defaultValue != null){ @@ -1581,15 +1598,20 @@ object RestActionBuilderV3 { } } else null + //values + val v = examples.map { it.first } + //optional names + val n = examples.map { it.second } + val exampleGene = if(examples.isNotEmpty()){ when{ NumberGene::class.java.isAssignableFrom(geneClass) - -> EnumGene(EXAMPLES_NAME, examples,0,true) + -> EnumGene(EXAMPLES_NAME, v,0,true, n) geneClass == StringGene::class.java || geneClass == Base64StringGene::class.java || geneClass == RegexGene::class.java - -> EnumGene(EXAMPLES_NAME, examples,0,false) + -> EnumGene(EXAMPLES_NAME, v,0,false, n) //TODO Arrays else -> { @@ -1692,7 +1714,7 @@ object RestActionBuilderV3 { currentSchema: SchemaOpenAPI, history: Deque = ArrayDeque(), options: Options, - examples: List, + examples: List>, messages: MutableList ): Gene { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt index 05f76b59fc..4891c05337 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt @@ -30,7 +30,12 @@ class EnumGene>( * to avoid specifying exact types. Still, should not be printed out as string. * Recall that an enum is just a group of constants that cannot be mutated */ - private val treatAsNotString : Boolean = false + private val treatAsNotString : Boolean = false, + /** + * An optional list of 'names' for each/some of the values in this enumeration. + * This is usually just extra information, eg, to recognize named "examples" in OpenAPI schemas + */ + private val valueNames: List? = null ) : SimpleGene(name) { companion object { @@ -81,6 +86,10 @@ class EnumGene>( if (index < 0 || index >= values.size) { throw IllegalArgumentException("Invalid index: $index") } + + if(valueNames != null && valueNames.size != values.size) { + throw IllegalArgumentException("Invalid valueNames size: ${valueNames.size}!=${values.size}") + } } if(treatAsNotString && values.isNotEmpty() && values[0] !is String){ @@ -98,7 +107,7 @@ class EnumGene>( override fun copyContent(): Gene { //recall: "values" is immutable - return EnumGene(name, values, index, treatAsNotString) + return EnumGene(name, values, index, treatAsNotString, valueNames) } override fun setValueWithRawString(value: String) { @@ -183,6 +192,10 @@ class EnumGene>( return values[index].toString() } + fun getValueName(): String?{ + return valueNames?.get(index) + } + override fun copyValueFrom(other: Gene): Boolean { if (other !is EnumGene<*>) { throw IllegalArgumentException("Invalid gene type ${other.javaClass}") diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt index dc523e6005..b4df860eb4 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt @@ -26,7 +26,12 @@ class ChoiceGene( /** * Potentially, associate different probabilities for the different choices */ - probabilities: List? = null + probabilities: List? = null, + /** + * Optional list of name values for each of choices. + * This is usually just extra information, eg, to recognize named "examples" in OpenAPI schemas + */ + valueNames: List? = null, ) : CompositeFixedGene(name, geneChoices), WrapperGene where T : Gene { @@ -39,6 +44,8 @@ class ChoiceGene( private val probabilities = probabilities?.toList() //make a copy + private val valueNames = valueNames?.toList() + init { if (geneChoices.isEmpty()) { throw IllegalArgumentException("The list of gene choices cannot be empty") @@ -50,6 +57,9 @@ class ChoiceGene( if(probabilities != null && probabilities.size != geneChoices.size){ throw IllegalArgumentException("If probabilities are defined, then they must be same number as the genes") } + if(valueNames != null && valueNames.size != geneChoices.size) { + throw IllegalArgumentException("If value names are defined, then they must be same number as the genes") + } } @@ -167,6 +177,10 @@ class ChoiceGene( .getValueAsRawString() } + fun getValueName(): String?{ + return valueNames?.get(activeGeneIndex) + } + /** * Copies the value of the other gene. The other gene * does not have to be [ChoiceGene]. @@ -260,7 +274,8 @@ class ChoiceGene( name, activeChoice = this.activeGeneIndex, geneChoices = this.geneChoices.map { it.copy() }.toList(), - probabilities = probabilities // immutable + probabilities = probabilities, // immutable + valueNames = valueNames // immutable ) /** From a894a916e7f310be44d8f8d49789f2890112b87e Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Mon, 27 Oct 2025 13:39:19 +0100 Subject: [PATCH 2/3] improved writing of examples comments --- .../core/output/service/RestTestCaseWriter.kt | 28 ++++++++++++++++++- .../core/search/gene/collection/EnumGene.kt | 5 ++-- .../gene/interfaces/NamedExamplesGene.kt | 10 +++++++ .../core/search/gene/wrapper/ChoiceGene.kt | 5 ++-- 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/NamedExamplesGene.kt diff --git a/core/src/main/kotlin/org/evomaster/core/output/service/RestTestCaseWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/service/RestTestCaseWriter.kt index 36cc84c686..7870281982 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/service/RestTestCaseWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/service/RestTestCaseWriter.kt @@ -6,6 +6,7 @@ import org.evomaster.core.output.Lines import org.evomaster.core.output.SqlWriter import org.evomaster.core.output.TestCase import org.evomaster.core.output.TestWriterUtils +import org.evomaster.core.problem.api.param.Param import org.evomaster.core.problem.enterprise.EnterpriseActionResult import org.evomaster.core.problem.httpws.HttpWsAction import org.evomaster.core.problem.httpws.HttpWsCallResult @@ -15,12 +16,18 @@ import org.evomaster.core.problem.rest.data.RestCallResult import org.evomaster.core.problem.rest.data.RestIndividual import org.evomaster.core.problem.rest.link.RestLinkParameter import org.evomaster.core.problem.rest.param.BodyParam +import org.evomaster.core.problem.rest.param.HeaderParam +import org.evomaster.core.problem.rest.param.PathParam +import org.evomaster.core.problem.rest.param.QueryParam import org.evomaster.core.problem.rest.service.CallGraphService import org.evomaster.core.search.action.Action import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.EvaluatedIndividual import org.evomaster.core.search.Individual +import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.interfaces.NamedExamplesGene import org.evomaster.core.search.gene.utils.GeneUtils +import org.evomaster.core.search.gene.wrapper.ChoiceGene import org.evomaster.core.utils.StringUtils import org.slf4j.LoggerFactory import java.nio.file.Path @@ -542,6 +549,25 @@ class RestTestCaseWriter : HttpWsTestCaseWriter { return ind.seeFullTreeGenes() .filter { it.name == RestActionBuilderV3.EXAMPLES_NAME } .filter { it.staticCheckIfImpactPhenotype() } - .map { it.getValueAsRawString() } + .map { + val name = if(it is NamedExamplesGene){ + "(${it.getValueName()?: "-"}) " + } else { + "" + } + + val param = it.getFirstParent { p -> p is Param } + val pName = when(param) { + is QueryParam -> "QUERY: ${param.name}" + is HeaderParam -> "HEADER: ${param.name}" + is PathParam -> "PATH: ${param.name}" + is BodyParam -> "BODY" + else -> "" + } + + val value = it.getValueAsRawString() + + "$name$pName -> $value" + } } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt index 4891c05337..162762bb79 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/collection/EnumGene.kt @@ -2,6 +2,7 @@ package org.evomaster.core.search.gene.collection import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.interfaces.NamedExamplesGene import org.evomaster.core.search.gene.string.StringGene import org.evomaster.core.search.gene.root.SimpleGene import org.evomaster.core.search.gene.utils.GeneUtils @@ -36,7 +37,7 @@ class EnumGene>( * This is usually just extra information, eg, to recognize named "examples" in OpenAPI schemas */ private val valueNames: List? = null -) : SimpleGene(name) { +) : SimpleGene(name), NamedExamplesGene { companion object { @@ -192,7 +193,7 @@ class EnumGene>( return values[index].toString() } - fun getValueName(): String?{ + override fun getValueName(): String?{ return valueNames?.get(index) } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/NamedExamplesGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/NamedExamplesGene.kt new file mode 100644 index 0000000000..272c22949e --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/NamedExamplesGene.kt @@ -0,0 +1,10 @@ +package org.evomaster.core.search.gene.interfaces + +/** + * A gene representing possible different examples provided by the user. + * Such examples might have unique names/ids used to easily identify them + */ +interface NamedExamplesGene { + + fun getValueName(): String? +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt index b4df860eb4..b2ccdcad91 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/wrapper/ChoiceGene.kt @@ -3,6 +3,7 @@ package org.evomaster.core.search.gene.wrapper import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.interfaces.NamedExamplesGene import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.service.AdaptiveParameterControl @@ -33,7 +34,7 @@ class ChoiceGene( */ valueNames: List? = null, -) : CompositeFixedGene(name, geneChoices), WrapperGene where T : Gene { +) : CompositeFixedGene(name, geneChoices), NamedExamplesGene, WrapperGene where T : Gene { companion object { private val log: Logger = LoggerFactory.getLogger(ChoiceGene::class.java) @@ -177,7 +178,7 @@ class ChoiceGene( .getValueAsRawString() } - fun getValueName(): String?{ + override fun getValueName(): String?{ return valueNames?.get(activeGeneIndex) } From 482520cb0d4a59b995ec731688309eb10bd02aba Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Mon, 27 Oct 2025 13:40:59 +0100 Subject: [PATCH 3/3] updated release notes --- release_notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/release_notes.md b/release_notes.md index ab2c2f8d4e..e0a4fca48a 100644 --- a/release_notes.md +++ b/release_notes.md @@ -11,6 +11,9 @@ Under development in `master` branch. - Option _--disabledOracleCodes_ to disable the checking of specific fault types based on their WFC fault codes. By default, all fault types are checked for. - Option _--endpointExclude_ to exclude specific REST endpoints from the fuzzing. +### Improvements + +- Generated tests for REST APIs now do have a better summaries for the use of "examples" entries. ### Addressed GitHub Issues