works
This commit is contained in:
parent
e89a95f88e
commit
decd52836f
5 changed files with 120 additions and 51 deletions
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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, ...)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue