diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolApplication.kt new file mode 100644 index 0000000000..b2af75962a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolApplication.kt @@ -0,0 +1,35 @@ +package com.foo.rest.examples.spring.openapi.v3.examplepool + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping(path = ["/api/examplepool"]) +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +open class ExamplePoolApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(ExamplePoolApplication::class.java, *args) + } + } + + @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE]) + fun post(@RequestBody dto: ExamplePoolDto) : ResponseEntity{ + + if(dto.x== "foo" && dto.y== "bar"){ + return ResponseEntity.ok("OK") + } + + return ResponseEntity.status(400).build() + } +} + diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolDto.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolDto.kt new file mode 100644 index 0000000000..3ae3585ad8 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolDto.kt @@ -0,0 +1,8 @@ +package com.foo.rest.examples.spring.openapi.v3.examplepool + +class ExamplePoolDto( + + var x: String? = null, + + var y: String? = null +) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-examplepool.yaml b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-examplepool.yaml new file mode 100644 index 0000000000..3a96a524ab --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/static/openapi-examplepool.yaml @@ -0,0 +1,45 @@ +openapi: 3.1.0 +info: + title: OpenAPI definition + version: v0 +servers: + - url: http://localhost:8080 + description: Generated server url +paths: + /api/examplepool: + post: + tags: + - example-pool-application + operationId: post + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ExamplePoolDto' + examples: + first: + value: + x: foo + y: '123' + second: + value: + x: '456' + y: bar + required: true + responses: + '200': + description: OK + content: + '*/*': + schema: + type: string +components: + schemas: + ExamplePoolDto: + type: object + properties: + x: + type: string + y: + type: string \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolController.kt new file mode 100644 index 0000000000..0d09bba7c2 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/examplepool/ExamplePoolController.kt @@ -0,0 +1,16 @@ +package com.foo.rest.examples.spring.openapi.v3.examplepool + +import com.foo.rest.examples.spring.openapi.v3.SpringController +import org.evomaster.client.java.controller.problem.ProblemInfo +import org.evomaster.client.java.controller.problem.RestProblem + +class ExamplePoolController : SpringController(ExamplePoolApplication::class.java){ + + + override fun getProblemInfo(): ProblemInfo { + return RestProblem( + "http://localhost:$sutPort/openapi-examplepool.yaml", + null + ) + } +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/examplepool/ExamplePoolEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/examplepool/ExamplePoolEMTest.kt new file mode 100644 index 0000000000..23a5a649a6 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/examplepool/ExamplePoolEMTest.kt @@ -0,0 +1,41 @@ +package org.evomaster.e2etests.spring.openapi.v3.examplepool + +import com.foo.rest.examples.spring.openapi.v3.examplepool.ExamplePoolController +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class ExamplePoolEMTest : SpringTestBase(){ + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(ExamplePoolController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "ExamplePool", + 100 + ) { args: MutableList -> + + setOption(args, "useObjectExampleDataPool", "true") + setOption(args, "blackBox", "true") + setOption(args, "base", baseUrlOfSut) + setOption(args, "schema", "src/main/resources/static/openapi-examplepool.yaml") + + val solution = initAndRun(args) + + Assertions.assertTrue(solution.individuals.size >= 1) + + assertHasAtLeastOne(solution, HttpVerb.POST, 400, "/api/examplepool", null) + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/examplepool", "OK") + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 09b1dff31a..27985ef89f 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -3151,6 +3151,9 @@ class EMConfig { " If so, those will be added to the data pool.") var useDictionaryDataPool = false + @Cfg("Feed the individual entries of object examples to the data pool.") + var useObjectExampleDataPool = true + @Cfg("Specify the naming strategy for test cases.") var namingStrategy = defaultTestCaseNamingStrategy 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 6f9fd44dbc..50452574c1 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 @@ -784,7 +784,7 @@ object RestActionBuilderV3 { gene = OptionalGene(name, gene) } - val contentTypeGene = EnumGene("contentType", bodies.keys) + val contentTypeGene = EnumGene(RestGeneSpecialNames.CONTENT_TYPE.name, bodies.keys) val bodyParam = BodyParam(gene, contentTypeGene) .apply { this.description = description } @@ -1814,10 +1814,10 @@ object RestActionBuilderV3 { val n = examples.map{it.second} // names val exampleGene = if(examples.isNotEmpty()){ - ChoiceGene(UserExamplesGene.EXAMPLES_NAME, v, valueNames = n) + ChoiceGene(RestGeneSpecialNames.SCHEMA_EXAMPLES.name, v, valueNames = n) } else null val defaultGene = if(defaultValue != null){ - duplicateObjectWithExampleFields("default", mainGene, defaultValue) + duplicateObjectWithExampleFields(RestGeneSpecialNames.DEFAULT.name, mainGene, defaultValue) } else null /* @@ -1838,6 +1838,7 @@ object RestActionBuilderV3 { if(exampleValue.has(f.name)){ val e = exampleValue.get(f.name) if(e.isTextual){ + //WARN: if modify this might need update feedObjectExamplesToDataPool EnumGene(f.name, listOf(asRawString(e.textValue())), 0, false) } else if(e.isObject) { val nested = f.getWrappedGene(ObjectGene::class.java) @@ -1997,12 +1998,12 @@ object RestActionBuilderV3 { val defaultGene = if(defaultValue != null){ when{ NumberGene::class.java.isAssignableFrom(geneClass) - -> EnumGene("default", listOf(defaultValue.toString()),0,true) + -> EnumGene(RestGeneSpecialNames.DEFAULT.name, listOf(defaultValue.toString()),0,true) geneClass == StringGene::class.java || geneClass == Base64StringGene::class.java || geneClass == RegexGene::class.java - -> EnumGene("default", listOf(asRawString(defaultValue)),0,false) + -> EnumGene(RestGeneSpecialNames.DEFAULT.name, listOf(asRawString(defaultValue)),0,false) //TODO Arrays else -> { @@ -2020,12 +2021,12 @@ object RestActionBuilderV3 { val exampleGene = if(examples.isNotEmpty()){ when{ NumberGene::class.java.isAssignableFrom(geneClass) - -> EnumGene(UserExamplesGene.EXAMPLES_NAME, v,0,true, n) + -> EnumGene(RestGeneSpecialNames.SCHEMA_EXAMPLES.name, v,0,true, n) geneClass == StringGene::class.java || geneClass == Base64StringGene::class.java || geneClass == RegexGene::class.java - -> EnumGene(UserExamplesGene.EXAMPLES_NAME, v,0,false, n) + -> EnumGene(RestGeneSpecialNames.SCHEMA_EXAMPLES.name, v,0,false, n) //TODO Arrays else -> { diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestGeneSpecialNames.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestGeneSpecialNames.kt new file mode 100644 index 0000000000..278a890417 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestGeneSpecialNames.kt @@ -0,0 +1,14 @@ +package org.evomaster.core.problem.rest.builder + +enum class RestGeneSpecialNames { + /** + * Name given to enum genes representing data examples coming from OpenAPI schema + */ + SCHEMA_EXAMPLES, + + /** + * Name representing a "default" entry value, as declared in OpenAPI schema + */ + DEFAULT, + CONTENT_TYPE +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/sampler/AbstractRestSampler.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/sampler/AbstractRestSampler.kt index f826a3bdfc..8a6a43d48a 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/sampler/AbstractRestSampler.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/sampler/AbstractRestSampler.kt @@ -18,6 +18,7 @@ import org.evomaster.core.problem.httpws.service.HttpWsSampler import org.evomaster.core.problem.rest.* import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 import org.evomaster.core.problem.rest.builder.RestActionBuilderV3.buildActionBasedOnUrl +import org.evomaster.core.problem.rest.builder.RestGeneSpecialNames import org.evomaster.core.problem.rest.data.Endpoint import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.core.problem.rest.data.RestCallAction @@ -35,10 +36,11 @@ import org.evomaster.core.problem.rest.service.AIResponseClassifier import org.evomaster.core.problem.rest.service.RestIndividualBuilder import org.evomaster.core.remote.SutProblemException import org.evomaster.core.search.action.Action +import org.evomaster.core.search.gene.collection.EnumGene import org.evomaster.core.search.gene.wrapper.CustomMutationRateGene import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.search.gene.string.StringGene -import org.evomaster.core.search.service.WarningsAggregator +import org.evomaster.core.search.service.DataPool import org.evomaster.core.search.tracer.Traceable import org.evomaster.core.search.warning.GeneralWarning import org.evomaster.core.search.warning.WarningCategory @@ -47,6 +49,7 @@ import org.slf4j.LoggerFactory import javax.annotation.PostConstruct + abstract class AbstractRestSampler : HttpWsSampler() { companion object { @@ -68,6 +71,9 @@ abstract class AbstractRestSampler : HttpWsSampler() { @Inject protected lateinit var externalServiceHandler: HttpWsExternalServiceHandler + @Inject + protected lateinit var dataPool: DataPool + protected val adHocInitialIndividuals: MutableList = mutableListOf() lateinit var schemaHolder: RestSchema @@ -139,6 +145,10 @@ abstract class AbstractRestSampler : HttpWsSampler() { initializeDerivedParamRules(problem.derivedParams) } + if(config.useObjectExampleDataPool){ + feedObjectExamplesToDataPool(actionCluster) + } + initSqlInfo(infoDto) initHostnameInfo(infoDto) @@ -162,6 +172,8 @@ abstract class AbstractRestSampler : HttpWsSampler() { log.debug("Done initializing {}", AbstractRestSampler::class.simpleName) } + + override fun sampleRandomAction(noAuthP: Double): HttpWsAction { val action = super.sampleRandomAction(noAuthP) @@ -334,6 +346,10 @@ abstract class AbstractRestSampler : HttpWsSampler() { initSeededTests() } + if(config.useObjectExampleDataPool){ + feedObjectExamplesToDataPool(actionCluster) + } + log.debug("Done initializing {}", AbstractRestSampler::class.simpleName) } @@ -434,4 +450,18 @@ abstract class AbstractRestSampler : HttpWsSampler() { } } + private fun feedObjectExamplesToDataPool(actionCluster: Map) { + + actionCluster.values.asSequence() + .flatMap { it.seeAllGenes() } + .filterIsInstance>() + //Not the cleanest option, but adding a further tag on Gene would likely had been worse. + //This is based on what done in RestActionBuilderV3 + .filter{!it.treatAsNotString && it.valueNames==null && it.values.size == 1} + .filter{g -> RestGeneSpecialNames.entries.none { e -> e.name == g.name } } + .forEach { + dataPool.addValue(it.name, it.getValueAsRawString()) + } + } + } 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 20db38acaa..036c6ce142 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 @@ -36,12 +36,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, + 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 + val valueNames: List? = null ) : SimpleGene(name), UserExamplesGene { companion object { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/UserExamplesGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/UserExamplesGene.kt index 3a37adcb77..252b8d9a5f 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/UserExamplesGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/interfaces/UserExamplesGene.kt @@ -1,5 +1,6 @@ package org.evomaster.core.search.gene.interfaces +import org.evomaster.core.problem.rest.builder.RestGeneSpecialNames import org.evomaster.core.search.gene.Gene /** @@ -12,18 +13,11 @@ import org.evomaster.core.search.gene.Gene */ interface UserExamplesGene { - companion object { - /** - * Name given to enum genes representing data examples coming from OpenAPI schema - */ - const val EXAMPLES_NAME = "SCHEMA_EXAMPLES" - } - /** * Check if this gene is used to store examples provided by the user */ fun isUsedForExamples() : Boolean{ - return (this as Gene).name == EXAMPLES_NAME + return (this as Gene).name == RestGeneSpecialNames.SCHEMA_EXAMPLES.name } /** diff --git a/docs/options.md b/docs/options.md index e870a9a1c3..eb008a40c1 100644 --- a/docs/options.md +++ b/docs/options.md @@ -244,6 +244,7 @@ There are 3 types of options: |`useExtraSqlDbConstraintsProbability`| __Double__. Whether to analyze how SQL databases are accessed to infer extra constraints from the business logic. An example is javax/jakarta annotation constraints defined on JPA entities. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.9`.| |`useMethodReplacement`| __Boolean__. Apply method replacement heuristics to smooth the search landscape. Note that the method replacement instrumentations would still be applied, it is just that their testing targets will be ignored in the fitness function if this option is set to false. *Default value*: `true`.| |`useNonIntegerReplacement`| __Boolean__. Apply non-integer numeric comparison heuristics to smooth the search landscape. *Default value*: `true`.| +|`useObjectExampleDataPool`| __Boolean__. Feed the individual entries of object examples to the data pool. *Default value*: `true`.| |`useResponseDataPool`| __Boolean__. Enable the collection of response data, to feed new individuals based on field names matching. *Default value*: `true`.| |`useTimeInFeedbackSampling`| __Boolean__. Whether to use timestamp info on the execution time of the tests for sampling (e.g., to reward the quickest ones). *Default value*: `true`.| |`wbProbabilityUseDataPool`| __Double__. Specify the probability of using the data pool when sampling test cases. This is for white-box (wb) mode. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.2`.|