Skip to content

Commit

Permalink
Support ScalaJS 0.6.0 (fix #122)
Browse files Browse the repository at this point in the history
This commit is heavily inspired by 6e369b8 from @intheknow, but
much simplified. This commit does not try to merge the SBT framework
for JVM and JS, instead it only focuses implementing support for
ScalaJS 0.6.0. We should try to merge as much of the JVM/JS code
as possible in the future, though.
  • Loading branch information
rickynils committed Feb 5, 2015
1 parent 9442397 commit 30e9cf2
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 37 deletions.
5 changes: 2 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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: _*)
Expand Down
181 changes: 156 additions & 25 deletions js/src/main/scala/org/scalacheck/ScalaCheckFramework.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
""
}

}
Expand Down
6 changes: 5 additions & 1 deletion js/src/main/scala/org/scalacheck/util/Testable.scala
Original file line number Diff line number Diff line change
@@ -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 { }
2 changes: 1 addition & 1 deletion project/plugin.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
18 changes: 12 additions & 6 deletions src/main/scala/org/scalacheck/Prop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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))
}
}
2 changes: 1 addition & 1 deletion src/test/scala/org/scalacheck/TestFingerprint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 30e9cf2

Please sign in to comment.