【问题标题】:Testing an assertion that something must not compile测试某个东西不能编译的断言
【发布时间】:2013-02-14 01:10:39
【问题描述】:

问题

当我使用支持类型级编程的库时,我经常发现自己在编写类似以下的 cmets(来自 Paul Snively at Strange Loop 2012 提供的 an example):

// 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 存储库中的 an example

/**
 * 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 这样的库的上下文中很有用,几天前我写了以下内容作为对this question 的回答的第一次快速尝试:

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]

我惊讶地发现 HList 的类型级别 unique 的这种实现不起作用,因为在后一种情况下,Shapeless 很乐意找到 FilterAux 实例。换句话说,即使您可能不希望它编译,以下内容也会编译:

implicitly[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]

在这种情况下,我看到的是a bug——或者至少是一些错误的东西——它是has since been fixed

更一般地说,我们可以想象想要检查隐含在我对FilterAux应该如何与单元测试之类的东西一起工作的期望中的那种不变量——尽管听起来很奇怪谈论像这样测试类型级代码,以及最近关于类型 vs. 测试的相对优点的所有辩论。

我的问题

问题是我不知道任何类型的测试框架(适用于任何平台)允许程序员断言某些东西不能编译

对于FilterAux 案例,我可以想象的一种方法是使用旧的implicit-argument-with-null-default trick

def assertNoInstanceOf[T](implicit instance: T = null) = assert(instance == null)

这会让您在单元测试中编写以下内容:

assertNoInstanceOf[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]

不过,以下内容会更加方便和富有表现力:

assertDoesntCompile(('a' :: 'b' :: HNil).unique[Char])

我想要这个。我的问题是,是否有人知道有任何测试库或框架支持远程类似的东西——对于 Scala 来说是理想的,但我会满足于任何东西。

【问题讨论】:

  • 我假设您不想将您的上下文提供给解释器的实例并以这种方式进行测试?
  • @RexKerr:不是手动的,不是。我很乐意编写一个采用这种方法的框架,我认为这不会太难,但我更愿意发现其他人已经为我编写了它。
  • 我听到一位“Scala Types”(播客)人员谈论他正在做的工作,以验证无效的源代码不会无意中被编译器接受。我想是尤维·马索里。如果您擅长搜索,它可能会出现在演出说明中...

标签: scala testing types shapeless type-level-computation


【解决方案1】:

不是一个框架,但 Jorge Ortiz (@JorgeO) 提到了他在 2012 年在 NEScala 为 Foursquare 的 Rogue 库的测试添加的一些实用程序,这些工具支持非编译测试:您可以找到示例 here。很长一段时间以来,我一直想将这样的东西添加到 shapeless 中。

最近,Roland Kuhn (@rolandkuhn) 在tests for Akka typed channels 中添加了类似的机制,这次使用 Scala 2.10 的运行时编译。

当然,这些都是动态测试:如果不应该编译的东西在(测试)运行时出现,它们就会失败。无类型宏可能提供静态选项:即。宏可以接受无类型树,对其进行类型检查,如果成功则抛出类型错误)。这可能是在无形的宏观天堂分支上尝试的东西。但显然不是 2.10.0 或更早版本的解决方案。

更新

由于回答了这个问题,另一种方法,感谢 Stefan Zeiger (@StefanZeiger), has surfaced。这个很有趣,因为就像上面提到的无类型宏一样,它是编译时而不是(测试)运行时检查,但是它也与 Scala 2.10.x 兼容。因此,我认为它比 Roland 的方法更可取。

我现在为 2.9.x using Jorge's approach2.10.x using Stefan's approachmacro paradise using the untyped macro approach 添加了 shapeless 实现。对应测试的例​​子可以在here for 2.9.xhere for 2.10.xhere for macro paradise找到。

无类型的宏测试是最干净的,但 Stefan 的 2.10.x 兼容方法紧随其后。

【讨论】:

  • 你最后一段中的建议works。如果你不想通过这种方式实现一堆测试来针对topic/macro-paradise 提出拉取请求,你最好现在阻止我。 :)
  • @MilesSabin:这些内容中的任何一个都可以在某个地方的官方版本中获得吗?我在 shapeless_2.10:1.2.4 中没有看到 shapeless.test 包。
  • 睁大眼睛,期待在不久的将来发布一个无形的 2.0.0 里程碑版本。
【解决方案2】:

ScalaTest 2.1.0 的 Assertions 语法如下:

assertTypeError("val s: String = 1")

对于Matchers

"val s: String = 1" shouldNot compile

【讨论】:

  • Here 是一个例子,当shouldshouldNot 在相同的情况下运行而没有失败。我做错了什么还是可能是一个错误?
【解决方案3】:

你知道 Scala 项目中的partest 吗?例如。 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

它不适合您的问题(因为您没有根据断言指定测试用例),但可能是一种方法和/或给您一个良好的开端。

【讨论】:

  • +1 非常感谢——我以前从未关注过partest。不过,我认为 Miles 的答案中的方法可能更清洁。
【解决方案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】:

    µTest 中的 compileError 宏就是这样做的:

    compileError("true * false")
    // CompileError.Type("value * is not a member of Boolean")
    
    compileError("(}")
    // CompileError.Parse("')' expected but '}' found.")
    

    【讨论】:

      猜你喜欢
      • 2014-06-26
      • 2016-09-27
      • 2018-05-23
      • 1970-01-01
      • 2023-01-03
      • 1970-01-01
      • 1970-01-01
      • 2019-02-23
      • 1970-01-01
      相关资源
      最近更新 更多