This commit is contained in:
Claudio Maggioni 2024-12-18 21:55:31 +01:00
parent 16b4367ed0
commit 07f5451064
4 changed files with 56 additions and 47 deletions

View file

@ -8,6 +8,9 @@ import scala.language.postfixOps
*/ */
object GenerativeFluentAssertionsDSL: object GenerativeFluentAssertionsDSL:
extension (inline property: AssertionProperty)
inline def `unary_!`: AssertionProperty = NegatedAssertionProperty(property)
/** /**
* Due to operator precedence, '!==' is the only comparison operator that is considered an "assignment operation" * Due to operator precedence, '!==' is the only comparison operator that is considered an "assignment operation"
* and thus has lower precedence than alphanumeric operators. Therefore, we must handle "!==" as the final link of * and thus has lower precedence than alphanumeric operators. Therefore, we must handle "!==" as the final link of
@ -39,6 +42,19 @@ object GenerativeFluentAssertionsDSL:
assertionsGenerativeImpl('assertions) assertionsGenerativeImpl('assertions)
} }
private def unwrapShouldBeNegatedAssertion(assertionExpr: Expr[AssertionProperty], negated: Boolean = false)(
using Quotes
): (Expr[String], Boolean) =
import quotes.reflect.*
assertionExpr match
case '{ NegatedAssertionProperty($innerExpr: AssertionProperty) } => unwrapShouldBeNegatedAssertion(innerExpr, !negated)
case '{ be.selectDynamic($propertyNameExpr: String) } => (propertyNameExpr, negated)
case what => report.errorAndAbort(
"'be' negation unwrapping: Invalid expression, must be a 'should be' assertion.",
what
)
/** Macro implementation. The method is recursive, each single expression is /** Macro implementation. The method is recursive, each single expression is
* then separately rewritten into the generateAssertion method. The method * then separately rewritten into the generateAssertion method. The method
* returns a compiler error if the expression is not an AssertionProperty. * returns a compiler error if the expression is not an AssertionProperty.
@ -84,12 +100,9 @@ object GenerativeFluentAssertionsDSL:
val generatedAssertion = assertionExpr match val generatedAssertion = assertionExpr match
// be assertion. Check the explicit selectDynamic to extract the property name // be assertion. Check the explicit selectDynamic to extract the property name
case '{ case '{ (${ subjectExpr }: subjectType) should ($assertionProperty: AssertionProperty) } =>
(${ subjectExpr }: subjectType) should be.selectDynamic( val (propertyNameExpr, negated) = unwrapShouldBeNegatedAssertion(assertionProperty)
$propertyNameExpr generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr, negated)
)
} =>
generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr)
// The more complicated structure of property type provider construction in // The more complicated structure of property type provider construction in
// the should have assertion. Again, the applyDynamic is explicit. // the should have assertion. Again, the applyDynamic is explicit.
// The quoted pattern also extracts: the type of the assertion subject, // The quoted pattern also extracts: the type of the assertion subject,
@ -101,10 +114,12 @@ object GenerativeFluentAssertionsDSL:
} => } =>
println(assertionExpr.show) println(assertionExpr.show)
val subjectExpr = '{ $typeProvider.subject } val subjectExpr = '{ $typeProvider.subject }
val negatedExpr = '{ $typeProvider.negated }
generateShouldHaveAssertion[subjectType, valueType]( generateShouldHaveAssertion[subjectType, valueType](
subjectExpr, subjectExpr,
propertyNameExpr, propertyNameExpr,
valueExpr valueExpr,
negatedExpr
) )
case '{ case '{
(${ typeProvider }: BePropertyTypeProvider[subjectType]) (${ typeProvider }: BePropertyTypeProvider[subjectType])
@ -119,7 +134,6 @@ object GenerativeFluentAssertionsDSL:
} => } =>
'{} // placeholder '{} // placeholder
case _ => case _ =>
println(assertionExpr.show)
report.errorAndAbort( report.errorAndAbort(
"Invalid expression, must be a 'should be' or 'should have' assertion. ", "Invalid expression, must be a 'should be' or 'should have' assertion. ",
assertionExpr assertionExpr
@ -130,7 +144,8 @@ object GenerativeFluentAssertionsDSL:
private def generateShouldBeAssertion[T]( private def generateShouldBeAssertion[T](
subject: Expr[T], subject: Expr[T],
propertyNameExpr: Expr[String] propertyNameExpr: Expr[String],
negated: Boolean
)(using Type[T])(using Quotes): Expr[Unit] = )(using Type[T])(using Quotes): Expr[Unit] =
import quotes.reflect.* import quotes.reflect.*
@ -153,11 +168,12 @@ object GenerativeFluentAssertionsDSL:
// Constructs a 'select' tree, that is, a selection to the field or method // Constructs a 'select' tree, that is, a selection to the field or method
// of the subject. // of the subject.
val call = Select(subject.asTerm, validSymbol).asExprOf[Boolean] val call = Select(subject.asTerm, validSymbol).asExprOf[Boolean]
val expected = '{ !${Expr(negated)} }
val assertion = '{ val assertion = '{
assert( assert(
$call, $call == $expected,
s"${$subject} was not ${$propertyNameExpr} (${$validSymbolNameExpr} is false)" s"${$subject} was${if ($expected) " not" else ""} ${$propertyNameExpr} (${$validSymbolNameExpr} is ${if ($expected) "false" else "true"})"
) )
} }
assertion assertion
@ -167,7 +183,8 @@ object GenerativeFluentAssertionsDSL:
private def generateShouldHaveAssertion[T, R]( private def generateShouldHaveAssertion[T, R](
subjectExpr: Expr[T], subjectExpr: Expr[T],
propertyNameExpr: Expr[String], propertyNameExpr: Expr[String],
propertyValueExpr: Expr[Condition[R]] propertyValueExpr: Expr[Condition[R]],
negatedExpr: Expr[Boolean]
)(using Type[T], Type[R])(using Quotes): Expr[Unit] = )(using Type[T], Type[R])(using Quotes): Expr[Unit] =
import quotes.reflect.* import quotes.reflect.*
@ -200,11 +217,13 @@ object GenerativeFluentAssertionsDSL:
*/ */
val assertion = '{ val assertion = '{
val subject = $subjectExpr val subject = $subjectExpr
val negated = $negatedExpr
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), condition.test(value) == !negated,
s"${subject}.${$propertyNameExpr} " + condition.failedMessage + " but " + value s"${subject}.${$propertyNameExpr} " + condition.failedMessage(negated) + " but " + value
) )
} }
report.info(assertion.show, subjectExpr.asTerm.pos) report.info(assertion.show, subjectExpr.asTerm.pos)

