diff options
Diffstat (limited to 'examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing')
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) + } + +} |