Full test coverage for the baseline API.

This commit is contained in:
Pat Garrity 2025-11-03 10:09:15 -06:00
parent c114da203e
commit af109358b5
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
9 changed files with 269 additions and 32 deletions

View file

@ -2,7 +2,6 @@ package gs.predicate.v0.api
import cats.Applicative import cats.Applicative
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Predicate.Result.forall
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
/** Implements logical AND. /** Implements logical AND.
@ -18,7 +17,7 @@ final class And[F[_]: Applicative, -A](
/** @inheritDocs /** @inheritDocs
*/ */
override def eval(input: A): F[Predicate.Result] = override def eval(input: A): F[Predicate.Result] =
ps.map(_.eval(input)).sequence.map(_.forall()) ps.map(_.eval(input)).sequence.map(_.allMatch())
object And: object And:

View file

@ -2,7 +2,6 @@ package gs.predicate.v0.api
import cats.Applicative import cats.Applicative
import cats.syntax.all.* import cats.syntax.all.*
import gs.predicate.v0.api.Predicate.Result.forany
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
/** Implements logical OR. /** Implements logical OR.
@ -18,7 +17,7 @@ final class Or[F[_]: Applicative, -A](
/** @inheritDocs /** @inheritDocs
*/ */
override def eval(input: A): F[Predicate.Result] = override def eval(input: A): F[Predicate.Result] =
ps.map(_.eval(input)).sequence.map(_.forany()) ps.map(_.eval(input)).sequence.map(_.anyMatch())
object Or: object Or:

View file

