Подтвердить что ты не робот

Тестирование утверждения о том, что что-то не должно компилироваться

Проблема

Когда я работаю с библиотеками, поддерживающими программирование на уровне, я часто нахожу, что пишу комментарии, подобные следующим (из пример представленный 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, но я соглашусь на что-нибудь.

4b9b3361

Ответ 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.")