49

問題

型レベルのプログラミングをサポートするライブラリを使用しているとき、次のようなコメントを書いている自分に気付くことがよくあります ( Strange Loop 2012 で Paul Snivelyによって提示された例から):

// 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この型レベルの forの実装がHListうまくいかないことに驚いたのFilterAuxは、後者の場合、Shapeless は喜んでインスタンスを見つけるからです。言い換えれば、おそらくコンパイルされないと予想される場合でも、次のコードはコンパイルされます。

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

この場合、私が見たのはバグ、または少なくともバグのようなものであり、その後修正されました.

より一般的には、単体テストのようなものでどのように機能するFilterAux についての私の期待に暗示されている種類の不変条件をチェックしたいことを想像できます。型テストの相対的なメリットに関する最近の議論。

私の質問

問題は、プログラマーが何かをコンパイルしてはならないことを主張できるようにするテスト フレームワーク (プラットフォームを問わない) を私が知らないことです。

この場合に想像できる 1 つのアプローチはFilterAux、古いImplicit-argument-with-null-default トリックを使用することです。

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

これにより、単体テストで次のように記述できます。

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

ただし、次の例は、はるかに便利で表現力豊かです。

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

これ欲しい。私の質問は、このようなものをリモートでサポートするテスト ライブラリまたはフレームワークを誰かが知っているかどうかです。

4

5 に答える 5

26

フレームワークではありませんが、Jorge Ortiz ( @JorgeO ) は、2012 年に NEScala で Foursquare の Rogue ライブラリのテストに追加した、非コンパイルのテストをサポートするいくつかのユーティリティについて言及しました:ここで例を見つけることができます。私はかなり長い間、シェイプレスにこのようなものを追加するつもりでした.

最近では、Roland Kuhn ( @rolandkuhn ) が、今回は Scala 2.10 のランタイム コンパイルを使用して、Akka 型付きチャネルのテストに同様のメカニズムを追加しました。

もちろん、これらはどちらも動的テストです。コンパイルすべきではないものが実行されると、(テスト) 実行時に失敗します。型指定されていないマクロは、静的オプションを提供する場合があります。マクロは型指定されていないツリーを受け入れ、それを型チェックし、成功した場合は型エラーをスローする可能性があります)。これは、形のないマクロパラダイスブランチで実験するものかもしれません. しかし、明らかに 2.10.0 以前のソリューションではありません。

アップデート

質問に答えて以来、Stefan Zeiger ( @StefanZeiger )による別のアプローチが浮上しています。これが興味深いのは、上で触れた型指定のないマクロのように、(テスト) ランタイム チェックではなくコンパイル時であるためです。ただし、Scala 2.10.x とも互換性があります。そのため、ローランドのアプローチよりも好ましいと思います。

Jorge のアプローチを使用して 2.9.x 用に、Stefan のアプローチを使用して2.10.x 用に、型指定されていないマクロ アプローチを使用してマクロ パラダイス用に shapeless に実装を追加しました。対応するテストの例は、2.9.x の場合はこちら、2.10.x の場合はこちらmacro paradise の場合はこちらです。

型指定されていないマクロ テストは最もクリーンですが、Stefan の 2.10.x 互換アプローチは僅差で 2 番目です。

于 2013-02-28T10:19:45.553 に答える
9

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

それはあなたの質問に完全に適合するわけではありませんが (アサーションに関してテスト ケースを指定していないため)、アプローチであったり、有利なスタートを切ることができるかもしれません。

于 2013-02-28T10:52:03.353 に答える
6

提供されたリンクに基づいて、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]") 
}
于 2013-03-10T14:46:43.293 に答える