This commit is contained in:
Claudio Maggioni 2024-12-22 22:19:25 +01:00
parent e89a95f88e
commit decd52836f
5 changed files with 120 additions and 51 deletions

View file

@ -38,7 +38,14 @@ object GenerativeFluentAssertionsDSL:
import quotes.reflect.* import quotes.reflect.*
property match property match
case '{ (${ typeProvider }: HavePropertyTypeProvider[subjectType, elemType]) case '{ (${ typeProvider }: HavePropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr: String)(satisfying)
.$asInstanceOf$[SatisfyingNotEquals[T]] } => '{
$typeProvider
.applyDynamic($propertyNameExpr)(satisfying.notEquals[T]($obj, $ord))
.$asInstanceOf$[AssertionProperty]
}
case '{ (${ typeProvider }: ContainsPropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr: String)(satisfying) .applyDynamic($propertyNameExpr: String)(satisfying)
.$asInstanceOf$[SatisfyingNotEquals[T]] } => '{ .$asInstanceOf$[SatisfyingNotEquals[T]] } => '{
$typeProvider $typeProvider
@ -120,19 +127,31 @@ object GenerativeFluentAssertionsDSL:
// The quoted pattern also extracts: the type of the assertion subject, // The quoted pattern also extracts: the type of the assertion subject,
// and the type of the property (actually... not exactly that. Why?) // and the type of the property (actually... not exactly that. Why?)
case '{ case '{
(${ typeProvider }: HavePropertyTypeProvider[subjectType, elemType]) (${ typeProvider }: HavePropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType]) .applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType])
.$asInstanceOf$[AssertionProperty] .$asInstanceOf$[AssertionProperty]
} => } =>
'{} val subjectExpr = '{ $typeProvider.subject }
// val subjectExpr = '{ $typeProvider.subject } val checkerExpr = '{ $typeProvider.checker }
// val negatedExpr = '{ $typeProvider.negated } generateShouldHaveAssertion[subjectType, valueType](
// generateShouldHaveAssertion[subjectType, valueType]( subjectExpr,
// subjectExpr, checkerExpr,
// propertyNameExpr, propertyNameExpr,
// valueExpr, valueExpr
// negatedExpr )
// ) case '{
(${ typeProvider }: ContainsPropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType])
.$asInstanceOf$[AssertionProperty]
} =>
val subjectExpr = '{ $typeProvider.subject }
val checkerExpr = '{ $typeProvider.checker }
generateShouldContainAssertion[subjectType, valueType](
subjectExpr,
checkerExpr,
propertyNameExpr,
valueExpr
)
case '{ case '{
(${ typeProvider }: BePropertyTypeProvider[subjectType]) (${ typeProvider }: BePropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr)($unit) .applyDynamic($propertyNameExpr)($unit)
@ -192,17 +211,18 @@ object GenerativeFluentAssertionsDSL:
end if end if
end generateShouldBeAssertion end generateShouldBeAssertion
private def generateShouldHaveAssertion[T, R]( private def generateShouldHaveAssertion[E, R](
subjectExpr: Expr[T], subjectExpr: Expr[E],
propertyNameExpr: Expr[String], checkerExpr: Expr[SinglePropertyChecker],
propertyValueExpr: Expr[Condition[R]], propertyNameExpr: Expr[String],
negatedExpr: Expr[Boolean] propertyValueExpr: Expr[Condition[R]]
)(using Type[T], Type[R])(using Quotes): Expr[Unit] = )(using Type[E], Type[R])(using Quotes): Expr[Unit] =
import quotes.reflect.* import quotes.reflect.*
val subjectTypeRepr = TypeRepr.of[T] val subjectTypeRepr = TypeRepr.of[E]
val optionalCandidateSymbol = val optionalCandidateSymbol =
findCandidateSymbol[T, R](propertyNameExpr.valueOrAbort) findCandidateSymbol[E, R](propertyNameExpr.valueOrAbort)
if optionalCandidateSymbol.isEmpty then if optionalCandidateSymbol.isEmpty then
report.errorAndAbort( report.errorAndAbort(
@ -212,22 +232,62 @@ object GenerativeFluentAssertionsDSL:
else else
val candidateMethod = optionalCandidateSymbol.get val candidateMethod = optionalCandidateSymbol.get
val assertion = '{ val assertion: Expr[Unit] = '{
val subject = $subjectExpr val subject = $subjectExpr
val negated = $negatedExpr val checker = $checkerExpr
val condition = $propertyValueExpr val condition = $propertyValueExpr
lazy val value = ${ Select(('subject).asTerm, candidateMethod).asExprOf[R] } lazy val value = ${ Select(('subject).asTerm, candidateMethod).asExprOf[R] }
assert( assert(
condition.test(value) == !negated, checker.check(value, condition),
s"$subject.${$propertyNameExpr} " + condition.failedMessage(negated) + " but " + value s"$subject.${$propertyNameExpr} ${checker.failedMessage} ${condition.failedMessage} but ${value}"
) )
} }
report.info(assertion.show, subjectExpr.asTerm.pos) report.info(assertion.show, subjectExpr.asTerm.pos)
assertion assertion
end if end if
end generateShouldHaveAssertion end generateShouldHaveAssertion
private def generateShouldContainAssertion[E, R](
subjectExpr: Expr[Iterable[E]],
checkerExpr: Expr[IterablePropertyChecker],
propertyNameExpr: Expr[String],
propertyValueExpr: Expr[Condition[R]]
)(using Type[E], Type[R])(using Quotes): Expr[Unit] =
import quotes.reflect.*
val subjectTypeRepr = TypeRepr.of[E]
val optionalCandidateSymbol =
findCandidateSymbol[E, R](propertyNameExpr.valueOrAbort)
if optionalCandidateSymbol.isEmpty then
report.errorAndAbort(
s"Assertion Error: No field or arity-0 method with name ${propertyNameExpr.valueOrAbort} on type ${subjectTypeRepr.show}",
propertyNameExpr
)
else
val candidateMethod = optionalCandidateSymbol.get
val assertion: Expr[Unit] = '{
val subject = $subjectExpr
val checker = $checkerExpr
val condition = $propertyValueExpr
lazy val values = subject.map(element => ${ Select(('element).asTerm, candidateMethod).asExprOf[R] })
assert(
checker.check(values, condition),
s"$subject.${$propertyNameExpr} ${checker.failedMessage} ${condition.failedMessage}"
)
}
report.info(assertion.show, subjectExpr.asTerm.pos)
assertion
end if
end generateShouldContainAssertion
/** Looks for candidate symbols with a given set of possible names on a type. /** Looks for candidate symbols with a given set of possible names on a type.
* *
* Improved version w.r.t. the one presented during the lecture. * Improved version w.r.t. the one presented during the lecture.

View file

@ -23,7 +23,7 @@ sealed trait SatisfyingNotEquals[T]
case class Condition[-T](predicate: T => Boolean, op: String, expected: Any): case class Condition[-T](predicate: T => Boolean, op: String, expected: Any):
def test(value: T): Boolean = predicate(value) def test(value: T): Boolean = predicate(value)
def failedMessage(negated: Boolean): String = s"${if (negated) "is" else "is not"} ${op} ${expected}" def failedMessage: String = s"${op} ${expected}"
case object satisfying: case object satisfying:
def `===`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.equiv(_, toWhat), "===", toWhat) def `===`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.equiv(_, toWhat), "===", toWhat)
@ -38,35 +38,45 @@ class BePropertyTypeProvider[T](val subject: T) extends Selectable:
def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ??? def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ???
end BePropertyTypeProvider end BePropertyTypeProvider
sealed trait PropertyChecker[T, E]: sealed trait PropertyChecker:
val failedMessage: String val failedMessage: String
def check(t: T, condition: Condition[E]): Boolean
case class SingleChecker[T]() extends PropertyChecker[T, T]: sealed trait SinglePropertyChecker extends PropertyChecker:
override def check(t: T, condition: Condition[T]): Boolean = condition.test(t) def check[T](t: T, condition: Condition[T]): Boolean
case object SingleChecker extends SinglePropertyChecker:
override def check[T](t: T, condition: Condition[T]): Boolean = condition.test(t)
override val failedMessage: String = "is not" override val failedMessage: String = "is not"
case class NegatedChecker[T]() extends PropertyChecker[T, T]: case object NegatedChecker extends SinglePropertyChecker:
override def check(t: T, condition: Condition[T]): Boolean = !condition.test(t) override def check[T](t: T, condition: Condition[T]): Boolean = !condition.test(t)
override val failedMessage: String = "is" override val failedMessage: String = "is"
case class AllChecker[E]() extends PropertyChecker[Iterable[E], E]: sealed trait IterablePropertyChecker extends PropertyChecker:
override def check(t: Iterable[E], condition: Condition[E]): Boolean = t.forall(e => condition.test(e)) def check[T](t: Iterable[T], condition: Condition[T]): Boolean
case object AllChecker extends IterablePropertyChecker:
override def check[T](t: Iterable[T], condition: Condition[T]): Boolean = t.forall(e => condition.test(e))
override val failedMessage: String = "has elements not satisfying" override val failedMessage: String = "has elements not satisfying"
case class SomeChecker[E]() extends PropertyChecker[Iterable[E], E]: case object SomeChecker extends IterablePropertyChecker:
override def check(t: Iterable[E], condition: Condition[E]): Boolean = t.exists(e => condition.test(e)) override def check[T](t: Iterable[T], condition: Condition[T]): Boolean = t.exists(e => condition.test(e))
override val failedMessage: String = "has no elements satisfying" override val failedMessage: String = "has no elements satisfying"
case class NoChecker[E]() extends PropertyChecker[Iterable[E], E]: case object NoChecker extends IterablePropertyChecker:
override def check(t: Iterable[E], condition: Condition[E]): Boolean = t.forall(e => !condition.test(e)) override def check[T](t: Iterable[T], condition: Condition[T]): Boolean = t.forall(e => !condition.test(e))
override val failedMessage: String = "has elements satisfying" override val failedMessage: String = "has elements satisfying"
class HavePropertyTypeProvider[T, E](val subject: T, val checker: PropertyChecker[T, E]) extends Selectable: class HavePropertyTypeProvider[T](val subject: T, val checker: SinglePropertyChecker) extends Selectable:
def applyDynamic[U](fieldName: String)(arg: satisfying.type): SatisfyingNotEquals[U] = ??? def applyDynamic[U](fieldName: String)(arg: satisfying.type): SatisfyingNotEquals[U] = ???
def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ??? def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ???
end HavePropertyTypeProvider end HavePropertyTypeProvider
class ContainsPropertyTypeProvider[T](val subject: Iterable[T], val checker: IterablePropertyChecker) extends Selectable:
def applyDynamic[U](fieldName: String)(arg: satisfying.type): SatisfyingNotEquals[U] = ???
def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ???
end ContainsPropertyTypeProvider
/** A trait for each type of assertion (be or have). /** A trait for each type of assertion (be or have).
*/ */
sealed trait AssertionDSLVerb sealed trait AssertionDSLVerb
@ -77,7 +87,6 @@ case object be_ extends AssertionDSLVerb
case object have extends AssertionDSLVerb: case object have extends AssertionDSLVerb:
inline def `unary_!`: have.type = ??? inline def `unary_!`: have.type = ???
case object contain extends AssertionDSLVerb case object contain extends AssertionDSLVerb
case object `with` extends AssertionDSLVerb case object `with` extends AssertionDSLVerb
@ -85,15 +94,15 @@ case class ContainsVerb[T](subject: Iterable[T])
extension [T](inline containsVerb: ContainsVerb[T]) extension [T](inline containsVerb: ContainsVerb[T])
transparent inline def allElements(inline verb: `with`.type) = ${ transparent inline def allElements(inline verb: `with`.type) = ${
ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ AllChecker[T]() }) ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ AllChecker })
} }
transparent inline def someElements(inline verb: `with`.type) = ${ transparent inline def someElements(inline verb: `with`.type) = ${
ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ SomeChecker[T]() }) ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ SomeChecker })
} }
transparent inline def noElements(inline verb: `with`.type) = ${ transparent inline def noElements(inline verb: `with`.type) = ${
ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ NoChecker[T]() }) ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ NoChecker })
} }
/** Implicit class to add 'should' assertions to a generic type T. /** Implicit class to add 'should' assertions to a generic type T.

View file

@ -27,7 +27,7 @@ case class Box(label: String, weight: Mass, price: Money)
assertions: assertions:
/* be.property assertions */ /* be.property assertions */
// assert(person.adult, ...) // assert(person.adult, ...)
person should !be.adult person should !(!be.adult)
// assert(List().isEmpty, ...) // assert(List().isEmpty, ...)
List() should be.empty List() should be.empty
// assert(List(1,2,3).nonEmpty) // assert(List(1,2,3).nonEmpty)
@ -46,7 +46,7 @@ case class Box(label: String, weight: Mass, price: Money)
List() should have size satisfying >= 0 List() should have size satisfying >= 0
List(3) should have size satisfying !== 0 List(3) should have size satisfying !== 0
List(50, 2, 3) should have head satisfying < 100 List(50, 2, 3) should have head satisfying < 100
box should !have weight satisfying <= 300.0.kg box should !(!have) weight satisfying <= 300.0.kg
// we wrap the strings in StringOps instances in order to resolve the scala method "size". This would otherwise // we wrap the strings in StringOps instances in order to resolve the scala method "size". This would otherwise
// not happen automatically as the string types would resolve to "java.lang.String" thus not allowing the type // not happen automatically as the string types would resolve to "java.lang.String" thus not allowing the type
@ -58,8 +58,8 @@ case class Box(label: String, weight: Mass, price: Money)
new StringOps(""), new StringOps(""),
new StringOps("alice") new StringOps("alice")
) should contain someElements `with` size satisfying === 0 ) should contain someElements `with` size satisfying === 0
List(Seq(), Seq(), Seq()) should contain allElements `with` size satisfying >= 0 // List(Seq(), Seq(), Seq()) should contain allElements `with` size satisfying >= 0
List(List(1,2), List(20,1), List(3,4)) should contain noElements `with` head satisfying === 3 // List(List(1,2), List(20,1), List(3,4)) should contain noElements `with` head satisfying === 3
/* should have assertions */ /* should have assertions */
// // assert(List(1).head == 1, ...) // // assert(List(1).head == 1, ...)

