Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Hedgehog #82

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}"
}
Expand All @@ -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"
}
Expand Down
113 changes: 113 additions & 0 deletions hedgehog/src/decrel/hedgehog/gen.scala
Original file line number Diff line number Diff line change
@@ -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]]

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
147 changes: 147 additions & 0 deletions hedgehog/test/src/decrel/hedgehog/genSpec.scala
Original file line number Diff line number Diff line change
@@ -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)))
}
}
)
}