From 9d75ff453656fd5a960dea713b9fe08437100f72 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Thu, 4 Jun 2026 08:45:05 +0200 Subject: [PATCH 1/6] preparing format inference based on name/description info --- .../kotlin/org/evomaster/core/EMConfig.kt | 7 ++ .../rest/builder/RestActionBuilderV3.kt | 83 ++++++++++++++++++- .../gene/datetime/FormatForDatesAndTimes.kt | 1 + .../org/evomaster/core/utils/StringUtils.kt | 20 +++++ docs/options.md | 1 + 5 files changed, 111 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index cc041e3535..43ab1a9a82 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -3247,6 +3247,13 @@ class EMConfig { var defaultDelayInSecondsFor429 = 10 + @Experimental + @Cfg("When dealing with string data, infer constraints based on the name." + + " For example, a string field called 'uuid' likely is going to represent an UUID." + + " This is an heuristics, and unrestricted strings would still be sampled with a given probability.") + var inferFormatFromNames = false + + fun getProbabilityUseDataPool() : Double{ return if(blackBox){ bbProbabilityUseDataPool 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 306d746182..4b921c44d6 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 @@ -14,6 +14,7 @@ import io.swagger.v3.oas.models.media.ObjectSchema import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.parameters.Parameter import io.swagger.v3.oas.models.responses.ApiResponse +import org.checkerframework.checker.units.qual.g import org.evomaster.client.java.instrumentation.shared.ClassToSchemaUtils.OPENAPI_COMPONENT_NAME import org.evomaster.client.java.instrumentation.shared.ClassToSchemaUtils.OPENAPI_SCHEMA_NAME import org.evomaster.client.java.instrumentation.shared.TaintInputName @@ -51,8 +52,11 @@ import org.evomaster.core.search.gene.placeholder.LimitObjectGene 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 @@ -1024,7 +1028,7 @@ object RestActionBuilderV3 { "boolean" -> return BooleanGene(name) "string" -> { return if (schema.pattern == null) { - createNonObjectGeneWithSchemaConstraints( + val gene = createNonObjectGeneWithSchemaConstraints( schema, name, StringGene::class.java, @@ -1034,6 +1038,12 @@ object RestActionBuilderV3 { examples, messages = messages ) //StringGene(name) + + if(true){ //TODO inferFormatFromNames + heuristicInferFormatFromName(gene, name, schema.description) + } else { + gene + } } else { try { createNonObjectGeneWithSchemaConstraints( @@ -1202,6 +1212,77 @@ 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 ->} //TODO + 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,"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 -> } //TODO + 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 + } + /** * @param referenceTypeName is the name of object type */ diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt index 31c528ced9..3345ffa4fc 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt @@ -3,6 +3,7 @@ package org.evomaster.core.search.gene.datetime enum class FormatForDatesAndTimes { // YYYY-MM-DDTHH:MM:SS + // ISO 8601 ISO_LOCAL, diff --git a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt index 26ec83ac4b..4601bf4e04 100644 --- a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt @@ -2,6 +2,26 @@ package org.evomaster.core.utils object StringUtils { + private val delimiters = listOf(' ', '\n', '\r', '\t', ',', '.','!','?',';','"','\'','-','_') + + fun hasWord(text: String, word: String): Boolean { + + var start = text.indexOf(word, 0, true) + + while(start >= 0){ + + val before = start == 0 || delimiters.contains(text[start-1]) + val end = start + word.length + val after = end == text.length || delimiters.contains(text[end-1]) + + if(before && after){ + return true + } + start = text.indexOf(word, start, true) + } + return false + } + /** * Capitalizes a word, lowercasing the rest of the word. For example, stringProperty would be modified into * Stringproperty. diff --git a/docs/options.md b/docs/options.md index aa6b5227d1..38e355682f 100644 --- a/docs/options.md +++ b/docs/options.md @@ -294,6 +294,7 @@ There are 3 types of options: |`heuristicsForRedis`| __Boolean__. Tracking of Redis commands to improve test generation. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`heuristicsForSQLAdvanced`| __Boolean__. If using SQL heuristics, enable more advanced version. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`httpOracles`| __Boolean__. Extra checks on HTTP properties in returned responses, used as automated oracles to detect faults. *Default value*: `false`.| +|`inferFormatFromNames`| __Boolean__. When dealing with string data, infer constraints based on the name. For example, a string field called 'uuid' likely is going to represent an UUID. This is an heuristics, and unrestricted strings would still be sampled with a given probability. *Default value*: `false`.| |`initStructureMutationProbability`| __Double__. Probability of applying a mutation that can change the structure of test's initialization if it has. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.0`.| |`instrumentMR_CASSANDRA`| __Boolean__. Execute instrumentation for method replace with category CASSANDRA. Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin on the JVM. *Default value*: `false`.| |`instrumentMR_DYNAMODB`| __Boolean__. Execute instrumentation for method replace with category DYNAMODB. Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin on the JVM. *Default value*: `false`.| From 35d5dfa2434b7178f5a25d9cf79905cf40a2cc5e Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Thu, 4 Jun 2026 08:56:03 +0200 Subject: [PATCH 2/6] option inferFormatFromNames --- .../problem/rest/builder/RestActionBuilderV3.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 293d3c8064..7aa88fc5ef 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 @@ -53,7 +53,6 @@ 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.uri.UriGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.gene.wrapper.NullableGene import org.evomaster.core.utils.StringUtils @@ -113,6 +112,8 @@ object RestActionBuilderV3 { val usingWhiteBox: Boolean = true, val enableAdvancedFormats: Boolean = true, + + val inferFormatFromNames: Boolean = true, ){ constructor(config: EMConfig): this( enableConstraintHandling = config.enableSchemaConstraintHandling, @@ -120,7 +121,8 @@ object RestActionBuilderV3 { probUseDefault = config.probRestDefault, probUseExamples = config.probRestExamples, usingWhiteBox = !config.blackBox, - enableAdvancedFormats = config.enableAdvancedFormats + enableAdvancedFormats = config.enableAdvancedFormats, + inferFormatFromNames = config.inferFormatFromNames ) init { @@ -1049,7 +1051,7 @@ object RestActionBuilderV3 { messages = messages ) //StringGene(name) - if(true){ //TODO inferFormatFromNames + if(options.inferFormatFromNames){ heuristicInferFormatFromName(gene, name, schema.description) } else { gene @@ -1236,7 +1238,7 @@ object RestActionBuilderV3 { 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 ->} //TODO + 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 } @@ -1266,7 +1268,7 @@ object RestActionBuilderV3 { 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 -> } //TODO + StringUtils.hasWord(description,"email") -> handleDescriptionMatch(name,gene){n -> createEmailGene(n)} else -> gene } } @@ -1377,7 +1379,7 @@ object RestActionBuilderV3 { private fun createEmailGene( name: String, - options: Options + options: Options? = null ): Gene { /* From fa9de7125efaddb92f49bb99953d7274eb44e3cc Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Thu, 4 Jun 2026 09:07:49 +0200 Subject: [PATCH 3/6] fixed compilation error --- .../spring/examples/adaptivehypermutation/DeterminismTest.java | 2 +- .../spring/examples/adaptivehypermutation/DeterminismTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java b/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java index 80a61ac578..75c9268ddd 100644 --- a/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java +++ b/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java @@ -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))); } diff --git a/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java b/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java index 2ed5d64793..2ddc3b2b3e 100644 --- a/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java +++ b/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java @@ -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)); }); } From e6bf2e772b88de2dca199ae80ccf8783195590cb Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Thu, 4 Jun 2026 09:34:06 +0200 Subject: [PATCH 4/6] fixed hasWord() --- .../org/evomaster/core/utils/StringUtils.kt | 12 +++++++++--- .../evomaster/core/utils/StringUtilsTest.kt | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt index 4601bf4e04..ea98bfc57d 100644 --- a/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt @@ -2,8 +2,14 @@ package org.evomaster.core.utils object StringUtils { - private val delimiters = listOf(' ', '\n', '\r', '\t', ',', '.','!','?',';','"','\'','-','_') + private val delimiters = listOf(' ', '\n', '\r', '\t', ',', '.','!','?',';','"','\'','-','_','(',')') + /** + * Check if the given [text] contains the specified [word]. + * This is not a simple "contains" check, as we need to make sure we are not dealing + * with any embedding of another word. + * eg, 'url' should not match when finding 'curling'. + */ fun hasWord(text: String, word: String): Boolean { var start = text.indexOf(word, 0, true) @@ -12,12 +18,12 @@ object StringUtils { val before = start == 0 || delimiters.contains(text[start-1]) val end = start + word.length - val after = end == text.length || delimiters.contains(text[end-1]) + val after = end == text.length || delimiters.contains(text[end]) if(before && after){ return true } - start = text.indexOf(word, start, true) + start = text.indexOf(word, end, true) } return false } diff --git a/core/src/test/kotlin/org/evomaster/core/utils/StringUtilsTest.kt b/core/src/test/kotlin/org/evomaster/core/utils/StringUtilsTest.kt index ebe7bbb7d5..71e72b7c0b 100644 --- a/core/src/test/kotlin/org/evomaster/core/utils/StringUtilsTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/utils/StringUtilsTest.kt @@ -5,6 +5,24 @@ import org.junit.jupiter.api.Test class StringUtilsTest{ + @Test + fun testHasWord(){ + + assertTrue(StringUtils.hasWord("Hello World!", "hello")) + assertTrue(StringUtils.hasWord("This is a date in ISO 8601 format", "date")) + assertTrue(StringUtils.hasWord("Date, data, datum!", "DATE")) + assertTrue(StringUtils.hasWord("URL!", "url")) + assertTrue(StringUtils.hasWord("Dashes '-' should still work, eg., link-url", "url")) + assertTrue(StringUtils.hasWord("The link (URL) to the page", "url")) + assertTrue(StringUtils.hasWord("it was\nurl\nall along", "url")) + assertTrue(StringUtils.hasWord("xdate, ydate.zdate- date\tdatum", "date")) + + + assertFalse(StringUtils.hasWord("The joys of curling", "url")) + assertFalse(StringUtils.hasWord("The linkURL to the page", "url")) + assertFalse(StringUtils.hasWord("xdate, ydate.zdate- dat\te\tdatum", "date")) + } + @Test fun testLinesWithMaxLength(){ From 3534d92ec22aa25bb1b903d0c83c993841ed6fa8 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Thu, 4 Jun 2026 09:54:49 +0200 Subject: [PATCH 5/6] clarification --- core/src/main/kotlin/org/evomaster/core/EMConfig.kt | 6 ++++-- docs/options.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 88dc8deed3..e0ee18b069 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -3252,9 +3252,11 @@ class EMConfig { @Experimental - @Cfg("When dealing with string data, infer constraints based on the name." + + @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." + - " This is an heuristics, and unrestricted strings would still be sampled with a given probability.") + " 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 diff --git a/docs/options.md b/docs/options.md index 1764f7203a..d2d7ebd3a6 100644 --- a/docs/options.md +++ b/docs/options.md @@ -295,7 +295,7 @@ There are 3 types of options: |`heuristicsForRedis`| __Boolean__. Tracking of Redis commands to improve test generation. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`heuristicsForSQLAdvanced`| __Boolean__. If using SQL heuristics, enable more advanced version. *Depends on*: `blackBox=false`. *Default value*: `false`.| |`httpOracles`| __Boolean__. Extra checks on HTTP properties in returned responses, used as automated oracles to detect faults. *Default value*: `false`.| -|`inferFormatFromNames`| __Boolean__. When dealing with string data, infer constraints based on the name. For example, a string field called 'uuid' likely is going to represent an UUID. This is an heuristics, and unrestricted strings would still be sampled with a given probability. *Default value*: `false`.| +|`inferFormatFromNames`| __Boolean__. 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. *Default value*: `false`.| |`initStructureMutationProbability`| __Double__. Probability of applying a mutation that can change the structure of test's initialization if it has. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.0`.| |`instrumentMR_CASSANDRA`| __Boolean__. Execute instrumentation for method replace with category CASSANDRA. Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin on the JVM. *Default value*: `false`.| |`instrumentMR_DYNAMODB`| __Boolean__. Execute instrumentation for method replace with category DYNAMODB. Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin on the JVM. *Default value*: `false`.| From 8aed715c88529375bd04d81743330d29dbbeabc7 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Thu, 4 Jun 2026 11:30:04 +0200 Subject: [PATCH 6/6] e2e for infer format --- .../BBAdvancedFormatsApplication.kt | 2 +- .../inferformat/BBInferFormatApplication.kt | 97 +++++++++++++++++++ .../bb/inferformat/BBInferFormatDto.kt | 12 +++ .../bb/inferformat/BBInferFormatController.kt | 6 ++ .../BBAdvancedFormatsEMTest.kt | 1 + .../bb/inferformat/BBInferFormatEMTest.kt | 52 ++++++++++ .../rest/builder/RestActionBuilderV3.kt | 5 +- .../evomaster/core/search/gene/UUIDGene.kt | 12 +++ .../core/search/gene/datetime/DateGene.kt | 14 +++ .../core/search/gene/datetime/DateTimeGene.kt | 33 +++++++ .../gene/datetime/FormatForDatesAndTimes.kt | 15 ++- .../core/search/gene/datetime/TimeGene.kt | 19 ++++ 12 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatDto.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/inferformat/BBInferFormatEMTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/advancedformats/BBAdvancedFormatsApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/advancedformats/BBAdvancedFormatsApplication.kt index 0cac89d9a4..bc2e988fd6 100644 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/advancedformats/BBAdvancedFormatsApplication.kt +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/advancedformats/BBAdvancedFormatsApplication.kt @@ -43,7 +43,7 @@ open class BBAdvancedFormatsApplication { @RequestParam(required = true) x: String? ) : ResponseEntity { - if (x == null || !x.contains('@')) { + if (x == null || !x.contains('@') || !x.contains('.')) { return ResponseEntity.status(400).build() } diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatApplication.kt new file mode 100644 index 0000000000..59519c0167 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatApplication.kt @@ -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) { + SpringApplication.run(BBInferFormatApplication::class.java, *args) + } + } + + + @GetMapping("/uuid") + open fun getUuid( + @RequestParam(required = true) uuid: String? + ) : ResponseEntity { + + 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 { + + 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 { + + 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 { + + UUID.fromString(dto.foo) + + CoveredTargets.cover("description-uuid") + + return ResponseEntity.status(200).body("OK") + } + + @PostMapping("/date") + open fun postDate( + @RequestBody dto: BBInferFormatDto + ) : ResponseEntity { + + LocalDateTime.parse(dto.bar!!) + + CoveredTargets.cover("description-date") + + return ResponseEntity.status(200).body("OK") + } +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatDto.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatDto.kt new file mode 100644 index 0000000000..768e5bf678 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatDto.kt @@ -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, +) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatController.kt new file mode 100644 index 0000000000..1791bcff5e --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/inferformat/BBInferFormatController.kt @@ -0,0 +1,6 @@ +package com.foo.rest.examples.bb.inferformat + +import com.foo.rest.examples.bb.SpringController + + +class BBInferFormatController : SpringController(BBInferFormatApplication::class.java) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/advancedformats/BBAdvancedFormatsEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/advancedformats/BBAdvancedFormatsEMTest.kt index c51be0d8d4..cc86201d39 100644 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/advancedformats/BBAdvancedFormatsEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/advancedformats/BBAdvancedFormatsEMTest.kt @@ -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) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/inferformat/BBInferFormatEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/inferformat/BBInferFormatEMTest.kt new file mode 100644 index 0000000000..87e4092004 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/inferformat/BBInferFormatEMTest.kt @@ -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 -> + + 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") + } + } +} 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 7aa88fc5ef..8f322cc1af 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 @@ -1051,7 +1051,7 @@ object RestActionBuilderV3 { messages = messages ) //StringGene(name) - if(options.inferFormatFromNames){ + if(options.inferFormatFromNames){ heuristicInferFormatFromName(gene, name, schema.description) } else { gene @@ -1262,6 +1262,7 @@ object RestActionBuilderV3 { 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) } @@ -1389,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 } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/UUIDGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/UUIDGene.kt index 561eca6f78..d070707ca6 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/UUIDGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/UUIDGene.kt @@ -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 + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateGene.kt index a636246eaa..82c466918a 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateGene.kt @@ -209,4 +209,18 @@ class DateGene( return false } + override fun unsafeSetFromStringValue(value: String): Boolean { + + val formatter = DateTimeFormatter.ofPattern("YYYY-MM-DD") + val date = try{ + LocalDate.parse(value, formatter) + }catch (ex: DateTimeParseException){ + return false + } + + year.value = date.year + month.value = date.monthValue + day.value = date.dayOfMonth + return true + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateTimeGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateTimeGene.kt index c7592f4890..f513b6a697 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateTimeGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/DateTimeGene.kt @@ -15,6 +15,11 @@ import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMuta import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException /** * Using RFC3339 @@ -168,4 +173,32 @@ open class DateTimeGene( return false } + override fun unsafeSetFromStringValue(value: String): Boolean { + + val pd = DateTimeFormatter.ofPattern("YYYY-MM-DD") + val pt = DateTimeFormatter.ofPattern("HH:MM:SS") + + val (dateValue, timeValue) = try{ + if(format == FormatForDatesAndTimes.RFC3339) { + val dateTime = OffsetDateTime.parse(value) + + val dateValue = dateTime.format(pd) + val timeValue = dateTime.format(pt) + Pair(dateValue, timeValue) + + } else{ + val formatter = DateTimeFormatter.ofPattern(format.pattern) + val dateTime = LocalDateTime.parse(value, formatter) + + val dateValue = dateTime.format(pd) + val timeValue = dateTime.format(pt) + Pair(dateValue, timeValue) + } + }catch (e: Exception){ + return false + } + + return date.unsafeSetFromStringValue(dateValue) && time.unsafeSetFromStringValue(timeValue) + } + } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt index 3345ffa4fc..b649820772 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/FormatForDatesAndTimes.kt @@ -1,18 +1,15 @@ package org.evomaster.core.search.gene.datetime -enum class FormatForDatesAndTimes { +enum class FormatForDatesAndTimes( + val pattern: String +) { - // YYYY-MM-DDTHH:MM:SS // ISO 8601 - ISO_LOCAL, - + ISO_LOCAL("YYYY-MM-DDTHH:MM:SS"), //https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 - // // YYYY-MM-DDTHH:MM:SS[.mmm](Z|+-hh) - RFC3339, - + RFC3339("YYYY-MM-DDTHH:MM:SS[.mmm](Z|+-hh)"), - // YYYY-MM-DD HH:MM:SS // Note the missing T. used for example in SQL - DATETIME + DATETIME("YYYY-MM-DD HH:MM:SS") } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/TimeGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/TimeGene.kt index 27691cd388..41bd8e642e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/TimeGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/datetime/TimeGene.kt @@ -16,6 +16,9 @@ import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMuta import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException /** * Using RFC3339 @@ -254,4 +257,20 @@ class TimeGene( return !onlyValidTimes || isValidTime() } + override fun unsafeSetFromStringValue(value: String): Boolean { + + val formatter = DateTimeFormatter.ofPattern("HH:MM:SS") + val localTime = try{ + LocalTime.parse(value, formatter) + }catch(ex: DateTimeParseException){ + return false + } + + hour.value = localTime.hour + minute.value = localTime.minute + second.value = localTime.second + + //TODO milliseconds and offset + return true + } } \ No newline at end of file