From decd52836f0a4869a5505e506b293771453d7648 Mon Sep 17 00:00:00 2001 From: Claudio Maggioni Date: Sun, 22 Dec 2024 22:19:25 +0100 Subject: [PATCH] works --- .../GenerativeFluentAssertionsDSL.scala | 106 ++++++++++++++---- .../GenerativeFluentAssertionsDSLSyntax.scala | 45 +++++--- .../ch/usi/si/msde/edsl/lecture10/Main.scala | 8 +- .../lecture10/ShouldContainTypeProvider.scala | 6 +- .../lecture10/ShouldHaveTypeProvider.scala | 6 +- 5 files changed, 120 insertions(+), 51 deletions(-) diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala index c14412d..d4879be 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala @@ -38,7 +38,14 @@ object GenerativeFluentAssertionsDSL: import quotes.reflect.* 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) .$asInstanceOf$[SatisfyingNotEquals[T]] } => '{ $typeProvider @@ -120,19 +127,31 @@ object GenerativeFluentAssertionsDSL: // The quoted pattern also extracts: the type of the assertion subject, // and the type of the property (actually... not exactly that. Why?) case '{ - (${ typeProvider }: HavePropertyTypeProvider[subjectType, elemType]) + (${ typeProvider }: HavePropertyTypeProvider[subjectType]) .applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType]) .$asInstanceOf$[AssertionProperty] } => - '{} -// val subjectExpr = '{ $typeProvider.subject } -// val negatedExpr = '{ $typeProvider.negated } -// generateShouldHaveAssertion[subjectType, valueType]( -// subjectExpr, -// propertyNameExpr, -// valueExpr, -// negatedExpr -// ) + val subjectExpr = '{ $typeProvider.subject } + val checkerExpr = '{ $typeProvider.checker } + generateShouldHaveAssertion[subjectType, valueType]( + subjectExpr, + checkerExpr, + propertyNameExpr, + valueExpr + ) + 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 '{ (${ typeProvider }: BePropertyTypeProvider[subjectType]) .applyDynamic($propertyNameExpr)($unit) @@ -192,17 +211,18 @@ object GenerativeFluentAssertionsDSL: end if end generateShouldBeAssertion - private def generateShouldHaveAssertion[T, R]( - subjectExpr: Expr[T], - propertyNameExpr: Expr[String], - propertyValueExpr: Expr[Condition[R]], - negatedExpr: Expr[Boolean] - )(using Type[T], Type[R])(using Quotes): Expr[Unit] = + private def generateShouldHaveAssertion[E, R]( + subjectExpr: Expr[E], + checkerExpr: Expr[SinglePropertyChecker], + propertyNameExpr: Expr[String], + propertyValueExpr: Expr[Condition[R]] + )(using Type[E], Type[R])(using Quotes): Expr[Unit] = import quotes.reflect.* - val subjectTypeRepr = TypeRepr.of[T] + val subjectTypeRepr = TypeRepr.of[E] + val optionalCandidateSymbol = - findCandidateSymbol[T, R](propertyNameExpr.valueOrAbort) + findCandidateSymbol[E, R](propertyNameExpr.valueOrAbort) if optionalCandidateSymbol.isEmpty then report.errorAndAbort( @@ -212,22 +232,62 @@ object GenerativeFluentAssertionsDSL: else val candidateMethod = optionalCandidateSymbol.get - val assertion = '{ + val assertion: Expr[Unit] = '{ val subject = $subjectExpr - val negated = $negatedExpr + val checker = $checkerExpr val condition = $propertyValueExpr lazy val value = ${ Select(('subject).asTerm, candidateMethod).asExprOf[R] } assert( - condition.test(value) == !negated, - s"$subject.${$propertyNameExpr} " + condition.failedMessage(negated) + " but " + value + checker.check(value, condition), + s"$subject.${$propertyNameExpr} ${checker.failedMessage} ${condition.failedMessage} but ${value}" ) } + report.info(assertion.show, subjectExpr.asTerm.pos) assertion end if 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. * * Improved version w.r.t. the one presented during the lecture. diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala index 583bf02..d3b492c 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala @@ -23,7 +23,7 @@ sealed trait SatisfyingNotEquals[T] 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}" + def failedMessage: String = s"${op} ${expected}" case object satisfying: 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 = ??? end BePropertyTypeProvider -sealed trait PropertyChecker[T, E]: +sealed trait PropertyChecker: val failedMessage: String - def check(t: T, condition: Condition[E]): Boolean -case class SingleChecker[T]() extends PropertyChecker[T, T]: - override def check(t: T, condition: Condition[T]): Boolean = condition.test(t) +sealed trait SinglePropertyChecker extends PropertyChecker: + 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" -case class NegatedChecker[T]() extends PropertyChecker[T, T]: - override def check(t: T, condition: Condition[T]): Boolean = !condition.test(t) +case object NegatedChecker extends SinglePropertyChecker: + override def check[T](t: T, condition: Condition[T]): Boolean = !condition.test(t) override val failedMessage: String = "is" -case class AllChecker[E]() extends PropertyChecker[Iterable[E], E]: - override def check(t: Iterable[E], condition: Condition[E]): Boolean = t.forall(e => condition.test(e)) +sealed trait IterablePropertyChecker extends PropertyChecker: + 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" -case class SomeChecker[E]() extends PropertyChecker[Iterable[E], E]: - override def check(t: Iterable[E], condition: Condition[E]): Boolean = t.exists(e => condition.test(e)) +case object SomeChecker extends IterablePropertyChecker: + 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" -case class NoChecker[E]() extends PropertyChecker[Iterable[E], E]: - override def check(t: Iterable[E], condition: Condition[E]): Boolean = t.forall(e => !condition.test(e)) +case object NoChecker 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 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(fieldName: String)(arg: Any): AssertionProperty = ??? 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). */ sealed trait AssertionDSLVerb @@ -77,7 +87,6 @@ case object be_ extends AssertionDSLVerb case object have extends AssertionDSLVerb: inline def `unary_!`: have.type = ??? - case object contain extends AssertionDSLVerb case object `with` extends AssertionDSLVerb @@ -85,15 +94,15 @@ case class ContainsVerb[T](subject: Iterable[T]) extension [T](inline containsVerb: ContainsVerb[T]) 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) = ${ - ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ SomeChecker[T]() }) + ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ SomeChecker }) } 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. diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala index 6b35fb2..44bd9dc 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala @@ -27,7 +27,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) @@ -46,7 +46,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 // 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 @@ -58,8 +58,8 @@ case class Box(label: String, weight: Mass, price: Money) new StringOps(""), new StringOps("alice") ) should contain someElements `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(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 /* should have assertions */ // // assert(List(1).head == 1, ...) diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala index c2d77df..cdf1d00 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala @@ -7,7 +7,7 @@ object ShouldContainTypeProvider: /** The type provider for the should have assertion, which generates a refined * 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] = import quotes.reflect.* @@ -75,7 +75,7 @@ object ShouldContainTypeProvider: ) val refinedType = refineTypeBySymbols( - TypeRepr.of[HavePropertyTypeProvider[Iterable[T], T]], + TypeRepr.of[ContainsPropertyTypeProvider[T]], fields ++ arityZeroMethods ) @@ -87,6 +87,6 @@ object ShouldContainTypeProvider: // exact type is unknown until compilation time. refinedType.asType match case '[tpe] => // tpe is the exact refined type - '{ HavePropertyTypeProvider[Iterable[T], T]($subject, $checker).asInstanceOf[tpe] } + '{ ContainsPropertyTypeProvider[T]($subject, $checker).asInstanceOf[tpe] } end ShouldContainTypeProvider diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala index f068c0f..842b7fb 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala @@ -82,11 +82,11 @@ object ShouldHaveTypeProvider: ) val refinedType = refineTypeBySymbols( - TypeRepr.of[HavePropertyTypeProvider[T, T]], + TypeRepr.of[HavePropertyTypeProvider[T]], 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 // type that can be used in some quoted code. @@ -95,6 +95,6 @@ object ShouldHaveTypeProvider: // exact type is unknown until compilation time. refinedType.asType match case '[tpe] => // tpe is the exact refined type - '{ HavePropertyTypeProvider[T, T]($subject, $checker).asInstanceOf[tpe] } + '{ HavePropertyTypeProvider[T]($subject, $checker).asInstanceOf[tpe] } end ShouldHaveTypeProvider