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:
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"
* 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)
}
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
* then separately rewritten into the generateAssertion method. The method
* returns a compiler error if the expression is not an AssertionProperty.
@ -84,12 +100,9 @@ object GenerativeFluentAssertionsDSL:
val generatedAssertion = assertionExpr match
// be assertion. Check the explicit selectDynamic to extract the property name
case '{
(${ subjectExpr }: subjectType) should be.selectDynamic(
$propertyNameExpr
)
} =>
generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr)
case '{ (${ subjectExpr }: subjectType) should ($assertionProperty: AssertionProperty) } =>
val (propertyNameExpr, negated) = unwrapShouldBeNegatedAssertion(assertionProperty)
generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr, negated)
// The more complicated structure of property type provider construction in
// the should have assertion. Again, the applyDynamic is explicit.
// The quoted pattern also extracts: the type of the assertion subject,
@ -101,10 +114,12 @@ object GenerativeFluentAssertionsDSL:
} =>
println(assertionExpr.show)
val subjectExpr = '{ $typeProvider.subject }
val negatedExpr = '{ $typeProvider.negated }
generateShouldHaveAssertion[subjectType, valueType](
subjectExpr,
propertyNameExpr,
valueExpr
valueExpr,
negatedExpr
)
case '{
(${ typeProvider }: BePropertyTypeProvider[subjectType])
@ -119,7 +134,6 @@ object GenerativeFluentAssertionsDSL:
} =>
'{} // placeholder
case _ =>
println(assertionExpr.show)
report.errorAndAbort(
"Invalid expression, must be a 'should be' or 'should have' assertion. ",
assertionExpr
@ -130,7 +144,8 @@ object GenerativeFluentAssertionsDSL:
private def generateShouldBeAssertion[T](
subject: Expr[T],
propertyNameExpr: Expr[String]
propertyNameExpr: Expr[String],
negated: Boolean
)(using Type[T])(using Quotes): Expr[Unit] =
import quotes.reflect.*
@ -153,11 +168,12 @@ object GenerativeFluentAssertionsDSL:
// Constructs a 'select' tree, that is, a selection to the field or method
// of the subject.
val call = Select(subject.asTerm, validSymbol).asExprOf[Boolean]
val expected = '{ !${Expr(negated)} }
val assertion = '{
assert(
$call,
s"${$subject} was not ${$propertyNameExpr} (${$validSymbolNameExpr} is false)"
$call == $expected,
s"${$subject} was${if ($expected) " not" else ""} ${$propertyNameExpr} (${$validSymbolNameExpr} is ${if ($expected) "false" else "true"})"
)
}
assertion
@ -167,7 +183,8 @@ object GenerativeFluentAssertionsDSL:
private def generateShouldHaveAssertion[T, R](
subjectExpr: Expr[T],
propertyNameExpr: Expr[String],
propertyValueExpr: Expr[Condition[R]]
propertyValueExpr: Expr[Condition[R]],
negatedExpr: Expr[Boolean]
)(using Type[T], Type[R])(using Quotes): Expr[Unit] =
import quotes.reflect.*
@ -200,11 +217,13 @@ object GenerativeFluentAssertionsDSL:
*/
val assertion = '{
val subject = $subjectExpr
val negated = $negatedExpr
val condition = $propertyValueExpr
lazy val value = ${ Select(('subject).asTerm, candidateMethod).asExprOf[R] }
assert(
condition.test(value),
s"${subject}.${$propertyNameExpr} " + condition.failedMessage + " but " + value
condition.test(value) == !negated,
s"${subject}.${$propertyNameExpr} " + condition.failedMessage(negated) + " but " + value
)
}
report.info(assertion.show, subjectExpr.asTerm.pos)

View file

@ -3,6 +3,7 @@ package ch.usi.si.msde.edsl.lecture10
import scala.language.dynamics
sealed trait AssertionProperty
case class NegatedAssertionProperty(prop: AssertionProperty) extends AssertionProperty
/** Represents (but with no implementation) the result of a call to
* be.selectDynamic(String), used to specify the property. The semantics of
@ -19,35 +20,24 @@ end DynamicShouldBeProperty
*/
sealed trait SatisfyingNotEquals[T]
case class Condition[T](predicate: (T, T) => Boolean, op: String, expected: T):
def test(value: T): Boolean = predicate(value, expected)
def failedMessage: String = s"is not ${op} ${expected}"
case class Condition[-T](predicate: T => Boolean, op: String, expected: Any):
def test(value: T): Boolean = predicate(value)
def failedMessage(negated: Boolean): String = s"${if (negated) "is" else "is not"} ${op} ${expected}"
case object satisfying:
def `===`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.equiv, "===", toWhat)
def notEquals[T](toWhat: T, ord: Ordering[T]): Condition[T] = Condition(!ord.equiv(_, _), "!==", 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.gt, ">", 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.gteq, ">=", 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), "!==", 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), ">", 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), ">=", toWhat)
class BePropertyTypeProvider[T](val subject: T) extends Selectable:
def selectDynamic(fieldName: String): AssertionProperty = ???
def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ???
end BePropertyTypeProvider
/** The class that implements the syntactic aspect of the type provider. It
* 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:
class HavePropertyTypeProvider[T](val subject: T, val negated: Boolean) extends Selectable:
def applyDynamic[U](fieldName: String)(arg: satisfying.type): SatisfyingNotEquals[U] = ???
def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ???
end HavePropertyTypeProvider
@ -58,7 +48,11 @@ sealed trait AssertionDSLVerb
case object be extends AssertionDSLVerb with Dynamic:
def selectDynamic(fieldName: String): AssertionProperty = ???
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.
*
@ -90,11 +84,11 @@ extension [T](subject: T)
*
* The method semantics is then provided by the assertions macro.
*
* @param verbWord
* @param verb
* The have word.
* @return
* a type provider for properties of type T.
*/
transparent inline def should(inline verbWord: have.type) = ${
ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject)
transparent inline def should(inline verb: HaveVerb) = ${
ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject, '{verb.negated})
}

View file

@ -26,7 +26,7 @@ case class Box(label: String, weight: Mass, price: Money)
assertions:
/* be.property assertions */
// assert(person.adult, ...)
person should be.adult
person should !be.adult
// assert(List().isEmpty, ...)
List() should be.empty
// 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(3) should have size satisfying !== 0
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 */
// // 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
* 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
): Expr[Any] =
import quotes.reflect.*
@ -86,10 +86,6 @@ object ShouldHaveTypeProvider:
// exact type is unknown until compilation time.
refinedType.asType match
case '[tpe] => // tpe is the exact refined type
val res = '{
val p = HavePropertyTypeProvider[T]($subject)
p.asInstanceOf[tpe]
}
res
'{ HavePropertyTypeProvider[T]($subject, $negated).asInstanceOf[tpe] }
end ShouldHaveTypeProvider