Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String>) {
SpringApplication.run(ExamplePoolApplication::class.java, *args)
}
}

@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
fun post(@RequestBody dto: ExamplePoolDto) : ResponseEntity<String>{

if(dto.x== "foo" && dto.y== "bar"){
return ResponseEntity.ok("OK")
}

return ResponseEntity.status(400).build()
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.foo.rest.examples.spring.openapi.v3.examplepool

class ExamplePoolDto(

var x: String? = null,

var y: String? = null
)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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<String> ->

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")
}
}
}
3 changes: 3 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/EMConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ object RestActionBuilderV3 {
gene = OptionalGene(name, gene)
}

val contentTypeGene = EnumGene<String>("contentType", bodies.keys)
val contentTypeGene = EnumGene<String>(RestGeneSpecialNames.CONTENT_TYPE.name, bodies.keys)
val bodyParam = BodyParam(gene, contentTypeGene)
.apply { this.description = description }

Expand Down Expand Up @@ -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

/*
Expand All @@ -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<String>(f.name, listOf(asRawString(e.textValue())), 0, false)
} else if(e.isObject) {
val nested = f.getWrappedGene(ObjectGene::class.java)
Expand Down Expand Up @@ -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<String>("default", listOf(asRawString(defaultValue)),0,false)
-> EnumGene<String>(RestGeneSpecialNames.DEFAULT.name, listOf(asRawString(defaultValue)),0,false)

//TODO Arrays
else -> {
Expand All @@ -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<String>(UserExamplesGene.EXAMPLES_NAME, v,0,false, n)
-> EnumGene<String>(RestGeneSpecialNames.SCHEMA_EXAMPLES.name, v,0,false, n)

//TODO Arrays
else -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -47,6 +49,7 @@ import org.slf4j.LoggerFactory
import javax.annotation.PostConstruct



abstract class AbstractRestSampler : HttpWsSampler<RestIndividual>() {

companion object {
Expand All @@ -68,6 +71,9 @@ abstract class AbstractRestSampler : HttpWsSampler<RestIndividual>() {
@Inject
protected lateinit var externalServiceHandler: HttpWsExternalServiceHandler

@Inject
protected lateinit var dataPool: DataPool

protected val adHocInitialIndividuals: MutableList<RestIndividual> = mutableListOf()

lateinit var schemaHolder: RestSchema
Expand Down Expand Up @@ -139,6 +145,10 @@ abstract class AbstractRestSampler : HttpWsSampler<RestIndividual>() {
initializeDerivedParamRules(problem.derivedParams)
}

if(config.useObjectExampleDataPool){
feedObjectExamplesToDataPool(actionCluster)
}

initSqlInfo(infoDto)

initHostnameInfo(infoDto)
Expand All @@ -162,6 +172,8 @@ abstract class AbstractRestSampler : HttpWsSampler<RestIndividual>() {
log.debug("Done initializing {}", AbstractRestSampler::class.simpleName)
}



override fun sampleRandomAction(noAuthP: Double): HttpWsAction {

val action = super.sampleRandomAction(noAuthP)
Expand Down Expand Up @@ -334,6 +346,10 @@ abstract class AbstractRestSampler : HttpWsSampler<RestIndividual>() {
initSeededTests()
}

if(config.useObjectExampleDataPool){
feedObjectExamplesToDataPool(actionCluster)
}

log.debug("Done initializing {}", AbstractRestSampler::class.simpleName)
}

Expand Down Expand Up @@ -434,4 +450,18 @@ abstract class AbstractRestSampler : HttpWsSampler<RestIndividual>() {
}
}

private fun feedObjectExamplesToDataPool(actionCluster: Map<String, Action>) {

actionCluster.values.asSequence()
.flatMap { it.seeAllGenes() }
.filterIsInstance<EnumGene<String>>()
//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())
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ class EnumGene<T : Comparable<T>>(
* 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<String?>? = null
val valueNames: List<String?>? = null
) : SimpleGene(name), UserExamplesGene {

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -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

/**
Expand All @@ -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
}

/**
Expand Down
1 change: 1 addition & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.|
Expand Down
Loading