View file

@ -7,7 +7,7 @@ object ShouldContainTypeProvider:
/** The type provider for the should have assertion, which generates a refined /** The type provider for the should have assertion, which generates a refined
* structural type for type T. * structural type for type T.
*/ */
def generateShouldContainTypeProvider[T](containsExpr: Expr[ContainsVerb[T]], checker: Expr[PropertyChecker[Iterable[T], T]]) def generateShouldContainTypeProvider[T](containsExpr: Expr[ContainsVerb[T]], checker: Expr[IterablePropertyChecker])
(using Type[T], Quotes): Expr[Any] = (using Type[T], Quotes): Expr[Any] =
import quotes.reflect.* import quotes.reflect.*
@ -75,7 +75,7 @@ object ShouldContainTypeProvider:
) )
val refinedType = refineTypeBySymbols( val refinedType = refineTypeBySymbols(
TypeRepr.of[HavePropertyTypeProvider[Iterable[T], T]], TypeRepr.of[ContainsPropertyTypeProvider[T]],
fields ++ arityZeroMethods fields ++ arityZeroMethods
) )
@ -87,6 +87,6 @@ object ShouldContainTypeProvider:
// exact type is unknown until compilation time. // exact type is unknown until compilation time.
refinedType.asType match refinedType.asType match
case '[tpe] => // tpe is the exact refined type case '[tpe] => // tpe is the exact refined type
'{ HavePropertyTypeProvider[Iterable[T], T]($subject, $checker).asInstanceOf[tpe] } '{ ContainsPropertyTypeProvider[T]($subject, $checker).asInstanceOf[tpe] }
end ShouldContainTypeProvider end ShouldContainTypeProvider

