From 218b94937240761c18e2df4eb78d5832b155f9c6 Mon Sep 17 00:00:00 2001 From: Haemin Yoo Date: Sun, 14 May 2023 17:40:44 +0900 Subject: [PATCH 1/3] Add hedgehog module --- build.sc | 12 +++ hedgehog/src/decrel/hedgehog/gen.scala | 113 +++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 hedgehog/src/decrel/hedgehog/gen.scala diff --git a/build.sc b/build.sc index ab018ae..cbec449 100644 --- a/build.sc +++ b/build.sc @@ -60,6 +60,16 @@ object ziotest extends PureCrossModule { } +object hedgehog extends PureCrossModule { + + override def moduleDeps = Seq(core) + + override def ivyDeps = Agg( + D.hedgehog + ) + +} + object cats extends PureCrossModule { override def moduleDeps = Seq(core) @@ -128,6 +138,7 @@ object D { def fetch = ivy"com.47deg::fetch::${V.fetch}" def scalacheck = ivy"org.scalacheck::scalacheck::${V.scalacheck}" def cats = ivy"org.typelevel::cats-core::${V.cats}" + def hedgehog = ivy"qa.hedgehog::hedgehog-core::${V.hedgehog}" def kindProjector = ivy"org.typelevel:::kind-projector:${V.kindProjector}" } @@ -144,6 +155,7 @@ object V { def fetch = "3.1.2" def izumiReflect = "2.3.8" def scalacheck = "1.17.0" + def hedgehog = "0.10.1" def kindProjector = "0.13.2" } diff --git a/hedgehog/src/decrel/hedgehog/gen.scala b/hedgehog/src/decrel/hedgehog/gen.scala new file mode 100644 index 0000000..43422ec --- /dev/null +++ b/hedgehog/src/decrel/hedgehog/gen.scala @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2022 Haemin Yoo + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package decrel.hedgehog + +import decrel.Relation +import decrel.reify.monofunctor.* +import hedgehog.* +import hedgehog.predef.* + +import scala.collection.{ mutable, IterableOps } + +trait gen extends module[Gen] { + + //////// Basic premises + + override protected def flatMap[A, B](gen: Gen[A])(f: A => Gen[B]): Gen[B] = + gen.flatMap(f) + + override protected def map[A, B](gen: Gen[A])(f: A => B): Gen[B] = + gen.map(f) + + override protected def succeed[A](a: A): Gen[A] = + Gen.constant(a) + + //////// Syntax for implementing relations + + implicit final class GenObjectOps(private val gen: Gen.type) { + + def relationSingle[Rel, In, Out]( + relation: Rel & Relation.Single[In, Out] + )( + f: In => Gen[Out] + ): Proof.Single[Rel & Relation.Single[In, Out], In, Out] = + new Proof.Single[Rel & Relation.Single[In, Out], In, Out] { + override val reify: ReifiedRelation[In, Out] = reifiedRelation(f) + } + + def relationOptional[Rel, In, Out]( + relation: Rel & Relation.Optional[In, Out] + )( + f: In => Gen[Option[Out]] + ): Proof.Optional[Rel & Relation.Optional[In, Out], In, Out] = + new Proof.Optional[Rel & Relation.Optional[In, Out], In, Out] { + override val reify: ReifiedRelation[In, Option[Out]] = reifiedRelation(f) + } + + def relationMany[Rel, In, Out, CC[+A] <: Iterable[A] & IterableOps[A, CC, CC[A]]]( + relation: Rel & Relation.Many[In, List, Out] + )( + f: In => Gen[CC[Out]] + ): Proof.Many[Rel & Relation.Many[In, CC, Out], In, CC, Out] = + new Proof.Many[Rel & Relation.Many[In, CC, Out], In, CC, Out] { + override val reify: ReifiedRelation[In, CC[Out]] = reifiedRelation(f) + } + } + + private def reifiedRelation[In, Out](f: In => Gen[Out]): ReifiedRelation[In, Out] = + new ReifiedRelation.Custom[In, Out] { + override def apply(in: In): Gen[Out] = + applyMultiple(List(in)).map(_.head) + + override def applyMultiple[Coll[+A] <: Iterable[A] & IterableOps[A, Coll, Coll[A]]]( + ins: Coll[In] + ): Gen[Coll[Out]] = { + val F = implicitly[Applicative[Gen]] // Monad for Gen wasn't stack safe last time I checked + + def addElem[A]( + listGen: Gen[mutable.Builder[A, Coll[A]]], + aGen: Gen[A] + ): Gen[mutable.Builder[A, Coll[A]]] = + F.ap(listGen)(F.map(aGen)(a => _.addOne(a))) + + val ins_ : IterableOps[In, Coll, Coll[In]] = ins + val factory = ins_.iterableFactory + val iterator = ins_.iterator + val builder: mutable.Builder[Out, Coll[Out]] = factory.newBuilder[Out] + + if (iterator.hasNext) { + val head = f(iterator.next()) + var builderGen: Gen[mutable.Builder[Out, Coll[Out]]] = + head.map(builder.addOne) + + while (iterator.hasNext) { + val in = iterator.next() + val out: Gen[Out] = f(in) + builderGen = addElem(builderGen, out) + } + + builderGen.map(_.result()) + } else { + Gen.constant(factory.empty[Out]) + } + } + } + + //////// Syntax for using relations in tests + + implicit final class GenOps[A](private val gen: Gen[A]) { + + def expand[Rel, B](rel: Rel & Relation[A, B])(implicit + proof: Proof[Rel & Relation[A, B], A, B] + ): Gen[B] = gen.flatMap(rel.reify(proof).apply) + + } +} + +object gen extends gen From 0005caa6f71422ac4ecd62a16d1532291e1c4bfc Mon Sep 17 00:00:00 2001 From: Haemin Yoo Date: Wed, 5 Jul 2023 21:58:18 +0900 Subject: [PATCH 2/3] Remove unnecessary comment --- hedgehog/src/decrel/hedgehog/gen.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hedgehog/src/decrel/hedgehog/gen.scala b/hedgehog/src/decrel/hedgehog/gen.scala index 43422ec..2e2aea3 100644 --- a/hedgehog/src/decrel/hedgehog/gen.scala +++ b/hedgehog/src/decrel/hedgehog/gen.scala @@ -68,7 +68,7 @@ trait gen extends module[Gen] { override def applyMultiple[Coll[+A] <: Iterable[A] & IterableOps[A, Coll, Coll[A]]]( ins: Coll[In] ): Gen[Coll[Out]] = { - val F = implicitly[Applicative[Gen]] // Monad for Gen wasn't stack safe last time I checked + val F = implicitly[Applicative[Gen]] def addElem[A]( listGen: Gen[mutable.Builder[A, Coll[A]]], From 381a5ffd990d0276063726b30a8ef7cd3e8fe4a8 Mon Sep 17 00:00:00 2001 From: Haemin Yoo Date: Thu, 6 Jul 2023 01:12:25 +0900 Subject: [PATCH 3/3] --wip-- [skip ci] --- .../test/src/decrel/hedgehog/genSpec.scala | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 hedgehog/test/src/decrel/hedgehog/genSpec.scala diff --git a/hedgehog/test/src/decrel/hedgehog/genSpec.scala b/hedgehog/test/src/decrel/hedgehog/genSpec.scala new file mode 100644 index 0000000..0c97f7c --- /dev/null +++ b/hedgehog/test/src/decrel/hedgehog/genSpec.scala @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2022 Haemin Yoo + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package decrel.hedgehog + +import decrel.* +import decrel.hedgehog.gen.* +import _root_.hedgehog.* +import zio.test.Assertion.* +import zio.test.{ Gen as _, * } + +object genSpec extends ZIOSpecDefault { + + // Relation descriptions + case class Rental(id: Rental.Id, bookId: Book.Id, userId: User.Id) + object Rental { + case class Id(value: String) + + case object self extends Relation.Self[Rental] + case object fetch extends Relation.Single[Rental.Id, Rental] + case object book extends Relation.Single[Rental, Book] + case object user extends Relation.Single[Rental, User] + } + case class Book(id: Book.Id, currentRental: Option[Rental.Id]) + object Book { + case class Id(value: String) + + case object self extends Relation.Self[Book] + case object fetch extends Relation.Single[Book.Id, Book] + case object currentRental extends Relation.Optional[Book, Rental] + } + case class User(id: User.Id, currentRentals: List[Rental.Id]) + object User { + case class Id(value: String) + + case object self extends Relation.Self[User] + case object fetch extends Relation.Single[User.Id, User] + case object currentRentals extends Relation.Many[User, List, Rental] + } + + // Basic Generators + object gen { + private val genString = Gen.string(Gen.char('a', 'z'), Range.linear(0, 10)) + + val rentalId: Gen[Rental.Id] = genString.map(Rental.Id) + val bookId: Gen[Book.Id] = genString.map(Book.Id) + val userId: Gen[User.Id] = genString.map(User.Id) + } + + // Relation Implementations + // Note: not all relations are implemented; we need only implement the ones we use + + implicit val rentalFetch: Proof.Single[ + Rental.fetch.type & Relation.Single[Rental.Id, Rental], + Rental.Id, + Rental + ] = Gen.relationSingle(Rental.fetch) { id => + for { + bookId <- gen.bookId + userId <- gen.userId + } yield Gen.constant(Rental(id, bookId, userId)) + } + + implicit val rentalBook: Proof.Single[ + Rental.book.type & Relation.Single[Rental, Book], + Rental, + Book + ] = Gen.relationSingle(Rental.book) { rental => + Gen.constant(Book(rental.bookId, Some(rental.id))) + } + + implicit val rentalUser: Proof.Single[ + Rental.user.type & Relation.Single[Rental, User], + Rental, + User + ] = Gen.relationSingle(Rental.user) { rental => + Gen.constant(User(rental.userId, List(rental.id))) + } + + implicit val userCurrentRentals: Proof.Many[ + User.currentRentals.type & Relation.Many[User, List, Rental], + User, + List, + Rental + ] = Gen.relationMany(User.currentRentals) { user => + Gen + // Use `expand` even when implementing other relations + .list(gen.rentalId.expand(Rental.fetch), Range.linear(0, 10)) + // Make it consistent + .map(_.map(_.copy(userId = user.id))) + } + + override def spec: Spec[Environment, Any] = + suite("proof - zio.test.Gen")( + test("Simple relation") { + val relation = Rental.fetch + + val staticRentalId = Rental.Id("foo") + val rentalIdGen = Gen.constant(staticRentalId) + + check(rentalIdGen.expand(relation)) { (rental: Rental) => + assert(rental.id)(equalTo(staticRentalId)) + } + }, + test("Composing with &") { + val relation = Rental.fetch & Rental.fetch + + check(gen.rentalId.expand(relation)) { case (rental1: Rental, rental2: Rental) => + assert(rental1.id)(equalTo(rental2.id)) + assert(rental1)(not(equalTo(rental2))) + } + }, + test("Composing with >>:") { + val relation = Rental.fetch >>: Rental.book + + val staticRentalId = Rental.Id("foo") + val rentalIdGen = Gen.constant(staticRentalId) + + check(rentalIdGen.expand(relation)) { (book: Book) => + assert(book.currentRental)(isSome(equalTo(staticRentalId))) + } + }, + test("Composing with <>:") { + val relation = Rental.fetch <>: Rental.book + + val staticRentalId = Rental.Id("foo") + val rentalIdGen = Gen.constant(staticRentalId) + + check(rentalIdGen.expand(relation)) { case (rental, book) => + assert(rental.id)(equalTo(staticRentalId)) + assert(book.currentRental)(isSome(equalTo(staticRentalId))) + } + }, + test("Complex relations") { + val relation = Rental.fetch >>: Rental.user >>: (User.self & User.currentRentals) + + check(gen.rentalId.expand(relation)) { case (user, rentals) => + assert(rentals.map(_.userId))(forall(equalTo(user.id))) + } + } + ) +}