@ -1,5 +1,6 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.Applicative
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
/** A _Predicate_ is some function that accepts any input and emits some /** A _Predicate_ is some function that accepts any input and emits some
@ -48,6 +49,12 @@ trait Predicate[F[_], -A]:
object Predicate: object Predicate:
given CanEqual[Predicate[?, ?], Predicate[?, ?]] = CanEqual.derived
def alwaysTrue[F[_]: Applicative]: Predicate[F, Any] = True[F]
def alwaysFalse[F[_]: Applicative]: Predicate[F, Any] = False[F]
/** The result of evaluating a [[Predicate]] is a Boolean value where: /** The result of evaluating a [[Predicate]] is a Boolean value where:
* *
* - `true`: The predicate matched the given input. * - `true`: The predicate matched the given input.
@ -80,14 +87,15 @@ object Predicate:
extension (results: List[Result]) extension (results: List[Result])
/** @return /** @return
* True if all results are true. * True if all results are true. False if no results are given.
*/ */
def forall(): Result = results.forall(x => x) def allMatch(): Result = results.nonEmpty && results.forall(x => x)
/** @return /** @return
* True if any results match. * True if any results match. False if no results are given.
*/ */
def forany(): Result = results.find(x => x).isDefined def anyMatch(): Result =
results.nonEmpty && results.find(x => x).isDefined
extension (result: Result) extension (result: Result)
/** @return /** @return

View file

@ -1,27 +1,28 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.effect.IO import cats.effect.IO
import gs.uuid.v0.UUID
import support.IOSuite import support.IOSuite
class AndTests extends IOSuite: class AndTests extends IOSuite:
iotest("should return true if all are true") { iotest("should return true if all are true") {
val id = UUID.v7()
val and = And(True[IO], True[IO], True[IO]) val and = And(True[IO], True[IO], True[IO])
val and2 = And(id, True[IO], True[IO], True[IO])
for result <- and.eval(()) for
yield assertEquals(result.unwrap(), true) result <- and.eval(())
result2 <- and2.eval(())
yield
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
} }
iotest("should return false if any are false") { iotest("should return false if any are false") {
val id = UUID.v7()
val and = And(True[IO], False[IO], True[IO]) val and = And(True[IO], False[IO], True[IO])
val and2 = And(id, True[IO], False[IO], True[IO])
for result <- and.eval(())
yield assertEquals(result.unwrap(), false)
}
iotest("should return false for an empty list") {
val and = And.empty[IO]
val and2 = And[IO, Any]()
for for
result <- and.eval(()) result <- and.eval(())
@ -30,3 +31,33 @@ class AndTests extends IOSuite:
assertEquals(result.unwrap(), false) assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false) assertEquals(result2.unwrap(), false)
} }
iotest("should return false for an empty list") {
val and = And.empty[IO]
val and2 = And[IO, Any]()
val and3 = And[IO, Any](UUID.v7())
for
result <- and.eval(())
result2 <- and2.eval(())
result3 <- and3.eval(())
yield
assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false)
assertEquals(result3.unwrap(), false)
}
iotest("should return the underlying predicate for a singular entry") {
val p = True[IO]
val and = And(p)
val and2 = And(UUID.v7(), p)
for
result <- and.eval(())
result2 <- and2.eval(())
yield
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
assertEquals(p, and)
assertEquals(p, and2)
}

View file

@ -0,0 +1,35 @@
package gs.predicate.v0.api
import cats.effect.IO
import gs.uuid.v0.UUID
import support.IOSuite
class FalseTests extends IOSuite:
iotest("should return false") {
val predicate = False[IO]
for result <- predicate.eval(())
yield assertEquals(result.unwrap(), false)
}
iotest("should provide a unique identifier") {
val id = UUID.v7()
val predicate = False[IO](id)
for result <- predicate.eval(())
yield
assertEquals(result.unwrap(), false)
assertEquals(predicate.id, id)
assertEquals(predicate.hashCode(), id.hashCode())
assertEquals(predicate.toString(), s"predicate:${id.withoutDashes()}")
}
test("should support equality") {
val predicate = False[IO]
val p2 = False[IO](predicate.id)
val p3 = False[IO]
assertEquals(predicate, predicate)
assertEquals(predicate, p2)
assertNotEquals(predicate, p3)
}

View file

@ -1,27 +1,28 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.effect.IO import cats.effect.IO
import gs.uuid.v0.UUID
import support.IOSuite import support.IOSuite
class OrTests extends IOSuite: class OrTests extends IOSuite:
iotest("should return true if any are true") { iotest("should return true if any are true") {
val id = UUID.v7()
val or = Or(False[IO], True[IO], False[IO]) val or = Or(False[IO], True[IO], False[IO])
val or2 = Or(id, False[IO], True[IO], False[IO])
for result <- or.eval(()) for
yield assertEquals(result.unwrap(), true) result <- or.eval(())
result2 <- or2.eval(())
yield
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
} }
iotest("should return false if all are false") { iotest("should return false if all are false") {
val id = UUID.v7()
val or = Or(False[IO], False[IO], False[IO]) val or = Or(False[IO], False[IO], False[IO])
val or2 = Or(id, False[IO], False[IO], False[IO])
for result <- or.eval(())
yield assertEquals(result.unwrap(), false)
}
iotest("should return false for an empty list") {
val or = Or.empty[IO]
val or2 = Or[IO, Any]()
for for
result <- or.eval(()) result <- or.eval(())
@ -30,3 +31,33 @@ class OrTests extends IOSuite:
assertEquals(result.unwrap(), false) assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false) assertEquals(result2.unwrap(), false)
} }
iotest("should return false for an empty list") {
val or = Or.empty[IO]
val or2 = Or[IO, Any]()
val or3 = Or[IO, Any](UUID.v7())
for
result <- or.eval(())
result2 <- or2.eval(())
result3 <- or3.eval(())
yield
assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false)
assertEquals(result3.unwrap(), false)
}
iotest("should return the underlying predicate for a singular entry") {
val p = True[IO]
val or = Or(p)
val or2 = Or(UUID.v7(), p)
for
result <- or.eval(())
result2 <- or2.eval(())
yield
assertEquals(result.unwrap(), true)
assertEquals(result2.unwrap(), true)
assertEquals(p, or)
assertEquals(p, or2)
}

View file

@ -0,0 +1,82 @@
package gs.predicate.v0.api
import Predicate.Result
import gs.predicate.v0.api.Predicate.Result.allMatch
import munit.FunSuite
class PredicateResultTests extends FunSuite:
test("should represent matched and missed") {
val r1 = Result.matched()
val r2 = Result.missed()
assertEquals(r1.unwrap(), true)
assertEquals(r1.isMatch, true)
assertEquals(r1.isMiss, false)
assertEquals(r2.unwrap(), false)
assertEquals(r2.isMatch, false)
assertEquals(r2.isMiss, true)
}
test("should instantiate given some Boolean value") {
val r1 = Result(true)
val r2 = Result(false)
assertEquals(r1.unwrap(), true)
assertEquals(r1.isMatch, true)
assertEquals(r1.isMiss, false)
assertEquals(r2.unwrap(), false)
assertEquals(r2.isMatch, false)
assertEquals(r2.isMiss, true)
}
test("should support logical AND") {
val r1 = Result.matched()
val r2 = Result.missed()
val r3 = Result.matched()
val r4 = Result.matched()
val r5 = Result.missed()
assertEquals(r1.and(r3).and(r4), Result.matched())
assertEquals(r1.and(r2).and(r4), Result.missed())
assertEquals(r5.and(r1).and(r4), Result.missed())
}
test("should support logical OR") {
val r1 = Result.matched()
val r2 = Result.missed()
val r3 = Result.matched()
val r4 = Result.matched()
val r5 = Result.missed()
assertEquals(r1.or(r3).or(r4), Result.matched())
assertEquals(r1.or(r2).or(r4), Result.matched())
assertEquals(r5.or(r1).or(r4), Result.matched())
assertEquals(r2.or(r5), Result.missed())
}
test("should support all (true if all results are true)") {
val r1 = Result.matched()
val r2 = Result.missed()
val r3 = Result.matched()
val r4 = Result.matched()
val r5 = Result.missed()
val l1 = List(r1, r3, r4)
val l2 = List(r1, r2, r3, r4, r5)
val l3 = List()
assertEquals(l1.allMatch(), Result.matched())
assertEquals(l2.allMatch(), Result.missed())
assertEquals(l3.allMatch(), Result.missed())
}
test("should support any (true if any results are true)") {
val r1 = Result.matched()
val r2 = Result.missed()
val r3 = Result.matched()
val r4 = Result.matched()
val r5 = Result.missed()
val l1 = List(r1, r3, r4)
val l2 = List(r1, r2, r3, r4, r5)
val l3 = List(r2, r5)
val l4 = List.empty[Result]
assertEquals(l1.anyMatch(), Result.matched())
assertEquals(l2.anyMatch(), Result.matched())
assertEquals(l3.anyMatch(), Result.missed())
assertEquals(l4.anyMatch(), Result.missed())
}

View file

@ -0,0 +1,17 @@
package gs.predicate.v0.api
import munit.FunSuite
class PredicateTests extends FunSuite:
test("should not equal non-predicate objects") {
val x = "other"
val p = Predicate.alwaysTrue[cats.Id]
assertEquals(p.equals(x), false)
assertNotEquals(p, null)
}
test("should provide a default toString implementation") {
val p = Predicate.alwaysFalse[cats.Id]
assertEquals(p.toString(), s"predicate:${p.id.withoutDashes()}")
}

View file

@ -0,0 +1,35 @@
package gs.predicate.v0.api
import cats.effect.IO
import gs.uuid.v0.UUID
import support.IOSuite
class TrueTests extends IOSuite:
iotest("should return true") {
val predicate = True[IO]
for result <- predicate.eval(())
yield assertEquals(result.unwrap(), true)
}
iotest("should provide a unique identifier") {
val id = UUID.v7()
val predicate = True[IO](id)
for result <- predicate.eval(())
yield
assertEquals(result.unwrap(), true)
assertEquals(predicate.id, id)
assertEquals(predicate.hashCode(), id.hashCode())
assertEquals(predicate.toString(), s"predicate:${id.withoutDashes()}")
}
test("should support equality") {
val predicate = True[IO]
val p2 = True[IO](predicate.id)
val p3 = True[IO]
assertEquals(predicate, predicate)
assertEquals(predicate, p2)
assertNotEquals(predicate, p3)
}