View file

@ -82,11 +82,11 @@ object ShouldHaveTypeProvider:
) )
val refinedType = refineTypeBySymbols( val refinedType = refineTypeBySymbols(
TypeRepr.of[HavePropertyTypeProvider[T, T]], TypeRepr.of[HavePropertyTypeProvider[T]],
fields ++ arityZeroMethods fields ++ arityZeroMethods
) )
val checker = if (negated) '{ NegatedChecker[T]() } else '{ SingleChecker[T]() } val checker = if (negated) '{ NegatedChecker } else '{ SingleChecker }
// This is the way to extract a (reflection) type to a // This is the way to extract a (reflection) type to a
// type that can be used in some quoted code. // type that can be used in some quoted code.
@ -95,6 +95,6 @@ object ShouldHaveTypeProvider:
// exact type is unknown until compilation time. // exact type is unknown until compilation time.
refinedType.asType match refinedType.asType match
case '[tpe] => // tpe is the exact refined type case '[tpe] => // tpe is the exact refined type
'{ HavePropertyTypeProvider[T, T]($subject, $checker).asInstanceOf[tpe] } '{ HavePropertyTypeProvider[T]($subject, $checker).asInstanceOf[tpe] }
end ShouldHaveTypeProvider end ShouldHaveTypeProvider