scala - Probando una afirmación de que algo no debe compilarse
testing types (5)
¿Sabes acerca de la partest en el proyecto Scala? Eg CompilerTest tiene el siguiente documento:
/** For testing compiler internals directly.
* Each source code string in "sources" will be compiled, and
* the check function will be called with the source code and the
* resulting CompilationUnit. The check implementation should
* test for what it wants to test and fail (via assert or other
* exception) if it is not happy.
*/
Es capaz de verificar, por ejemplo, si esta fuente https://github.com/scala/scala/blob/master/test/files/neg/divergent-implicit.scala tendrá este resultado https://github.com/scala/scala/blob/master/test/files/neg/divergent-implicit.check
No se ajusta perfectamente a su pregunta (ya que no especifica sus casos de prueba en términos de aseveraciones), pero puede ser un enfoque y / o darle una ventaja.
El problema
Cuando trabajo con bibliotecas que admiten programación a nivel de tipo, a menudo me encuentro escribiendo comentarios como los siguientes (de un ejemplo presentado por Paul Snively en Strange Loop 2012 ):
// But these invalid sequences don''t compile:
// isValid(_3 :: _1 :: _5 :: _8 :: _8 :: _2 :: _8 :: _6 :: _5 :: HNil)
// isValid(_3 :: _4 :: _5 :: _8 :: _8 :: _2 :: _8 :: _6 :: HNil)
O esto, a partir de un ejemplo en el repositorio Shapeless :
/**
* If we wanted to confirm that the list uniquely contains `Foo` or any
* subtype of `Foo`, we could first use `unifySubtypes` to upcast any
* subtypes of `Foo` in the list to `Foo`.
*
* The following would not compile, for example:
*/
//stuff.unifySubtypes[Foo].unique[Foo]
Esta es una forma muy aproximada de indicar algún hecho sobre el comportamiento de estos métodos, y podríamos imaginarnos querer hacer que estas afirmaciones sean más formales: para pruebas unitarias o de regresión, etc.
Para dar un ejemplo concreto de por qué esto podría ser útil en el contexto de una biblioteca como Shapeless, hace unos días escribí lo siguiente como un primer intento rápido de respuesta a esta pregunta :
import shapeless._
implicit class Uniqueable[L <: HList](l: L) {
def unique[A](implicit ev: FilterAux[L, A, A :: HNil]) = ev(l).head
}
Donde la intención es que esto compilará:
(''a'' :: ''b :: HNil).unique[Char]
Si bien esto no:
(''a'' :: ''b'' :: HNil).unique[Char]
Me sorprendió descubrir que esta implementación de un tipo de nivel unique
para HList
no funcionó, porque Shapeless estaría feliz de encontrar una instancia de FilterAux
en este último caso. En otras palabras, se compilaría lo siguiente, aunque es probable que no lo haga:
implicitly[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]
En este caso, lo que estaba viendo era una falla, o al menos algo erróneo, y desde entonces se solucionó .
De manera más general, podemos imaginarnos deseando comprobar el tipo de invariante que estaba implícito en mis expectativas sobre cómo debería funcionar FilterAux
con algo así como una prueba de unidad, por extraño que parezca hablar de probar un código de tipo de este tipo, con todos los debates recientes sobre el mérito relativo de los tipos frente a las pruebas.
Mi pregunta
El problema es que no conozco ningún tipo de marco de prueba (para cualquier plataforma) que permita al programador afirmar que algo no debe compilarse .
Un enfoque que puedo imaginar para el caso FilterAux
sería usar el antiguo truco de argumento implícito con nulo predeterminado :
def assertNoInstanceOf[T](implicit instance: T = null) = assert(instance == null)
Lo cual te permitiría escribir lo siguiente en tu prueba de unidad:
assertNoInstanceOf[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]
Lo siguiente sería muchísimo más conveniente y expresivo:
assertDoesntCompile((''a'' :: ''b'' :: HNil).unique[Char])
Quiero esto. Mi pregunta es si alguien sabe de alguna biblioteca de pruebas o framework que soporte algo remotamente parecido, idealmente para Scala, pero me conformaré con cualquier cosa.
Basado en los enlaces provistos por Miles Sabin
, pude usar la versión de Akka
import scala.tools.reflect.ToolBox
object TestUtils {
def eval(code: String, compileOptions: String = "-cp target/classes"): Any = {
val tb = mkToolbox(compileOptions)
tb.eval(tb.parse(code))
}
def mkToolbox(compileOptions: String = ""): ToolBox[_ <: scala.reflect.api.Universe] = {
val m = scala.reflect.runtime.currentMirror
m.mkToolBox(options = compileOptions)
}
}
Luego en mis pruebas lo usé así
def result = TestUtils.eval(
"""|import ee.ui.events.Event
|import ee.ui.events.ReadOnlyEvent
|
|val myObj = new {
| private val writableEvent = Event[Int]
| val event:ReadOnlyEvent[Int] = writableEvent
|}
|
|// will not compile:
|myObj.event.fire
|""".stripMargin)
result must throwA[ToolBoxError].like {
case e =>
e.getMessage must contain("value fire is not a member of ee.ui.events.ReadOnlyEvent[Int]")
}
La macro compileError
en μTest hace precisamente eso:
compileError("true * false")
// CompileError.Type("value * is not a member of Boolean")
compileError("(}")
// CompileError.Parse("'')'' expected but ''}'' found.")
No es un marco, pero Jorge Ortiz ( @JorgeO ) mencionó algunas utilidades que agregó a las pruebas para la biblioteca Rogue de Foursquare en NEScala en 2012, que respaldan las pruebas de no compilación: puede encontrar ejemplos here . He tenido la intención de agregar algo así a algo sin forma durante bastante tiempo.
Más recientemente, Roland Kuhn ( @rolandkuhn ) ha agregado un mecanismo similar, esta vez usando la compilación de tiempo de ejecución de Scala 2.10, a las pruebas para los canales escritos en Akka .
Estas son pruebas dinámicas, por supuesto: fallan en el tiempo de ejecución (prueba) si algo que no debería compilar lo hace. Las macros sin cargo pueden proporcionar una opción estática: es decir. una macro podría aceptar un árbol sin tipo, escriba check it y lanzar un tipo de error si tiene éxito). Esto podría ser algo con lo que experimentar en la rama macro-paradisíaca de lo informe. Pero no es una solución para 2.10.0 o anterior, obviamente.
Actualizar
Desde que respondieron la pregunta, surgió otro enfoque, debido a Stefan Zeiger ( @StefanZeiger ). Esto es interesante porque, al igual que la macro sin tipo que se mencionó anteriormente, es una verificación en tiempo de compilación en lugar de (prueba), sin embargo, también es compatible con Scala 2.10.x. Como tal, creo que es preferible al enfoque de Roland.
Ahora he agregado implementaciones a shapeless para 2.9.x usando el enfoque de Jorge , para 2.10.x usando el enfoque de Stefan y para macro paraíso usando el enfoque macro sin tipo . Ejemplos de las pruebas correspondientes se pueden encontrar aquí para 2.9.x , aquí para 2.10.xy aquí para macro paraíso .
Las pruebas macro sin tipo son las más limpias, pero el enfoque compatible con Stefan 2.10.x es el segundo más cercano.
ScalaTest 2.1.0 tiene la siguiente sintaxis para Assertions :
assertTypeError("val s: String = 1")
Y para Matchers :
"val s: String = 1" shouldNot compile