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
Expand Up @@ -43,7 +43,7 @@ open class BBAdvancedFormatsApplication {
@RequestParam(required = true) x: String?
) : ResponseEntity<String> {

if (x == null || !x.contains('@')) {
if (x == null || !x.contains('@') || !x.contains('.')) {
return ResponseEntity.status(400).build()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.foo.rest.examples.bb.inferformat

import org.evomaster.e2etests.utils.CoveredTargets
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.net.URI
import java.time.Instant
import java.time.LocalDateTime
import java.util.UUID

@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@RequestMapping(path = ["/api/inferformat"])
@RestController
open class BBInferFormatApplication {

companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(BBInferFormatApplication::class.java, *args)
}
}


@GetMapping("/uuid")
open fun getUuid(
@RequestParam(required = true) uuid: String?
) : ResponseEntity<String> {

if (uuid == null) {
return ResponseEntity.status(400).build()
}

UUID.fromString(uuid)

CoveredTargets.cover("uuid")

return ResponseEntity.status(200).body("OK")
}

@GetMapping("/email")
open fun getEmail(
@RequestParam(required = true) theEmail: String?
) : ResponseEntity<String> {

if (theEmail == null || !theEmail.contains('@') || !theEmail.contains('.')) {
return ResponseEntity.status(400).build()
}

CoveredTargets.cover("email")

return ResponseEntity.status(200).body("OK")
}

@GetMapping("/uri")
open fun getUri(
@RequestParam(required = true) addressUri: String?
) : ResponseEntity<String> {

if (addressUri == null) {
return ResponseEntity.status(400).build()
}

URI(addressUri)

CoveredTargets.cover("uri")

return ResponseEntity.status(200).body("OK")
}


@PostMapping("/uuid")
open fun postUuid(
@RequestBody dto: BBInferFormatDto
) : ResponseEntity<String> {

UUID.fromString(dto.foo)

CoveredTargets.cover("description-uuid")

return ResponseEntity.status(200).body("OK")
}

@PostMapping("/date")
open fun postDate(
@RequestBody dto: BBInferFormatDto
) : ResponseEntity<String> {

LocalDateTime.parse(dto.bar!!)

CoveredTargets.cover("description-date")

return ResponseEntity.status(200).body("OK")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.foo.rest.examples.bb.inferformat

import io.swagger.v3.oas.annotations.media.Schema

class BBInferFormatDto(

@field:Schema(description = "This is a UUID field.")
var foo: String? = null,

@field:Schema(description = "This is a field representing a date in ISO 8601 format.")
var bar: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.foo.rest.examples.bb.inferformat

import com.foo.rest.examples.bb.SpringController


class BBInferFormatController : SpringController(BBInferFormatApplication::class.java)
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class BBAdvancedFormatsEMTest : SpringTestBase() {

setOption(args, "schema", "$baseUrlOfSut/openapi-bbadvancedformats.json")
setOption(args, "enableAdvancedFormats", "true")
setOption(args, "inferFormatFromNames", "false")

val solution = initAndRun(args)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.evomaster.e2etests.spring.rest.bb.inferformat


import com.foo.rest.examples.bb.inferformat.BBInferFormatController
import org.evomaster.core.output.OutputFormat
import org.evomaster.core.problem.rest.data.HttpVerb
import org.evomaster.e2etests.spring.rest.bb.SpringTestBase
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

class BBInferFormatEMTest : SpringTestBase() {

companion object {
init {
shouldApplyInstrumentation = false
}

@BeforeAll
@JvmStatic
fun init() {
initClass(BBInferFormatController())
}
}

@ParameterizedTest
@EnumSource
fun testBlackBoxOutput(outputFormat: OutputFormat) {

executeAndEvaluateBBTest(
outputFormat,
"inferformat",
200,
3,
listOf("uuid","uri","email","description-uuid","description-date")
){ args: MutableList<String> ->

setOption(args, "enableAdvancedFormats", "false")
setOption(args, "inferFormatFromNames", "true")

val solution = initAndRun(args)

assertTrue(solution.individuals.size >= 1)
assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/inferformat/uuid", "OK")
assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/inferformat/uri", "OK")
assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/inferformat/email", "OK")
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/inferformat/uuid", "OK")
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/inferformat/date", "OK")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void testDeterminismOfLog(boolean enableConstraintHandling){
OpenAPI schema = (new OpenAPIParser()).readLocation("swagger-ahm/ahm.json", null, null).getOpenAPI();
isDeterminismConsumer( new ArrayList<>(), (args) -> RestActionBuilderV3.INSTANCE
.getModelsFromSwagger(schema, new LinkedHashMap<>(),
new RestActionBuilderV3.Options(false,enableConstraintHandling,false,0.0,0.0,true,false)));
new RestActionBuilderV3.Options(false,enableConstraintHandling,false,0.0,0.0,true,false,false)));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void testDeterminismOfLog(boolean enableConstraintHandling){
OpenAPI schema = (new OpenAPIParser()).readLocation("swagger-ahm/ahm.json", null, null).getOpenAPI();
isDeterminismConsumer( new ArrayList<>(), (args) -> {
RestActionBuilderV3.INSTANCE.getModelsFromSwagger(schema, new LinkedHashMap<>(),
new RestActionBuilderV3.Options(false,enableConstraintHandling,false,0.0,0.0,true,false));
new RestActionBuilderV3.Options(false,enableConstraintHandling,false,0.0,0.0,true,false,false));
});
}

Expand Down
9 changes: 9 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/EMConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3251,6 +3251,15 @@ class EMConfig {
var defaultDelayInSecondsFor429 = 10


@Experimental
@Cfg("When dealing with string data, infer constraints based on the name or description." +
" For example, a string field called 'uuid' likely is going to represent an UUID." +
" A string property referring to 'ISO 8601' in its description might be a date." +
" And so on." +
" This is just an heuristics though, and unrestricted strings would still be sampled with a given probability.")
var inferFormatFromNames = false


fun getProbabilityUseDataPool() : Double{
return if(blackBox){
bbProbabilityUseDataPool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ import org.evomaster.core.search.gene.regex.RegexGene
import org.evomaster.core.search.gene.string.Base64StringGene
import org.evomaster.core.search.gene.string.StringGene
import org.evomaster.core.search.gene.uri.UriGene
import org.evomaster.core.search.gene.uri.UrlHttpGene
import org.evomaster.core.search.gene.utils.GeneUtils
import org.evomaster.core.search.gene.wrapper.NullableGene
import org.evomaster.core.utils.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.URI
Expand Down Expand Up @@ -110,14 +112,17 @@ object RestActionBuilderV3 {
val usingWhiteBox: Boolean = true,

val enableAdvancedFormats: Boolean = true,

val inferFormatFromNames: Boolean = true,
){
constructor(config: EMConfig): this(
enableConstraintHandling = config.enableSchemaConstraintHandling,
invalidData = config.allowInvalidData,
probUseDefault = config.probRestDefault,
probUseExamples = config.probRestExamples,
usingWhiteBox = !config.blackBox,
enableAdvancedFormats = config.enableAdvancedFormats
enableAdvancedFormats = config.enableAdvancedFormats,
inferFormatFromNames = config.inferFormatFromNames
)

init {
Expand Down Expand Up @@ -1035,7 +1040,7 @@ object RestActionBuilderV3 {
"boolean" -> return BooleanGene(name)
"string" -> {
return if (schema.pattern == null) {
createNonObjectGeneWithSchemaConstraints(
val gene = createNonObjectGeneWithSchemaConstraints(
schema,
name,
StringGene::class.java,
Expand All @@ -1045,6 +1050,12 @@ object RestActionBuilderV3 {
examples,
messages = messages
) //StringGene(name)

if(options.inferFormatFromNames){
heuristicInferFormatFromName(gene, name, schema.description)
} else {
gene
}
} else {
try {
createNonObjectGeneWithSchemaConstraints(
Expand Down Expand Up @@ -1213,6 +1224,78 @@ object RestActionBuilderV3 {
throw IllegalArgumentException("Cannot handle combination $type/$format")
}

private fun heuristicInferFormatFromName(
gene: Gene,
name: String,
description: String?
): Gene {

//TODO should handle min/max length constraint, if any

//RFC 3339, RFC-3339, RFC3339
//ISO 8601, ISO-8601, ISO8601
val rfc3339 = description!=null && description.contains("rfc",true) && description.contains("3339",true)
val iso8601 = description!=null && description.contains("iso",true) && description.contains("8601",true)

handleNameMatch("uuid",name,gene){n -> UUIDGene(n)}?.let { return it }
handleNameMatch("email",name,gene){n -> createEmailGene(n)}?.let { return it }
handleNameMatch("uri", name, gene){n -> UriGene(n) }?.let { return it }
handleNameMatch("url", name, gene){n -> UrlHttpGene(n) }?.let { return it }
handleNameMatch("website", name, gene){n -> UrlHttpGene(n) }?.let { return it }
handleNameMatch("href", name, gene){n -> UrlHttpGene(n) }?.let { return it }
handleNameMatch("date", name, gene){n ->
if(rfc3339) {
DateTimeGene(n, format = FormatForDatesAndTimes.RFC3339)
} else if(iso8601){
DateTimeGene(n, format = FormatForDatesAndTimes.ISO_LOCAL)
} else {
DateGene(n)
}
}?.let { return it }

if(description == null){
//nothing we can infer further
return gene
}

//if no name match, look at description, if any
return when{
rfc3339 -> handleDescriptionMatch(name,gene){DateTimeGene(name, format = FormatForDatesAndTimes.RFC3339)}
iso8601 -> handleDescriptionMatch(name,gene){DateTimeGene(name, format = FormatForDatesAndTimes.ISO_LOCAL)}
StringUtils.hasWord(description,"uuid") -> handleDescriptionMatch(name,gene){n -> UUIDGene(n) }
StringUtils.hasWord(description,"date") -> handleDescriptionMatch(name,gene){n -> DateGene(n) }
StringUtils.hasWord(description,"uri") -> handleDescriptionMatch(name,gene){n -> UriGene(n)}
StringUtils.hasWord(description,"url") -> handleDescriptionMatch(name,gene){n -> UrlHttpGene(n) }
StringUtils.hasWord(description,"urls") -> handleDescriptionMatch(name,gene){n -> UrlHttpGene(n) }
StringUtils.hasWord(description,"website") -> handleDescriptionMatch(name,gene){n -> UrlHttpGene(n) }
StringUtils.hasWord(description,"href") -> handleDescriptionMatch(name,gene){n -> UrlHttpGene(n) }
StringUtils.hasWord(description,"email") -> handleDescriptionMatch(name,gene){n -> createEmailGene(n)}
else -> gene
}
}

private fun handleDescriptionMatch(
name: String,
gene: Gene,
producer: (String) -> Gene): Gene {

val pDescriptionMatch = 0.5

return ChoiceGene(name, listOf(producer(name), gene), probabilities = listOf(pDescriptionMatch, 1-pDescriptionMatch))
}

private fun handleNameMatch(format: String, name: String, gene: Gene, producer: (String) -> Gene) : Gene?{

val pNameMatch = 0.8

if(name.startsWith(format,true) || name.endsWith(format, true)) {
val choice = ChoiceGene(name, listOf(producer(name),gene), probabilities = listOf(pNameMatch,1.0-pNameMatch))
return choice
}

return null
}

private fun createGeneBasedOnAdvancedFormats(
format: String,
schema: Schema<*>,
Expand Down Expand Up @@ -1297,7 +1380,7 @@ object RestActionBuilderV3 {

private fun createEmailGene(
name: String,
options: Options
options: Options? = null
): Gene {

/*
Expand All @@ -1307,7 +1390,7 @@ object RestActionBuilderV3 {
After all, here we just need to sample valid emails, and not verify
if a string is a valid email.
*/
return RegexHandler.createGeneForJVM("[A-Za-z0-9]{2,}@[A-Za-z0-9]+.[A-Za-z]{2,}")
return RegexHandler.createGeneForJVM("[A-Za-z0-9]{2,}@[A-Za-z0-9]+\\.[A-Za-z]{2,}")
.apply { this.name = name }
}

Expand Down
12 changes: 12 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/search/gene/UUIDGene.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,16 @@ class UUIDGene(
return this.mostSigBits.unsafeCopyValueFrom(gene.mostSigBits)
&& this.leastSigBits.unsafeCopyValueFrom(gene.leastSigBits)
}

override fun unsafeSetFromStringValue(value: String): Boolean {

val uuid = try{
UUID.fromString(value)
} catch (e: IllegalArgumentException){
return false
}
mostSigBits.value = uuid.mostSignificantBits
leastSigBits.value = uuid.leastSignificantBits
return true
}
}
Loading
Loading