Тестирование утверждения о том, что что-то не должно компилироваться
Проблема
Когда я работаю с библиотеками, поддерживающими программирование на уровне, я часто нахожу, что пишу комментарии, подобные следующим (из пример представленный Paul Snively at 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)
Или это, из пример в 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]
Это очень грубый способ указать некоторый факт о поведении этих методов, и мы могли бы предположить, что хотим сделать эти утверждения более формальными - для тестирования единиц или регрессий и т.д.
Чтобы дать конкретный пример того, почему это может быть полезно в контексте библиотеки типа Shapeless, несколько дней назад я написал следующее как быструю первую попытку ответа на этот вопрос:
import shapeless._
implicit class Uniqueable[L <: HList](l: L) {
def unique[A](implicit ev: FilterAux[L, A, A :: HNil]) = ev(l).head
}
Если намерение заключается в том, что это будет скомпилировано:
('a' :: 'b :: HNil).unique[Char]
Пока это не будет:
('a' :: 'b' :: HNil).unique[Char]
Я с удивлением обнаружил, что эта реализация типа unique
для HList
не работает, потому что Shapeless с радостью найдет экземпляр FilterAux
в последнем случае. Другими словами, следующее компиляция, хотя вы, вероятно, ожидаете, что это не будет:
implicitly[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]
В этом случае то, что я видел, было ошибка - или, по крайней мере, что-то ошибка - и это с тех пор было исправлено.
В более общем плане, мы можем предположить, что хотите проверить тип инварианта, который был скрыт в моих ожиданиях относительно того, как FilterAux
должен работать с чем-то вроде unit test - как ни странно, поскольку может показаться, что речь идет о типе тестирования, как и этот, со всеми недавними дебатами об относительном достоинстве типов против тестов.
Мой вопрос
Проблема в том, что я не знаю какой-либо структуры тестирования (для любой платформы), которая позволяет программисту утверждать, что что-то не должно компилироваться.
Один подход, который я могу себе представить для случая FilterAux
, - использовать старый трюк с неявным аргументом с нулевым значением:
def assertNoInstanceOf[T](implicit instance: T = null) = assert(instance == null)
Что бы вы могли написать следующее в unit test:
assertNoInstanceOf[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]
Следующее было бы более удобным и выразительным:
assertDoesntCompile(('a' :: 'b' :: HNil).unique[Char])
Я хочу это. Мой вопрос: знает ли кто-нибудь о какой-либо тестовой библиотеке или фреймворке, которая поддерживает что-либо удаленно, как это - идеально подходит для Scala, но я соглашусь на что-нибудь.
Ответы
Ответ 1
Не рамки, но Хорхе Ортис (@JorgeO) упомянул некоторые утилиты, которые он добавил к испытаниям библиотеки Foursquare Rogue в NEScala в 2012 году, которая тесты поддержки для некомпиляции: здесь можно найти примеры здесь. Я уже давно хотел добавить что-то вроде бесформенного.
Совсем недавно Roland Kuhn (@rolandkuhn) добавил аналогичный механизм, на этот раз используя Scala 2.10 компиляцию во время выполнения, до тесты для типизированных каналов Akka.
Это, конечно, и динамические тесты: они не работают (test), если что-то, что не следует компилировать. Нетипированные макросы могут предоставлять статический параметр: т.е. макрос может принять нетипизированное дерево, тип проверить его и выбросить ошибку типа, если он преуспеет). Это может быть чем-то, что можно было бы экспериментировать с маневра-расовой ветвью бесформенного. Но не решение для 2.10.0 или ранее, очевидно.
Обновление
С учетом ответа на другой вопрос, из-за Стефана Зейгера (@StefanZeiger), всплыл. Это интересно, потому что, как и нетипизированный макрос, указанный выше, это время компиляции, а не проверка (проверка), но оно также совместимо с Scala 2.10.x. Поэтому я считаю предпочтительным подход Роланда.
Теперь я добавил варианты без формальности для 2.9.x с использованием подхода Хорхе, для 2.10.x с использованием подхода Stefan и для макро-рай с использованием нетипизированного макроса. Примеры соответствующих тестов можно найти здесь для 2.9.x, здесь для 2.10.x и здесь для макро-рая.
Неизученные макропроцессоры являются самыми чистыми, но совместимость с Stefan 2.10.x является близкой секундой.
Ответ 2
ScalaTest 2.1.0 имеет следующий синтаксис для Assertions:
assertTypeError("val s: String = 1")
И для Matchers:
"val s: String = 1" shouldNot compile
Ответ 3
Знаете ли вы о partest в проекте Scala? Например. CompilerTest имеет следующий документ:
/** 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.
*/
Он может проверить, может ли этот источник https://github.com/scala/scala/blob/master/test/files/neg/divergent-implicit.scala получить https://github.com/scala/scala/blob/master/test/files/neg/divergent-implicit.check
Это не подходит для вашего вопроса (так как вы не указываете свои тестовые примеры с точки зрения утверждений), но может быть подходом и/или дать вам начало.
Ответ 4
На основе ссылок, предоставленных Miles Sabin
, я смог использовать версию 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)
}
}
Тогда в моих тестах я использовал его так:
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]")
}
Ответ 5
Макрос compileError
в μTest делает только это:
compileError("true * false")
// CompileError.Type("value * is not a member of Boolean")
compileError("(}")
// CompileError.Parse("')' expected but '}' found.")