diff --git a/core-parent/pom.xml b/core-parent/pom.xml
index c57c828cb0..a4de02a50e 100644
--- a/core-parent/pom.xml
+++ b/core-parent/pom.xml
@@ -67,6 +67,11 @@
swagger-request-validator-core
2.44.9
+
+ com.webfuzzing
+ overlay-jvm
+ 0.1.1
+
diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayApplication.kt
new file mode 100644
index 0000000000..15d07a54a1
--- /dev/null
+++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayApplication.kt
@@ -0,0 +1,16 @@
+package com.foo.rest.examples.spring.openapi.v3.overlay
+
+import org.springframework.boot.SpringApplication
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
+
+@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
+open class OverlayApplication {
+
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ SpringApplication.run(OverlayApplication::class.java, *args)
+ }
+ }
+}
\ No newline at end of file
diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayRest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayRest.kt
new file mode 100644
index 0000000000..4595091f6d
--- /dev/null
+++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayRest.kt
@@ -0,0 +1,27 @@
+package com.foo.rest.examples.spring.openapi.v3.overlay
+
+import com.foo.rest.examples.spring.openapi.v3.charescaperegex.CharEscapeRegexDto
+import com.foo.rest.examples.spring.openapi.v3.stringlength.StringLengthDto
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+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
+import java.util.regex.Pattern
+import javax.validation.Valid
+import javax.ws.rs.QueryParam
+
+@RestController
+@RequestMapping(path = ["/api/overlay"])
+open class OverlayRest {
+
+ @GetMapping
+ open fun get( @QueryParam("x") x: String, @QueryParam("y") y: String) : ResponseEntity{
+
+ return ResponseEntity.ok("x=$x , y=$y")
+ }
+
+}
\ No newline at end of file
diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/subfolder/y.yaml b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/subfolder/y.yaml
new file mode 100644
index 0000000000..2cbc011215
--- /dev/null
+++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/subfolder/y.yaml
@@ -0,0 +1,10 @@
+overlay: 1.1.0
+info:
+ title: Add example to x parameter
+ version: 1.0.0
+actions:
+ - target: $.paths.['/api/overlay'].get.parameters[?(@.name == 'y')]
+ update:
+ examples:
+ foo:
+ value: "Some Y value like 777"
\ No newline at end of file
diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/x.yaml b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/x.yaml
new file mode 100644
index 0000000000..53ea7053cd
--- /dev/null
+++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/x.yaml
@@ -0,0 +1,10 @@
+overlay: 1.1.0
+info:
+ title: Add example to x parameter
+ version: 1.0.0
+actions:
+ - target: $.paths.['/api/overlay'].get.parameters[?(@.name == 'x')]
+ update:
+ examples:
+ foo:
+ value: "Some X value like 1234"
\ No newline at end of file
diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/z.json b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/z.json
new file mode 100644
index 0000000000..7cfaf2c952
--- /dev/null
+++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/resources/overlay/z.json
@@ -0,0 +1,19 @@
+{
+ "overlay": "1.1.0",
+ "info": {
+ "title": "Add example to non-existing z parameter",
+ "version": "1.0.0"
+ },
+ "actions": [
+ {
+ "target": "$.paths.['/api/overlay'].get.parameters[?(@.name == 'z')]",
+ "update": {
+ "examples": {
+ "foo": {
+ "value": "Some X value like 1234"
+ }
+ }
+ }
+ }
+ ]
+}
\ 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/overlay/OverlayController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayController.kt
new file mode 100644
index 0000000000..2267f89d56
--- /dev/null
+++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/overlay/OverlayController.kt
@@ -0,0 +1,5 @@
+package com.foo.rest.examples.spring.openapi.v3.overlay
+
+import com.foo.rest.examples.spring.openapi.v3.SpringController
+
+class OverlayController : SpringController(OverlayApplication::class.java)
\ 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/overlay/OverlayEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/overlay/OverlayEMTest.kt
new file mode 100644
index 0000000000..91b3ff9910
--- /dev/null
+++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/overlay/OverlayEMTest.kt
@@ -0,0 +1,160 @@
+package org.evomaster.e2etests.spring.openapi.v3.overlay
+
+import com.foo.rest.examples.spring.openapi.v3.overlay.OverlayController
+import com.foo.rest.examples.spring.openapi.v3.stringlength.StringLengthController
+import org.evomaster.core.config.ConfigProblemException
+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
+import org.junit.jupiter.api.assertThrows
+
+class OverlayEMTest : SpringTestBase(){
+
+ companion object {
+ @BeforeAll
+ @JvmStatic
+ fun init() {
+ initClass(OverlayController())
+ }
+ }
+
+ //these should be kept in sink with what written in the Overlay files
+ private val X = "Some X value like 1234"
+ private val Y = "Some Y value like 777"
+
+ @Test
+ fun testRunEM_Overlay_None() {
+
+ runTestHandlingFlakyAndCompilation(
+ "Overlay_None",
+ 100
+ ) { args: MutableList ->
+
+ val solution = initAndRun(args)
+
+ Assertions.assertTrue(solution.individuals.size >= 1)
+ assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/overlay", null)
+ assertNone(solution, HttpVerb.GET, 200, "/api/overlay", X)
+ assertNone(solution, HttpVerb.GET, 200, "/api/overlay", Y)
+ }
+ }
+
+
+ @Test
+ fun testRunEM_Overlay_X() {
+
+ runTestHandlingFlakyAndCompilation(
+ "Overlay_X",
+ 100
+ ) { args: MutableList ->
+
+ setOption(args, "overlay", "src/main/resources/overlay/x.yaml")
+
+ val solution = initAndRun(args)
+
+ Assertions.assertTrue(solution.individuals.size >= 1)
+ assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/overlay", X)
+ assertNone(solution, HttpVerb.GET, 200, "/api/overlay", Y)
+ }
+ }
+
+ @Test
+ fun testRunEM_Overlay_Y_BB() {
+
+ runTestHandlingFlakyAndCompilation(
+ "Overlay_Y_BB",
+ 100
+ ) { args: MutableList ->
+
+ setOption(args, "overlay", "src/main/resources/overlay/subfolder/y.yaml")
+ setOption(args, "blackBox", "true")
+ setOption(args, "bbTargetUrl", baseUrlOfSut)
+ setOption(args, "bbSwaggerUrl", "$baseUrlOfSut/v3/api-docs")
+
+ val solution = initAndRun(args)
+
+ Assertions.assertTrue(solution.individuals.size >= 1)
+ assertNone(solution, HttpVerb.GET, 200, "/api/overlay", X)
+ assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/overlay", Y)
+ }
+ }
+
+
+ @Test
+ fun testRunEM_Overlay_Z_fail() {
+
+ runTestHandlingFlakyAndCompilation(
+ "Overlay_Z_fail",
+ 100
+ ) { args: MutableList ->
+
+ setOption(args, "overlay", "src/main/resources/overlay/z.json")
+ //default behavior must be non-lenient
+
+ //z does not exist
+ assertThrows { initAndRun(args) }
+ }
+ }
+
+ @Test
+ fun testRunEM_Overlay_Z_lenient() {
+
+ runTestHandlingFlakyAndCompilation(
+ "Overlay_Z_lenient",
+ 100
+ ) { args: MutableList ->
+
+ setOption(args, "overlay", "src/main/resources/overlay/z.json")
+ setOption(args, "overlayLenient", "true")
+
+ //z does not exist... but, when lenient, shouldn't fail
+ val solution = initAndRun(args)
+
+ Assertions.assertTrue(solution.individuals.size >= 1)
+ assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/overlay", null)
+ assertNone(solution, HttpVerb.GET, 200, "/api/overlay", X)
+ assertNone(solution, HttpVerb.GET, 200, "/api/overlay", Y)
+ }
+ }
+
+ @Test
+ fun testRunEM_Overlay_folder() {
+
+ runTestHandlingFlakyAndCompilation(
+ "Overlay_folder",
+ 100
+ ) { args: MutableList ->
+
+ setOption(args, "overlay", "src/main/resources/overlay")
+ //by default, z.json will be picked, and so failed because non-lenient
+
+ //z does not exist
+ assertThrows { initAndRun(args) }
+ }
+ }
+
+
+ @Test
+ fun testRunEM_Overlay_folder_filtered() {
+
+ runTestHandlingFlakyAndCompilation(
+ "Overlay_folder_filtered",
+ 100
+ ) { args: MutableList ->
+
+ setOption(args, "overlay", "src/main/resources/overlay")
+ //make sure to not pick-up z.json
+ setOption(args, "overlayFileSuffixes", ".yaml")
+
+ val solution = initAndRun(args)
+
+ Assertions.assertTrue(solution.individuals.size >= 1)
+ assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/overlay", X)
+ assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/overlay", Y)
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/core/pom.xml b/core/pom.xml
index e2097fe2bf..9d2cd099aa 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -39,6 +39,10 @@
com.webfuzzing
commons
+
+ com.webfuzzing
+ overlay-jvm
+
diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt
index 6bd096e17f..92b20d209b 100644
--- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt
+++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt
@@ -634,7 +634,6 @@ class EMConfig {
throw ConfigProblemException("'bbTargetUrl' should be set only in black-box mode")
}
- // ONUR, this line is changed since it did not compile in the previous case.
if (!endpointFocus.isNullOrBlank() && !endpointPrefix.isNullOrBlank()) {
throw ConfigProblemException("both 'endpointFocus' and 'endpointPrefix' are set")
}
@@ -913,7 +912,7 @@ class EMConfig {
if (Files.exists(path) && !Files.isWritable(path)) {
throw ConfigProblemException("Parameter '${m.name}' refers to a file that already" +
- " exists, but that cannot be written/replace to: $path")
+ " exists, but that cannot be written/replaced to: $path")
}
if (Files.exists(path) && Files.isDirectory(path)) {
throw ConfigProblemException("Parameter '${m.name}' refers to a file that is instead an" +
@@ -921,6 +920,26 @@ class EMConfig {
}
}
}
+
+ m.annotations.find { it is ExistingPath }?.also{
+ val ep = it as ExistingPath
+ if(! (ep.canBeBlank && parameterValue.isBlank())){
+ val path = try {
+ Paths.get(parameterValue).toAbsolutePath()
+ } catch (e: InvalidPathException) {
+ throw ConfigProblemException("Parameter '${m.name}' is not a valid FS path: ${e.message}")
+ }
+
+ if (!Files.exists(path)){
+ throw ConfigProblemException("File/folder for '${m.name}' does not exist: $path")
+ }
+
+ if( ep.shouldBeWritable && !Files.isWritable(path)){
+ throw ConfigProblemException("Parameter '${m.name}' refers to a file that" +
+ " exists, but it cannot be written/replaced to: $path")
+ }
+ }
+ }
}
@@ -1052,7 +1071,7 @@ class EMConfig {
/**
- * This represent one of the main properties to set in EvoMaster.
+ * This represents one of the main properties to set in EvoMaster.
* Those are the ones most likely going to be set by practitioners.
* Note: most of the other properties are mainly for experiments
*/
@@ -1074,6 +1093,13 @@ class EMConfig {
@MustBeDocumented
annotation class FilePath(val canBeBlank: Boolean = false)
+ /**
+ * Either a file or a folder, that MUST already exist and can be read.
+ */
+ @Target(AnnotationTarget.PROPERTY)
+ @MustBeDocumented
+ annotation class ExistingPath(val canBeBlank: Boolean = false, val shouldBeWritable: Boolean = false)
+
//------------------------------------------------------------------------
@@ -3049,6 +3075,32 @@ class EMConfig {
" path element of the URL will not change).")
var overrideAuthExternalEndpointURL : String? = null
+ @Experimental
+ @ExistingPath(true,false)
+ @Cfg("Specify an OAI Overlay file path, or a folder containing those." +
+ " In this latter case, Overlay files will be searched recursively in the nested folder, matching" +
+ " a given list of configurable suffixes." +
+ " Each Overlay will be applied to the target OpenAPI schema." +
+ " If more than one Overlay file is applied, no specific ordering of transformations is enforced.")
+ var overlay = ""
+
+ @Experimental
+ @Cfg("Comma ',' separated list of file name suffixes." +
+ " When scanning a folder for OAI Overlay files, any file with name matching any one of these" +
+ " suffixes will be loaded and applied." +
+ " For example, '.json' could be used to match all JSON files." +
+ " If the folder contains also other types of files with same extension, you might need to define" +
+ " some naming convention, and then use suffixes based on it, e.g., '-overlay.yaml' to match" +
+ " all YAML files whose name ends in 'overlay', like 'example-overlay.yaml'.")
+ var overlayFileSuffixes = ".json,.yaml,.yml"
+
+ @Experimental
+ @Cfg("When applying Overlay transformations, by default EvoMaster will crash immediately" +
+ " if there is any issue with the transformations, e.g., if some transformations are not applied" +
+ " because the JSON Path selectors found no applicable node in the OpenAPI schema." +
+ " This option can be used to override such behavior, and let the fuzzing go on without" +
+ " applying any overlay.")
+ var overlayLenient = false
fun getProbabilityUseDataPool() : Double{
return if(blackBox){
diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaOpenAPI.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaOpenAPI.kt
index 0c26073caa..6a8efe370f 100644
--- a/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaOpenAPI.kt
+++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaOpenAPI.kt
@@ -1,19 +1,67 @@
package org.evomaster.core.problem.rest.schema
+import com.webfuzzing.overlayjvm.Processor
import io.swagger.v3.oas.models.OpenAPI
+import org.slf4j.LoggerFactory
class SchemaOpenAPI(
/**
* The actual raw value of the schema file, as a string
*/
- val schemaRaw : String,
+ val schemaRaw: String,
/**
* A parsed schema
*/
val schemaParsed: OpenAPI,
/**
- * the location the schema was retrieved from.
- * It can be null, eg, if provided as string in our own EM tests
+ * information about the location the schema was retrieved from, e.g.,
+ * from file, URL or in memory in our tests.
*/
val sourceLocation: SchemaLocation
-)
\ No newline at end of file
+) {
+
+ companion object {
+ private val log = LoggerFactory.getLogger(SchemaOpenAPI::class.java)
+ }
+
+
+ fun withOverlays(overlays: List?, lenient: Boolean): SchemaOpenAPI {
+ if (overlays.isNullOrEmpty()) {
+ return this
+ }
+
+ val infoForLenient = "If this behavior is expected, you need to explicitly use '--overlayLenient true'" +
+ " to avoid EvoMaster from stopping."
+
+ var modified = schemaRaw
+ overlays.forEach {
+ val res = try{
+ Processor.applyOverlay(modified,it)
+ } catch (e : Exception){
+ val message = "Failed to apply overlay transformation: ${e.message}"
+ if(lenient){
+ log.warn(message)
+ return@forEach
+ } else {
+ throw IllegalArgumentException("$message\n$infoForLenient",e)
+ }
+ }
+
+ if(res.warnings.isNotEmpty()){
+ val message = "Issues (${res.warnings.size}) with applying Overlay transformation:\n${res.warnings.joinToString("\n")}"
+ if(lenient){
+ log.warn(message)
+ //here we do not return, as we still apply the partial transformations
+ } else {
+ throw IllegalArgumentException("$message\n$infoForLenient")
+ }
+ }
+
+ modified = res.transformedSchema
+ }
+
+ return OpenApiAccess.parseOpenApi(modified, this.sourceLocation)
+ }
+
+
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaUtils.kt
index ec19c3f3fc..f044ec03db 100644
--- a/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaUtils.kt
+++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/schema/SchemaUtils.kt
@@ -8,8 +8,16 @@ import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.oas.models.parameters.Parameter
import io.swagger.v3.oas.models.parameters.RequestBody
import io.swagger.v3.oas.models.responses.ApiResponse
+import org.evomaster.core.logging.LoggingUtil
import java.net.URI
import java.net.URISyntaxException
+import java.nio.file.Files
+import java.nio.file.Paths
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.exists
+import kotlin.io.path.name
+import kotlin.io.path.readText
+import kotlin.io.path.walk
/**
* https://swagger.io/docs/specification/v3_0/using-ref/
@@ -256,4 +264,41 @@ object SchemaUtils {
return response
}
+
+ /**
+ * depending on whether path is a file or folder, return a list with 1 or more Overlays.
+ * Suffix check is only done if folder.
+ * If path is empty, return null.
+ */
+ fun readOverlayFiles(path: String, suffixes: String): List? {
+
+ if(path.isBlank()){
+ //nothing to do
+ return null
+ }
+
+ val ap = Paths.get(path).toAbsolutePath()
+
+ if(!ap.exists()){
+ throw IllegalArgumentException("Path $path does not point to any existing file or folder")
+ }
+
+ if(Files.isRegularFile(ap)){
+ //only one file
+ LoggingUtil.getInfoLogger().info("Retrieving Overlay from: $path")
+ return listOf(ap.readText())
+ }
+
+ val options = suffixes.split(',').map { it.trim() }
+
+ LoggingUtil.getInfoLogger().info("Scanning for Overlay files with possible suffix '$suffixes' in $path")
+
+ return ap.walk()
+ .filter{file -> options.any{s -> file.name.endsWith(s) } }
+ .map {
+ LoggingUtil.getInfoLogger().info("Retrieving Overlay from: ${it.absolutePathString()}")
+ it.readText()
+ }
+ .toList()
+ }
}
\ 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 57df7a1083..344f2546bb 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
@@ -6,6 +6,7 @@ import org.evomaster.client.java.controller.api.dto.problem.ExternalServiceDto
import org.evomaster.client.java.instrumentation.shared.TaintInputName
import org.evomaster.core.AnsiColor
import org.evomaster.core.EMConfig
+import org.evomaster.core.config.ConfigProblemException
import org.evomaster.core.logging.LoggingUtil
import org.evomaster.core.output.service.PartialOracles
import org.evomaster.core.problem.enterprise.SampleType
@@ -26,6 +27,8 @@ import org.evomaster.core.problem.rest.param.QueryParam
import org.evomaster.core.problem.rest.schema.OpenApiAccess
import org.evomaster.core.problem.rest.schema.RestSchema
import org.evomaster.core.problem.rest.schema.SchemaLocation
+import org.evomaster.core.problem.rest.schema.SchemaOpenAPI
+import org.evomaster.core.problem.rest.schema.SchemaUtils
import org.evomaster.core.problem.rest.seeding.Parser
import org.evomaster.core.problem.rest.seeding.postman.PostmanParser
import org.evomaster.core.problem.rest.service.AIResponseClassifier
@@ -110,7 +113,9 @@ abstract class AbstractRestSampler : HttpWsSampler() {
} else {
throw SutProblemException("No info on the OpenAPI schema was provided")
}
- schemaHolder = RestSchema(swagger)
+ val transformed = applyOverlays(swagger)
+
+ schemaHolder = RestSchema(transformed)
schemaHolder.validate()
// The code should never reach this line without a valid swagger.
@@ -281,6 +286,27 @@ abstract class AbstractRestSampler : HttpWsSampler() {
}
+ private fun retrieveOverlays() : List? {
+ val overlays = SchemaUtils.readOverlayFiles(config.overlay, config.overlayFileSuffixes)
+
+ if(overlays!=null && overlays.isEmpty()){
+ throw ConfigProblemException("Could not find any Overlay file in '${config.overlay}' given the suffixes '${config.overlayFileSuffixes}'")
+ }
+
+ return overlays
+ }
+
+ private fun applyOverlays(openApi: SchemaOpenAPI) : SchemaOpenAPI{
+
+ val overlays = retrieveOverlays()
+
+ val transformed = try{
+ openApi.withOverlays(overlays, config.overlayLenient)
+ }catch (e: Exception){
+ throw ConfigProblemException(e.message ?: "Failed to apply overlays")
+ }
+ return transformed
+ }
private fun initForBlackBox() {
@@ -288,8 +314,9 @@ abstract class AbstractRestSampler : HttpWsSampler() {
// retrieve the swagger
val swagger = OpenApiAccess.getOpenAPIFromLocation(configuration.bbSwaggerUrl, authentications)
+ val transformed = applyOverlays(swagger)
- schemaHolder = RestSchema(swagger)
+ schemaHolder = RestSchema(transformed)
schemaHolder.validate()
actionCluster.clear()
diff --git a/docs/options.md b/docs/options.md
index 7f5c5369b0..57383b6e6b 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -300,6 +300,9 @@ There are 3 types of options:
|`maxTestSizeStrategy`| __Enum__. Specify a strategy to handle a max size of a test. *Valid values*: `SPECIFIED, DPC_INCREASING, DPC_DECREASING`. *Default value*: `SPECIFIED`.|
|`mutationTargetsSelectionStrategy`| __Enum__. Specify a strategy to select targets for evaluating mutation. *Valid values*: `FIRST_NOT_COVERED_TARGET, EXPANDED_UPDATED_NOT_COVERED_TARGET, UPDATED_NOT_COVERED_TARGET`. *Default value*: `FIRST_NOT_COVERED_TARGET`.|
|`onePlusLambdaLambdaOffspringSize`| __Int__. 1+(λ,λ) GA: number of offspring (λ) per generation. *Constraints*: `min=1.0`. *Default value*: `4`.|
+|`overlay`| __String__. Specify an OAI Overlay file path, or a folder containing those. In this latter case, Overlay files will be searched recursively in the nested folder, matching a given list of configurable suffixes. Each Overlay will be applied to the target OpenAPI schema. If more than one Overlay file is applied, no specific ordering of transformations is enforced. *Default value*: `""`.|
+|`overlayFileSuffixes`| __String__. Comma ',' separated list of file name suffixes. When scanning a folder for OAI Overlay files, any file with name matching any one of these suffixes will be loaded and applied. For example, '.json' could be used to match all JSON files. If the folder contains also other types of files with same extension, you might need to define some naming convention, and then use suffixes based on it, e.g., '-overlay.yaml' to match all YAML files whose name ends in 'overlay', like 'example-overlay.yaml'. *Default value*: `.json,.yaml,.yml`.|
+|`overlayLenient`| __Boolean__. When applying Overlay transformations, by default EvoMaster will crash immediately if there is any issue with the transformations, e.g., if some transformations are not applied because the JSON Path selectors found no applicable node in the OpenAPI schema. This option can be used to override such behavior, and let the fuzzing go on without applying any overlay. *Default value*: `false`.|
|`prematureStopStrategy`| __Enum__. Specify how 'improvement' is defined: either any kind of improvement even if partial (ANY), or at least one new target is fully covered (NEW). *Valid values*: `ANY, NEW`. *Default value*: `NEW`.|
|`probOfHandlingLength`| __Double__. Specify a probability of applying length handling. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.0`.|
|`probOfHarvestingResponsesFromActualExternalServices`| __Double__. a probability of harvesting actual responses from external services as seeds. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.0`.|