diff --git a/build.sbt b/build.sbt index 7fde86331..2703d09e5 100644 --- a/build.sbt +++ b/build.sbt @@ -62,11 +62,10 @@ lazy val sharedSettings = mimaDefaultSettings ++ Seq( lazy val js = project.in(file("js")) .settings(sharedSettings: _*) - .settings(scalaJSSettings: _*) .settings( - libraryDependencies += "org.scala-lang.modules.scalajs" %% "scalajs-test-bridge" % scalaJSVersion, - ScalaJSKeys.scalaJSTestFramework in Test := "org.scalacheck.ScalaCheckFramework" + libraryDependencies += "org.scala-js" %% "scalajs-test-interface" % scalaJSVersion ) + .enablePlugins(ScalaJSPlugin) lazy val jvm = project.in(file("jvm")) .settings(sharedSettings: _*) diff --git a/js/src/main/scala/org/scalacheck/ScalaCheckFramework.scala b/js/src/main/scala/org/scalacheck/ScalaCheckFramework.scala index 6c6e2f502..c3d26720e 100644 --- a/js/src/main/scala/org/scalacheck/ScalaCheckFramework.scala +++ b/js/src/main/scala/org/scalacheck/ScalaCheckFramework.scala @@ -9,43 +9,174 @@ package org.scalacheck -object ScalaCheckFramework extends scalajs.test.TestFramework { +import sbt.testing._ +import org.scalajs.testinterface.TestUtils +import util.Pretty - import scalajs.test.{Test => TestJS} - import scalajs.test.TestOutput - import scalajs.js.{Array, Function0} +private abstract class ScalaCheckRunner( + val args: Array[String], + val remoteArgs: Array[String], + val loader: ClassLoader +) extends Runner { - // TODO Complete the parser, and move it to the shared src, replacing - // CmdLineParser (to avoid using parser combinators entirely) - private def parseParams(args: Array[String]): Option[Test.Parameters] = { - var params = Test.Parameters.default.withMinSuccessfulTests(10) - Some(params) + import java.util.concurrent.atomic.AtomicInteger + + def deserializeTask(task: String, deserializer: String => TaskDef) = + ScalaCheckTask(deserializer(task), this) + + def serializeTask(task: Task, serializer: TaskDef => String) = + serializer(task.taskDef) + + def tasks(taskDefs: Array[TaskDef]): Array[Task] = + taskDefs.map(ScalaCheckTask(_, this)) + + val successCount = new AtomicInteger(0) + + val failureCount = new AtomicInteger(0) + + val errorCount = new AtomicInteger(0) + + val testCount = new AtomicInteger(0) + + def testDone(status: Status) = { + status match { + case Status.Success => successCount.incrementAndGet() + case Status.Error => errorCount.incrementAndGet() + case Status.Skipped => errorCount.incrementAndGet() + case Status.Failure => failureCount.incrementAndGet() + case _ => failureCount.incrementAndGet() + } + testCount.incrementAndGet() } +} - def runTest(out: TestOutput, args: Array[String])(tf: Function0[TestJS]) = { +private case class ScalaCheckTask( + taskDef: TaskDef, + runner: ScalaCheckRunner +) extends Task { + + val tags: Array[String] = Array() + + def execute( + eventHandler: EventHandler, + loggers: Array[Logger], + continuation: Array[Task] => Unit + ): Unit = continuation(execute(eventHandler, loggers)) + + def execute( + eventHandler: EventHandler, + loggers: Array[Logger] + ): Array[Task] = { + + def asEvent(n: String, r: Test.Result) = new Event { + val status = r.status match { + case Test.Passed => Status.Success + case _:Test.Proved => Status.Success + case _:Test.Failed => Status.Failure + case Test.Exhausted => Status.Skipped + case _:Test.PropException => Status.Error + } + val throwable = r.status match { + case Test.PropException(_, e, _) => new OptionalThrowable(e) + case _:Test.Failed => new OptionalThrowable( + new Exception(Pretty.pretty(r,Pretty.Params(0))) + ) + case _ => new OptionalThrowable() + } + val fullyQualifiedName = n + val selector = new TestSelector(n) + val fingerprint = taskDef.fingerprint + val duration = -1L + } val testCallback = new Test.TestCallback { override def onPropEval(n: String, w: Int, s: Int, d: Int) = {} - override def onTestResult(n: String, r: Test.Result) = { - import util.Pretty._ + import Pretty.{pretty, Params} val verbosityOpts = Set("-verbosity", "-v") - val verbosity = args.grouped(2).filter(twos => verbosityOpts(twos.head)).toSeq.headOption.map(_.last).map(_.toInt).getOrElse(0) - val msg = s"$n: ${pretty(r, Params(verbosity))}" - if(r.passed) out.succeeded(s"+ $msg") - else out.failure(s"! $msg") + val verbosity = runner.args + .grouped(2).filter(twos => verbosityOpts(twos.head)) + .toSeq.headOption.map(_.last).map(_.toInt).getOrElse(0) + val logMsg = + (if (r.passed) "+ " else "! ") + s"$n: ${pretty(r, Params(verbosity))}" + val ev = asEvent(n, r) + eventHandler.handle(ev) + loggers.foreach(l => l.info(logMsg)) + runner.testDone(ev.status) } } - parseParams(args) match { - case None => out.error("Unable to parse ScalaCheck arguments: $args") - case Some(params) => - val prms = params.withTestCallback(testCallback) - val obj = tf() - if(obj.isInstanceOf[Properties]) - Test.checkProperties(prms, obj.asInstanceOf[Properties]) - else if(obj.isInstanceOf[Prop]) - Test.check(prms, obj.asInstanceOf[Prop]) + // TODO Hard-coded params! We should parse runner.args instead + val prms = Test.Parameters.default + .withMinSuccessfulTests(10) + .withTestCallback(testCallback) + .withCustomClassLoader(Some(runner.loader)) + + import taskDef.{fingerprint, fullyQualifiedName} + + val fp = fingerprint.asInstanceOf[SubclassFingerprint] + val obj = + if (fp.isModule) TestUtils.loadModule(fullyQualifiedName, runner.loader) + else TestUtils.newInstance(fullyQualifiedName, runner.loader)(Seq()) + + if(obj.isInstanceOf[Properties]) + Test.checkProperties(prms, obj.asInstanceOf[Properties]) + else + Test.check(prms, obj.asInstanceOf[Prop]) + + Array() + } + +} + +final class ScalaCheckFramework extends Framework { + + private def mkFP(mod: Boolean, cname: String, noArgCons: Boolean = true) = + new SubclassFingerprint { + def superclassName(): String = cname + val isModule = mod + def requireNoArgConstructor(): Boolean = noArgCons + } + + val name = "ScalaCheck" + + def fingerprints(): Array[Fingerprint] = Array( + mkFP(false, "org.scalacheck.Properties"), + mkFP(false, "org.scalacheck.Prop"), + mkFP(true, "org.scalacheck.Properties"), + mkFP(true, "org.scalacheck.Prop") + ) + + def runner(args: Array[String], remoteArgs: Array[String], + loader: ClassLoader + ): Runner = new ScalaCheckRunner(args, remoteArgs, loader) { + + def receiveMessage(msg: String): Option[String] = msg(0) match { + case 'd' => + val Array(t,s,f,e) = msg.tail.split(',') + testCount.addAndGet(t.toInt) + successCount.addAndGet(s.toInt) + failureCount.addAndGet(f.toInt) + errorCount.addAndGet(e.toInt) + None + } + + def done = { + val heading = if (testCount == successCount) "Passed" else "Failed" + s"$heading: Total $testCount, Failed $failureCount, Errors $errorCount, Passed $successCount" + } + + } + + def slaveRunner(args: Array[String], remoteArgs: Array[String], + loader: ClassLoader, send: String => Unit + ): Runner = new ScalaCheckRunner(args, remoteArgs, loader) { + + def receiveMessage(msg: String) = None + + def done = { + send(s"d$testCount,$successCount,$failureCount,$errorCount") + "" } } diff --git a/js/src/main/scala/org/scalacheck/util/Testable.scala b/js/src/main/scala/org/scalacheck/util/Testable.scala index cca9e9811..5a90205ab 100644 --- a/js/src/main/scala/org/scalacheck/util/Testable.scala +++ b/js/src/main/scala/org/scalacheck/util/Testable.scala @@ -1,4 +1,8 @@ package org.scalacheck.util +import scala.scalajs.js.annotation._ + /** Marks a type as testable */ -trait Testable extends scalajs.test.Test { } +@JSExportDescendentClasses +@JSExportDescendentObjects +trait Testable { } diff --git a/project/plugin.sbt b/project/plugin.sbt index 0445ba628..1df0f7be5 100644 --- a/project/plugin.sbt +++ b/project/plugin.sbt @@ -2,4 +2,4 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.6") addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3") -addSbtPlugin("org.scala-lang.modules.scalajs" % "scalajs-sbt-plugin" % "0.5.6") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.0") diff --git a/src/main/scala/org/scalacheck/Prop.scala b/src/main/scala/org/scalacheck/Prop.scala index bbbe1da8b..74ad07235 100644 --- a/src/main/scala/org/scalacheck/Prop.scala +++ b/src/main/scala/org/scalacheck/Prop.scala @@ -15,6 +15,12 @@ import language.reflectiveCalls import util.{Pretty, FreqMap, Buildable, ConsoleReporter, Testable} import scala.annotation.tailrec +/** Helper class to satisfy ScalaJS compilation. Do not use this directly, + * use [[Prop.apply]] instead. */ +class PropFromFun(f: Gen.Parameters => Prop.Result) extends Prop { + def apply(prms: Gen.Parameters) = f(prms) +} + trait Prop extends Testable { import Prop.{Result, Proof, True, False, Exception, Undecided, @@ -282,11 +288,11 @@ object Prop { } /** Create a new property from the given function. */ - def apply(f: Parameters => Result): Prop = new Prop { - def apply(prms: Parameters) = try f(prms) catch { + def apply(f: Parameters => Result): Prop = new PropFromFun(prms => + try f(prms) catch { case e: Throwable => Result(status = Exception(e)) } - } + ) /** Create a property that returns the given result */ def apply(r: Result): Prop = Prop.apply(prms => r) @@ -917,8 +923,8 @@ object Prop { /** Ensures that the property expression passed in completes within the given * space of time. */ - def within(maximumMs: Long)(wrappedProp: => Prop): Prop = new Prop { - @tailrec private def attempt(prms: Parameters, endTime: Long): Result = { + def within(maximumMs: Long)(wrappedProp: => Prop): Prop = { + @tailrec def attempt(prms: Parameters, endTime: Long): Result = { val result = wrappedProp.apply(prms) if (System.currentTimeMillis > endTime) { (if(result.failure) result else Result(status = False)).label("Timeout") @@ -927,6 +933,6 @@ object Prop { else attempt(prms, endTime) } } - def apply(prms: Parameters) = attempt(prms, System.currentTimeMillis + maximumMs) + Prop.apply(prms => attempt(prms, System.currentTimeMillis + maximumMs)) } } diff --git a/src/test/scala/org/scalacheck/TestFingerprint.scala b/src/test/scala/org/scalacheck/TestFingerprint.scala index 15a4897c8..20207a414 100644 --- a/src/test/scala/org/scalacheck/TestFingerprint.scala +++ b/src/test/scala/org/scalacheck/TestFingerprint.scala @@ -5,7 +5,7 @@ class PropClass extends Prop { def apply(prms: Gen.Parameters) = Prop.proved(prms) } -class PropObject extends Prop { +object PropObject extends Prop { // TODO: Give prop name when #90 is implemented def apply(prms: Gen.Parameters) = Prop.proved(prms) }