summaryrefslogtreecommitdiff
path: root/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing
diff options
context:
space:
mode:
Diffstat (limited to 'examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing')
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala35
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala15
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala22
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala9
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala52
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala190
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala37
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala110
8 files changed, 470 insertions, 0 deletions
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala
new file mode 100644
index 0000000..f13c195
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala
@@ -0,0 +1,35 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing.{Event => SbtEvent, _}
+
+class Events(taskDef: TaskDef) {
+
+ abstract class Event(val status: Status,
+ val throwable: OptionalThrowable = new OptionalThrowable) extends SbtEvent {
+ val fullyQualifiedName = taskDef.fullyQualifiedName
+ val fingerprint = taskDef.fingerprint
+ val selector = taskDef.selectors.headOption.getOrElse(new SuiteSelector)
+ val duration = -1L
+ }
+
+ case class Error(exception: Throwable) extends Event(
+ Status.Error, new OptionalThrowable(exception))
+
+ case class Failure(exception: Throwable) extends Event(
+ Status.Failure, new OptionalThrowable(exception))
+
+ case object Succeeded extends Event(Status.Success)
+ case object Skipped extends Event(Status.Skipped)
+ case object Pending extends Event(Status.Pending)
+ case object Ignored extends Event(Status.Ignored)
+ case object Canceled extends Event(Status.Canceled)
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala
new file mode 100644
index 0000000..bfe0ffc
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala
@@ -0,0 +1,15 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import scala.scalajs.tools.classpath.CompleteClasspath
+
+/** A dummy ClassLoader to pass on Scala.js ClasspathContents to tests */
+final case class JSClasspathLoader(cp: CompleteClasspath) extends ClassLoader
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala
new file mode 100644
index 0000000..dfebe00
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala
@@ -0,0 +1,22 @@
+package scala.scalajs.sbtplugin.testing
+
+import scala.scalajs.tools.logging._
+import sbt.testing.{ Logger => SbtTestLogger }
+
+class SbtTestLoggerAccWrapper(logger: Seq[SbtTestLogger]) extends Logger {
+
+ import scala.scalajs.sbtplugin.Implicits._
+ import Level._
+
+ def log(level: Level, message: => String): Unit = level match {
+ case Error => logger.foreach(_.error(message))
+ case Warn => logger.foreach(_.warn(message))
+ case Info => logger.foreach(_.info(message))
+ case Debug => logger.foreach(_.debug(message))
+ }
+
+ def success(message: => String): Unit = logger.foreach(_.info(message))
+
+ def trace(t: => Throwable): Unit = logger.foreach(_.trace(t))
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala
new file mode 100644
index 0000000..b4cb09b
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala
@@ -0,0 +1,9 @@
+package scala.scalajs.sbtplugin.testing
+
+/** Dummy Exception to wrap stack traces passed to SBT */
+class TestException(
+ message: String,
+ stackTrace: Array[StackTraceElement]
+) extends Exception(message) {
+ override def getStackTrace = stackTrace
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala
new file mode 100644
index 0000000..ab43bfe
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala
@@ -0,0 +1,52 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.classpath._
+
+import sbt._
+import sbt.testing._
+import sbt.classpath.ClasspathFilter
+
+import java.net.URLClassLoader
+
+class TestFramework(
+ environment: JSEnv,
+ jsConsole: JSConsole,
+ testFramework: String) extends Framework {
+
+ val name = "Scala.js Test Framework"
+
+ lazy val fingerprints = Array[Fingerprint](f1)
+
+ private val f1 = new SubclassFingerprint {
+ val isModule = true
+ val superclassName = "scala.scalajs.testbridge.Test"
+ val requireNoArgConstructor = true
+ }
+
+ def runner(args: Array[String], remoteArgs: Array[String],
+ testClassLoader: ClassLoader): Runner = {
+
+ val jsClasspath = extractClasspath(testClassLoader)
+ new TestRunner(environment, jsClasspath, jsConsole,
+ testFramework, args, remoteArgs)
+ }
+
+ /** extract classpath from ClassLoader (which must be a JSClasspathLoader) */
+ private def extractClasspath(cl: ClassLoader) = cl match {
+ case cl: JSClasspathLoader => cl.cp
+ case _ =>
+ sys.error("The Scala.js framework only works with a class loader of " +
+ s"type JSClasspathLoader (${cl.getClass} given)")
+ }
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala
new file mode 100644
index 0000000..9aad956
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala
@@ -0,0 +1,190 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing.Logger
+import sbt.testing.EventHandler
+
+import scala.scalajs.tools.env.JSConsole
+import scala.scalajs.tools.sourcemap.SourceMapper
+import scala.scalajs.tools.classpath.{CompleteClasspath, IRClasspath}
+
+import scala.collection.mutable.ArrayBuffer
+
+import scala.util.Try
+
+import java.util.regex._
+
+/** This parses the messages sent from the test bridge and forwards
+ * the calls to SBT. It also buffers all log messages and allows to
+ * pipe them to multiple loggers in a synchronized fashion. This
+ * ensures that log messages aren't interleaved due to parallelism.
+ */
+class TestOutputConsole(
+ base: JSConsole,
+ handler: EventHandler,
+ events: Events,
+ classpath: CompleteClasspath,
+ noSourceMap: Boolean) extends JSConsole {
+
+ import TestOutputConsole._
+ import events._
+
+ private val traceBuf = new ArrayBuffer[StackTraceElement]
+ private val logBuffer = new ArrayBuffer[LogElement]
+
+ /* See #727: source mapping does not work with CompleteIRClasspath, so
+ * don't bother to try.
+ */
+ private val ignoreSourceMapping =
+ noSourceMap || classpath.isInstanceOf[IRClasspath]
+
+ private lazy val sourceMapper = new SourceMapper(classpath)
+
+ override def log(msg: Any): Unit = {
+ val data = msg.toString
+ val sepPos = data.indexOf("|")
+
+ if (sepPos == -1)
+ log(_.error, s"Malformed message: $data")
+ else {
+ val op = data.substring(0, sepPos)
+ val message = unescape(data.substring(sepPos + 1))
+
+ op match {
+ case "console-log" =>
+ base.log(message)
+ case "error" =>
+ val trace = getTrace()
+ logWithEvent(_.error,
+ messageWithStack(message, trace),
+ Error(new TestException(message, trace))
+ )
+ case "failure" =>
+ val trace = getTrace()
+ logWithEvent(_.error,
+ messageWithStack(message, trace),
+ Failure(new TestException(message, trace))
+ )
+ case "succeeded" =>
+ noTrace()
+ logWithEvent(_.info, message, Succeeded)
+ case "skipped" =>
+ noTrace()
+ logWithEvent(_.info, message, Skipped)
+ case "pending" =>
+ noTrace()
+ logWithEvent(_.info, message, Pending)
+ case "ignored" =>
+ noTrace()
+ logWithEvent(_.info, message, Ignored)
+ case "canceled" =>
+ noTrace()
+ logWithEvent(_.info, message, Canceled)
+ case "error-log" =>
+ noTrace()
+ log(_.error, message)
+ case "info" =>
+ noTrace()
+ log(_.info, message)
+ case "warn" =>
+ noTrace()
+ log(_.warn, message)
+ case "trace" =>
+ val Array(className, methodName, fileName,
+ lineNumberStr, columnNumberStr) = message.split('|')
+
+ def tryParse(num: String, name: String) = Try(num.toInt).getOrElse {
+ log(_.warn, s"Couldn't parse $name number in StackTrace: $num")
+ -1
+ }
+
+ val lineNumber = tryParse(lineNumberStr, "line")
+ val columnNumber = tryParse(columnNumberStr, "column")
+
+ val ste =
+ new StackTraceElement(className, methodName, fileName, lineNumber)
+
+ if (ignoreSourceMapping)
+ traceBuf += ste
+ else
+ traceBuf += sourceMapper.map(ste, columnNumber)
+ case _ =>
+ noTrace()
+ log(_.error, s"Unknown op: $op. Originating log message: $data")
+ }
+ }
+ }
+
+ private def noTrace() = {
+ if (traceBuf.nonEmpty)
+ log(_.warn, s"Discarding ${traceBuf.size} stack elements")
+ traceBuf.clear()
+ }
+
+ private def getTrace() = {
+ val res = traceBuf.toArray
+ traceBuf.clear()
+ res
+ }
+
+ private def messageWithStack(message: String, stack: Array[StackTraceElement]): String =
+ message + stack.mkString("\n", "\n", "")
+
+ private def log(method: LogMethod, message: String): Unit =
+ logBuffer.append(LogElement(method, message))
+
+ private def logWithEvent(method: LogMethod,
+ message: String, event: Event): Unit = {
+ handler handle event
+ log(method, message)
+ }
+
+ def pipeLogsTo(loggers: Array[Logger]): Unit = {
+ TestOutputConsole.synchronized {
+ for {
+ LogElement(method, message) <- logBuffer
+ logger <- loggers
+ } method(logger) {
+ if (logger.ansiCodesSupported) message
+ else removeColors(message)
+ }
+ }
+ }
+
+ def allLogs: List[LogElement] = logBuffer.toList
+
+ private val colorPattern = raw"\033\[\d{1,2}m"
+
+ private def removeColors(message: String): String =
+ message.replaceAll(colorPattern, "")
+
+ private val unEscPat = Pattern.compile("(\\\\\\\\|\\\\n|\\\\r)")
+ private def unescape(message: String): String = {
+ val m = unEscPat.matcher(message)
+ val res = new StringBuffer()
+ while (m.find()) {
+ val repl = m.group() match {
+ case "\\\\" => "\\\\"
+ case "\\n" => "\n"
+ case "\\r" => "\r"
+ }
+ m.appendReplacement(res, repl);
+ }
+ m.appendTail(res);
+ res.toString
+ }
+
+}
+
+object TestOutputConsole {
+ type LogMethod = Logger => (String => Unit)
+ case class LogElement(method: LogMethod, message: String)
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala
new file mode 100644
index 0000000..e5ca2a2
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala
@@ -0,0 +1,37 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing._
+
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.classpath._
+
+class TestRunner(
+ environment: JSEnv,
+ classpath: CompleteClasspath,
+ jsConsole: JSConsole,
+ testFramework: String,
+ val args: Array[String],
+ val remoteArgs: Array[String]) extends Runner {
+
+ def tasks(taskDefs: Array[TaskDef]): Array[Task] = if (_done) {
+ throw new IllegalStateException("Done has already been called")
+ } else {
+ taskDefs.map(TestTask(environment, classpath, jsConsole, testFramework, args))
+ }
+
+ def done(): String = {
+ _done = true
+ ""
+ }
+
+ private var _done = false
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala
new file mode 100644
index 0000000..b1cabb9
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala
@@ -0,0 +1,110 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing._
+
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+
+import scala.scalajs.sbtplugin.JSUtils._
+
+import scala.annotation.tailrec
+import scala.util.control.NonFatal
+
+class TestTask(
+ env: JSEnv,
+ classpath: CompleteClasspath,
+ jsConsole: JSConsole,
+ testFramework: String,
+ args: Array[String],
+ val taskDef: TaskDef) extends Task {
+
+ import TestTask._
+
+ val tags = Array.empty[String]
+ val options = readArgs(args.toList)
+
+ def execute(eventHandler: EventHandler,
+ loggers: Array[Logger]): Array[Task] = {
+
+ val runnerFile = testRunnerFile(options.frameworkArgs)
+ val testConsole = new TestOutputConsole(jsConsole, eventHandler,
+ new Events(taskDef), classpath, options.noSourceMap)
+ val logger = new SbtTestLoggerAccWrapper(loggers)
+
+ // Actually execute test
+ env.jsRunner(classpath, runnerFile, logger, testConsole).run()
+
+ testConsole.pipeLogsTo(loggers)
+
+ Array.empty
+ }
+
+ private def testRunnerFile(args: List[String]) = {
+ val testKey = taskDef.fullyQualifiedName
+
+ // Note that taskDef does also have the selector, fingerprint and
+ // explicitlySpecified value we could pass to the framework. However, we
+ // believe that these are only moderately useful. Therefore, we'll silently
+ // ignore them.
+
+ val jsArgArray = listToJS(args)
+ new MemVirtualJSFile("Generated test launcher file").
+ withContent(s"""this${dot2bracket(testFramework)}().safeRunTest(
+ | scala.scalajs.testbridge.internal.ConsoleTestOutput(),
+ | $jsArgArray,
+ | this${dot2bracket(testKey)});""".stripMargin)
+ }
+
+
+}
+
+object TestTask {
+
+ def apply(environment: JSEnv, classpath: CompleteClasspath,
+ jsConsole: JSConsole, testFramework: String, args: Array[String]
+ )(taskDef: TaskDef) =
+ new TestTask(environment, classpath, jsConsole,
+ testFramework, args, taskDef)
+
+ case class ArgOptions(
+ noSourceMap: Boolean,
+ frameworkArgs: List[String]
+ )
+
+ private def readArgs(args0: List[String]) = {
+ // State for each option
+ var noSourceMap = false
+
+ def mkOptions(frameworkArgs: List[String]) =
+ ArgOptions(noSourceMap, frameworkArgs)
+
+ @tailrec
+ def read0(args: List[String]): ArgOptions = args match {
+ case "-no-source-map" :: xs =>
+ noSourceMap = true
+ read0(xs)
+
+ // Explicitly end our argument list
+ case "--" :: xs =>
+ mkOptions(xs)
+
+ // Unknown argument
+ case xs =>
+ mkOptions(xs)
+
+ }
+
+ read0(args0)
+ }
+
+}