View file

@ -3,6 +3,7 @@ package ch.usi.si.msde.edsl.lecture10
import scala.language.dynamics import scala.language.dynamics
sealed trait AssertionProperty sealed trait AssertionProperty
case class NegatedAssertionProperty(prop: AssertionProperty) extends AssertionProperty
/** Represents (but with no implementation) the result of a call to /** Represents (but with no implementation) the result of a call to
* be.selectDynamic(String), used to specify the property. The semantics of * be.selectDynamic(String), used to specify the property. The semantics of
@ -19,35 +20,24 @@ end DynamicShouldBeProperty
*/ */
sealed trait SatisfyingNotEquals[T] sealed trait SatisfyingNotEquals[T]
case class Condition[T](predicate: (T, T) => Boolean, op: String, expected: T): case class Condition[-T](predicate: T => Boolean, op: String, expected: Any):
def test(value: T): Boolean = predicate(value, expected) def test(value: T): Boolean = predicate(value)
def failedMessage: String = s"is not ${op} ${expected}" def failedMessage(negated: Boolean): String = s"${if (negated) "is" else "is not"} ${op} ${expected}"
case object satisfying: case object satisfying:
def `===`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.equiv, "===", toWhat) def `===`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.equiv(_, toWhat), "===", toWhat)
def notEquals[T](toWhat: T, ord: Ordering[T]): Condition[T] = Condition(!ord.equiv(_, _), "!==", toWhat) def notEquals[T](toWhat: T, ord: Ordering[T]): Condition[T] = Condition(!ord.equiv(_, toWhat), "!==", toWhat)
def `<`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lt, "<", toWhat) def `<`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lt(_, toWhat), "<", toWhat)
def `>`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gt, ">", toWhat) def `>`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gt(_, toWhat), ">", toWhat)
def `<=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lteq, "<=", toWhat) def `<=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lteq(_, toWhat), "<=", toWhat)
def `>=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gteq, ">=", toWhat) def `>=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gteq(_, toWhat), ">=", toWhat)
class BePropertyTypeProvider[T](val subject: T) extends Selectable: class BePropertyTypeProvider[T](val subject: T) extends Selectable:
def selectDynamic(fieldName: String): AssertionProperty = ??? def selectDynamic(fieldName: String): AssertionProperty = ???
def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ??? def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ???
end BePropertyTypeProvider end BePropertyTypeProvider
/** The class that implements the syntactic aspect of the type provider. It class HavePropertyTypeProvider[T](val subject: T, val negated: Boolean) extends Selectable:
* extends Selectable and implements applyDynamic with arity 1, so that any
* object of this type can be invoked with an arbitrary method with 1 argument,
* and a specific return type.
*
* Selectable types can be refined with a list of specific methods that are
* available and thus checked by the compiler. The macro in the should method
* generates a refinement of this type according to type T, so that - for any
* arity zero method or field in T - say foo: X, the type provider has a
* corresponding method def foo(arg: X): AssertionProperty.
*/
class HavePropertyTypeProvider[T](val subject: T) 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
@ -58,7 +48,11 @@ sealed trait AssertionDSLVerb
case object be extends AssertionDSLVerb with Dynamic: case object be extends AssertionDSLVerb with Dynamic:
def selectDynamic(fieldName: String): AssertionProperty = ??? def selectDynamic(fieldName: String): AssertionProperty = ???
case object be_ extends AssertionDSLVerb case object be_ extends AssertionDSLVerb
case object have extends AssertionDSLVerb
sealed trait HaveVerb(val negated: Boolean):
inline def `unary_!`: HaveVerb = HaveNegation(this)
case class HaveNegation(haveVerb: HaveVerb) extends HaveVerb(!haveVerb.negated)
case object have extends HaveVerb(false)
/** Implicit class to add 'should' assertions to a generic type T. /** Implicit class to add 'should' assertions to a generic type T.
* *
@ -90,11 +84,11 @@ extension [T](subject: T)
* *
* The method semantics is then provided by the assertions macro. * The method semantics is then provided by the assertions macro.
* *
* @param verbWord * @param verb
* The have word. * The have word.
* @return * @return
* a type provider for properties of type T. * a type provider for properties of type T.
*/ */
transparent inline def should(inline verbWord: have.type) = ${ transparent inline def should(inline verb: HaveVerb) = ${
ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject) ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject, '{verb.negated})
} }

View file

@ -26,7 +26,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)
@ -45,7 +45,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
/* should have assertions */ /* should have assertions */
// // assert(List(1).head == 1, ...) // // assert(List(1).head == 1, ...)

View file

@ -7,7 +7,7 @@ object ShouldHaveTypeProvider:
/** 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 generateShouldHaveTypeProvider[T](subject: Expr[T])(using Type[T])(using def generateShouldHaveTypeProvider[T](subject: Expr[T], negated: Expr[Boolean])(using Type[T])(using
Quotes Quotes
): Expr[Any] = ): Expr[Any] =
import quotes.reflect.* import quotes.reflect.*
@ -86,10 +86,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
val res = '{ '{ HavePropertyTypeProvider[T]($subject, $negated).asInstanceOf[tpe] }
val p = HavePropertyTypeProvider[T]($subject)
p.asInstanceOf[tpe]
}
res
end ShouldHaveTypeProvider end ShouldHaveTypeProvider