diff --git a/README.md b/README.md index c46284f..e1ad1b9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,24 @@ A handy utility for generating strings that match given regular expression crite - [vavr-test](https://github.com/vavr-io/vavr-test) # Usage +## Functionaljava Quickcheck +Include the following dependency into your project: + +```groovy +testImplementation "com.github.simy4.coregex:coregex-functionaljava-quickcheck" +``` + +Use the provided `CoregexArbirary` class to generate a string that would match the regular expression predicate: + +```java +@RunWith(PropertyTestRunner.class) +class MyTest { + public Property myProperty(@ForAll @Regex("[a-zA-Z]{3}") String str) { + return property(CoregexArbitrary.of("[a-zA-Z]{3}"), str -> prop(3 == str.length())); + } +} +``` + ## Jqwik Include the following dependency into your project: diff --git a/build.sbt b/build.sbt index 245d592..05fe060 100644 --- a/build.sbt +++ b/build.sbt @@ -60,7 +60,7 @@ lazy val root = (project in file(".")) name := "coregex-parent", publish / skip := true ) - .aggregate(core, jqwik, junitQuickcheck, kotest, scalacheck, vavrTest) + .aggregate(core, functionaljavaQuickcheck, jqwik, junitQuickcheck, kotest, scalacheck, vavrTest) lazy val core = (project in file("core")) .settings( @@ -76,6 +76,23 @@ lazy val core = (project in file("core")) .settings(javaLibSettings(8)) .settings(jacocoSettings) +lazy val functionaljavaQuickcheck = (project in file("functionaljava-quickcheck")) + .settings( + name := "functionaljava-quickcheck", + moduleName := "coregex-functionaljava-quickcheck", + description := "Functionaljava quickcheck bindings for coregex library.", + headerEndYear := Some(2024), + libraryDependencies ++= Seq( + "org.functionaljava" % "functionaljava-quickcheck" % "5.0" % Provided, + "junit" % "junit" % "4.13.2" % Test, + "com.github.sbt" % "junit-interface" % "0.13.3" % Test + ), + testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-v") + ) + .settings(javaLibSettings(8)) + .settings(jacocoSettings) + .dependsOn(core) + lazy val jqwik = (project in file("jqwik")) .settings( name := "jqwik", diff --git a/functionaljava-quickcheck/src/main/java/com/github/simy4/coregex/functionaljava/quickcheck/CoregexArbitrary.java b/functionaljava-quickcheck/src/main/java/com/github/simy4/coregex/functionaljava/quickcheck/CoregexArbitrary.java new file mode 100644 index 0000000..ce66403 --- /dev/null +++ b/functionaljava-quickcheck/src/main/java/com/github/simy4/coregex/functionaljava/quickcheck/CoregexArbitrary.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021-2024 Alex Simkin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.simy4.coregex.functionaljava.quickcheck; + +import com.github.simy4.coregex.core.Coregex; +import com.github.simy4.coregex.core.rng.RandomRNG; +import fj.test.Gen; +import java.util.regex.Pattern; + +public final class CoregexArbitrary { + public static Gen of(String regex) { + return of(regex, 0); + } + + public static Gen of(String regex, int flags) { + Coregex coregex = Coregex.from(Pattern.compile(regex, flags)); + return Gen.gen( + size -> { + Coregex sized = coregex.sized(Math.max(size, coregex.minLength())); + return rand -> sized.generate(new RandomRNG(rand.choose(Long.MIN_VALUE, Long.MAX_VALUE))); + }); + } + + private CoregexArbitrary() { + throw new UnsupportedOperationException("new"); + } +} diff --git a/functionaljava-quickcheck/src/test/java/com/github/simy4/coregex/functionaljava/quickcheck/CoregexArbitraryTest.java b/functionaljava-quickcheck/src/test/java/com/github/simy4/coregex/functionaljava/quickcheck/CoregexArbitraryTest.java new file mode 100644 index 0000000..82d8209 --- /dev/null +++ b/functionaljava-quickcheck/src/test/java/com/github/simy4/coregex/functionaljava/quickcheck/CoregexArbitraryTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021-2024 Alex Simkin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.simy4.coregex.functionaljava.quickcheck; + +import static fj.test.Property.prop; +import static fj.test.Property.property; + +import fj.Equal; +import fj.Try; +import fj.data.List; +import fj.test.Arbitrary; +import fj.test.Property; +import fj.test.runner.PropertyTestRunner; +import java.net.InetAddress; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import org.junit.runner.RunWith; + +@RunWith(PropertyTestRunner.class) +public class CoregexArbitraryTest { + private static final Equal stringEqual = Equal.stringEqual; + private static final Equal> listEqual = Equal.listEqual(stringEqual); + + public Property shouldGenerateMatchingUUIDString() { + return property( + CoregexArbitrary.of( + "[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}"), + uuid -> prop(stringEqual.eq(uuid, UUID.fromString(uuid).toString()))); + } + + public Property shouldGenerateMatchingIPv4String() { + return property( + CoregexArbitrary.of( + "((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])"), + ipv4 -> { + String[] expected = ipv4.split("\\."); + String[] actual = + Try.io(() -> InetAddress.getByName(ipv4).getHostAddress().split("\\.")) + .safe() + .run() + .success(); + return prop(expected.length == actual.length) + .and( + List.arrayList(expected) + .zip(List.arrayList(actual)) + .foldLeft( + (acc, p) -> + acc.and(prop(Integer.parseInt(p._1()) == Integer.parseInt(p._2()))), + prop(true))); + }); + } + + public Property shouldGenerateMatchingIsoDateString() { + DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT; + return property( + CoregexArbitrary.of( + "[12]\\d{3}-(?:0[1-9]|1[012])-(?:0[1-9]|1\\d|2[0-8])T(?:1\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{2}[1-9])?Z"), + iso8601Date -> + prop(stringEqual.eq(iso8601Date, formatter.format(formatter.parse(iso8601Date))))); + } + + public Property shouldGenerateUniqueStrings() { + return property( + Arbitrary.arbList(CoregexArbitrary.of("[a-zA-Z0-9]{32,}")), + strings -> + strings.foldLeft( + (acc, s) -> + acc.and(prop(s.length() >= 32)) + .and(prop(s.chars().allMatch(Character::isLetterOrDigit))), + prop(true))); + } +}