From 278d86534247a032fbb35ce7b189d16ee044e306 Mon Sep 17 00:00:00 2001 From: Samy Date: Thu, 14 Nov 2024 15:17:12 +0530 Subject: [PATCH 01/16] Pattern Validation - Max Length Issue Fix --- .../specmatic/core/pattern/StringPattern.kt | 16 ++++++++++---- .../core/pattern/StringPatternTest.kt | 22 +++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 4bb2b5fed..ba51c002f 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -67,20 +67,28 @@ data class StringPattern ( return JSONArrayValue(valueList) } + //Tells us the minimum length to be used for random string private val randomStringLength: Int = when { minLength != null && 5 < minLength -> minLength maxLength != null && 5 > maxLength -> maxLength else -> 5 } - + override fun generate(resolver: Resolver): Value { val defaultExample: Value? = resolver.resolveExample(example, this) - if (regex != null) { - if(defaultExample == null) - return StringValue(Generex(regex.removePrefix("^").removeSuffix("$")).random(randomStringLength)) + if(defaultExample == null) { + if (maxLength != null) + return StringValue( + Generex(regex.removePrefix("^").removeSuffix("$")).random( + randomStringLength, + maxLength + ) + ) + return StringValue(Generex(regex.removePrefix("^").removeSuffix("$")).random(randomStringLength)) + } val defaultExampleMatchResult = matches(defaultExample, resolver) if(defaultExampleMatchResult.isSuccess()) diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index 9b01414af..286580106 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -92,10 +92,20 @@ internal class StringPatternTest { @MethodSource("lengthTestValues") fun `generate string value of appropriate length matching minLength and maxLength parameters`(min: Int?, max: Int?, length: Int) { val result = StringPattern(minLength = min, maxLength = max).generate(Resolver()) as StringValue - - assertThat(result.string.length).isEqualTo(length) + val generatedLength = result.string.length + val randomStringDefaultLength = 5; + // If max is provided, ensure the generated length is within the range of min and max + if (max != null) { + // Ensure that the generated string length is between min (or 0 if min is null) and max + assertThat(generatedLength).isGreaterThanOrEqualTo(min ?: 0) + assertThat(generatedLength).isLessThanOrEqualTo(max) + } else { + // If max is not provided, ensure the generated length is at least the min (or randomStringDefaultLength if min is null) + assertThat(generatedLength).isGreaterThanOrEqualTo(min ?: randomStringDefaultLength) + } } + @Test fun `string should encompass enum of string`() { val result: Result = StringPattern().encompasses( @@ -240,4 +250,12 @@ internal class StringPatternTest { fun `string pattern encompasses email`() { assertThat(StringPattern().encompasses(EmailPattern(), Resolver(), Resolver())).isInstanceOf(Result.Success::class.java) } + + @Test + fun `should fail to generate string when maxLength is less than minLength`() { + val exception = assertThrows { + StringPattern(minLength = 6, maxLength = 4) + } + assertThat(exception.message).isEqualTo("maxLength cannot be less than minLength") + } } From f0661cac9d6bdd6e4c584c591b64d2936451718d Mon Sep 17 00:00:00 2001 From: Samy Date: Sat, 16 Nov 2024 11:00:40 +0530 Subject: [PATCH 02/16] Min Length Fix Tested --- .../io/specmatic/core/pattern/StringPattern.kt | 13 ++++++------- .../io/specmatic/core/pattern/StringPatternTest.kt | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index ba51c002f..d1e27a556 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -68,11 +68,10 @@ data class StringPattern ( } //Tells us the minimum length to be used for random string - private val randomStringLength: Int = + private val patternMinLength: Int = when { - minLength != null && 5 < minLength -> minLength - maxLength != null && 5 > maxLength -> maxLength - else -> 5 + minLength != null && minLength > 0 -> minLength + else -> 1 } override fun generate(resolver: Resolver): Value { @@ -82,12 +81,12 @@ data class StringPattern ( if (maxLength != null) return StringValue( Generex(regex.removePrefix("^").removeSuffix("$")).random( - randomStringLength, + patternMinLength, maxLength ) ) - return StringValue(Generex(regex.removePrefix("^").removeSuffix("$")).random(randomStringLength)) + return StringValue(Generex(regex.removePrefix("^").removeSuffix("$")).random(patternMinLength)) } val defaultExampleMatchResult = matches(defaultExample, resolver) @@ -104,7 +103,7 @@ data class StringPattern ( return defaultExample } - return StringValue(randomString(randomStringLength)) + return StringValue(randomString(patternMinLength)) } override fun newBasedOn(row: Row, resolver: Resolver): Sequence> { diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index 286580106..4799ccd31 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -37,7 +37,7 @@ internal class StringPatternTest { @Test fun `should generate 5 character long random string when min and max length are not specified`() { - assertThat(StringPattern().generate(Resolver()).toStringLiteral().length).isEqualTo(5) + assertThat(StringPattern().generate(Resolver()).toStringLiteral().length).isEqualTo(1) } @Test @@ -93,15 +93,15 @@ internal class StringPatternTest { fun `generate string value of appropriate length matching minLength and maxLength parameters`(min: Int?, max: Int?, length: Int) { val result = StringPattern(minLength = min, maxLength = max).generate(Resolver()) as StringValue val generatedLength = result.string.length - val randomStringDefaultLength = 5; + val minPatternLength = min ?: 1; // If max is provided, ensure the generated length is within the range of min and max if (max != null) { // Ensure that the generated string length is between min (or 0 if min is null) and max - assertThat(generatedLength).isGreaterThanOrEqualTo(min ?: 0) + assertThat(generatedLength).isGreaterThanOrEqualTo(minPatternLength) assertThat(generatedLength).isLessThanOrEqualTo(max) } else { // If max is not provided, ensure the generated length is at least the min (or randomStringDefaultLength if min is null) - assertThat(generatedLength).isGreaterThanOrEqualTo(min ?: randomStringDefaultLength) + assertThat(generatedLength).isGreaterThanOrEqualTo(minPatternLength) } } From a6b4f197f803fca8f7cb6f347b08b0f021d92326 Mon Sep 17 00:00:00 2001 From: Samy Date: Sat, 16 Nov 2024 13:30:12 +0530 Subject: [PATCH 03/16] Default case updated --- .../io/specmatic/core/pattern/StringPattern.kt | 3 ++- .../io/specmatic/core/pattern/StringPatternTest.kt | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index d1e27a556..82f999961 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -71,7 +71,8 @@ data class StringPattern ( private val patternMinLength: Int = when { minLength != null && minLength > 0 -> minLength - else -> 1 + maxLength != null && maxLength < 5 -> 1 + else -> 5 } override fun generate(resolver: Resolver): Value { diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index 4799ccd31..f589650e7 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -93,15 +93,21 @@ internal class StringPatternTest { fun `generate string value of appropriate length matching minLength and maxLength parameters`(min: Int?, max: Int?, length: Int) { val result = StringPattern(minLength = min, maxLength = max).generate(Resolver()) as StringValue val generatedLength = result.string.length - val minPatternLength = min ?: 1; + val patternMinLength: Int = + when { + min != null && min > 0 -> min + max != null && max < 5 -> 1 + else -> 5 + } + // If max is provided, ensure the generated length is within the range of min and max if (max != null) { // Ensure that the generated string length is between min (or 0 if min is null) and max - assertThat(generatedLength).isGreaterThanOrEqualTo(minPatternLength) + assertThat(generatedLength).isGreaterThanOrEqualTo(patternMinLength) assertThat(generatedLength).isLessThanOrEqualTo(max) } else { // If max is not provided, ensure the generated length is at least the min (or randomStringDefaultLength if min is null) - assertThat(generatedLength).isGreaterThanOrEqualTo(minPatternLength) + assertThat(generatedLength).isGreaterThanOrEqualTo(patternMinLength) } } From 06611820159ae6553d39d5f577e95d944c44b192 Mon Sep 17 00:00:00 2001 From: Samy Date: Sat, 16 Nov 2024 14:21:23 +0530 Subject: [PATCH 04/16] Test case fixed --- .../test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index f589650e7..437150eb5 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -37,7 +37,7 @@ internal class StringPatternTest { @Test fun `should generate 5 character long random string when min and max length are not specified`() { - assertThat(StringPattern().generate(Resolver()).toStringLiteral().length).isEqualTo(1) + assertThat(StringPattern().generate(Resolver()).toStringLiteral().length).isEqualTo(5) } @Test From 63a8fadcc3265bde919ccae847e2e07fb1583cd1 Mon Sep 17 00:00:00 2001 From: Samy Date: Mon, 18 Nov 2024 14:22:45 +0530 Subject: [PATCH 05/16] Cleaned code- Removed Cognitive Complexity --- .../specmatic/core/pattern/StringPattern.kt | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 82f999961..317d90b01 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -11,7 +11,8 @@ import io.specmatic.core.value.Value import java.nio.charset.StandardCharsets import java.util.* -data class StringPattern ( +data class +StringPattern ( override val typeAlias: String? = null, val minLength: Int? = null, val maxLength: Int? = null, @@ -77,34 +78,26 @@ data class StringPattern ( override fun generate(resolver: Resolver): Value { val defaultExample: Value? = resolver.resolveExample(example, this) - if (regex != null) { - if(defaultExample == null) { - if (maxLength != null) - return StringValue( - Generex(regex.removePrefix("^").removeSuffix("$")).random( - patternMinLength, - maxLength - ) - ) - - return StringValue(Generex(regex.removePrefix("^").removeSuffix("$")).random(patternMinLength)) - } - val defaultExampleMatchResult = matches(defaultExample, resolver) - if(defaultExampleMatchResult.isSuccess()) - return defaultExample - - throw ContractException("Schema example ${defaultExample.toStringLiteral()} does not match pattern $regex") + return if (regex != null) { + handleRegex(defaultExample,resolver) + } else { + defaultExample?.takeIf { it is StringValue } + ?: StringValue(randomString(patternMinLength)) } - - if(defaultExample != null) { - if(defaultExample !is StringValue) - throw ContractException("Schema example ${defaultExample.toStringLiteral()} is not a string") - + } + private fun handleRegex(defaultExample: Value?,resolver: Resolver): Value { + if (defaultExample == null) { + val cleanedRegex = regex!!.removePrefix("^").removeSuffix("$") + val randomValue = maxLength?.let { + Generex(cleanedRegex).random(patternMinLength, it) + } ?: Generex(cleanedRegex).random(patternMinLength) + return StringValue(randomValue) + } + if (matches(defaultExample, resolver).isSuccess()) { return defaultExample } - - return StringValue(randomString(patternMinLength)) + throw ContractException("Schema example ${defaultExample.toStringLiteral()} does not match pattern $regex") } override fun newBasedOn(row: Row, resolver: Resolver): Sequence> { From 6b177cff607921dc555b9235faebaa577ac28311 Mon Sep 17 00:00:00 2001 From: Samy Date: Mon, 18 Nov 2024 16:17:41 +0530 Subject: [PATCH 06/16] Pattern Validation Logic for Minimum Size Regex --- .../io/specmatic/core/pattern/StringPattern.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 317d90b01..01393d095 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -1,8 +1,12 @@ package io.specmatic.core.pattern import com.mifmif.common.regex.Generex +import dk.brics.automaton.Automaton +import dk.brics.automaton.RegExp import io.specmatic.core.Resolver import io.specmatic.core.Result +import io.specmatic.core.log.StringLog +import io.specmatic.core.log.consoleLog import io.specmatic.core.mismatchResult import io.specmatic.core.pattern.config.NegativePatternConfiguration import io.specmatic.core.value.JSONArrayValue @@ -11,6 +15,7 @@ import io.specmatic.core.value.Value import java.nio.charset.StandardCharsets import java.util.* + data class StringPattern ( override val typeAlias: String? = null, @@ -23,6 +28,17 @@ StringPattern ( if (minLength != null && maxLength != null && minLength > maxLength) { throw IllegalArgumentException("maxLength cannot be less than minLength") } + + if(regex != null) { + val automaton: Automaton = RegExp(regex).toAutomaton() + val min = automaton.getShortestExample(true).length + when { + minLength != null && min < minLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") + maxLength != null && min > maxLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") + } + } } override fun matches(sampleData: Value?, resolver: Resolver): Result { From 80c5f511b1019aa8b4d81dd387814339bc2e9a4a Mon Sep 17 00:00:00 2001 From: Samy Date: Mon, 18 Nov 2024 16:18:44 +0530 Subject: [PATCH 07/16] Pattern Validation Logic for Minimum Size Regex --- core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/build.gradle b/core/build.gradle index 23d84a996..4e4455d9a 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -53,7 +53,7 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_version" implementation 'com.github.mifmif:generex:1.0.2' - + implementation 'dk.brics:automaton:1.12-1' implementation 'com.squareup.okhttp3:okhttp:4.12.0' testImplementation 'org.assertj:assertj-core:3.26.3' testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version" From 13e7d5374603cf5c8419f36b6ca1649cf1c305e9 Mon Sep 17 00:00:00 2001 From: Samy Date: Thu, 21 Nov 2024 14:44:31 +0530 Subject: [PATCH 08/16] File formatted --- .../specmatic/core/pattern/StringPattern.kt | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 01393d095..befb63689 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -17,7 +17,7 @@ import java.util.* data class -StringPattern ( +StringPattern( override val typeAlias: String? = null, val minLength: Int? = null, val maxLength: Int? = null, @@ -29,15 +29,16 @@ StringPattern ( throw IllegalArgumentException("maxLength cannot be less than minLength") } - if(regex != null) { + if (regex != null) { val automaton: Automaton = RegExp(regex).toAutomaton() - val min = automaton.getShortestExample(true).length - when { - minLength != null && min < minLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") - maxLength != null && min > maxLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") - } + val min = automaton.getShortestExample(true).length + when { + minLength != null && min < minLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") + + maxLength != null && min > maxLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") + } } } @@ -57,7 +58,7 @@ StringPattern ( sampleData, resolver.mismatchMessages ) - if(regex != null && !Regex(regex).matches(sampleData.toStringLiteral())) { + if (regex != null && !Regex(regex).matches(sampleData.toStringLiteral())) { return mismatchResult( """string that matches regex /$regex/""", sampleData, @@ -67,6 +68,7 @@ StringPattern ( return Result.Success() } + else -> mismatchResult("string", sampleData, resolver.mismatchMessages) } } @@ -96,13 +98,14 @@ StringPattern ( val defaultExample: Value? = resolver.resolveExample(example, this) return if (regex != null) { - handleRegex(defaultExample,resolver) + handleRegex(defaultExample, resolver) } else { defaultExample?.takeIf { it is StringValue } ?: StringValue(randomString(patternMinLength)) } } - private fun handleRegex(defaultExample: Value?,resolver: Resolver): Value { + + private fun handleRegex(defaultExample: Value?, resolver: Resolver): Value { if (defaultExample == null) { val cleanedRegex = regex!!.removePrefix("^").removeSuffix("$") val randomValue = maxLength?.let { @@ -132,7 +135,11 @@ StringPattern ( override fun newBasedOn(resolver: Resolver): Sequence = sequenceOf(this) - override fun negativeBasedOn(row: Row, resolver: Resolver, config: NegativePatternConfiguration): Sequence> { + override fun negativeBasedOn( + row: Row, + resolver: Resolver, + config: NegativePatternConfiguration + ): Sequence> { val current = this return sequence { From 36cdf4e6f81f34e89a72bbdc4ea9414d17773d8e Mon Sep 17 00:00:00 2001 From: Samy Date: Thu, 21 Nov 2024 14:48:23 +0530 Subject: [PATCH 09/16] File formatted --- .../specmatic/core/pattern/StringPattern.kt | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index befb63689..2ee5903e3 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -5,8 +5,6 @@ import dk.brics.automaton.Automaton import dk.brics.automaton.RegExp import io.specmatic.core.Resolver import io.specmatic.core.Result -import io.specmatic.core.log.StringLog -import io.specmatic.core.log.consoleLog import io.specmatic.core.mismatchResult import io.specmatic.core.pattern.config.NegativePatternConfiguration import io.specmatic.core.value.JSONArrayValue @@ -15,9 +13,8 @@ import io.specmatic.core.value.Value import java.nio.charset.StandardCharsets import java.util.* - data class -StringPattern( +StringPattern ( override val typeAlias: String? = null, val minLength: Int? = null, val maxLength: Int? = null, @@ -27,17 +24,17 @@ StringPattern( init { if (minLength != null && maxLength != null && minLength > maxLength) { throw IllegalArgumentException("maxLength cannot be less than minLength") - } - - if (regex != null) { - val automaton: Automaton = RegExp(regex).toAutomaton() - val min = automaton.getShortestExample(true).length - when { - minLength != null && min < minLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") - maxLength != null && min > maxLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") + if (regex != null) { + val automaton: Automaton = RegExp(regex).toAutomaton() + val min = automaton.getShortestExample(true).length + when { + minLength != null && min < minLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") + + maxLength != null && min > maxLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") + } } } } @@ -58,7 +55,7 @@ StringPattern( sampleData, resolver.mismatchMessages ) - if (regex != null && !Regex(regex).matches(sampleData.toStringLiteral())) { + if(regex != null && !Regex(regex).matches(sampleData.toStringLiteral())) { return mismatchResult( """string that matches regex /$regex/""", sampleData, @@ -68,7 +65,6 @@ StringPattern( return Result.Success() } - else -> mismatchResult("string", sampleData, resolver.mismatchMessages) } } @@ -95,30 +91,29 @@ StringPattern( } override fun generate(resolver: Resolver): Value { - val defaultExample: Value? = resolver.resolveExample(example, this) + val defaultExample = resolver.resolveExample(example, this) - return if (regex != null) { - handleRegex(defaultExample, resolver) - } else { - defaultExample?.takeIf { it is StringValue } - ?: StringValue(randomString(patternMinLength)) + // Validate the default example + defaultExample?.let { + if (matches(it, resolver).isSuccess()) { + return it + } + throw ContractException("Schema example ${it.toStringLiteral()} does not match pattern $regex") } + + // Generate a value based on regex or length constraints + return regex?.let { generateFromRegex() } ?: StringValue(randomString(patternMinLength)) } - private fun handleRegex(defaultExample: Value?, resolver: Resolver): Value { - if (defaultExample == null) { - val cleanedRegex = regex!!.removePrefix("^").removeSuffix("$") - val randomValue = maxLength?.let { - Generex(cleanedRegex).random(patternMinLength, it) - } ?: Generex(cleanedRegex).random(patternMinLength) - return StringValue(randomValue) - } - if (matches(defaultExample, resolver).isSuccess()) { - return defaultExample - } - throw ContractException("Schema example ${defaultExample.toStringLiteral()} does not match pattern $regex") + private fun generateFromRegex(): Value { + val cleanedRegex = regex!!.removePrefix("^").removeSuffix("$") + val generatedValue = maxLength?.let { + Generex(cleanedRegex).random(patternMinLength, it) + } ?: Generex(cleanedRegex).random(patternMinLength) + return StringValue(generatedValue) } + override fun newBasedOn(row: Row, resolver: Resolver): Sequence> { val minLengthExample: ReturnValue? = minLength?.let { HasValue(ExactValuePattern(StringValue(randomString(it))), "minimum length string") @@ -135,11 +130,7 @@ StringPattern( override fun newBasedOn(resolver: Resolver): Sequence = sequenceOf(this) - override fun negativeBasedOn( - row: Row, - resolver: Resolver, - config: NegativePatternConfiguration - ): Sequence> { + override fun negativeBasedOn(row: Row, resolver: Resolver, config: NegativePatternConfiguration): Sequence> { val current = this return sequence { From 4634e6a062dc6bcf8080fdec676006937b096323 Mon Sep 17 00:00:00 2001 From: Samy Date: Thu, 21 Nov 2024 14:50:38 +0530 Subject: [PATCH 10/16] File formatted --- .../specmatic/core/pattern/StringPattern.kt | 55 +++++++++---------- .../core/pattern/StringPatternTest.kt | 51 +++++++++-------- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 2ee5903e3..65ead9bbc 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -13,8 +13,7 @@ import io.specmatic.core.value.Value import java.nio.charset.StandardCharsets import java.util.* -data class -StringPattern ( +data class StringPattern( override val typeAlias: String? = null, val minLength: Int? = null, val maxLength: Int? = null, @@ -24,17 +23,16 @@ StringPattern ( init { if (minLength != null && maxLength != null && minLength > maxLength) { throw IllegalArgumentException("maxLength cannot be less than minLength") - - if (regex != null) { - val automaton: Automaton = RegExp(regex).toAutomaton() - val min = automaton.getShortestExample(true).length - when { - minLength != null && min < minLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") - - maxLength != null && min > maxLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") - } + } + if (regex != null) { + val automaton: Automaton = RegExp(regex).toAutomaton() + val min = automaton.getShortestExample(true).length + when { + minLength != null && min < minLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") + + maxLength != null && min > maxLength -> + throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") } } } @@ -55,7 +53,7 @@ StringPattern ( sampleData, resolver.mismatchMessages ) - if(regex != null && !Regex(regex).matches(sampleData.toStringLiteral())) { + if (regex != null && !Regex(regex).matches(sampleData.toStringLiteral())) { return mismatchResult( """string that matches regex /$regex/""", sampleData, @@ -65,6 +63,7 @@ StringPattern ( return Result.Success() } + else -> mismatchResult("string", sampleData, resolver.mismatchMessages) } } @@ -82,7 +81,6 @@ StringPattern ( return JSONArrayValue(valueList) } - //Tells us the minimum length to be used for random string private val patternMinLength: Int = when { minLength != null && minLength > 0 -> minLength @@ -93,7 +91,6 @@ StringPattern ( override fun generate(resolver: Resolver): Value { val defaultExample = resolver.resolveExample(example, this) - // Validate the default example defaultExample?.let { if (matches(it, resolver).isSuccess()) { return it @@ -101,19 +98,12 @@ StringPattern ( throw ContractException("Schema example ${it.toStringLiteral()} does not match pattern $regex") } - // Generate a value based on regex or length constraints - return regex?.let { generateFromRegex() } ?: StringValue(randomString(patternMinLength)) + return regex?.let { + val regexWithoutCaretAndDollar = regex.removePrefix("^").removeSuffix("$") + StringValue(generateFromRegex(regexWithoutCaretAndDollar, patternMinLength, maxLength)) + } ?: StringValue(randomString(patternMinLength)) } - private fun generateFromRegex(): Value { - val cleanedRegex = regex!!.removePrefix("^").removeSuffix("$") - val generatedValue = maxLength?.let { - Generex(cleanedRegex).random(patternMinLength, it) - } ?: Generex(cleanedRegex).random(patternMinLength) - return StringValue(generatedValue) - } - - override fun newBasedOn(row: Row, resolver: Resolver): Sequence> { val minLengthExample: ReturnValue? = minLength?.let { HasValue(ExactValuePattern(StringValue(randomString(it))), "minimum length string") @@ -130,7 +120,11 @@ StringPattern ( override fun newBasedOn(resolver: Resolver): Sequence = sequenceOf(this) - override fun negativeBasedOn(row: Row, resolver: Resolver, config: NegativePatternConfiguration): Sequence> { + override fun negativeBasedOn( + row: Row, + resolver: Resolver, + config: NegativePatternConfiguration + ): Sequence> { val current = this return sequence { @@ -172,6 +166,11 @@ StringPattern ( override val pattern: Any = "(string)" override fun toString(): String = pattern.toString() + + private fun generateFromRegex(regexWithoutCaretAndDollar: String, minLength: Int, maxLength: Int?): String = + maxLength?.let { + Generex(regexWithoutCaretAndDollar).random(minLength, it) + } ?: Generex(regexWithoutCaretAndDollar).random(minLength) } fun randomString(length: Int = 5): String { diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index 437150eb5..8437ded12 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -72,45 +72,48 @@ internal class StringPatternTest { companion object { @JvmStatic - fun lengthTestValues(): Stream { + fun minLengthMaxLengthAndExpectedLength(): Stream { return Stream.of( Arguments.of(null, 10, 5), - Arguments.of(null, 4, 4), - Arguments.of(1, 10, 5), - Arguments.of(1, 4, 4), - Arguments.of(1, 5, 5), + Arguments.of(null, 4, 1), + Arguments.of(1, 10, 1), Arguments.of(5, 10, 5), - Arguments.of(6, 10, 6), Arguments.of(6, null, 6), - Arguments.of(3, null, 5), Arguments.of(null, null, 5) ) } + + @JvmStatic + fun regexMinLengthAndMaxLengthAndExpectedLength(): Stream { + return Stream.of( + Arguments.of("^[a-z]*\$", null, null, 5), + Arguments.of("^[a-z0-9]{6,}\$", 3, 10, 6), + Arguments.of(null, 1, 10, 1), + ) + } } @ParameterizedTest - @MethodSource("lengthTestValues") - fun `generate string value of appropriate length matching minLength and maxLength parameters`(min: Int?, max: Int?, length: Int) { + @MethodSource("minLengthMaxLengthAndExpectedLength") + fun `generate string value as per minLength and maxLength`(min: Int?, max: Int?, expectedLengthOfGeneratedValue: Int) { val result = StringPattern(minLength = min, maxLength = max).generate(Resolver()) as StringValue val generatedLength = result.string.length - val patternMinLength: Int = - when { - min != null && min > 0 -> min - max != null && max < 5 -> 1 - else -> 5 - } - // If max is provided, ensure the generated length is within the range of min and max - if (max != null) { - // Ensure that the generated string length is between min (or 0 if min is null) and max - assertThat(generatedLength).isGreaterThanOrEqualTo(patternMinLength) - assertThat(generatedLength).isLessThanOrEqualTo(max) - } else { - // If max is not provided, ensure the generated length is at least the min (or randomStringDefaultLength if min is null) - assertThat(generatedLength).isGreaterThanOrEqualTo(patternMinLength) - } + assertThat(generatedLength).isGreaterThanOrEqualTo(expectedLengthOfGeneratedValue) + max?.let { assertThat(generatedLength).isLessThanOrEqualTo(it) } } + @ParameterizedTest + @MethodSource("regexMinLengthAndMaxLengthAndExpectedLength") + fun `generate string value as per regex in conjunction with minLength and maxLength`(regex: String?, min: Int?, max: Int?, expectedLength: Int) { + val result = StringPattern(minLength = min, maxLength = max, regex = regex).generate(Resolver()) as StringValue + val generatedString = result.string + val generatedLength = generatedString.length + + assertThat(generatedLength).isGreaterThanOrEqualTo(expectedLength) + max?.let { assertThat(generatedLength).isLessThanOrEqualTo(it) } + regex?.let { assertThat(generatedString).matches(regex) } + } @Test fun `string should encompass enum of string`() { From 5b4ae8264da28082e505bd51941885555fce30e4 Mon Sep 17 00:00:00 2001 From: Samy Date: Thu, 21 Nov 2024 15:19:02 +0530 Subject: [PATCH 11/16] Fixed test cases --- .../kotlin/io/specmatic/core/pattern/StringPatternTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index 8437ded12..ccaadb1d8 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -199,7 +199,7 @@ internal class StringPatternTest { @Test @Tag(GENERATION) fun `negative value for regex should be generated when regex is provided`() { - val minLength = 10 + val minLength = 2 val maxLength = 20 val result = StringPattern( @@ -218,7 +218,7 @@ internal class StringPatternTest { @Test @Tag(GENERATION) fun `should exclude data type based negatives when withDataTypeNegatives config is false`() { - val minLength = 10 + val minLength = 2 val maxLength = 20 val result = StringPattern( From 0adddd68222e604d4b6dc371048342513c6a6e49 Mon Sep 17 00:00:00 2001 From: Samy Date: Tue, 26 Nov 2024 16:26:48 +0530 Subject: [PATCH 12/16] String Pattern Validation Updated --- .../specmatic/core/pattern/StringPattern.kt | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 65ead9bbc..11ae5370b 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -24,19 +24,35 @@ data class StringPattern( if (minLength != null && maxLength != null && minLength > maxLength) { throw IllegalArgumentException("maxLength cannot be less than minLength") } - if (regex != null) { - val automaton: Automaton = RegExp(regex).toAutomaton() - val min = automaton.getShortestExample(true).length - when { - minLength != null && min < minLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") + regex?.let { + regexMinLengthValidation(it) + regexMaxLengthValidation(it) + } + + } + + private fun regexMinLengthValidation(it: String) { + val automaton = RegExp(it).toAutomaton() - maxLength != null && min > maxLength -> - throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") + minLength?.let { minLen -> + val min = automaton.getShortestExample(true).length + if (min < minLen) { + throw IllegalArgumentException("Invalid Regex - min cannot be less than regex least size") + } else if (maxLength != null && min > maxLength) { + throw IllegalArgumentException("Invalid Regex - min cannot be more than regex max size") } } } + private fun regexMaxLengthValidation(it: String) { + maxLength?.let { maxLen -> + val regexWithoutCaretAndDollar = it.removePrefix("^").removeSuffix("$") + runCatching { + StringValue(generateFromRegex(regexWithoutCaretAndDollar, 5, maxLen + 1)) + }.getOrNull() ?: throw IllegalArgumentException("Invalid Regex - max cannot be more than regex max size") + } + } + override fun matches(sampleData: Value?, resolver: Resolver): Result { if (sampleData?.hasTemplate() == true) return Result.Success() @@ -81,10 +97,10 @@ data class StringPattern( return JSONArrayValue(valueList) } - private val patternMinLength: Int = + private val randomStringLength: Int = when { - minLength != null && minLength > 0 -> minLength - maxLength != null && maxLength < 5 -> 1 + minLength != null && 5 < minLength -> minLength + maxLength != null && 5 > maxLength -> maxLength else -> 5 } @@ -100,8 +116,10 @@ data class StringPattern( return regex?.let { val regexWithoutCaretAndDollar = regex.removePrefix("^").removeSuffix("$") - StringValue(generateFromRegex(regexWithoutCaretAndDollar, patternMinLength, maxLength)) - } ?: StringValue(randomString(patternMinLength)) + regexMinLengthValidation(it) + regexMaxLengthValidation(it) + StringValue(generateFromRegex(regexWithoutCaretAndDollar, randomStringLength, maxLength)) + } ?: StringValue(randomString(randomStringLength)) } override fun newBasedOn(row: Row, resolver: Resolver): Sequence> { From 8453faf91f0c27e96691c550de86b22f7e5aff35 Mon Sep 17 00:00:00 2001 From: Samy Date: Wed, 27 Nov 2024 12:02:56 +0530 Subject: [PATCH 13/16] Specmatic String Pattern Tests --- .../specmatic/core/pattern/StringPattern.kt | 16 ++++--- .../core/pattern/StringPatternTest.kt | 48 +++++++++++++++++-- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 11ae5370b..30d76a0b5 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -25,8 +25,9 @@ data class StringPattern( throw IllegalArgumentException("maxLength cannot be less than minLength") } regex?.let { - regexMinLengthValidation(it) - regexMaxLengthValidation(it) + val regexWithoutCaretAndDollar = it.removePrefix("^").removeSuffix("$") + regexMinLengthValidation(regexWithoutCaretAndDollar) + regexMaxLengthValidation(regexWithoutCaretAndDollar) } } @@ -46,10 +47,11 @@ data class StringPattern( private fun regexMaxLengthValidation(it: String) { maxLength?.let { maxLen -> - val regexWithoutCaretAndDollar = it.removePrefix("^").removeSuffix("$") - runCatching { - StringValue(generateFromRegex(regexWithoutCaretAndDollar, 5, maxLen + 1)) - }.getOrNull() ?: throw IllegalArgumentException("Invalid Regex - max cannot be more than regex max size") + val generatedString = generateFromRegex(it, maxLen+1) + + if (generatedString.length > maxLen) { + throw IllegalArgumentException("Invalid Regex - max cannot be more than regex max size") + } } } @@ -185,7 +187,7 @@ data class StringPattern( override val pattern: Any = "(string)" override fun toString(): String = pattern.toString() - private fun generateFromRegex(regexWithoutCaretAndDollar: String, minLength: Int, maxLength: Int?): String = + private fun generateFromRegex(regexWithoutCaretAndDollar: String, minLength: Int, maxLength: Int? = null): String = maxLength?.let { Generex(regexWithoutCaretAndDollar).random(minLength, it) } ?: Generex(regexWithoutCaretAndDollar).random(minLength) diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index ccaadb1d8..73d4ef33a 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -87,7 +87,7 @@ internal class StringPatternTest { fun regexMinLengthAndMaxLengthAndExpectedLength(): Stream { return Stream.of( Arguments.of("^[a-z]*\$", null, null, 5), - Arguments.of("^[a-z0-9]{6,}\$", 3, 10, 6), + Arguments.of("^[a-z0-9]{6,10}\$", 3, 10, 6), Arguments.of(null, 1, 10, 1), ) } @@ -205,16 +205,54 @@ internal class StringPatternTest { val result = StringPattern( minLength = minLength, maxLength = maxLength, - regex = "^[^0-9]*$" + regex = "^[^0-9]{15}$" ).negativeBasedOn(Row(), Resolver()).map { it.value }.toList() assertThat( result.filterIsInstance().filter { - it.regex == "^[^0-9]*\$_" + it.regex == "^[^0-9]{15}\$_" } ).hasSize(1) } + @Test + @Tag(GENERATION) + fun `regex sould throw validation issue for patterns less than min size as per api contract`() { + val minLength = 2 + val maxLength = 15 + + val result = runCatching { + StringPattern( + minLength = minLength, + maxLength = maxLength, + regex = "^.{0,4}$" + ).negativeBasedOn(Row(), Resolver()).map { it.value }.toList() + } + + result.onFailure { exception -> + assertThat(exception.message).isEqualTo("Invalid Regex - min cannot be less than regex least size") + } + } + + @Test + @Tag(GENERATION) + fun `regex sould throw validation issue for patterns more than max size as per api contract`() { + val minLength = 2 + val maxLength = 15 + + val result = runCatching { + StringPattern( + minLength = minLength, + maxLength = maxLength, + regex = "^.{2,14}$" + ).negativeBasedOn(Row(), Resolver()).map { it.value }.toList() + } + + result.onFailure { exception -> + assertThat(exception.message).isEqualTo("Invalid Regex - max cannot be more than regex max size") + } + } + @Test @Tag(GENERATION) fun `should exclude data type based negatives when withDataTypeNegatives config is false`() { @@ -224,7 +262,7 @@ internal class StringPatternTest { val result = StringPattern( minLength = minLength, maxLength = maxLength, - regex = "^[^0-9]*$" + regex = "^[^0-9]{15}$" ).negativeBasedOn( Row(), Resolver(), @@ -239,7 +277,7 @@ internal class StringPatternTest { ).hasSize(0) assertThat( - result.filterIsInstance().filter { it.regex == "^[^0-9]*\$_" } + result.filterIsInstance().filter { it.regex == "^[^0-9]{15}\$_" } ).hasSize(1) assertThat( From 3ff35eceec9d1a45a7b738453c031da82c40b076 Mon Sep 17 00:00:00 2001 From: Samy Date: Wed, 27 Nov 2024 12:53:43 +0530 Subject: [PATCH 14/16] Specmatic String Pattern Tests Rectified --- .../main/kotlin/io/specmatic/core/pattern/StringPattern.kt | 4 ++-- .../kotlin/io/specmatic/core/pattern/StringPatternTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt index 30d76a0b5..061774222 100644 --- a/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt +++ b/core/src/main/kotlin/io/specmatic/core/pattern/StringPattern.kt @@ -118,8 +118,8 @@ data class StringPattern( return regex?.let { val regexWithoutCaretAndDollar = regex.removePrefix("^").removeSuffix("$") - regexMinLengthValidation(it) - regexMaxLengthValidation(it) + regexMinLengthValidation(regexWithoutCaretAndDollar) + regexMaxLengthValidation(regexWithoutCaretAndDollar) StringValue(generateFromRegex(regexWithoutCaretAndDollar, randomStringLength, maxLength)) } ?: StringValue(randomString(randomStringLength)) } diff --git a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt index 73d4ef33a..7890a4fd9 100644 --- a/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt +++ b/core/src/test/kotlin/io/specmatic/core/pattern/StringPatternTest.kt @@ -87,7 +87,7 @@ internal class StringPatternTest { fun regexMinLengthAndMaxLengthAndExpectedLength(): Stream { return Stream.of( Arguments.of("^[a-z]*\$", null, null, 5), - Arguments.of("^[a-z0-9]{6,10}\$", 3, 10, 6), + Arguments.of("^[a-z0-9]{6,10}\$", 6, 10, 6), Arguments.of(null, 1, 10, 1), ) } From ef5f8e0a36d84bb0e4097b69ec19f49a8b7afe41 Mon Sep 17 00:00:00 2001 From: Samy Date: Wed, 27 Nov 2024 13:24:01 +0530 Subject: [PATCH 15/16] Specmatic String Pattern Tests Rectified --- .../io/specmatic/conversions/RegexSupportTest.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/core/src/test/kotlin/io/specmatic/conversions/RegexSupportTest.kt b/core/src/test/kotlin/io/specmatic/conversions/RegexSupportTest.kt index c49d8e8bf..85ea4391a 100644 --- a/core/src/test/kotlin/io/specmatic/conversions/RegexSupportTest.kt +++ b/core/src/test/kotlin/io/specmatic/conversions/RegexSupportTest.kt @@ -20,6 +20,7 @@ class RegexSupportTest { @Test fun `invalid regex results in exception`() { + assertThatThrownBy { val feature = OpenApiSpecification.fromYAML( """ --- @@ -46,17 +47,7 @@ class RegexSupportTest { 204: description: "Get person by id" content: {} - """.trimIndent(), "").toFeature() - - val executor = object : TestExecutor { - override fun execute(request: HttpRequest): HttpResponse { - return HttpResponse(204) - } - } - - assertThatThrownBy { - feature.executeTests(executor) - }.satisfies(Consumer { + """.trimIndent(), "").toFeature()}.satisfies(Consumer { assertThat(it).isInstanceOf(ContractException::class.java) }) } From a0fc1fabfc2928d0ebecbe096904acce37fd833d Mon Sep 17 00:00:00 2001 From: Samy Date: Thu, 28 Nov 2024 11:15:26 +0530 Subject: [PATCH 16/16] Fixed status code 1 issue with backward compatibility --- .../BackwardCompatibilityCheckBaseCommand.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/application/src/main/kotlin/application/backwardCompatibility/BackwardCompatibilityCheckBaseCommand.kt b/application/src/main/kotlin/application/backwardCompatibility/BackwardCompatibilityCheckBaseCommand.kt index 37980d827..86e7463e1 100644 --- a/application/src/main/kotlin/application/backwardCompatibility/BackwardCompatibilityCheckBaseCommand.kt +++ b/application/src/main/kotlin/application/backwardCompatibility/BackwardCompatibilityCheckBaseCommand.kt @@ -92,7 +92,10 @@ abstract class BackwardCompatibilityCheckBaseCommand : Callable { ).filter { File(it).exists() && File(it).isValidSpec() }.toSet().also { - if(it.isEmpty()) exitWithMessage("No specs were changed, skipping the check.") + it.takeIf { it.isEmpty() }?.run { + logger.log("$newLine No specs were changed, skipping the check.$newLine") + exitProcess(0) + } } }