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/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..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 @@ -30,8 +31,13 @@ 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 -) : SimpleGene(name) { + 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), NamedExamplesGene { companion object { @@ -81,6 +87,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 +108,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 +193,10 @@ class EnumGene>( return values[index].toString() } + override 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/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 dc523e6005..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 @@ -26,9 +27,14 @@ 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 { +) : CompositeFixedGene(name, geneChoices), NamedExamplesGene, WrapperGene where T : Gene { companion object { private val log: Logger = LoggerFactory.getLogger(ChoiceGene::class.java) @@ -39,6 +45,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 +58,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 +178,10 @@ class ChoiceGene( .getValueAsRawString() } + override 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 +275,8 @@ class ChoiceGene( name, activeChoice = this.activeGeneIndex, geneChoices = this.geneChoices.map { it.copy() }.toList(), - probabilities = probabilities // immutable + probabilities = probabilities, // immutable + valueNames = valueNames // immutable ) /** 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