From 5cef245e9aeb89c7ab445f53c7f3ec935e6f47d6 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 2 Apr 2014 18:28:11 +0200 Subject: [PATCH 001/133] Fix scala-js/scala-js#446: Make setting out of jsEnv (and sanitize stuff) --- .../scala/scalajs/tools/env/JSConsole.scala | 15 +++++++++++++ .../scala/scala/scalajs/tools/env/JSEnv.scala | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 tools/src/main/scala/scala/scalajs/tools/env/JSConsole.scala create mode 100644 tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala diff --git a/tools/src/main/scala/scala/scalajs/tools/env/JSConsole.scala b/tools/src/main/scala/scala/scalajs/tools/env/JSConsole.scala new file mode 100644 index 0000000..a93768f --- /dev/null +++ b/tools/src/main/scala/scala/scalajs/tools/env/JSConsole.scala @@ -0,0 +1,15 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +/** Trait representing a JS console */ +trait JSConsole { + def log(msg: Any): Unit +} diff --git a/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala b/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala new file mode 100644 index 0000000..1015bf7 --- /dev/null +++ b/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala @@ -0,0 +1,22 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.logging._ + +trait JSEnv { + /** Run the code in the virtual file. Return Some() if failed + * None otherwise + */ + def runJS(classpath: JSClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): Option[String] +} From f335399248920fc5691739091c7b826400cf8ba2 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 3 Apr 2014 09:12:04 +0200 Subject: [PATCH 002/133] Fix scala-js/scala-js#441: Add Node.js environment --- .../scala/scalajs/sbtplugin/JSUtils.scala | 31 ++++ .../sbtplugin/env/AvailableFilesCache.scala | 65 +++++++++ .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 136 ++++++++++++++++++ .../sbtplugin/env/nodejs/NodeJSEnv.scala | 70 +++++++++ 4 files changed, 302 insertions(+) create mode 100644 sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala create mode 100644 sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala create mode 100644 sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala create mode 100644 sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala new file mode 100644 index 0000000..d73f4e1 --- /dev/null +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala @@ -0,0 +1,31 @@ +package scala.scalajs.sbtplugin + +object JSUtils { + def listToJS(xs: List[String]): String = + xs.map(toJSstr _).mkString("[",",","]") + + /** (almost) stolen from scala.scalajs.compiler.JSPrinters */ + def toJSstr(str: String): String = { + /* Note that Java and JavaScript happen to use the same encoding for + * Unicode, namely UTF-16, which means that 1 char from Java always equals + * 1 char in JavaScript. */ + val builder = new StringBuilder() + builder.append('"') + str foreach { + case '\\' => builder.append("\\\\") + case '"' => builder.append("\\\"") + case '\u0007' => builder.append("\\a") + case '\u0008' => builder.append("\\b") + case '\u0009' => builder.append("\\t") + case '\u000A' => builder.append("\\n") + case '\u000B' => builder.append("\\v") + case '\u000C' => builder.append("\\f") + case '\u000D' => builder.append("\\r") + case c => + if (c >= 32 && c <= 126) builder.append(c.toChar) // ASCII printable characters + else builder.append(f"\\u$c%04x") + } + builder.append('"') + builder.result() + } +} diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala new file mode 100644 index 0000000..62929cf --- /dev/null +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala @@ -0,0 +1,65 @@ +package scala.scalajs.sbtplugin.env + +import scala.scalajs.tools.io._ +import sbt.IO +import scala.collection.mutable +import java.io.File + +/** Helper class to write files that should be available on the classpath to a + * temporary directory. Also performs caching of this directory. + * + * This class is NOT thread safe + */ +final class AvailableFilesCache { + + // Map from extracted cached files to version + var cached = mutable.Map.empty[String, Option[Any]] + + var curTmpDir = newTmpDir() + + def cacheFiles(vfiles: Seq[VirtualJSFile]): File = { + if (!curTmpDir.exists) { + // Someone removed our tmp dir. Invalidate cache, create new one + cached.clear() + curTmpDir = newTmpDir() + } + + // Check if we need to remove any files + if (!cached.isEmpty) { + val deleted = cached.keySet -- vfiles.map(_.name).toSet + deleted foreach { deleteFile _ } + } + + // Add new files + for (vfile <- vfiles if !isUpToDate(vfile)) { + copyFile(vfile) + cached += vfile.name -> vfile.version + } + + curTmpDir + } + + private def isUpToDate(vfile: VirtualJSFile) = { + val oldVersion = cached.getOrElse(vfile.name, None) + oldVersion.isDefined && oldVersion == vfile.version + } + + private def deleteFile(fname: String) = cacheFile(fname).delete() + private def copyFile(vfile: VirtualJSFile) = vfile match { + case vfile: FileVirtualJSFile => + IO.copyFile(vfile.file, cacheFile(vfile), preserveLastModified = true) + case _ => + IO.write(cacheFile(vfile), vfile.content, append = false) + } + + private def cacheFile(vfile: VirtualJSFile): File = cacheFile(vfile.name) + private def cacheFile(fname: String): File = new File(curTmpDir, fname) + + /** create a new temporary directory that is deleted on exit */ + private def newTmpDir() = { + val dir = IO.createTemporaryDirectory + dir.deleteOnExit() + dir + } + +} diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala new file mode 100644 index 0000000..68e152c --- /dev/null +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -0,0 +1,136 @@ +package scala.scalajs.sbtplugin.env + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.env._ +import scala.scalajs.tools.logging._ + +import scala.scalajs.sbtplugin.JSUtils._ + +import java.io.{ Console => _, _ } +import scala.io.Source + +abstract class ExternalJSEnv( + final protected val additionalArgs: Seq[String], + final protected val additionalEnv: Seq[String]) extends JSEnv { + + import ExternalJSEnv._ + + /** Printable name of this VM */ + protected def vmName: String + + /** Command to execute (on shell) for this VM */ + protected def executable: String + + /** JS files used to setup VM */ + protected def initFiles: Seq[VirtualJSFile] = Nil + + /** Sends required data to VM Stdin (can throw) */ + protected def sendVMStdin(args: RunJSArgs, out: OutputStream): Unit = {} + + /** Fire up an instance of the VM and send js input to it. + * Don't care about exceptions. Calling code will catch and display + * an error message. + */ + def runJS(classpath: JSClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): Option[String] = { + + val runJSArgs = RunJSArgs(classpath, code, logger, console) + + val vmInst = startVM(runJSArgs) + + // Prepare and send input to VM + val out = vmInst.getOutputStream() + try { sendVMStdin(runJSArgs, out) } + finally { out.close() } + + // We are now executing. Pipe stdout to console + pipeToConsole(vmInst.getInputStream(), console) + + // We are probably done (stdin is closed). Report any errors + val errSrc = Source.fromInputStream(vmInst.getErrorStream(), "UTF-8") + try { errSrc.getLines.foreach(err => logger.error(err)) } + finally { errSrc.close } + + // Make sure we are done. + vmInst.waitFor() + + // Get return value and return + val retVal = vmInst.exitValue + + if (retVal == 0) None + else Some(s"$vmName exited with code $retVal") + + } + + /** send a bunch of JS files to an output stream */ + final protected def sendJS(files: Seq[VirtualJSFile], out: OutputStream) = { + val writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) + + try { files.foreach { writeJSFile(_, writer) } } + finally { writer.close() } + } + + /** write a single JS file to a writer using an include fct if appropriate */ + protected def writeJSFile(file: VirtualJSFile, writer: Writer) = { + file match { + case file: FileVirtualJSFile => + val fname = toJSstr(file.file.getAbsolutePath) + writer.write(s"require($fname);\n") + case _ => + writer.write(file.content) + writer.write('\n') + } + } + + /** pipe lines from input stream to JSConsole */ + final protected def pipeToConsole(in: InputStream, console: JSConsole) = { + val source = Source.fromInputStream(in, "UTF-8") + try { source.getLines.foreach(console.log _) } + finally { source.close() } + } + + protected def startVM(args: RunJSArgs): Process = { + val vmArgs = getVMArgs(args) + val vmEnv = getVMEnv(args) + + val allArgs = (executable +: vmArgs).toArray + sys.runtime.exec(allArgs, vmEnv.toArray) + } + + /** VM arguments excluding executable. Override to adapt. + * Overrider is responsible to add additionalArgs. + */ + protected def getVMArgs(args: RunJSArgs): Seq[String] = additionalArgs + + /** VM environment. Override to adapt. + * Override is responsible to add additionalEnv + */ + protected def getVMEnv(args: RunJSArgs): Seq[String] = additionalEnv + + /** Get the path where available files from classpath are stored. + * Uses caching mechanism to create a temp dir with all files + */ + protected def getRequirePath(args: RunJSArgs): File = + availFileCache.get.cacheFiles(args.classpath.otherJSFiles) + + /** Get files that are passed to VM */ + protected def getJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = + initFiles ++ (args.classpath.mainJSFiles :+ args.code) + + // File Cache for files to be imported + private val availFileCache = new ThreadLocal[AvailableFilesCache] { + override def initialValue() = new AvailableFilesCache + } + +} + +object ExternalJSEnv { + + case class RunJSArgs( + classpath: JSClasspath, + code: VirtualJSFile, + logger: Logger, + console: JSConsole) + +} diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala new file mode 100644 index 0000000..97645aa --- /dev/null +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -0,0 +1,70 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.sbtplugin.env.nodejs + +import scala.scalajs.sbtplugin.env._ + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.env._ +import scala.scalajs.tools.logging._ + +import scala.scalajs.sbtplugin.JSUtils._ + +import java.io.{ Console => _, _ } +import scala.io.Source + +class NodeJSEnv( + nodejsPath: Option[String], + addArgs: Seq[String], + addEnv: Seq[String]) extends ExternalJSEnv(addArgs, addEnv) { + + import ExternalJSEnv._ + + protected def vmName: String = "node.js" + protected def executable: String = nodejsPath.getOrElse("node") + + // Helper constructors + def this( + nodejsPath: String, + args: Seq[String] = Seq.empty, + env: Seq[String] = Seq.empty) = this(Some(nodejsPath), args, env) + + def this() = this(None, Seq.empty, Seq.empty) + + // We need to define importScripts + override protected def initFiles: Seq[VirtualJSFile] = Seq { + new MemVirtualJSFile("importScripts.js").withContent( + """ + global.importScripts = function() { + for (i in arguments) { + // Require script and add stuff to global object + var module = require(arguments[i]); + for (elem in module) + global[elem] = module[elem]; + } + }; + """) + } + + // Send code to Stdin + override protected def sendVMStdin(args: RunJSArgs, out: OutputStream): Unit = { + sendJS(getJSFiles(args), out) + } + + // Node.js specific (system) environment + override protected def getVMEnv(args: RunJSArgs) = { + val nodeIncludePath = getRequirePath(args).getAbsolutePath + + Seq(s"NODE_PATH=$nodeIncludePath", + "NODE_MODULE_CONTEXTS=0") ++ super.getVMEnv(args) + } + +} From 59f457a1d6f5500cc73e0de3016623b6958d7174 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 22 Apr 2014 16:54:31 +0200 Subject: [PATCH 003/133] Fix scala-js/scala-js#500: Avoid removal of duplicate percentage signs by Node.js --- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 21 +++++++++-- .../sbtplugin/test/env/EmptyJSClasspath.scala | 9 +++++ .../sbtplugin/test/env/JSEnvTest.scala | 30 ++++++++++++++++ .../sbtplugin/test/env/NodeJSTest.scala | 35 +++++++++++++++++++ .../sbtplugin/test/env/StoreJSConsole.scala | 14 ++++++++ .../sbtplugin/test/env/StoreLogger.scala | 29 +++++++++++++++ 6 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala create mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala create mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala create mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreJSConsole.scala create mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreLogger.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 97645aa..49c116c 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -39,8 +39,8 @@ class NodeJSEnv( def this() = this(None, Seq.empty, Seq.empty) - // We need to define importScripts - override protected def initFiles: Seq[VirtualJSFile] = Seq { + // We need to define importScripts and hack console.log + override protected def initFiles: Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("importScripts.js").withContent( """ global.importScripts = function() { @@ -51,8 +51,23 @@ class NodeJSEnv( global[elem] = module[elem]; } }; + """), + new MemVirtualJSFile("nodeConsoleHack.js").withContent( + """ + // Hack console log to duplicate double % signs + (function() { + var oldLog = console.log; + var newLog = function() { + var args = arguments; + if (args.length >= 1) { + args[0] = args[0].replace(/%/g, "%%"); + } + oldLog.apply(console, args); + }; + console.log = newLog; + })(); """) - } + ) // Send code to Stdin override protected def sendVMStdin(args: RunJSArgs, out: OutputStream): Unit = { diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala new file mode 100644 index 0000000..8284cf9 --- /dev/null +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala @@ -0,0 +1,9 @@ +package scala.scalajs.sbtplugin.test.env + +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.io._ + +object EmptyJSClasspath extends JSClasspath { + def mainJSFiles: Seq[VirtualJSFile] = Seq.empty + def otherJSFiles: Seq[VirtualJSFile] = Seq.empty +} diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala new file mode 100644 index 0000000..55dcafe --- /dev/null +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala @@ -0,0 +1,30 @@ +package scala.scalajs.sbtplugin.test.env + +import scala.scalajs.tools.env.JSEnv +import scala.scalajs.tools.io.MemVirtualJSFile + +import org.junit.Assert._ + +abstract class JSEnvTest { + + protected def newJSEnv: JSEnv + + implicit class RunMatcher(codeStr: String) { + def hasOutput(expectedOut: String) = { + + val console = new StoreJSConsole() + val logger = new StoreLogger() + val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) + + val res = newJSEnv.runJS(EmptyJSClasspath, code, logger, console) + + val log = logger.getLog + + assertTrue("VM shouldn't fail on snippet. Msg: " + res, res.isEmpty) + assertTrue("VM shouldn't procude log. Log:\n" + + log.mkString("\n"), log.isEmpty) + assertEquals("Output should match", expectedOut, console.getLog) + } + } + +} diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala new file mode 100644 index 0000000..e6e582c --- /dev/null +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala @@ -0,0 +1,35 @@ +package scala.scalajs.sbtplugin.test.env + +import scala.scalajs.sbtplugin.env.nodejs.NodeJSEnv + +import org.junit.Test + +class NodeJSTest extends JSEnvTest { + + protected def newJSEnv = new NodeJSEnv + + /** Node.js strips double percentage signs - #500 */ + @Test + def percentageTest = { + val counts = 1 to 15 + val argcs = 1 to 3 + val strings = counts.map("%" * _) + + val strlists = for { + count <- argcs + string <- strings + } yield List.fill(count)(string) + + val codes = for { + strlist <- strlists + } yield { + val args = strlist.map(s => s""""$s"""").mkString(", ") + s"console.log($args);\n" + } + + val result = strlists.map(_.mkString(" ") + "\n").mkString("") + + codes.mkString("").hasOutput(result) + } + +} diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreJSConsole.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreJSConsole.scala new file mode 100644 index 0000000..9c7a84a --- /dev/null +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreJSConsole.scala @@ -0,0 +1,14 @@ +package scala.scalajs.sbtplugin.test.env + +import scala.scalajs.tools.env._ + +class StoreJSConsole extends JSConsole { + private[this] val buf = new StringBuilder() + + def log(msg: Any): Unit = { + buf.append(msg.toString) + buf.append('\n') + } + + def getLog: String = buf.toString +} diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreLogger.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreLogger.scala new file mode 100644 index 0000000..985b149 --- /dev/null +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreLogger.scala @@ -0,0 +1,29 @@ +package scala.scalajs.sbtplugin.test.env + +import scala.scalajs.tools.logging._ + +import scala.collection.mutable.ListBuffer + +class StoreLogger extends Logger { + import StoreLogger._ + + private[this] val buf = new ListBuffer[LogElem] + + def log(level: Level, message: => String): Unit = + buf += Log(level, message) + def success(message: => String): Unit = + buf += Success(message) + def trace(t: => Throwable): Unit = + buf += Trace(t) + + def getLog: List[LogElem] = buf.toList +} + +object StoreLogger { + + abstract class LogElem + case class Log(level: Level, message: String) extends LogElem + case class Success(message: String) extends LogElem + case class Trace(t: Throwable) extends LogElem + +} From 63bebcf64a2bfd03541e215c6f0643cec3203d70 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 29 Apr 2014 17:58:53 +0200 Subject: [PATCH 004/133] Fix scala-js/scala-js#561: Make hacked Node.js console accept non-String values --- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 4 ++-- .../sbtplugin/test/env/NodeJSTest.scala | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 49c116c..f53c348 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -59,8 +59,8 @@ class NodeJSEnv( var oldLog = console.log; var newLog = function() { var args = arguments; - if (args.length >= 1) { - args[0] = args[0].replace(/%/g, "%%"); + if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { + args[0] = args[0].toString().replace(/%/g, "%%"); } oldLog.apply(console, args); }; diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala index e6e582c..1a1bcc3 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala @@ -32,4 +32,23 @@ class NodeJSTest extends JSEnvTest { codes.mkString("").hasOutput(result) } + /** Node.js console.log hack didn't allow to log non-Strings - #561 */ + @Test + def nonStringTest = { + + """ + console.log(1); + console.log(undefined); + console.log(null); + console.log({}); + console.log([1,2]); + """ hasOutput + """|1 + |undefined + |null + |[object Object] + |1,2 + |""".stripMargin + } + } From 1423cf6165a40afeb235d27bafb922f8cc731f8d Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 30 Apr 2014 19:22:40 +0200 Subject: [PATCH 005/133] Add JS dependency tracking via manifests. A JS_DEPENDENCIES manifest file is added to each configuration classpath and exported in JARs. This way, libraries may declare what exact JS files should be included from a WebJar they depend on. (Or what JS files they provide themselves should be included). Example usages can be found in the build definitions for the jasmine-test-framework and the testing example. - This completely removes `importScripts` and fixes scala-js/scala-js#457 - The project's classpath is fully available even in the post package stages. This is necessary to allow for discovery of dependency manifests and JS libraries the project depends on. --- .../sbtplugin/env/AvailableFilesCache.scala | 65 ------------------- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 13 +--- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 21 +----- .../sbtplugin/test/env/EmptyJSClasspath.scala | 2 +- 4 files changed, 5 insertions(+), 96 deletions(-) delete mode 100644 sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala deleted file mode 100644 index 62929cf..0000000 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/AvailableFilesCache.scala +++ /dev/null @@ -1,65 +0,0 @@ -package scala.scalajs.sbtplugin.env - -import scala.scalajs.tools.io._ -import sbt.IO -import scala.collection.mutable -import java.io.File - -/** Helper class to write files that should be available on the classpath to a - * temporary directory. Also performs caching of this directory. - * - * This class is NOT thread safe - */ -final class AvailableFilesCache { - - // Map from extracted cached files to version - var cached = mutable.Map.empty[String, Option[Any]] - - var curTmpDir = newTmpDir() - - def cacheFiles(vfiles: Seq[VirtualJSFile]): File = { - if (!curTmpDir.exists) { - // Someone removed our tmp dir. Invalidate cache, create new one - cached.clear() - curTmpDir = newTmpDir() - } - - // Check if we need to remove any files - if (!cached.isEmpty) { - val deleted = cached.keySet -- vfiles.map(_.name).toSet - deleted foreach { deleteFile _ } - } - - // Add new files - for (vfile <- vfiles if !isUpToDate(vfile)) { - copyFile(vfile) - cached += vfile.name -> vfile.version - } - - curTmpDir - } - - private def isUpToDate(vfile: VirtualJSFile) = { - val oldVersion = cached.getOrElse(vfile.name, None) - oldVersion.isDefined && oldVersion == vfile.version - } - - private def deleteFile(fname: String) = cacheFile(fname).delete() - private def copyFile(vfile: VirtualJSFile) = vfile match { - case vfile: FileVirtualJSFile => - IO.copyFile(vfile.file, cacheFile(vfile), preserveLastModified = true) - case _ => - IO.write(cacheFile(vfile), vfile.content, append = false) - } - - private def cacheFile(vfile: VirtualJSFile): File = cacheFile(vfile.name) - private def cacheFile(fname: String): File = new File(curTmpDir, fname) - - /** create a new temporary directory that is deleted on exit */ - private def newTmpDir() = { - val dir = IO.createTemporaryDirectory - dir.deleteOnExit() - dir - } - -} diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index 68e152c..aa90af5 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -108,20 +108,9 @@ abstract class ExternalJSEnv( */ protected def getVMEnv(args: RunJSArgs): Seq[String] = additionalEnv - /** Get the path where available files from classpath are stored. - * Uses caching mechanism to create a temp dir with all files - */ - protected def getRequirePath(args: RunJSArgs): File = - availFileCache.get.cacheFiles(args.classpath.otherJSFiles) - /** Get files that are passed to VM */ protected def getJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = - initFiles ++ (args.classpath.mainJSFiles :+ args.code) - - // File Cache for files to be imported - private val availFileCache = new ThreadLocal[AvailableFilesCache] { - override def initialValue() = new AvailableFilesCache - } + initFiles ++ (args.classpath.jsFiles :+ args.code) } diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index f53c348..6d38c06 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -39,19 +39,8 @@ class NodeJSEnv( def this() = this(None, Seq.empty, Seq.empty) - // We need to define importScripts and hack console.log + // We need to hack console.log (for duplicate %) override protected def initFiles: Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("importScripts.js").withContent( - """ - global.importScripts = function() { - for (i in arguments) { - // Require script and add stuff to global object - var module = require(arguments[i]); - for (elem in module) - global[elem] = module[elem]; - } - }; - """), new MemVirtualJSFile("nodeConsoleHack.js").withContent( """ // Hack console log to duplicate double % signs @@ -75,11 +64,7 @@ class NodeJSEnv( } // Node.js specific (system) environment - override protected def getVMEnv(args: RunJSArgs) = { - val nodeIncludePath = getRequirePath(args).getAbsolutePath - - Seq(s"NODE_PATH=$nodeIncludePath", - "NODE_MODULE_CONTEXTS=0") ++ super.getVMEnv(args) - } + override protected def getVMEnv(args: RunJSArgs) = + "NODE_MODULE_CONTEXTS=0" +: super.getVMEnv(args) } diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala index 8284cf9..cc8af48 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala @@ -5,5 +5,5 @@ import scala.scalajs.tools.io._ object EmptyJSClasspath extends JSClasspath { def mainJSFiles: Seq[VirtualJSFile] = Seq.empty - def otherJSFiles: Seq[VirtualJSFile] = Seq.empty + def jsDependencies: Seq[VirtualJSFile] = Seq.empty } From 9629ea07a38d8dd4ef82b89191da32e421f7f4b2 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 9 Apr 2014 14:38:41 +0200 Subject: [PATCH 006/133] Add full support for projects requiring DOM - Fix scala-js/scala-js#442: Add Phantom.js environment - Add a setting whether a project requires the DOM --- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 22 +++++++++++++------ .../sbtplugin/env/nodejs/NodeJSEnv.scala | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index aa90af5..b139059 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -23,7 +23,7 @@ abstract class ExternalJSEnv( protected def executable: String /** JS files used to setup VM */ - protected def initFiles: Seq[VirtualJSFile] = Nil + protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = Nil /** Sends required data to VM Stdin (can throw) */ protected def sendVMStdin(args: RunJSArgs, out: OutputStream): Unit = {} @@ -64,13 +64,17 @@ abstract class ExternalJSEnv( } /** send a bunch of JS files to an output stream */ - final protected def sendJS(files: Seq[VirtualJSFile], out: OutputStream) = { + final protected def sendJS(files: Seq[VirtualJSFile], + out: OutputStream): Unit = { val writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) - - try { files.foreach { writeJSFile(_, writer) } } - finally { writer.close() } + try sendJS(files, writer) + finally writer.close() } + /** send a bunch of JS files to a writer */ + final protected def sendJS(files: Seq[VirtualJSFile], out: Writer): Unit = + files.foreach { writeJSFile(_, out) } + /** write a single JS file to a writer using an include fct if appropriate */ protected def writeJSFile(file: VirtualJSFile, writer: Writer) = { file match { @@ -108,9 +112,13 @@ abstract class ExternalJSEnv( */ protected def getVMEnv(args: RunJSArgs): Seq[String] = additionalEnv - /** Get files that are passed to VM */ + /** Get files that are a library (i.e. that do not run anything) */ + protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = + initFiles(args) ++ args.classpath.jsFiles + + /** Get all files that are passed to VM (libraries and code) */ protected def getJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = - initFiles ++ (args.classpath.jsFiles :+ args.code) + getLibJSFiles(args) :+ args.code } diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 6d38c06..9a0f439 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -40,7 +40,7 @@ class NodeJSEnv( def this() = this(None, Seq.empty, Seq.empty) // We need to hack console.log (for duplicate %) - override protected def initFiles: Seq[VirtualJSFile] = Seq( + override protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("nodeConsoleHack.js").withContent( """ // Hack console log to duplicate double % signs From 8c712676cffd2244f4f3318ba0ba96c8bfbe8594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 May 2014 17:33:40 +0200 Subject: [PATCH 007/133] Send printlns to the console, not the logger. --- .../scalajs/tools/env/ConsoleJSConsole.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tools/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala diff --git a/tools/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala b/tools/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala new file mode 100644 index 0000000..5b3d055 --- /dev/null +++ b/tools/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala @@ -0,0 +1,17 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +/** A JS console that prints on the console */ +object ConsoleJSConsole extends JSConsole { + override def log(msg: Any): Unit = { + Console.println(msg) + } +} From 89f4229c6ba0f08cb6daca752897074368aa9e96 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 16 May 2014 14:57:46 +0200 Subject: [PATCH 008/133] Fix scala-js/scala-js#610: Restructure classpaths for clean linking API The new API allows for classpaths to be partially linked (n.b. packageExternalDeps, this commit fixes scala-js/scala-js#609). --- .../scala/scalajs/sbtplugin/env/ExternalJSEnv.scala | 6 +++--- .../scalajs/sbtplugin/test/env/EmptyJSClasspath.scala | 9 --------- .../scala/scalajs/sbtplugin/test/env/JSEnvTest.scala | 6 ++++-- tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) delete mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index b139059..cc2dee4 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -32,7 +32,7 @@ abstract class ExternalJSEnv( * Don't care about exceptions. Calling code will catch and display * an error message. */ - def runJS(classpath: JSClasspath, code: VirtualJSFile, + def runJS(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): Option[String] = { val runJSArgs = RunJSArgs(classpath, code, logger, console) @@ -114,7 +114,7 @@ abstract class ExternalJSEnv( /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = - initFiles(args) ++ args.classpath.jsFiles + initFiles(args) ++ args.classpath.allCode /** Get all files that are passed to VM (libraries and code) */ protected def getJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = @@ -125,7 +125,7 @@ abstract class ExternalJSEnv( object ExternalJSEnv { case class RunJSArgs( - classpath: JSClasspath, + classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole) diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala deleted file mode 100644 index cc8af48..0000000 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/EmptyJSClasspath.scala +++ /dev/null @@ -1,9 +0,0 @@ -package scala.scalajs.sbtplugin.test.env - -import scala.scalajs.tools.classpath._ -import scala.scalajs.tools.io._ - -object EmptyJSClasspath extends JSClasspath { - def mainJSFiles: Seq[VirtualJSFile] = Seq.empty - def jsDependencies: Seq[VirtualJSFile] = Seq.empty -} diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala index 55dcafe..eff8cc0 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala @@ -2,6 +2,7 @@ package scala.scalajs.sbtplugin.test.env import scala.scalajs.tools.env.JSEnv import scala.scalajs.tools.io.MemVirtualJSFile +import scala.scalajs.tools.classpath.PartialClasspath import org.junit.Assert._ @@ -16,12 +17,13 @@ abstract class JSEnvTest { val logger = new StoreLogger() val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) - val res = newJSEnv.runJS(EmptyJSClasspath, code, logger, console) + val emptyCP = PartialClasspath.empty.resolve() + val res = newJSEnv.runJS(emptyCP, code, logger, console) val log = logger.getLog assertTrue("VM shouldn't fail on snippet. Msg: " + res, res.isEmpty) - assertTrue("VM shouldn't procude log. Log:\n" + + assertTrue("VM shouldn't produce log. Log:\n" + log.mkString("\n"), log.isEmpty) assertEquals("Output should match", expectedOut, console.getLog) } diff --git a/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala b/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala index 1015bf7..19a1a79 100644 --- a/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala +++ b/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala @@ -17,6 +17,6 @@ trait JSEnv { /** Run the code in the virtual file. Return Some() if failed * None otherwise */ - def runJS(classpath: JSClasspath, code: VirtualJSFile, + def runJS(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): Option[String] } From 037d57af46fa59653b58b7dfd14946f5ca207100 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 27 May 2014 18:46:36 +0200 Subject: [PATCH 009/133] Fix scala-js/scala-js#688: Remove return value of JSEnv#runJS. JSEnv#runJS used to return Some() if it failed due to historical reasons. This is no longer necessary and introduces useless clutter and confusion (e.g. runners failing without the outer code failing). runJS now throws an exception if it fails. --- .../scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala | 8 +++----- .../scala/scalajs/sbtplugin/test/env/JSEnvTest.scala | 3 +-- tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala | 6 ++---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index cc2dee4..74fe391 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -33,7 +33,7 @@ abstract class ExternalJSEnv( * an error message. */ def runJS(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): Option[String] = { + logger: Logger, console: JSConsole): Unit = { val runJSArgs = RunJSArgs(classpath, code, logger, console) @@ -57,10 +57,8 @@ abstract class ExternalJSEnv( // Get return value and return val retVal = vmInst.exitValue - - if (retVal == 0) None - else Some(s"$vmName exited with code $retVal") - + if (retVal != 0) + sys.error(s"$vmName exited with code $retVal") } /** send a bunch of JS files to an output stream */ diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala index eff8cc0..5fcf3a5 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala @@ -18,11 +18,10 @@ abstract class JSEnvTest { val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) val emptyCP = PartialClasspath.empty.resolve() - val res = newJSEnv.runJS(emptyCP, code, logger, console) + newJSEnv.runJS(emptyCP, code, logger, console) val log = logger.getLog - assertTrue("VM shouldn't fail on snippet. Msg: " + res, res.isEmpty) assertTrue("VM shouldn't produce log. Log:\n" + log.mkString("\n"), log.isEmpty) assertEquals("Output should match", expectedOut, console.getLog) diff --git a/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala b/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala index 19a1a79..f149d8f 100644 --- a/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala +++ b/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala @@ -14,9 +14,7 @@ import scala.scalajs.tools.classpath._ import scala.scalajs.tools.logging._ trait JSEnv { - /** Run the code in the virtual file. Return Some() if failed - * None otherwise - */ + /** Run the code in the virtual file. Throw if failure */ def runJS(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): Option[String] + logger: Logger, console: JSConsole): Unit } From 65f0a3f855c0ac2cdaef0a6359f430c075af6abc Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 19 Jun 2014 15:49:55 +0200 Subject: [PATCH 010/133] Fix scala-js/scala-js#740: Allow to run stuff with names that are not JS identifiers --- .../src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala index d73f4e1..a59f105 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala @@ -28,4 +28,8 @@ object JSUtils { builder.append('"') builder.result() } + + def dot2bracket(name: String): String = { + name.split('.').map(s => s"""[${toJSstr(s)}]""").mkString + } } From a962a324018b622c47b5299d5ae005c31f3a1064 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 3 Jul 2014 17:52:27 +0200 Subject: [PATCH 011/133] Fix scala-js/scala-js#799: Make PhantomJS runner fail if JS code fails --- .../sbtplugin/test/env/JSEnvTest.scala | 19 ++++++++++++++++--- .../scalajs/tools/env/NullJSConsole.scala | 5 +++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tools/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala index 5fcf3a5..be240cc 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala @@ -3,6 +3,8 @@ package scala.scalajs.sbtplugin.test.env import scala.scalajs.tools.env.JSEnv import scala.scalajs.tools.io.MemVirtualJSFile import scala.scalajs.tools.classpath.PartialClasspath +import scala.scalajs.tools.logging.NullLogger +import scala.scalajs.tools.env.NullJSConsole import org.junit.Assert._ @@ -11,13 +13,15 @@ abstract class JSEnvTest { protected def newJSEnv: JSEnv implicit class RunMatcher(codeStr: String) { - def hasOutput(expectedOut: String) = { + + val emptyCP = PartialClasspath.empty.resolve() + val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) + + def hasOutput(expectedOut: String): Unit = { val console = new StoreJSConsole() val logger = new StoreLogger() - val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) - val emptyCP = PartialClasspath.empty.resolve() newJSEnv.runJS(emptyCP, code, logger, console) val log = logger.getLog @@ -26,6 +30,15 @@ abstract class JSEnvTest { log.mkString("\n"), log.isEmpty) assertEquals("Output should match", expectedOut, console.getLog) } + + def fails(): Unit = { + try { + newJSEnv.runJS(emptyCP, code, NullLogger, NullJSConsole) + assertTrue("Code snipped should fail", false) + } catch { + case e: Exception => + } + } } } diff --git a/tools/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala b/tools/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala new file mode 100644 index 0000000..8147bbe --- /dev/null +++ b/tools/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala @@ -0,0 +1,5 @@ +package scala.scalajs.tools.env + +object NullJSConsole extends JSConsole { + def log(msg: Any): Unit = {} +} From 3e26c6c2dd2829d94e19fac4610504b543795913 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 24 Jul 2014 12:44:00 +0200 Subject: [PATCH 012/133] Fix scala-js/scala-js#865: Inherit existing shell environment in external runners --- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 32 ++++++++++++++++--- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 27 +++++++++++++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index 74fe391..1d9ff13 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -12,7 +12,11 @@ import scala.io.Source abstract class ExternalJSEnv( final protected val additionalArgs: Seq[String], - final protected val additionalEnv: Seq[String]) extends JSEnv { + final protected val additionalEnv: Map[String, String]) extends JSEnv { + + @deprecated("Use Map as environment instead", "0.5.3") + def this(additionalArgs: Seq[String], additionalEnv: Seq[String]) = + this(additionalArgs, ExternalJSEnv.splitEnv(additionalEnv)) import ExternalJSEnv._ @@ -96,8 +100,14 @@ abstract class ExternalJSEnv( val vmArgs = getVMArgs(args) val vmEnv = getVMEnv(args) - val allArgs = (executable +: vmArgs).toArray - sys.runtime.exec(allArgs, vmEnv.toArray) + val allArgs = executable +: vmArgs + val pBuilder = new ProcessBuilder(allArgs: _*) + + pBuilder.environment().clear() + for ((name, value) <- vmEnv) + pBuilder.environment().put(name, value) + + pBuilder.start() } /** VM arguments excluding executable. Override to adapt. @@ -106,9 +116,11 @@ abstract class ExternalJSEnv( protected def getVMArgs(args: RunJSArgs): Seq[String] = additionalArgs /** VM environment. Override to adapt. - * Override is responsible to add additionalEnv + * + * Default is `sys.env` and [[additionalEnv]] */ - protected def getVMEnv(args: RunJSArgs): Seq[String] = additionalEnv + protected def getVMEnv(args: RunJSArgs): Map[String, String] = + sys.env ++ additionalEnv /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = @@ -122,6 +134,16 @@ abstract class ExternalJSEnv( object ExternalJSEnv { + /** Helper for deprecated constructors */ + @deprecated("This is only a helper for compat constructors", "0.5.3") + def splitEnv(env: Seq[String]): Map[String, String] = { + val tups = for (str <- env) yield { + val Array(name, value) = str.split("=", 2) + (name, value) + } + tups.toMap + } + case class RunJSArgs( classpath: CompleteClasspath, code: VirtualJSFile, diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 9a0f439..cc8fd41 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -24,7 +24,7 @@ import scala.io.Source class NodeJSEnv( nodejsPath: Option[String], addArgs: Seq[String], - addEnv: Seq[String]) extends ExternalJSEnv(addArgs, addEnv) { + addEnv: Map[String, String]) extends ExternalJSEnv(addArgs, addEnv) { import ExternalJSEnv._ @@ -32,12 +32,29 @@ class NodeJSEnv( protected def executable: String = nodejsPath.getOrElse("node") // Helper constructors + def this( nodejsPath: String, args: Seq[String] = Seq.empty, - env: Seq[String] = Seq.empty) = this(Some(nodejsPath), args, env) + env: Map[String, String] = Map.empty) = + this(Some(nodejsPath), args, env) + + def this() = this(None, Seq.empty, Map.empty[String, String]) + + // Deprecated compat constructors + + @deprecated("Use Map as environment instead", "0.5.3") + def this(nodejsPath: String, env: Seq[String]) = + this(nodejsPath, env = ExternalJSEnv.splitEnv(env)) + + @deprecated("Use Map as environment instead", "0.5.3") + def this(nodejsPath: String, args: Seq[String], env: Seq[String]) = + this(nodejsPath, args, env = ExternalJSEnv.splitEnv(env)) - def this() = this(None, Seq.empty, Seq.empty) + @deprecated("Use Map as environment instead", "0.5.3") + def this(nodejsPath: Option[String], addArgs: Seq[String], + addEnv: Seq[String]) = + this(nodejsPath, addArgs, ExternalJSEnv.splitEnv(addEnv)) // We need to hack console.log (for duplicate %) override protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = Seq( @@ -64,7 +81,7 @@ class NodeJSEnv( } // Node.js specific (system) environment - override protected def getVMEnv(args: RunJSArgs) = - "NODE_MODULE_CONTEXTS=0" +: super.getVMEnv(args) + override protected def getVMEnv(args: RunJSArgs): Map[String, String] = + sys.env + ("NODE_MODULE_CONTEXTS" -> "0") ++ additionalEnv } From 9c11bafd433fa69abd190d6f0c4b2cbe4be18d17 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 27 Jul 2014 08:49:11 -0700 Subject: [PATCH 013/133] Fix scala-js/scala-js#706: Add mechanism to support CommonJS style require --- .../env/VirtualFileMaterializer.scala | 67 +++++++++++++++++++ .../sbtplugin/env/nodejs/NodeJSEnv.scala | 43 +++++++++++- 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala new file mode 100644 index 0000000..fca1c47 --- /dev/null +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala @@ -0,0 +1,67 @@ +package scala.scalajs.sbtplugin.env + +import scala.scalajs.tools.io.{IO => _, _} + +import sbt.IO + +import java.io.File + +/** A helper class to temporarily store virtual files to the filesystem. + * + * Can be used with tools that require real files. + * @param singleDir if true, forces files to be copied into + * [[cacheDir]]. Useful to setup include directories for + * example. + */ +final class VirtualFileMaterializer(singleDir: Boolean = false) { + + val cacheDir = { + val dir = IO.createTemporaryDirectory + dir.deleteOnExit() + dir + } + + /** Create a target file to write/copy to. Will also call + * deleteOnExit on the file. + */ + private def trgFile(name: String): File = { + val f = new File(cacheDir, name) + f.deleteOnExit() + f + } + + private def materializeFileVF(vf: FileVirtualFile): File = { + if (!singleDir) vf.file + else { + val trg = trgFile(vf.name) + IO.copyFile(vf.file, trg) + trg + } + } + + def materialize(vf: VirtualTextFile): File = vf match { + case vf: FileVirtualFile => materializeFileVF(vf) + case _ => + val trg = trgFile(vf.name) + IO.write(trg, vf.content) + trg + } + + def materialize(vf: VirtualBinaryFile): File = vf match { + case vf: FileVirtualFile => materializeFileVF(vf) + case _ => + val trg = trgFile(vf.name) + IO.write(trg, vf.content) + trg + } + + /** Removes the cache directory. Any operation on this + * VirtualFileMaterializer is invalid after [[close]] has been + * called. + */ + def close(): Unit = { + cacheDir.listFiles().foreach(_.delete) + cacheDir.delete() + } + +} diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index cc8fd41..efcc20d 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -10,10 +10,12 @@ package scala.scalajs.sbtplugin.env.nodejs import scala.scalajs.sbtplugin.env._ +import scala.scalajs.sbtplugin.JSUtils.toJSstr import scala.scalajs.tools.io._ import scala.scalajs.tools.classpath._ import scala.scalajs.tools.env._ +import scala.scalajs.tools.jsdep._ import scala.scalajs.tools.logging._ import scala.scalajs.sbtplugin.JSUtils._ @@ -21,6 +23,8 @@ import scala.scalajs.sbtplugin.JSUtils._ import java.io.{ Console => _, _ } import scala.io.Source +import scala.util.DynamicVariable + class NodeJSEnv( nodejsPath: Option[String], addArgs: Seq[String], @@ -31,6 +35,16 @@ class NodeJSEnv( protected def vmName: String = "node.js" protected def executable: String = nodejsPath.getOrElse("node") + private val _libCache = new DynamicVariable[VirtualFileMaterializer](null) + protected def libCache: VirtualFileMaterializer = _libCache.value + + final protected def withLibCache[T](body: => T): T = { + _libCache.withValue(new VirtualFileMaterializer(true)) { + try body + finally libCache.close() + } + } + // Helper constructors def this( @@ -56,6 +70,11 @@ class NodeJSEnv( addEnv: Seq[String]) = this(nodejsPath, addArgs, ExternalJSEnv.splitEnv(addEnv)) + // We need to initialize the libCache first + override def runJS(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): Unit = + withLibCache(super.runJS(classpath, code, logger, console)) + // We need to hack console.log (for duplicate %) override protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("nodeConsoleHack.js").withContent( @@ -75,6 +94,25 @@ class NodeJSEnv( """) ) + /** Libraries are loaded via require in Node.js */ + override protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = { + initFiles(args) ++ + args.classpath.jsLibs.map((requireLibrary _).tupled) ++ + args.classpath.scalaJSCode + } + + /** Rewrites a library virtual file to a require statement if possible */ + protected def requireLibrary(vf: VirtualJSFile, + info: ResolutionInfo): VirtualJSFile = { + info.commonJSName.fold(vf) { varname => + val fname = vf.name + libCache.materialize(vf) + new MemVirtualJSFile(s"require-$fname").withContent( + s"""$varname = require(${toJSstr(fname)});""" + ) + } + } + // Send code to Stdin override protected def sendVMStdin(args: RunJSArgs, out: OutputStream): Unit = { sendJS(getJSFiles(args), out) @@ -82,6 +120,9 @@ class NodeJSEnv( // Node.js specific (system) environment override protected def getVMEnv(args: RunJSArgs): Map[String, String] = - sys.env + ("NODE_MODULE_CONTEXTS" -> "0") ++ additionalEnv + sys.env ++ Seq( + "NODE_MODULE_CONTEXTS" -> "0", + "NODE_PATH" -> libCache.cacheDir.getAbsolutePath + ) ++ additionalEnv } From 577a49f867c6499346988cd9bb5367dc53651429 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 11 Jul 2014 14:29:34 +0200 Subject: [PATCH 014/133] Cross-build Scala.js tools --- .../src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala | 0 .../src/main/scala/scala/scalajs/tools/env/JSConsole.scala | 0 .../src/main/scala/scala/scalajs/tools/env/JSEnv.scala | 0 .../src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tools/{ => shared}/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala (100%) rename tools/{ => shared}/src/main/scala/scala/scalajs/tools/env/JSConsole.scala (100%) rename tools/{ => shared}/src/main/scala/scala/scalajs/tools/env/JSEnv.scala (100%) rename tools/{ => shared}/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala (100%) diff --git a/tools/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala similarity index 100% rename from tools/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala rename to tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala diff --git a/tools/src/main/scala/scala/scalajs/tools/env/JSConsole.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala similarity index 100% rename from tools/src/main/scala/scala/scalajs/tools/env/JSConsole.scala rename to tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala diff --git a/tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala similarity index 100% rename from tools/src/main/scala/scala/scalajs/tools/env/JSEnv.scala rename to tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala diff --git a/tools/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala similarity index 100% rename from tools/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala rename to tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala From 2d9d124dd9921bdda243f62a631f114a44c358b4 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 22 Aug 2014 16:31:08 +0200 Subject: [PATCH 015/133] Implement System.exit() through environment setting (scala-js/scala-js#958) This commit removes support for `__ScalaJSExportsNamespace`. Use `__ScalaJSEnv.exportsNamespace` instead. --- .../scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index efcc20d..a4d5089 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -77,7 +77,7 @@ class NodeJSEnv( // We need to hack console.log (for duplicate %) override protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("nodeConsoleHack.js").withContent( + new MemVirtualJSFile("nodeConsoleHack.js").withContent( """ // Hack console log to duplicate double % signs (function() { @@ -91,6 +91,12 @@ class NodeJSEnv( }; console.log = newLog; })(); + """), + new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( + """ + __ScalaJSEnv = { + exitFunction: function(status) { process.exit(status); } + }; """) ) From b0ab774862db5c6b3a34b02257108c5814356c61 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 11 Sep 2014 08:20:14 +0200 Subject: [PATCH 016/133] Fix scala-js/scala-js#1016: Respect NODE_PATH from the environment. In order to be able to use normal Node.js modules, the NodeJSEnv must respect NODE_PATH from the environment and properly merge it with its own provided library path (for CommonJS-style includes). --- .../scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index a4d5089..fc40374 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -125,10 +125,15 @@ class NodeJSEnv( } // Node.js specific (system) environment - override protected def getVMEnv(args: RunJSArgs): Map[String, String] = + override protected def getVMEnv(args: RunJSArgs): Map[String, String] = { + val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) + val nodePath = libCache.cacheDir.getAbsolutePath + + baseNodePath.fold("")(p => File.pathSeparator + p) + sys.env ++ Seq( "NODE_MODULE_CONTEXTS" -> "0", - "NODE_PATH" -> libCache.cacheDir.getAbsolutePath + "NODE_PATH" -> nodePath ) ++ additionalEnv + } } From 350652ec0cb13eecc7db114291d37acff8b658c2 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 18 Sep 2014 11:02:46 +0200 Subject: [PATCH 017/133] Fix scala-js/scala-js#1054: Automatically enable source-map-support in Node.js --- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index fc40374..145d2f9 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -75,8 +75,23 @@ class NodeJSEnv( logger: Logger, console: JSConsole): Unit = withLibCache(super.runJS(classpath, code, logger, console)) - // We need to hack console.log (for duplicate %) - override protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = Seq( + /** File(s) to automatically install source-map-support. + * Is used by [[initFiles]], override to change/disable. + */ + protected def installSourceMap(args: RunJSArgs): Seq[VirtualJSFile] = Seq( + new MemVirtualJSFile("sourceMapSupport.js").withContent( + """ + try { + require('source-map-support').install(); + } catch (e) {} + """ + ) + ) + + /** File(s) to hack console.log to prevent if from changing `%%` to `%`. + * Is used by [[initFiles]], override to change/disable. + */ + protected def fixPercentConsole(args: RunJSArgs): Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("nodeConsoleHack.js").withContent( """ // Hack console log to duplicate double % signs @@ -91,15 +106,29 @@ class NodeJSEnv( }; console.log = newLog; })(); - """), + """ + ) + ) + + /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`. + * Is used by [[initFiles]], override to change/disable. + */ + protected def runtimeEnv(args: RunJSArgs): Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( """ __ScalaJSEnv = { exitFunction: function(status) { process.exit(status); } }; - """) + """ + ) ) + /** Concatenates results from [[installSourceMap]], [[fixPercentConsole]] and + * [[runtimeEnv]] (in this order). + */ + override protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = + installSourceMap(args) ++ fixPercentConsole(args) ++ runtimeEnv(args) + /** Libraries are loaded via require in Node.js */ override protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = { initFiles(args) ++ From 3dbe808acf0a1c45922e9e816cc232457c5c3a37 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 19 Sep 2014 09:45:45 +0200 Subject: [PATCH 018/133] Remove deprecated members --- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 19 ++----------------- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 15 --------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index 1d9ff13..1551ef4 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -14,10 +14,6 @@ abstract class ExternalJSEnv( final protected val additionalArgs: Seq[String], final protected val additionalEnv: Map[String, String]) extends JSEnv { - @deprecated("Use Map as environment instead", "0.5.3") - def this(additionalArgs: Seq[String], additionalEnv: Seq[String]) = - this(additionalArgs, ExternalJSEnv.splitEnv(additionalEnv)) - import ExternalJSEnv._ /** Printable name of this VM */ @@ -116,10 +112,10 @@ abstract class ExternalJSEnv( protected def getVMArgs(args: RunJSArgs): Seq[String] = additionalArgs /** VM environment. Override to adapt. - * + * * Default is `sys.env` and [[additionalEnv]] */ - protected def getVMEnv(args: RunJSArgs): Map[String, String] = + protected def getVMEnv(args: RunJSArgs): Map[String, String] = sys.env ++ additionalEnv /** Get files that are a library (i.e. that do not run anything) */ @@ -133,17 +129,6 @@ abstract class ExternalJSEnv( } object ExternalJSEnv { - - /** Helper for deprecated constructors */ - @deprecated("This is only a helper for compat constructors", "0.5.3") - def splitEnv(env: Seq[String]): Map[String, String] = { - val tups = for (str <- env) yield { - val Array(name, value) = str.split("=", 2) - (name, value) - } - tups.toMap - } - case class RunJSArgs( classpath: CompleteClasspath, code: VirtualJSFile, diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 145d2f9..6959c7a 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -55,21 +55,6 @@ class NodeJSEnv( def this() = this(None, Seq.empty, Map.empty[String, String]) - // Deprecated compat constructors - - @deprecated("Use Map as environment instead", "0.5.3") - def this(nodejsPath: String, env: Seq[String]) = - this(nodejsPath, env = ExternalJSEnv.splitEnv(env)) - - @deprecated("Use Map as environment instead", "0.5.3") - def this(nodejsPath: String, args: Seq[String], env: Seq[String]) = - this(nodejsPath, args, env = ExternalJSEnv.splitEnv(env)) - - @deprecated("Use Map as environment instead", "0.5.3") - def this(nodejsPath: Option[String], addArgs: Seq[String], - addEnv: Seq[String]) = - this(nodejsPath, addArgs, ExternalJSEnv.splitEnv(addEnv)) - // We need to initialize the libCache first override def runJS(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): Unit = From 7f012bb02c83753f1530c9bc1bf8da56edc9fd82 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 14 Oct 2014 17:38:18 +0200 Subject: [PATCH 019/133] Fix scala-js/scala-js#1163: Remove packager and non-direct full optimizer --- .../scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 6959c7a..f414a5e 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -117,7 +117,7 @@ class NodeJSEnv( /** Libraries are loaded via require in Node.js */ override protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = { initFiles(args) ++ - args.classpath.jsLibs.map((requireLibrary _).tupled) ++ + args.classpath.jsLibs.map((requireLibrary _).tupled) :+ args.classpath.scalaJSCode } From d023c68e2a0549102c5754fd4c408a8bb49ef2cd Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 18 Oct 2014 09:57:20 +0200 Subject: [PATCH 020/133] Refactor JSEnv to support async running --- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 222 ++++++++++-------- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 208 ++++++++-------- .../sbtplugin/test/env/JSEnvTest.scala | 4 +- .../scala/scalajs/tools/env/AsyncJSEnv.scala | 19 ++ .../scalajs/tools/env/AsyncJSRunner.scala | 12 + .../scala/scala/scalajs/tools/env/JSEnv.scala | 6 +- .../scala/scalajs/tools/env/JSRunner.scala | 15 ++ 7 files changed, 288 insertions(+), 198 deletions(-) create mode 100644 tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala create mode 100644 tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala create mode 100644 tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index 1551ef4..9727aa0 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -12,9 +12,7 @@ import scala.io.Source abstract class ExternalJSEnv( final protected val additionalArgs: Seq[String], - final protected val additionalEnv: Map[String, String]) extends JSEnv { - - import ExternalJSEnv._ + final protected val additionalEnv: Map[String, String]) extends AsyncJSEnv { /** Printable name of this VM */ protected def vmName: String @@ -22,117 +20,157 @@ abstract class ExternalJSEnv( /** Command to execute (on shell) for this VM */ protected def executable: String - /** JS files used to setup VM */ - protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = Nil + protected class AbstractExtRunner(protected val classpath: CompleteClasspath, + protected val code: VirtualJSFile, protected val logger: Logger, + protected val console: JSConsole) { + + /** JS files used to setup VM */ + protected def initFiles(): Seq[VirtualJSFile] = Nil + + /** Sends required data to VM Stdin (can throw) */ + protected def sendVMStdin(out: OutputStream): Unit = {} + + /** VM arguments excluding executable. Override to adapt. + * Overrider is responsible to add additionalArgs. + */ + protected def getVMArgs(): Seq[String] = additionalArgs + + /** VM environment. Override to adapt. + * + * Default is `sys.env` and [[additionalEnv]] + */ + protected def getVMEnv(): Map[String, String] = + sys.env ++ additionalEnv + + /** Get files that are a library (i.e. that do not run anything) */ + protected def getLibJSFiles(): Seq[VirtualJSFile] = + initFiles() ++ classpath.allCode + + /** Get all files that are passed to VM (libraries and code) */ + protected def getJSFiles(): Seq[VirtualJSFile] = + getLibJSFiles() :+ code + + /** write a single JS file to a writer using an include fct if appropriate */ + protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = { + file match { + // TODO remove this case. It is VM specific + case file: FileVirtualJSFile => + val fname = toJSstr(file.file.getAbsolutePath) + writer.write(s"require($fname);\n") + case _ => + writer.write(file.content) + writer.write('\n') + } + } - /** Sends required data to VM Stdin (can throw) */ - protected def sendVMStdin(args: RunJSArgs, out: OutputStream): Unit = {} + /** Pipe stdin and stdout from/to VM */ + final protected def pipeVMData(vmInst: Process): Unit = { + // Send stdin to VM. + val out = vmInst.getOutputStream() + try { sendVMStdin(out) } + finally { out.close() } - /** Fire up an instance of the VM and send js input to it. - * Don't care about exceptions. Calling code will catch and display - * an error message. - */ - def runJS(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): Unit = { + // Pipe stdout to console + pipeToConsole(vmInst.getInputStream(), console) - val runJSArgs = RunJSArgs(classpath, code, logger, console) + // We are probably done (stdin is closed). Report any errors + val errSrc = Source.fromInputStream(vmInst.getErrorStream(), "UTF-8") + try { errSrc.getLines.foreach(err => logger.error(err)) } + finally { errSrc.close } + } - val vmInst = startVM(runJSArgs) + /** Wait for the VM to terminate, verify exit code */ + final protected def waitForVM(vmInst: Process): Unit = { + // Make sure we are done. + vmInst.waitFor() - // Prepare and send input to VM - val out = vmInst.getOutputStream() - try { sendVMStdin(runJSArgs, out) } - finally { out.close() } + // Get return value and return + val retVal = vmInst.exitValue + if (retVal != 0) + sys.error(s"$vmName exited with code $retVal") + } - // We are now executing. Pipe stdout to console - pipeToConsole(vmInst.getInputStream(), console) + protected def startVM(): Process = { + val vmArgs = getVMArgs() + val vmEnv = getVMEnv() - // We are probably done (stdin is closed). Report any errors - val errSrc = Source.fromInputStream(vmInst.getErrorStream(), "UTF-8") - try { errSrc.getLines.foreach(err => logger.error(err)) } - finally { errSrc.close } + val allArgs = executable +: vmArgs + val pBuilder = new ProcessBuilder(allArgs: _*) - // Make sure we are done. - vmInst.waitFor() + pBuilder.environment().clear() + for ((name, value) <- vmEnv) + pBuilder.environment().put(name, value) - // Get return value and return - val retVal = vmInst.exitValue - if (retVal != 0) - sys.error(s"$vmName exited with code $retVal") - } + pBuilder.start() + } - /** send a bunch of JS files to an output stream */ - final protected def sendJS(files: Seq[VirtualJSFile], - out: OutputStream): Unit = { - val writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) - try sendJS(files, writer) - finally writer.close() - } + /** send a bunch of JS files to an output stream */ + final protected def sendJS(files: Seq[VirtualJSFile], + out: OutputStream): Unit = { + val writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) + try sendJS(files, writer) + finally writer.close() + } - /** send a bunch of JS files to a writer */ - final protected def sendJS(files: Seq[VirtualJSFile], out: Writer): Unit = - files.foreach { writeJSFile(_, out) } - - /** write a single JS file to a writer using an include fct if appropriate */ - protected def writeJSFile(file: VirtualJSFile, writer: Writer) = { - file match { - case file: FileVirtualJSFile => - val fname = toJSstr(file.file.getAbsolutePath) - writer.write(s"require($fname);\n") - case _ => - writer.write(file.content) - writer.write('\n') + /** send a bunch of JS files to a writer */ + final protected def sendJS(files: Seq[VirtualJSFile], out: Writer): Unit = + files.foreach { writeJSFile(_, out) } + + /** pipe lines from input stream to JSConsole */ + final protected def pipeToConsole(in: InputStream, console: JSConsole) = { + val source = Source.fromInputStream(in, "UTF-8") + try { source.getLines.foreach(console.log _) } + finally { source.close() } } - } - /** pipe lines from input stream to JSConsole */ - final protected def pipeToConsole(in: InputStream, console: JSConsole) = { - val source = Source.fromInputStream(in, "UTF-8") - try { source.getLines.foreach(console.log _) } - finally { source.close() } } - protected def startVM(args: RunJSArgs): Process = { - val vmArgs = getVMArgs(args) - val vmEnv = getVMEnv(args) + protected class ExtRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole + ) extends AbstractExtRunner(classpath, code, logger, console) + with JSRunner { - val allArgs = executable +: vmArgs - val pBuilder = new ProcessBuilder(allArgs: _*) + def run(): Unit = { + val vmInst = startVM() - pBuilder.environment().clear() - for ((name, value) <- vmEnv) - pBuilder.environment().put(name, value) - - pBuilder.start() + pipeVMData(vmInst) + waitForVM(vmInst) + } } - /** VM arguments excluding executable. Override to adapt. - * Overrider is responsible to add additionalArgs. - */ - protected def getVMArgs(args: RunJSArgs): Seq[String] = additionalArgs - - /** VM environment. Override to adapt. - * - * Default is `sys.env` and [[additionalEnv]] - */ - protected def getVMEnv(args: RunJSArgs): Map[String, String] = - sys.env ++ additionalEnv + protected class AsyncExtRunner(classpath: CompleteClasspath, + code: VirtualJSFile, logger: Logger, console: JSConsole + ) extends AbstractExtRunner(classpath, code, logger, console) + with AsyncJSRunner { - /** Get files that are a library (i.e. that do not run anything) */ - protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = - initFiles(args) ++ args.classpath.allCode + private[this] var vmInst: Process = null + private[this] val thread = new Thread { + override def run(): Unit = pipeVMData(vmInst) + } - /** Get all files that are passed to VM (libraries and code) */ - protected def getJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = - getLibJSFiles(args) :+ args.code + def start(): Unit = { + require(vmInst == null, "start() may only be called once") + vmInst = startVM() + thread.start() + } -} + def isRunning(): Boolean = { + require(vmInst != null, "start() must have been called") + // Emulate JDK 8 Process.isAlive + try { + vmInst.exitValue() + false + } catch { + case e: IllegalThreadStateException => + true + } + } -object ExternalJSEnv { - case class RunJSArgs( - classpath: CompleteClasspath, - code: VirtualJSFile, - logger: Logger, - console: JSConsole) + def await(): Unit = { + require(vmInst != null, "start() must have been called") + thread.join() + waitForVM(vmInst) + } + } } diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index f414a5e..19d4b60 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -23,28 +23,14 @@ import scala.scalajs.sbtplugin.JSUtils._ import java.io.{ Console => _, _ } import scala.io.Source -import scala.util.DynamicVariable - class NodeJSEnv( nodejsPath: Option[String], addArgs: Seq[String], addEnv: Map[String, String]) extends ExternalJSEnv(addArgs, addEnv) { - import ExternalJSEnv._ - protected def vmName: String = "node.js" protected def executable: String = nodejsPath.getOrElse("node") - private val _libCache = new DynamicVariable[VirtualFileMaterializer](null) - protected def libCache: VirtualFileMaterializer = _libCache.value - - final protected def withLibCache[T](body: => T): T = { - _libCache.withValue(new VirtualFileMaterializer(true)) { - try body - finally libCache.close() - } - } - // Helper constructors def this( @@ -55,99 +41,119 @@ class NodeJSEnv( def this() = this(None, Seq.empty, Map.empty[String, String]) - // We need to initialize the libCache first - override def runJS(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): Unit = - withLibCache(super.runJS(classpath, code, logger, console)) - - /** File(s) to automatically install source-map-support. - * Is used by [[initFiles]], override to change/disable. - */ - protected def installSourceMap(args: RunJSArgs): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("sourceMapSupport.js").withContent( - """ - try { - require('source-map-support').install(); - } catch (e) {} - """ - ) - ) - - /** File(s) to hack console.log to prevent if from changing `%%` to `%`. - * Is used by [[initFiles]], override to change/disable. - */ - protected def fixPercentConsole(args: RunJSArgs): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("nodeConsoleHack.js").withContent( - """ - // Hack console log to duplicate double % signs - (function() { - var oldLog = console.log; - var newLog = function() { - var args = arguments; - if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { - args[0] = args[0].toString().replace(/%/g, "%%"); - } - oldLog.apply(console, args); - }; - console.log = newLog; - })(); - """ - ) - ) - - /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`. - * Is used by [[initFiles]], override to change/disable. - */ - protected def runtimeEnv(args: RunJSArgs): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( - """ - __ScalaJSEnv = { - exitFunction: function(status) { process.exit(status); } - }; - """ - ) - ) - - /** Concatenates results from [[installSourceMap]], [[fixPercentConsole]] and - * [[runtimeEnv]] (in this order). - */ - override protected def initFiles(args: RunJSArgs): Seq[VirtualJSFile] = - installSourceMap(args) ++ fixPercentConsole(args) ++ runtimeEnv(args) - - /** Libraries are loaded via require in Node.js */ - override protected def getLibJSFiles(args: RunJSArgs): Seq[VirtualJSFile] = { - initFiles(args) ++ - args.classpath.jsLibs.map((requireLibrary _).tupled) :+ - args.classpath.scalaJSCode + override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): JSRunner = { + new NodeRunner(classpath, code, logger, console) } - /** Rewrites a library virtual file to a require statement if possible */ - protected def requireLibrary(vf: VirtualJSFile, - info: ResolutionInfo): VirtualJSFile = { - info.commonJSName.fold(vf) { varname => - val fname = vf.name - libCache.materialize(vf) - new MemVirtualJSFile(s"require-$fname").withContent( - s"""$varname = require(${toJSstr(fname)});""" - ) - } + override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): AsyncJSRunner = { + new AsyncNodeRunner(classpath, code, logger, console) } - // Send code to Stdin - override protected def sendVMStdin(args: RunJSArgs, out: OutputStream): Unit = { - sendJS(getJSFiles(args), out) - } + protected class NodeRunner(classpath: CompleteClasspath, + code: VirtualJSFile, logger: Logger, console: JSConsole + ) extends ExtRunner(classpath, code, logger, console) + with AbstractNodeRunner + + protected class AsyncNodeRunner(classpath: CompleteClasspath, + code: VirtualJSFile, logger: Logger, console: JSConsole + ) extends AsyncExtRunner(classpath, code, logger, console) + with AbstractNodeRunner + + protected trait AbstractNodeRunner extends AbstractExtRunner { + + protected[this] val libCache = new VirtualFileMaterializer(true) + + /** File(s) to automatically install source-map-support. + * Is used by [[initFiles]], override to change/disable. + */ + protected def installSourceMap(): Seq[VirtualJSFile] = Seq( + new MemVirtualJSFile("sourceMapSupport.js").withContent( + """ + try { + require('source-map-support').install(); + } catch (e) {} + """ + ) + ) + + /** File(s) to hack console.log to prevent if from changing `%%` to `%`. + * Is used by [[initFiles]], override to change/disable. + */ + protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq( + new MemVirtualJSFile("nodeConsoleHack.js").withContent( + """ + // Hack console log to duplicate double % signs + (function() { + var oldLog = console.log; + var newLog = function() { + var args = arguments; + if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { + args[0] = args[0].toString().replace(/%/g, "%%"); + } + oldLog.apply(console, args); + }; + console.log = newLog; + })(); + """ + ) + ) + + /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`. + * Is used by [[initFiles]], override to change/disable. + */ + protected def runtimeEnv(): Seq[VirtualJSFile] = Seq( + new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( + """ + __ScalaJSEnv = { + exitFunction: function(status) { process.exit(status); } + }; + """ + ) + ) + + /** Concatenates results from [[installSourceMap]], [[fixPercentConsole]] and + * [[runtimeEnv]] (in this order). + */ + override protected def initFiles(): Seq[VirtualJSFile] = + installSourceMap() ++ fixPercentConsole() ++ runtimeEnv() + + /** Libraries are loaded via require in Node.js */ + override protected def getLibJSFiles(): Seq[VirtualJSFile] = { + initFiles() ++ + classpath.jsLibs.map((requireLibrary _).tupled) :+ + classpath.scalaJSCode + } + + /** Rewrites a library virtual file to a require statement if possible */ + protected def requireLibrary(vf: VirtualJSFile, + info: ResolutionInfo): VirtualJSFile = { + info.commonJSName.fold(vf) { varname => + val fname = vf.name + libCache.materialize(vf) + new MemVirtualJSFile(s"require-$fname").withContent( + s"""$varname = require(${toJSstr(fname)});""" + ) + } + } + + // Send code to Stdin + override protected def sendVMStdin(out: OutputStream): Unit = { + sendJS(getJSFiles(), out) + } - // Node.js specific (system) environment - override protected def getVMEnv(args: RunJSArgs): Map[String, String] = { - val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) - val nodePath = libCache.cacheDir.getAbsolutePath + - baseNodePath.fold("")(p => File.pathSeparator + p) + // Node.js specific (system) environment + override protected def getVMEnv(): Map[String, String] = { + val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) + val nodePath = libCache.cacheDir.getAbsolutePath + + baseNodePath.fold("")(p => File.pathSeparator + p) - sys.env ++ Seq( - "NODE_MODULE_CONTEXTS" -> "0", - "NODE_PATH" -> nodePath - ) ++ additionalEnv + sys.env ++ Seq( + "NODE_MODULE_CONTEXTS" -> "0", + "NODE_PATH" -> nodePath + ) ++ additionalEnv + } } } diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala index be240cc..2a44c80 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala @@ -22,7 +22,7 @@ abstract class JSEnvTest { val console = new StoreJSConsole() val logger = new StoreLogger() - newJSEnv.runJS(emptyCP, code, logger, console) + newJSEnv.jsRunner(emptyCP, code, logger, console).run() val log = logger.getLog @@ -33,7 +33,7 @@ abstract class JSEnvTest { def fails(): Unit = { try { - newJSEnv.runJS(emptyCP, code, NullLogger, NullJSConsole) + newJSEnv.jsRunner(emptyCP, code, NullLogger, NullJSConsole).run() assertTrue("Code snipped should fail", false) } catch { case e: Exception => diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala new file mode 100644 index 0000000..d439ae2 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala @@ -0,0 +1,19 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.logging._ + +trait AsyncJSEnv extends JSEnv { + def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): AsyncJSRunner +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala new file mode 100644 index 0000000..0865993 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala @@ -0,0 +1,12 @@ +package scala.scalajs.tools.env + +trait AsyncJSRunner { + /** Start the associated run and return. */ + def start(): Unit + + /** Checks whether this async runner is still running */ + def isRunning(): Boolean + + /** Await completion of the started Run. Throws if the run failed */ + def await(): Unit +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala index f149d8f..f1fbf44 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala @@ -14,7 +14,7 @@ import scala.scalajs.tools.classpath._ import scala.scalajs.tools.logging._ trait JSEnv { - /** Run the code in the virtual file. Throw if failure */ - def runJS(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): Unit + /** Prepare a runner for the code in the virtual file. */ + def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): JSRunner } diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala new file mode 100644 index 0000000..460fff0 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala @@ -0,0 +1,15 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +trait JSRunner { + /** Run the associated JS code. Throw if an error occurs. */ + def run(): Unit +} From a96ecd6de82c489d177f6ab0a22d6b6ef744d69c Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 3 Nov 2014 14:33:25 +0100 Subject: [PATCH 021/133] Remove JSEnv constructor-blow up. Use default args --- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 19d4b60..b63498c 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -24,22 +24,13 @@ import java.io.{ Console => _, _ } import scala.io.Source class NodeJSEnv( - nodejsPath: Option[String], - addArgs: Seq[String], - addEnv: Map[String, String]) extends ExternalJSEnv(addArgs, addEnv) { + nodejsPath: String = "node", + addArgs: Seq[String] = Seq.empty, + addEnv: Map[String, String] = Map.empty +) extends ExternalJSEnv(addArgs, addEnv) { protected def vmName: String = "node.js" - protected def executable: String = nodejsPath.getOrElse("node") - - // Helper constructors - - def this( - nodejsPath: String, - args: Seq[String] = Seq.empty, - env: Map[String, String] = Map.empty) = - this(Some(nodejsPath), args, env) - - def this() = this(None, Seq.empty, Map.empty[String, String]) + protected def executable: String = nodejsPath override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): JSRunner = { From c98cb199eaa45d9946eed54dfef8056f2e21d885 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 4 Nov 2014 14:40:26 +0100 Subject: [PATCH 022/133] Add ComJSEnv and make RhinoJSEnv a ComJSEnv --- .../scalajs/sbtplugin/test/env/ComTests.scala | 107 ++++++++++++++++++ .../scala/scalajs/tools/env/ComJSEnv.scala | 38 +++++++ .../scala/scalajs/tools/env/ComJSRunner.scala | 18 +++ 3 files changed, 163 insertions(+) create mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala create mode 100644 tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala create mode 100644 tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala new file mode 100644 index 0000000..ba0a6ca --- /dev/null +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala @@ -0,0 +1,107 @@ +package scala.scalajs.sbtplugin.test.env + +import scala.scalajs.tools.env._ +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath.PartialClasspath +import scala.scalajs.tools.logging._ + +import org.junit.Test +import org.junit.Assert._ + +/** A couple of tests that test communication for mix-in into a test suite */ +trait ComTests { + + protected def newJSEnv: ComJSEnv + + private def emptyCP = PartialClasspath.empty.resolve() + + private def comRunner(code: String) = { + val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) + newJSEnv.comRunner(emptyCP, codeVF, + new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) + } + + private def assertThrowClosed(msg: String, body: => Unit): Unit = { + val thrown = try { + body + false + } catch { + case _: ComJSEnv.ComClosedException => + true + } + + assertTrue(msg, thrown) + } + + @Test + def comCloseJVMTest = { + val com = comRunner(s""" + scalajsCom.init(function(msg) { scalajsCom.send("received: " + msg); }); + scalajsCom.send("Hello World"); + """) + + com.start() + + assertEquals("Hello World", com.receive()) + + for (i <- 0 to 10) { + com.send(i.toString) + assertEquals(s"received: $i", com.receive()) + } + + com.close() + com.await() + } + + @Test + def comCloseJSTest = { + val com = comRunner(s""" + scalajsCom.init(function(msg) {}); + for (var i = 0; i < 10; ++i) + scalajsCom.send("msg: " + i); + scalajsCom.close(); + """) + + com.start() + + for (i <- 0 until 10) + assertEquals(s"msg: $i", com.receive()) + + assertThrowClosed("Expect receive to throw after closing of channel", + com.receive()) + + com.await() + } + + @Test + def doubleCloseTest = { + val com = comRunner(s""" + var seen = 0; + scalajsCom.init(function(msg) { + scalajsCom.send("pong"); + if (++seen >= 10) + scalajsCom.close(); + }); + """) + + com.start() + + for (i <- 0 until 10) { + com.send("ping") + assertEquals("pong", com.receive()) + } + + com.close() + com.await() + } + + @Test + def noInitTest = { + val com = comRunner("") + + com.start() + com.send("Dummy") + com.await() + } + +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala new file mode 100644 index 0000000..882e46a --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala @@ -0,0 +1,38 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.env + +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.logging._ + +/** An [[AsyncJSEnv]] that provides communication to and from the JS VM. + * + * Inside the VM there is a global JavaScript object named `scalajsCom` that + * can be used to control the message channel. It's operations are: + * {{{ + * // initialize com (with callback) + * scalajsCom.init(function(msg) { console.log("Received: " + msg); }); + * + * // send a message to host system + * scalajsCom.send("my message"); + * + * // close com (releases callback, allowing VM to terminate) + * scalajsCom.close(); + * }}} + */ +trait ComJSEnv extends AsyncJSEnv { + def comRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): ComJSRunner +} + +object ComJSEnv { + class ComClosedException extends Exception("JSCom has been closed") +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala new file mode 100644 index 0000000..2194217 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala @@ -0,0 +1,18 @@ +package scala.scalajs.tools.env + +trait ComJSRunner extends AsyncJSRunner { + + /** Send a message to the JS VM. Throws if the message cannot be sent. */ + def send(msg: String): Unit + + /** Block until a message is received. Throws a [[ComClosedExcpetion]] + * if the channel is closed before a message is received. + */ + def receive(): String + + /** Close the communication channel. Allows the VM to terminate if it is + * still waiting for callback. + */ + def close(): Unit + +} From 6abedd5c06b3c5e11184c6473c3a762860fe3dfe Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 4 Nov 2014 14:42:25 +0100 Subject: [PATCH 023/133] Make NodeJSEnv a ComJSEnv --- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 141 +++++++++++++++++- .../sbtplugin/test/env/NodeJSTest.scala | 2 +- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index b63498c..9e727cf 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -21,13 +21,15 @@ import scala.scalajs.tools.logging._ import scala.scalajs.sbtplugin.JSUtils._ import java.io.{ Console => _, _ } +import java.net._ + import scala.io.Source class NodeJSEnv( nodejsPath: String = "node", addArgs: Seq[String] = Seq.empty, addEnv: Map[String, String] = Map.empty -) extends ExternalJSEnv(addArgs, addEnv) { +) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv { protected def vmName: String = "node.js" protected def executable: String = nodejsPath @@ -42,6 +44,11 @@ class NodeJSEnv( new AsyncNodeRunner(classpath, code, logger, console) } + override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile, + logger: Logger, console: JSConsole): ComJSRunner = { + new ComNodeRunner(classpath, code, logger, console) + } + protected class NodeRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole ) extends ExtRunner(classpath, code, logger, console) @@ -52,6 +59,138 @@ class NodeJSEnv( ) extends AsyncExtRunner(classpath, code, logger, console) with AbstractNodeRunner + protected class ComNodeRunner(classpath: CompleteClasspath, + code: VirtualJSFile, logger: Logger, console: JSConsole + ) extends AsyncNodeRunner(classpath, code, logger, console) + with ComJSRunner { + + /** Retry-timeout to wait for the JS VM to connect */ + private final val acceptTimeout = 1000 + + private[this] val serverSocket = + new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address + private[this] var comSocket: Socket = _ + private[this] var jvm2js: DataOutputStream = _ + private[this] var js2jvm: DataInputStream = _ + + private def comSetup = new MemVirtualJSFile("comSetup.js").withContent( + s""" + (function() { + // The socket for communication + var socket = null; + // The callback where received messages go + var recvCallback = null; + + // Buffers received data + var inBuffer = new Buffer(0); + + function onData(data) { + inBuffer = Buffer.concat([inBuffer, data]); + tryReadMsg(); + } + + function tryReadMsg() { + if (inBuffer.length < 2) return; + var msgLen = inBuffer.readInt16BE(0); + var byteLen = (msgLen + 1) * 2; + + if (inBuffer.length < byteLen) return; + var res = ""; + + for (var i = 0; i < msgLen; ++i) + res += String.fromCharCode(inBuffer.readInt16BE(2*(i+1))); + + inBuffer = inBuffer.slice(byteLen); + + recvCallback(res); + } + + global.scalajsCom = { + init: function(recvCB) { + if (socket !== null) throw new Error("Com already open"); + + var net = require('net'); + recvCallback = recvCB; + socket = net.connect(${serverSocket.getLocalPort}); + socket.on('data', onData); + }, + send: function(msg) { + if (socket === null) throw new Error("Com not open"); + + var len = msg.length; + var buf = new Buffer(2*(len+1)); + buf.writeInt16BE(len, 0); + for (var i = 0; i < len; ++i) + buf.writeInt16BE(msg.charCodeAt(i), (i+1)*2); + socket.write(buf); + }, + close: function() { + if (socket === null) throw new Error("Com not open"); + socket.end(); + socket.unref(); + } + } + }).call(this); + """ + ) + + def send(msg: String): Unit = { + if (awaitConnection()) { + jvm2js.writeShort(msg.length) + jvm2js.writeChars(msg) + jvm2js.flush() + } + } + + def receive(): String = { + if (!awaitConnection()) + throw new ComJSEnv.ComClosedException + try { + val len = js2jvm.readShort() + val carr = Array.fill(len)(js2jvm.readChar()) + String.valueOf(carr) + } catch { + case e: EOFException => + throw new ComJSEnv.ComClosedException + } + } + + def close(): Unit = { + serverSocket.close() + if (jvm2js != null) + jvm2js.close() + if (js2jvm != null) + js2jvm.close() + if (comSocket != null) + comSocket.close() + } + + /** Waits until the JS VM has established a connection or terminates + * @return true if the connection was established + */ + private def awaitConnection(): Boolean = { + serverSocket.setSoTimeout(acceptTimeout) + while (comSocket == null && isRunning) { + try { + comSocket = serverSocket.accept() + jvm2js = new DataOutputStream( + new BufferedOutputStream(comSocket.getOutputStream())) + js2jvm = new DataInputStream( + new BufferedInputStream(comSocket.getInputStream())) + } catch { + case to: SocketTimeoutException => + } + } + + comSocket != null + } + + override protected def initFiles(): Seq[VirtualJSFile] = + super.initFiles :+ comSetup + + override protected def finalize(): Unit = close() + } + protected trait AbstractNodeRunner extends AbstractExtRunner { protected[this] val libCache = new VirtualFileMaterializer(true) diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala index 1a1bcc3..9a58b5c 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala @@ -4,7 +4,7 @@ import scala.scalajs.sbtplugin.env.nodejs.NodeJSEnv import org.junit.Test -class NodeJSTest extends JSEnvTest { +class NodeJSTest extends JSEnvTest with ComTests { protected def newJSEnv = new NodeJSEnv From 7744c349236bbdbc47d9a0b0770a32588ba0070b Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 6 Nov 2014 17:04:38 +0100 Subject: [PATCH 024/133] Get rid of unnamed tuple in CompleteClasspath --- .../scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index 9e727cf..f0ea983 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -252,16 +252,15 @@ class NodeJSEnv( /** Libraries are loaded via require in Node.js */ override protected def getLibJSFiles(): Seq[VirtualJSFile] = { initFiles() ++ - classpath.jsLibs.map((requireLibrary _).tupled) :+ + classpath.jsLibs.map(requireLibrary) :+ classpath.scalaJSCode } /** Rewrites a library virtual file to a require statement if possible */ - protected def requireLibrary(vf: VirtualJSFile, - info: ResolutionInfo): VirtualJSFile = { - info.commonJSName.fold(vf) { varname => - val fname = vf.name - libCache.materialize(vf) + protected def requireLibrary(dep: ResolvedJSDependency): VirtualJSFile = { + dep.info.commonJSName.fold(dep.lib) { varname => + val fname = dep.lib.name + libCache.materialize(dep.lib) new MemVirtualJSFile(s"require-$fname").withContent( s"""$varname = require(${toJSstr(fname)});""" ) From 509d7a0fe3dcae193694bbe242831ba251c753af Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 7 Nov 2014 09:05:25 +0100 Subject: [PATCH 025/133] Fix scala-js/scala-js#1240: Add stop() to AsyncJSEnv --- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 21 +++++++++++++++++- .../scalajs/sbtplugin/test/env/ComTests.scala | 22 +++++++++++++++++++ .../scalajs/tools/env/AsyncJSRunner.scala | 3 +++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index 9727aa0..e4b7ba6 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -144,8 +144,16 @@ abstract class ExternalJSEnv( with AsyncJSRunner { private[this] var vmInst: Process = null + private[this] var ioThreadEx: Throwable = null + private[this] val thread = new Thread { - override def run(): Unit = pipeVMData(vmInst) + override def run(): Unit = { + try { + pipeVMData(vmInst) + } catch { + case e: Throwable => ioThreadEx = e + } + } } def start(): Unit = { @@ -154,6 +162,11 @@ abstract class ExternalJSEnv( thread.start() } + def stop(): Unit = { + require(vmInst != null, "start() must have been called") + vmInst.destroy() + } + def isRunning(): Boolean = { require(vmInst != null, "start() must have been called") // Emulate JDK 8 Process.isAlive @@ -170,6 +183,12 @@ abstract class ExternalJSEnv( require(vmInst != null, "start() must have been called") thread.join() waitForVM(vmInst) + + // At this point, the VM itself didn't fail. We need to check if + // anything bad happened while piping the data from the VM + + if (ioThreadEx != null) + throw ioThreadEx } } diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala index ba0a6ca..05c5a80 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala @@ -104,4 +104,26 @@ trait ComTests { com.await() } + @Test + def stopTest = { + val com = comRunner(s"""scalajsCom.init(function(msg) {});""") + + com.start() + + // Make sure the VM doesn't terminate. + Thread.sleep(1000) + + assertTrue("VM should still be running", com.isRunning) + + // Stop VM instead of closing channel + com.stop() + + try { + com.await() + fail("Stopped VM should be in failure state") + } catch { + case _: Throwable => + } + } + } diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala index 0865993..1e1f560 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala @@ -4,6 +4,9 @@ trait AsyncJSRunner { /** Start the associated run and return. */ def start(): Unit + /** Abort the associated run */ + def stop(): Unit + /** Checks whether this async runner is still running */ def isRunning(): Boolean From b2f790ae9451044bb51d80aecd21f4bd0126dd46 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 10 Nov 2014 15:00:09 +0100 Subject: [PATCH 026/133] Fix scala-js/scala-js#1239: Don't assume in ExtJSEnv that VM supports require --- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 12 +++--------- .../scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index e4b7ba6..8392689 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -52,15 +52,9 @@ abstract class ExternalJSEnv( /** write a single JS file to a writer using an include fct if appropriate */ protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = { - file match { - // TODO remove this case. It is VM specific - case file: FileVirtualJSFile => - val fname = toJSstr(file.file.getAbsolutePath) - writer.write(s"require($fname);\n") - case _ => - writer.write(file.content) - writer.write('\n') - } + // The only platform-independent way to do this in JS is to dump the file. + writer.write(file.content) + writer.write('\n') } /** Pipe stdin and stdout from/to VM */ diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index f0ea983..e27258a 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -272,6 +272,20 @@ class NodeJSEnv( sendJS(getJSFiles(), out) } + /** write a single JS file to a writer using an include fct if appropriate + * uses `require` if the file exists on the filesystem + */ + override protected def writeJSFile(file: VirtualJSFile, + writer: Writer): Unit = { + file match { + case file: FileVirtualJSFile => + val fname = toJSstr(file.file.getAbsolutePath) + writer.write(s"require($fname);\n") + case _ => + super.writeJSFile(file, writer) + } + } + // Node.js specific (system) environment override protected def getVMEnv(): Map[String, String] = { val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) From a797c418b2bce95754152f343d58b8a678a70ed8 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 11 Nov 2014 14:06:25 +0100 Subject: [PATCH 027/133] Fix scala-js/scala-js#1241: Add Future based API to AsyncJSEnv --- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 21 ++++++++++++++----- .../scalajs/sbtplugin/test/env/ComTests.scala | 13 ++++++++++++ .../scalajs/tools/env/AsyncJSRunner.scala | 8 +++++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index 8392689..e0aa557 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -10,6 +10,9 @@ import scala.scalajs.sbtplugin.JSUtils._ import java.io.{ Console => _, _ } import scala.io.Source +import scala.concurrent.{Future, Promise} +import scala.util.Try + abstract class ExternalJSEnv( final protected val additionalArgs: Seq[String], final protected val additionalEnv: Map[String, String]) extends AsyncJSEnv { @@ -139,21 +142,29 @@ abstract class ExternalJSEnv( private[this] var vmInst: Process = null private[this] var ioThreadEx: Throwable = null + private[this] val promise = Promise[Unit] private[this] val thread = new Thread { override def run(): Unit = { - try { - pipeVMData(vmInst) - } catch { - case e: Throwable => ioThreadEx = e + // This thread should not be interrupted, so it is safe to use Trys + val pipeResult = Try(pipeVMData(vmInst)) + val vmComplete = Try(waitForVM(vmInst)) + + // Store IO exception + pipeResult recover { + case e => ioThreadEx = e } + + // Chain Try's the other way: We want VM failure first, then IO failure + promise.complete(pipeResult orElse vmComplete) } } - def start(): Unit = { + def start(): Future[Unit] = { require(vmInst == null, "start() may only be called once") vmInst = startVM() thread.start() + promise.future } def stop(): Unit = { diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala index 05c5a80..8f36467 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala @@ -8,6 +8,9 @@ import scala.scalajs.tools.logging._ import org.junit.Test import org.junit.Assert._ +import scala.concurrent.Await +import scala.concurrent.duration.Duration + /** A couple of tests that test communication for mix-in into a test suite */ trait ComTests { @@ -126,4 +129,14 @@ trait ComTests { } } + @Test + def futureTest = { + val runner = comRunner("") + val fut = runner.start() + + Await.result(fut, Duration.Inf) + + assertFalse("VM should be terminated", runner.isRunning) + } + } diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala index 1e1f560..09e2dda 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala @@ -1,8 +1,12 @@ package scala.scalajs.tools.env +import scala.concurrent.Future + trait AsyncJSRunner { - /** Start the associated run and return. */ - def start(): Unit + /** Start the associated run and returns a Future that completes when the run + * terminates. + */ + def start(): Future[Unit] /** Abort the associated run */ def stop(): Unit From 8dada1ff9af0136c9128fcee6bd28f088ceaca8e Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 14 Nov 2014 15:42:30 +0100 Subject: [PATCH 028/133] Fix scala-js/scala-js#1266: Proper connection closing on Node.js ComJSEnv --- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 6 ++- .../sbtplugin/test/env/AsyncTests.scala | 37 +++++++++++++++++++ .../scalajs/sbtplugin/test/env/ComTests.scala | 28 ++++++-------- .../scala/scalajs/tools/env/ComJSRunner.scala | 6 ++- 4 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/AsyncTests.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index e27258a..b540d2a 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -127,7 +127,6 @@ class NodeJSEnv( close: function() { if (socket === null) throw new Error("Com not open"); socket.end(); - socket.unref(); } } }).call(this); @@ -165,6 +164,11 @@ class NodeJSEnv( comSocket.close() } + override def stop(): Unit = { + close() + super.stop() + } + /** Waits until the JS VM has established a connection or terminates * @return true if the connection was established */ diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/AsyncTests.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/AsyncTests.scala new file mode 100644 index 0000000..422c17b --- /dev/null +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/AsyncTests.scala @@ -0,0 +1,37 @@ +package scala.scalajs.sbtplugin.test.env + +import scala.scalajs.tools.env._ +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath.PartialClasspath +import scala.scalajs.tools.logging._ + +import org.junit.Test +import org.junit.Assert._ + +import scala.concurrent.Await +import scala.concurrent.duration.Duration + +/** A couple of tests that test communication for mix-in into a test suite */ +trait AsyncTests { + + protected def newJSEnv: AsyncJSEnv + + private def emptyCP = PartialClasspath.empty.resolve() + + private def asyncRunner(code: String) = { + val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) + newJSEnv.asyncRunner(emptyCP, codeVF, + new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) + } + + @Test + def futureTest = { + val runner = asyncRunner("") + val fut = runner.start() + + Await.result(fut, Duration.Inf) + + assertFalse("VM should be terminated", runner.isRunning) + } + +} diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala index 8f36467..4be5627 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala @@ -8,11 +8,8 @@ import scala.scalajs.tools.logging._ import org.junit.Test import org.junit.Assert._ -import scala.concurrent.Await -import scala.concurrent.duration.Duration - /** A couple of tests that test communication for mix-in into a test suite */ -trait ComTests { +trait ComTests extends AsyncTests { protected def newJSEnv: ComJSEnv @@ -56,8 +53,7 @@ trait ComTests { com.await() } - @Test - def comCloseJSTest = { + def comCloseJSTestCommon(timeout: Long) = { val com = comRunner(s""" scalajsCom.init(function(msg) {}); for (var i = 0; i < 10; ++i) @@ -67,15 +63,24 @@ trait ComTests { com.start() + Thread.sleep(timeout) + for (i <- 0 until 10) assertEquals(s"msg: $i", com.receive()) assertThrowClosed("Expect receive to throw after closing of channel", com.receive()) + com.close() com.await() } + @Test + def comCloseJSTest = comCloseJSTestCommon(0) + + @Test + def comCloseJSTestDelayed = comCloseJSTestCommon(1000) + @Test def doubleCloseTest = { val com = comRunner(s""" @@ -104,6 +109,7 @@ trait ComTests { com.start() com.send("Dummy") + com.close() com.await() } @@ -129,14 +135,4 @@ trait ComTests { } } - @Test - def futureTest = { - val runner = comRunner("") - val fut = runner.start() - - Await.result(fut, Duration.Inf) - - assertFalse("VM should be terminated", runner.isRunning) - } - } diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala index 2194217..44302b8 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala @@ -11,7 +11,11 @@ trait ComJSRunner extends AsyncJSRunner { def receive(): String /** Close the communication channel. Allows the VM to terminate if it is - * still waiting for callback. + * still waiting for callback. The JVM side **must** call close in + * order to be able to expect termination of the VM. + * + * Calling [[stop]] on a [ComJSRunner]] automatically closes the + * channel. */ def close(): Unit From 478bba997f8763b07612bf53f6aceb5e137c16be Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 18 Nov 2014 17:56:42 +0100 Subject: [PATCH 029/133] Increase max supported message size for NodeJSEnv --- .../sbtplugin/env/nodejs/NodeJSEnv.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index b540d2a..dfabe23 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -90,15 +90,15 @@ class NodeJSEnv( } function tryReadMsg() { - if (inBuffer.length < 2) return; - var msgLen = inBuffer.readInt16BE(0); - var byteLen = (msgLen + 1) * 2; + if (inBuffer.length < 4) return; + var msgLen = inBuffer.readInt32BE(0); + var byteLen = 4 + msgLen * 2; if (inBuffer.length < byteLen) return; var res = ""; for (var i = 0; i < msgLen; ++i) - res += String.fromCharCode(inBuffer.readInt16BE(2*(i+1))); + res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); inBuffer = inBuffer.slice(byteLen); @@ -118,10 +118,10 @@ class NodeJSEnv( if (socket === null) throw new Error("Com not open"); var len = msg.length; - var buf = new Buffer(2*(len+1)); - buf.writeInt16BE(len, 0); + var buf = new Buffer(4 + len * 2); + buf.writeInt32BE(len, 0); for (var i = 0; i < len; ++i) - buf.writeInt16BE(msg.charCodeAt(i), (i+1)*2); + buf.writeInt16BE(msg.charCodeAt(i), 4 + i * 2); socket.write(buf); }, close: function() { @@ -135,7 +135,7 @@ class NodeJSEnv( def send(msg: String): Unit = { if (awaitConnection()) { - jvm2js.writeShort(msg.length) + jvm2js.writeInt(msg.length) jvm2js.writeChars(msg) jvm2js.flush() } @@ -145,7 +145,7 @@ class NodeJSEnv( if (!awaitConnection()) throw new ComJSEnv.ComClosedException try { - val len = js2jvm.readShort() + val len = js2jvm.readInt() val carr = Array.fill(len)(js2jvm.readChar()) String.valueOf(carr) } catch { From 3064e5d2c9d6929375e7f727f7b11b8763b6e03d Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 25 Nov 2014 10:30:24 +0100 Subject: [PATCH 030/133] Additional tests for ComJSEnvs --- .../scalajs/sbtplugin/test/env/ComTests.scala | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala index 4be5627..31963e5 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala @@ -83,18 +83,12 @@ trait ComTests extends AsyncTests { @Test def doubleCloseTest = { - val com = comRunner(s""" - var seen = 0; - scalajsCom.init(function(msg) { - scalajsCom.send("pong"); - if (++seen >= 10) - scalajsCom.close(); - }); - """) + val n = 10 + val com = pingPongRunner(n) com.start() - for (i <- 0 until 10) { + for (i <- 0 until n) { com.send("ping") assertEquals("pong", com.receive()) } @@ -103,6 +97,39 @@ trait ComTests extends AsyncTests { com.await() } + @Test + def multiEnvTest = { + val n = 10 + val envs = List.fill(5)(pingPongRunner(10)) + + envs.foreach(_.start()) + + val ops = List[ComJSRunner => Unit]( + _.send("ping"), + com => assertEquals("pong", com.receive()) + ) + + for { + i <- 0 until n + env <- envs + op <- ops + } op(env) + + envs.foreach(_.close()) + envs.foreach(_.await()) + } + + private def pingPongRunner(count: Int) = { + comRunner(s""" + var seen = 0; + scalajsCom.init(function(msg) { + scalajsCom.send("pong"); + if (++seen >= $count) + scalajsCom.close(); + }); + """) + } + @Test def noInitTest = { val com = comRunner("") From 6203577937b8f2c65f56b71d8a579464013a59ea Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 25 Nov 2014 10:31:12 +0100 Subject: [PATCH 031/133] Fix scala-js/scala-js#1304: Fragment messages to limit total size to 32KB --- .../scalajs/sbtplugin/test/env/ComTests.scala | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala index 31963e5..c16decd 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala +++ b/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala @@ -130,6 +130,47 @@ trait ComTests extends AsyncTests { """) } + @Test + def largeMessageTest = { + // 1KB data + val baseMsg = new String(Array.tabulate(512)(_.toChar)) + val baseLen = baseMsg.length + + // Max message size: 1KB * 2^(2*iters+1) = 1MB + val iters = 4 + + val com = comRunner(""" + scalajsCom.init(function(msg) { + scalajsCom.send(msg + msg); + }); + """) + + com.start() + + com.send(baseMsg) + + def resultFactor(iters: Int) = Math.pow(2, 2 * iters + 1).toInt + + for (i <- 0 until iters) { + val reply = com.receive() + + val factor = resultFactor(i) + + assertEquals(baseLen * factor, reply.length) + + for (j <- 0 until factor) + assertEquals(baseMsg, reply.substring(j * baseLen, (j + 1) * baseLen)) + + com.send(reply + reply) + } + + val lastLen = com.receive().length + assertEquals(baseLen * resultFactor(iters), lastLen) + + com.close() + com.await() + } + @Test def noInitTest = { val com = comRunner("") From 5573bbe2c695a20a5816cb2b0fbcc2bcbcc273d4 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 26 Nov 2014 14:47:08 +0100 Subject: [PATCH 032/133] Centralize overriding of stop in ComJSEnv --- .../scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala | 5 ----- .../main/scala/scala/scalajs/tools/env/ComJSRunner.scala | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index dfabe23..c24ae6a 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -164,11 +164,6 @@ class NodeJSEnv( comSocket.close() } - override def stop(): Unit = { - close() - super.stop() - } - /** Waits until the JS VM has established a connection or terminates * @return true if the connection was established */ diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala index 44302b8..1af8ee1 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala @@ -19,4 +19,10 @@ trait ComJSRunner extends AsyncJSRunner { */ def close(): Unit + /** Abort the associated run. Also closes the communication channel. */ + abstract override def stop(): Unit = { + close() + super.stop() + } + } From 1fba71fbc201c0deb4d6739b0330b975fd1ac64e Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 27 Nov 2014 07:13:56 +0100 Subject: [PATCH 033/133] Improve information in thrown ComClosedExceptions --- .../scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala | 4 ++-- .../main/scala/scala/scalajs/tools/env/ComJSEnv.scala | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index c24ae6a..b0d36cb 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -143,14 +143,14 @@ class NodeJSEnv( def receive(): String = { if (!awaitConnection()) - throw new ComJSEnv.ComClosedException + throw new ComJSEnv.ComClosedException("Node.js isn't connected") try { val len = js2jvm.readInt() val carr = Array.fill(len)(js2jvm.readChar()) String.valueOf(carr) } catch { case e: EOFException => - throw new ComJSEnv.ComClosedException + throw new ComJSEnv.ComClosedException(e) } } diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala index 882e46a..790998b 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala +++ b/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala @@ -34,5 +34,12 @@ trait ComJSEnv extends AsyncJSEnv { } object ComJSEnv { - class ComClosedException extends Exception("JSCom has been closed") + private final val defaultMsg = "JSCom has been closed" + + class ComClosedException(msg: String, + cause: Throwable) extends Exception(msg, cause) { + def this() = this(defaultMsg, null) + def this(cause: Throwable) = this(defaultMsg, cause) + def this(msg: String) = this(msg, null) + } } From ee10cd404057c5712cf69c4668c62b738af91806 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 20 Nov 2014 15:37:05 +0100 Subject: [PATCH 034/133] Fix scala-js/scala-js#1073: New test framework interface (aligned with sbt) --- .../main/scala/scala/scalajs/sbtplugin/JSUtils.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala index a59f105..673ff55 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala @@ -32,4 +32,15 @@ object JSUtils { def dot2bracket(name: String): String = { name.split('.').map(s => s"""[${toJSstr(s)}]""").mkString } + + def selectOnGlobal(name: String): String = { + // If we are running in Node.js, we need to bracket select on + // global rather than this + jsGlobalExpr + dot2bracket(name) + } + + /** A JavaScript expression that has the global scope as value */ + val jsGlobalExpr: String = + """((typeof global === "object" && global && + global["Object"] === Object) ? global : this)""" } From 8688060e49a89c91e235b3372f3481492c1ae3f3 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 27 Nov 2014 14:18:29 +0100 Subject: [PATCH 035/133] Decouple VirtualFileMaterializer from sbt.IO --- .../env/VirtualFileMaterializer.scala | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala index fca1c47..5dde9b3 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala @@ -1,22 +1,23 @@ package scala.scalajs.sbtplugin.env -import scala.scalajs.tools.io.{IO => _, _} +import scala.annotation.tailrec -import sbt.IO +import scala.scalajs.tools.io._ import java.io.File /** A helper class to temporarily store virtual files to the filesystem. - * + * * Can be used with tools that require real files. * @param singleDir if true, forces files to be copied into * [[cacheDir]]. Useful to setup include directories for * example. */ final class VirtualFileMaterializer(singleDir: Boolean = false) { + import VirtualFileMaterializer._ val cacheDir = { - val dir = IO.createTemporaryDirectory + val dir = createTempDir() dir.deleteOnExit() dir } @@ -30,28 +31,21 @@ final class VirtualFileMaterializer(singleDir: Boolean = false) { f } - private def materializeFileVF(vf: FileVirtualFile): File = { - if (!singleDir) vf.file - else { - val trg = trgFile(vf.name) - IO.copyFile(vf.file, trg) - trg - } - } - def materialize(vf: VirtualTextFile): File = vf match { - case vf: FileVirtualFile => materializeFileVF(vf) + case vf: FileVirtualFile if !singleDir => + vf.file case _ => val trg = trgFile(vf.name) - IO.write(trg, vf.content) + IO.copyTo(vf, WritableFileVirtualTextFile(trg)) trg } def materialize(vf: VirtualBinaryFile): File = vf match { - case vf: FileVirtualFile => materializeFileVF(vf) + case vf: FileVirtualFile if !singleDir => + vf.file case _ => val trg = trgFile(vf.name) - IO.write(trg, vf.content) + IO.copyTo(vf, WritableFileVirtualBinaryFile(trg)) trg } @@ -64,4 +58,32 @@ final class VirtualFileMaterializer(singleDir: Boolean = false) { cacheDir.delete() } + /** Taken from Guava: + * https://github.com/google/guava/blob/1c285fc8d289c43b46aa55e7f90ec0359be5b69a/guava/src/com/google/common/io/Files.java#L413-L426 + */ + private def createTempDir(): File = { + val baseDir = new File(System.getProperty("java.io.tmpdir")) + val baseName = System.currentTimeMillis() + "-" + + @tailrec + def loop(tries: Int): File = { + val tempDir = new File(baseDir, baseName + tries) + if (tempDir.mkdir()) + tempDir + else if (tries < TempDirAttempts) + loop(tries + 1) + else { + throw new IllegalStateException("Failed to create directory within " + + s"$TempDirAttempts attempts (tried ${baseName}0 to " + + s"${baseName}${TempDirAttempts - 1})") + } + } + + loop(0) + } + +} + +object VirtualFileMaterializer { + private final val TempDirAttempts = 10000 } From a4fe854afffdae2cbe98192868734716ecd52ac4 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 27 Nov 2014 15:50:57 +0100 Subject: [PATCH 036/133] Remove sbtplugin.JSUtils --- .../scala/scalajs/sbtplugin/JSUtils.scala | 46 ------------------- .../scalajs/sbtplugin/env/ExternalJSEnv.scala | 2 - .../sbtplugin/env/nodejs/NodeJSEnv.scala | 11 ++--- 3 files changed, 5 insertions(+), 54 deletions(-) delete mode 100644 sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala deleted file mode 100644 index 673ff55..0000000 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala +++ /dev/null @@ -1,46 +0,0 @@ -package scala.scalajs.sbtplugin - -object JSUtils { - def listToJS(xs: List[String]): String = - xs.map(toJSstr _).mkString("[",",","]") - - /** (almost) stolen from scala.scalajs.compiler.JSPrinters */ - def toJSstr(str: String): String = { - /* Note that Java and JavaScript happen to use the same encoding for - * Unicode, namely UTF-16, which means that 1 char from Java always equals - * 1 char in JavaScript. */ - val builder = new StringBuilder() - builder.append('"') - str foreach { - case '\\' => builder.append("\\\\") - case '"' => builder.append("\\\"") - case '\u0007' => builder.append("\\a") - case '\u0008' => builder.append("\\b") - case '\u0009' => builder.append("\\t") - case '\u000A' => builder.append("\\n") - case '\u000B' => builder.append("\\v") - case '\u000C' => builder.append("\\f") - case '\u000D' => builder.append("\\r") - case c => - if (c >= 32 && c <= 126) builder.append(c.toChar) // ASCII printable characters - else builder.append(f"\\u$c%04x") - } - builder.append('"') - builder.result() - } - - def dot2bracket(name: String): String = { - name.split('.').map(s => s"""[${toJSstr(s)}]""").mkString - } - - def selectOnGlobal(name: String): String = { - // If we are running in Node.js, we need to bracket select on - // global rather than this - jsGlobalExpr + dot2bracket(name) - } - - /** A JavaScript expression that has the global scope as value */ - val jsGlobalExpr: String = - """((typeof global === "object" && global && - global["Object"] === Object) ? global : this)""" -} diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala index e0aa557..2923f25 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala @@ -5,8 +5,6 @@ import scala.scalajs.tools.classpath._ import scala.scalajs.tools.env._ import scala.scalajs.tools.logging._ -import scala.scalajs.sbtplugin.JSUtils._ - import java.io.{ Console => _, _ } import scala.io.Source diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala index b0d36cb..2e98fb3 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala @@ -10,7 +10,8 @@ package scala.scalajs.sbtplugin.env.nodejs import scala.scalajs.sbtplugin.env._ -import scala.scalajs.sbtplugin.JSUtils.toJSstr + +import scala.scalajs.ir.Utils.escapeJS import scala.scalajs.tools.io._ import scala.scalajs.tools.classpath._ @@ -18,8 +19,6 @@ import scala.scalajs.tools.env._ import scala.scalajs.tools.jsdep._ import scala.scalajs.tools.logging._ -import scala.scalajs.sbtplugin.JSUtils._ - import java.io.{ Console => _, _ } import java.net._ @@ -261,7 +260,7 @@ class NodeJSEnv( val fname = dep.lib.name libCache.materialize(dep.lib) new MemVirtualJSFile(s"require-$fname").withContent( - s"""$varname = require(${toJSstr(fname)});""" + s"""$varname = require("${escapeJS(fname)}");""" ) } } @@ -278,8 +277,8 @@ class NodeJSEnv( writer: Writer): Unit = { file match { case file: FileVirtualJSFile => - val fname = toJSstr(file.file.getAbsolutePath) - writer.write(s"require($fname);\n") + val fname = file.file.getAbsolutePath + writer.write(s"""require("${escapeJS(fname)}");\n""") case _ => super.writeJSFile(file, writer) } From 150ee0246ff5153b34d767c75ff4c39ce1e54ec0 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 27 Nov 2014 09:22:34 +0100 Subject: [PATCH 037/133] Fix scala-js/scala-js#1287: Reorganize package names and artifacts --- .../scala/org/scalajs/jsenv}/AsyncJSEnv.scala | 8 ++++---- .../scala/org/scalajs/jsenv}/AsyncJSRunner.scala | 2 +- .../main/scala/org/scalajs/jsenv}/ComJSEnv.scala | 8 ++++---- .../scala/org/scalajs/jsenv}/ComJSRunner.scala | 2 +- .../org/scalajs/jsenv}/ConsoleJSConsole.scala | 2 +- .../scala/org/scalajs/jsenv}/ExternalJSEnv.scala | 9 ++++----- .../main/scala/org/scalajs/jsenv}/JSConsole.scala | 2 +- .../src/main/scala/org/scalajs/jsenv}/JSEnv.scala | 8 ++++---- .../main/scala/org/scalajs/jsenv}/JSRunner.scala | 2 +- .../scala/org/scalajs/jsenv}/NullJSConsole.scala | 2 +- .../scalajs/jsenv}/VirtualFileMaterializer.scala | 4 ++-- .../org/scalajs/jsenv}/nodejs/NodeJSEnv.scala | 15 +++++++-------- .../org/scalajs/jsenv/test}/AsyncTests.scala | 11 ++++++----- .../scala/org/scalajs/jsenv/test}/ComTests.scala | 11 ++++++----- .../scala/org/scalajs/jsenv/test}/JSEnvTest.scala | 12 ++++++------ .../org/scalajs/jsenv/test}/NodeJSTest.scala | 4 ++-- .../org/scalajs/jsenv/test}/StoreJSConsole.scala | 4 ++-- .../org/scalajs/jsenv/test}/StoreLogger.scala | 4 ++-- 18 files changed, 55 insertions(+), 55 deletions(-) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/AsyncJSEnv.scala (82%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/AsyncJSRunner.scala (92%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/ComJSEnv.scala (91%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/ComJSRunner.scala (96%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/ConsoleJSConsole.scala (95%) rename {sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env => js-envs/src/main/scala/org/scalajs/jsenv}/ExternalJSEnv.scala (97%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/JSConsole.scala (95%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/JSEnv.scala (83%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/JSRunner.scala (95%) rename {tools/shared/src/main/scala/scala/scalajs/tools/env => js-envs/src/main/scala/org/scalajs/jsenv}/NullJSConsole.scala (70%) rename {sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env => js-envs/src/main/scala/org/scalajs/jsenv}/VirtualFileMaterializer.scala (97%) rename {sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env => js-envs/src/main/scala/org/scalajs/jsenv}/nodejs/NodeJSEnv.scala (96%) rename {sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env => js-envs/src/test/scala/org/scalajs/jsenv/test}/AsyncTests.scala (78%) rename {sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env => js-envs/src/test/scala/org/scalajs/jsenv/test}/ComTests.scala (95%) rename {sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env => js-envs/src/test/scala/org/scalajs/jsenv/test}/JSEnvTest.scala (76%) rename {sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env => js-envs/src/test/scala/org/scalajs/jsenv/test}/NodeJSTest.scala (91%) rename {sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env => js-envs/src/test/scala/org/scalajs/jsenv/test}/StoreJSConsole.scala (74%) rename {sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env => js-envs/src/test/scala/org/scalajs/jsenv/test}/StoreLogger.scala (88%) diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala similarity index 82% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala index d439ae2..01c5922 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala @@ -7,11 +7,11 @@ \* */ -package scala.scalajs.tools.env +package org.scalajs.jsenv -import scala.scalajs.tools.io._ -import scala.scalajs.tools.classpath._ -import scala.scalajs.tools.logging._ +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.classpath._ +import org.scalajs.core.tools.logging._ trait AsyncJSEnv extends JSEnv { def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile, diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala similarity index 92% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 09e2dda..5d78774 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -1,4 +1,4 @@ -package scala.scalajs.tools.env +package org.scalajs.jsenv import scala.concurrent.Future diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala similarity index 91% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala index 790998b..bd52341 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala @@ -7,11 +7,11 @@ \* */ -package scala.scalajs.tools.env +package org.scalajs.jsenv -import scala.scalajs.tools.io._ -import scala.scalajs.tools.classpath._ -import scala.scalajs.tools.logging._ +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.classpath._ +import org.scalajs.core.tools.logging._ /** An [[AsyncJSEnv]] that provides communication to and from the JS VM. * diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala similarity index 96% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala index 1af8ee1..9bdf01c 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/ComJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala @@ -1,4 +1,4 @@ -package scala.scalajs.tools.env +package org.scalajs.jsenv trait ComJSRunner extends AsyncJSRunner { diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala similarity index 95% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala index 5b3d055..6426350 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/ConsoleJSConsole.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala @@ -7,7 +7,7 @@ \* */ -package scala.scalajs.tools.env +package org.scalajs.jsenv /** A JS console that prints on the console */ object ConsoleJSConsole extends JSConsole { diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala similarity index 97% rename from sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 2923f25..d06a377 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -1,9 +1,8 @@ -package scala.scalajs.sbtplugin.env +package org.scalajs.jsenv -import scala.scalajs.tools.io._ -import scala.scalajs.tools.classpath._ -import scala.scalajs.tools.env._ -import scala.scalajs.tools.logging._ +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.classpath._ +import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } import scala.io.Source diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala similarity index 95% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala index a93768f..c671ca1 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSConsole.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala @@ -7,7 +7,7 @@ \* */ -package scala.scalajs.tools.env +package org.scalajs.jsenv /** Trait representing a JS console */ trait JSConsole { diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala similarity index 83% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index f1fbf44..38552bb 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -7,11 +7,11 @@ \* */ -package scala.scalajs.tools.env +package org.scalajs.jsenv -import scala.scalajs.tools.io._ -import scala.scalajs.tools.classpath._ -import scala.scalajs.tools.logging._ +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.classpath._ +import org.scalajs.core.tools.logging._ trait JSEnv { /** Prepare a runner for the code in the virtual file. */ diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala similarity index 95% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala index 460fff0..7a0b0b6 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/JSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala @@ -7,7 +7,7 @@ \* */ -package scala.scalajs.tools.env +package org.scalajs.jsenv trait JSRunner { /** Run the associated JS code. Throw if an error occurs. */ diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala similarity index 70% rename from tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala index 8147bbe..24b677d 100644 --- a/tools/shared/src/main/scala/scala/scalajs/tools/env/NullJSConsole.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala @@ -1,4 +1,4 @@ -package scala.scalajs.tools.env +package org.scalajs.jsenv object NullJSConsole extends JSConsole { def log(msg: Any): Unit = {} diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala similarity index 97% rename from sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala index 5dde9b3..f6838ac 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala @@ -1,8 +1,8 @@ -package scala.scalajs.sbtplugin.env +package org.scalajs.jsenv import scala.annotation.tailrec -import scala.scalajs.tools.io._ +import org.scalajs.core.tools.io._ import java.io.File diff --git a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala similarity index 96% rename from sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala rename to js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 2e98fb3..e9cca3f 100644 --- a/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -7,17 +7,16 @@ \* */ -package scala.scalajs.sbtplugin.env.nodejs +package org.scalajs.jsenv.nodejs -import scala.scalajs.sbtplugin.env._ +import org.scalajs.jsenv._ -import scala.scalajs.ir.Utils.escapeJS +import org.scalajs.core.ir.Utils.escapeJS -import scala.scalajs.tools.io._ -import scala.scalajs.tools.classpath._ -import scala.scalajs.tools.env._ -import scala.scalajs.tools.jsdep._ -import scala.scalajs.tools.logging._ +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.classpath._ +import org.scalajs.core.tools.jsdep._ +import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } import java.net._ diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala similarity index 78% rename from sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/AsyncTests.scala rename to js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index 422c17b..af06026 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -1,9 +1,10 @@ -package scala.scalajs.sbtplugin.test.env +package org.scalajs.jsenv.test -import scala.scalajs.tools.env._ -import scala.scalajs.tools.io._ -import scala.scalajs.tools.classpath.PartialClasspath -import scala.scalajs.tools.logging._ +import org.scalajs.jsenv._ + +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.classpath.PartialClasspath +import org.scalajs.core.tools.logging._ import org.junit.Test import org.junit.Assert._ diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala similarity index 95% rename from sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala rename to js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index c16decd..6167a65 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -1,9 +1,10 @@ -package scala.scalajs.sbtplugin.test.env +package org.scalajs.jsenv.test -import scala.scalajs.tools.env._ -import scala.scalajs.tools.io._ -import scala.scalajs.tools.classpath.PartialClasspath -import scala.scalajs.tools.logging._ +import org.scalajs.jsenv._ + +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.classpath.PartialClasspath +import org.scalajs.core.tools.logging._ import org.junit.Test import org.junit.Assert._ diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala similarity index 76% rename from sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala rename to js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala index 2a44c80..5a31cf0 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/JSEnvTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -1,10 +1,10 @@ -package scala.scalajs.sbtplugin.test.env +package org.scalajs.jsenv.test -import scala.scalajs.tools.env.JSEnv -import scala.scalajs.tools.io.MemVirtualJSFile -import scala.scalajs.tools.classpath.PartialClasspath -import scala.scalajs.tools.logging.NullLogger -import scala.scalajs.tools.env.NullJSConsole +import org.scalajs.jsenv._ + +import org.scalajs.core.tools.io.MemVirtualJSFile +import org.scalajs.core.tools.classpath.PartialClasspath +import org.scalajs.core.tools.logging.NullLogger import org.junit.Assert._ diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala similarity index 91% rename from sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala rename to js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index 9a58b5c..e033db5 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/NodeJSTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -1,6 +1,6 @@ -package scala.scalajs.sbtplugin.test.env +package org.scalajs.jsenv.test -import scala.scalajs.sbtplugin.env.nodejs.NodeJSEnv +import org.scalajs.jsenv.nodejs.NodeJSEnv import org.junit.Test diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreJSConsole.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreJSConsole.scala similarity index 74% rename from sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreJSConsole.scala rename to js-envs/src/test/scala/org/scalajs/jsenv/test/StoreJSConsole.scala index 9c7a84a..f4e60da 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreJSConsole.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreJSConsole.scala @@ -1,6 +1,6 @@ -package scala.scalajs.sbtplugin.test.env +package org.scalajs.jsenv.test -import scala.scalajs.tools.env._ +import org.scalajs.jsenv._ class StoreJSConsole extends JSConsole { private[this] val buf = new StringBuilder() diff --git a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreLogger.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala similarity index 88% rename from sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreLogger.scala rename to js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala index 985b149..f4aa837 100644 --- a/sbt-plugin/src/test/scala/scala/scalajs/sbtplugin/test/env/StoreLogger.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala @@ -1,6 +1,6 @@ -package scala.scalajs.sbtplugin.test.env +package org.scalajs.jsenv.test -import scala.scalajs.tools.logging._ +import org.scalajs.core.tools.logging._ import scala.collection.mutable.ListBuffer From 2b3dee73672506052fd5195e3c1a2ca41b800f0a Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 28 Nov 2014 13:47:02 +0100 Subject: [PATCH 038/133] Fix scala-js/scala-js#1325: Repeatedly decode incoming Node.js Com messages --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 19 +++++++------- .../org/scalajs/jsenv/test/AsyncTests.scala | 6 ++--- .../org/scalajs/jsenv/test/ComTests.scala | 5 +--- .../org/scalajs/jsenv/test/NodeJSTest.scala | 25 +++++++++++++++++++ 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index e9cca3f..71983bd 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -88,19 +88,20 @@ class NodeJSEnv( } function tryReadMsg() { - if (inBuffer.length < 4) return; - var msgLen = inBuffer.readInt32BE(0); - var byteLen = 4 + msgLen * 2; + while (inBuffer.length >= 4) { + var msgLen = inBuffer.readInt32BE(0); + var byteLen = 4 + msgLen * 2; - if (inBuffer.length < byteLen) return; - var res = ""; + if (inBuffer.length < byteLen) return; + var res = ""; - for (var i = 0; i < msgLen; ++i) - res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); + for (var i = 0; i < msgLen; ++i) + res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); - inBuffer = inBuffer.slice(byteLen); + inBuffer = inBuffer.slice(byteLen); - recvCallback(res); + recvCallback(res); + } } global.scalajsCom = { diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index af06026..971011a 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -3,7 +3,7 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath.PartialClasspath +import org.scalajs.core.tools.classpath._ import org.scalajs.core.tools.logging._ import org.junit.Test @@ -17,9 +17,9 @@ trait AsyncTests { protected def newJSEnv: AsyncJSEnv - private def emptyCP = PartialClasspath.empty.resolve() + protected def emptyCP: CompleteClasspath = PartialClasspath.empty.resolve() - private def asyncRunner(code: String) = { + protected def asyncRunner(code: String): AsyncJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) newJSEnv.asyncRunner(emptyCP, codeVF, new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 6167a65..0c7ff3b 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -3,7 +3,6 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath.PartialClasspath import org.scalajs.core.tools.logging._ import org.junit.Test @@ -14,9 +13,7 @@ trait ComTests extends AsyncTests { protected def newJSEnv: ComJSEnv - private def emptyCP = PartialClasspath.empty.resolve() - - private def comRunner(code: String) = { + protected def comRunner(code: String): ComJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) newJSEnv.comRunner(emptyCP, codeVF, new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index e033db5..ac89ff7 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -3,6 +3,7 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv.nodejs.NodeJSEnv import org.junit.Test +import org.junit.Assert._ class NodeJSTest extends JSEnvTest with ComTests { @@ -51,4 +52,28 @@ class NodeJSTest extends JSEnvTest with ComTests { |""".stripMargin } + @Test + def slowJSEnvTest = { + val com = comRunner(""" + setTimeout(function() { + scalajsCom.init(function(msg) { + scalajsCom.send("pong: " + msg); + }); + }, 1000); + """) + + val n = 20 + + com.start() + + for (_ <- 1 to n) + com.send("ping") + + for (_ <- 1 to n) + assertEquals(com.receive(), "pong: ping") + + com.close() + com.await() + } + } From 3a1164569f95d7d9de9064236a4d2c276c474472 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 28 Nov 2014 15:32:13 +0100 Subject: [PATCH 039/133] Get rid of ugly isAlive hack --- .../src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index d06a377..9339b5c 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -171,14 +171,7 @@ abstract class ExternalJSEnv( def isRunning(): Boolean = { require(vmInst != null, "start() must have been called") - // Emulate JDK 8 Process.isAlive - try { - vmInst.exitValue() - false - } catch { - case e: IllegalThreadStateException => - true - } + !promise.isCompleted } def await(): Unit = { From 5eec47d9bebf3375e2b41e8a7fb8f5032c498cba Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 28 Nov 2014 15:43:12 +0100 Subject: [PATCH 040/133] Fix scala-js/scala-js#1328: Handle socket errors in Node.js com --- .../src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 71983bd..ea60aa0 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -112,6 +112,12 @@ class NodeJSEnv( recvCallback = recvCB; socket = net.connect(${serverSocket.getLocalPort}); socket.on('data', onData); + socket.on('error', function(err) { + socket.end(); + // EPIPE on write is expected if the JVM closes + if (err.syscall !== "write" || err.code !== "EPIPE") + throw err; + }); }, send: function(msg) { if (socket === null) throw new Error("Com not open"); From de318a2c392a12fee0f69be8efe0cb336e7b6f07 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 30 Nov 2014 17:04:21 +0100 Subject: [PATCH 041/133] Add a JSEnv test for future completion when stopping (closes scala-js/scala-js#1339) --- .../org/scalajs/jsenv/test/ComTests.scala | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 0c7ff3b..285d89d 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -8,6 +8,9 @@ import org.scalajs.core.tools.logging._ import org.junit.Test import org.junit.Assert._ +import scala.concurrent.Await +import scala.concurrent.duration.Duration + /** A couple of tests that test communication for mix-in into a test suite */ trait ComTests extends AsyncTests { @@ -201,4 +204,26 @@ trait ComTests extends AsyncTests { } } + @Test + def futureStopTest = { + val com = comRunner(s"""scalajsCom.init(function(msg) {});""") + + val fut = com.start() + + // Make sure the VM doesn't terminate. + Thread.sleep(1000) + + assertTrue("VM should still be running", com.isRunning) + + // Stop VM instead of closing channel + com.stop() + + try { + Await.result(fut, Duration.Inf) + fail("Stopped VM should be in failure state") + } catch { + case _: Throwable => + } + } + } From 707827308b7691a7a3d9082c15564bb182bd77d7 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 30 Nov 2014 17:17:09 +0100 Subject: [PATCH 042/133] Restructure AsyncJSRunner to be Future based --- .../org/scalajs/jsenv/AsyncJSRunner.scala | 33 +++++++++++++++---- .../org/scalajs/jsenv/ExternalJSEnv.scala | 21 ++---------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 5d78774..8146204 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -1,19 +1,38 @@ package org.scalajs.jsenv -import scala.concurrent.Future +import scala.concurrent.{Future, Await} +import scala.concurrent.duration.Duration trait AsyncJSRunner { - /** Start the associated run and returns a Future that completes when the run - * terminates. + + /** A future that completes when the associated run has terminated. */ + def future: Future[Unit] + + /** + * Start the associated run and returns a Future that completes + * when the run terminates. The returned Future is equivalent to + * the one returned by [[future]]. */ def start(): Future[Unit] /** Abort the associated run */ def stop(): Unit - /** Checks whether this async runner is still running */ - def isRunning(): Boolean + /** + * Checks whether this async runner is still running. Strictly + * equivalent to + * + * {{{ + * !future.isCompleted + * }}} + */ + final def isRunning(): Boolean = !future.isCompleted - /** Await completion of the started Run. Throws if the run failed */ - def await(): Unit + /** Await completion of the started Run. Strictly equivalent to + * + * {{{ + * Await.result(future, Duration.Inf) + * }}} + */ + final def await(): Unit = Await.result(future, Duration.Inf) } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 9339b5c..78c7625 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -157,34 +157,19 @@ abstract class ExternalJSEnv( } } + def future: Future[Unit] = promise.future + def start(): Future[Unit] = { require(vmInst == null, "start() may only be called once") vmInst = startVM() thread.start() - promise.future + future } def stop(): Unit = { require(vmInst != null, "start() must have been called") vmInst.destroy() } - - def isRunning(): Boolean = { - require(vmInst != null, "start() must have been called") - !promise.isCompleted - } - - def await(): Unit = { - require(vmInst != null, "start() must have been called") - thread.join() - waitForVM(vmInst) - - // At this point, the VM itself didn't fail. We need to check if - // anything bad happened while piping the data from the VM - - if (ioThreadEx != null) - throw ioThreadEx - } } } From a75473b43ba394fc48a725715e84871c0fae14f2 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 30 Nov 2014 17:30:39 +0100 Subject: [PATCH 043/133] Fix scala-js/scala-js#1315: Add timeout to AsyncJSRunner.await and use it --- .../scala/org/scalajs/jsenv/AsyncJSRunner.scala | 11 +++++++++++ .../org/scalajs/jsenv/test/AsyncTests.scala | 6 ++++-- .../scala/org/scalajs/jsenv/test/ComTests.scala | 17 ++++++++--------- .../org/scalajs/jsenv/test/NodeJSTest.scala | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 8146204..977f6c7 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -35,4 +35,15 @@ trait AsyncJSRunner { * }}} */ final def await(): Unit = Await.result(future, Duration.Inf) + + /** Await completion of the started Run for the duration specified + * by [[atMost]]. Strictly equivalent to: + * + * {{{ + * Await.result(future, atMost) + * }}} + * + */ + final def await(atMost: Duration): Unit = Await.result(future, atMost) + } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index 971011a..c764497 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -10,11 +10,13 @@ import org.junit.Test import org.junit.Assert._ import scala.concurrent.Await -import scala.concurrent.duration.Duration +import scala.concurrent.duration._ /** A couple of tests that test communication for mix-in into a test suite */ trait AsyncTests { + protected final val DefaultTimeout: Duration = 10.seconds + protected def newJSEnv: AsyncJSEnv protected def emptyCP: CompleteClasspath = PartialClasspath.empty.resolve() @@ -30,7 +32,7 @@ trait AsyncTests { val runner = asyncRunner("") val fut = runner.start() - Await.result(fut, Duration.Inf) + Await.result(fut, DefaultTimeout) assertFalse("VM should be terminated", runner.isRunning) } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 285d89d..2b95a36 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -9,7 +9,6 @@ import org.junit.Test import org.junit.Assert._ import scala.concurrent.Await -import scala.concurrent.duration.Duration /** A couple of tests that test communication for mix-in into a test suite */ trait ComTests extends AsyncTests { @@ -51,7 +50,7 @@ trait ComTests extends AsyncTests { } com.close() - com.await() + com.await(DefaultTimeout) } def comCloseJSTestCommon(timeout: Long) = { @@ -73,7 +72,7 @@ trait ComTests extends AsyncTests { com.receive()) com.close() - com.await() + com.await(DefaultTimeout) } @Test @@ -95,7 +94,7 @@ trait ComTests extends AsyncTests { } com.close() - com.await() + com.await(DefaultTimeout) } @Test @@ -117,7 +116,7 @@ trait ComTests extends AsyncTests { } op(env) envs.foreach(_.close()) - envs.foreach(_.await()) + envs.foreach(_.await(DefaultTimeout)) } private def pingPongRunner(count: Int) = { @@ -169,7 +168,7 @@ trait ComTests extends AsyncTests { assertEquals(baseLen * resultFactor(iters), lastLen) com.close() - com.await() + com.await(DefaultTimeout) } @Test @@ -179,7 +178,7 @@ trait ComTests extends AsyncTests { com.start() com.send("Dummy") com.close() - com.await() + com.await(DefaultTimeout) } @Test @@ -197,7 +196,7 @@ trait ComTests extends AsyncTests { com.stop() try { - com.await() + com.await(DefaultTimeout) fail("Stopped VM should be in failure state") } catch { case _: Throwable => @@ -219,7 +218,7 @@ trait ComTests extends AsyncTests { com.stop() try { - Await.result(fut, Duration.Inf) + Await.result(fut, DefaultTimeout) fail("Stopped VM should be in failure state") } catch { case _: Throwable => diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index ac89ff7..9054836 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -73,7 +73,7 @@ class NodeJSTest extends JSEnvTest with ComTests { assertEquals(com.receive(), "pong: ping") com.close() - com.await() + com.await(DefaultTimeout) } } From c2706f5779187124625d274234644930a0d23599 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 1 Dec 2014 17:15:12 +0100 Subject: [PATCH 044/133] Fix scala-js/scala-js#1343: Add a RetryingComJSEnv and use it for CI --- .../src/test/scala/org/scalajs/jsenv/test/ComTests.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 2b95a36..9318675 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -15,10 +15,12 @@ trait ComTests extends AsyncTests { protected def newJSEnv: ComJSEnv + protected def logger: Logger = + new ScalaConsoleLogger(Level.Warn) + protected def comRunner(code: String): ComJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) - newJSEnv.comRunner(emptyCP, codeVF, - new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) + newJSEnv.comRunner(emptyCP, codeVF, logger, ConsoleJSConsole) } private def assertThrowClosed(msg: String, body: => Unit): Unit = { From 4f976a1230d4b1994842f3375fbfb31c82256e54 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 4 Dec 2014 13:50:35 +0100 Subject: [PATCH 045/133] Fix scala-js/scala-js#1059: Add a (basic) way to turn off source mapping --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index ea60aa0..87409c4 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -23,12 +23,21 @@ import java.net._ import scala.io.Source -class NodeJSEnv( - nodejsPath: String = "node", - addArgs: Seq[String] = Seq.empty, - addEnv: Map[String, String] = Map.empty +class NodeJSEnv private ( + nodejsPath: String, + addArgs: Seq[String], + addEnv: Map[String, String], + sourceMap: Boolean ) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv { + def this(nodejsPath: String = "node", addArgs: Seq[String] = Seq.empty, + addEnv: Map[String, String] = Map.empty) = { + this(nodejsPath, addArgs, addEnv, sourceMap = true) + } + + def withSourceMap(sourceMap: Boolean): NodeJSEnv = + new NodeJSEnv(nodejsPath, addArgs, addEnv, sourceMap) + protected def vmName: String = "node.js" protected def executable: String = nodejsPath @@ -202,15 +211,17 @@ class NodeJSEnv( /** File(s) to automatically install source-map-support. * Is used by [[initFiles]], override to change/disable. */ - protected def installSourceMap(): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("sourceMapSupport.js").withContent( - """ - try { - require('source-map-support').install(); - } catch (e) {} - """ - ) - ) + protected def installSourceMap(): Seq[VirtualJSFile] = { + if (sourceMap) Seq( + new MemVirtualJSFile("sourceMapSupport.js").withContent( + """ + try { + require('source-map-support').install(); + } catch (e) {} + """ + ) + ) else Seq() + } /** File(s) to hack console.log to prevent if from changing `%%` to `%`. * Is used by [[initFiles]], override to change/disable. From 8340efb01755d40accf87c8c37f4d3fd1055559d Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 3 Dec 2014 08:48:46 +0100 Subject: [PATCH 046/133] Only fail JSEnv test if log is bad --- .../scala/org/scalajs/jsenv/test/JSEnvTest.scala | 13 ++++++++++--- .../scala/org/scalajs/jsenv/test/StoreLogger.scala | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala index 5a31cf0..d4d2080 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -4,10 +4,12 @@ import org.scalajs.jsenv._ import org.scalajs.core.tools.io.MemVirtualJSFile import org.scalajs.core.tools.classpath.PartialClasspath -import org.scalajs.core.tools.logging.NullLogger +import org.scalajs.core.tools.logging._ import org.junit.Assert._ +import StoreLogger._ + abstract class JSEnvTest { protected def newJSEnv: JSEnv @@ -25,9 +27,14 @@ abstract class JSEnvTest { newJSEnv.jsRunner(emptyCP, code, logger, console).run() val log = logger.getLog + val hasBadLog = log exists { + case Log(level, _) if level >= Level.Warn => true + case Trace(_) => true + case _ => false + } - assertTrue("VM shouldn't produce log. Log:\n" + - log.mkString("\n"), log.isEmpty) + assertFalse("VM shouldn't log errors, warnings or traces. Log:\n" + + log.mkString("\n"), hasBadLog) assertEquals("Output should match", expectedOut, console.getLog) } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala index f4aa837..5d97dc3 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala @@ -21,9 +21,9 @@ class StoreLogger extends Logger { object StoreLogger { - abstract class LogElem - case class Log(level: Level, message: String) extends LogElem - case class Success(message: String) extends LogElem - case class Trace(t: Throwable) extends LogElem + sealed trait LogElem + final case class Log(level: Level, message: String) extends LogElem + final case class Success(message: String) extends LogElem + final case class Trace(t: Throwable) extends LogElem } From a2363a6cd96667c04c758509ec8fe08e08b1da29 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 2 Dec 2014 11:19:53 +0100 Subject: [PATCH 047/133] Fix scala-js/scala-js#1321: Implement setTimeout (et al.) in Rhino --- .../org/scalajs/jsenv/test/ComTests.scala | 2 +- .../org/scalajs/jsenv/test/NodeJSTest.scala | 2 +- .../scalajs/jsenv/test/TimeoutComTests.scala | 101 ++++++++++++++ .../org/scalajs/jsenv/test/TimeoutTests.scala | 132 ++++++++++++++++++ 4 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala create mode 100644 js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 9318675..7c0b066 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -184,7 +184,7 @@ trait ComTests extends AsyncTests { } @Test - def stopTest = { + def stopTestCom = { val com = comRunner(s"""scalajsCom.init(function(msg) {});""") com.start() diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index 9054836..0066984 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -5,7 +5,7 @@ import org.scalajs.jsenv.nodejs.NodeJSEnv import org.junit.Test import org.junit.Assert._ -class NodeJSTest extends JSEnvTest with ComTests { +class NodeJSTest extends TimeoutComTests { protected def newJSEnv = new NodeJSEnv diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala new file mode 100644 index 0000000..de28bcd --- /dev/null +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -0,0 +1,101 @@ +package org.scalajs.jsenv.test + +import org.junit.Test +import org.junit.Assert._ + +import scala.concurrent.duration._ + +trait TimeoutComTests extends TimeoutTests with ComTests { + + @Test + def delayedInitTest = { + + val com = comRunner(s""" + setTimeout(function() { + scalajsCom.init(function(msg) { + scalajsCom.send("Got: " + msg); + }); + }, 100); + """) + + val deadline = 100.millis.fromNow + + com.start() + + com.send("Hello World") + + assertEquals("Got: Hello World", com.receive()) + + assertTrue("Execution took too little time", deadline.isOverdue()) + + com.close() + com.await(DefaultTimeout) + + } + + @Test + def delayedReplyTest = { + + val com = comRunner(s""" + scalajsCom.init(function(msg) { + setTimeout(scalajsCom.send, 20, "Got: " + msg); + }); + """) + + com.start() + + for (i <- 1 to 10) { + val deadline = 19.millis.fromNow // give some slack + com.send(s"Hello World: $i") + assertEquals(s"Got: Hello World: $i", com.receive()) + assertTrue("Execution took too little time", deadline.isOverdue()) + } + + com.close() + com.await(DefaultTimeout) + + } + + @Test + def intervalSendTest = { + + val com = comRunner(s""" + scalajsCom.init(function(msg) {}); + var interval = setInterval(scalajsCom.send, 50, "Hello"); + setTimeout(clearInterval, 295, interval); + """) + + val deadline = 245.millis.fromNow + + com.start() + + for (i <- 1 to 5) + assertEquals("Hello", com.receive()) + + com.close() + com.await(DefaultTimeout) + + assertTrue("Execution took too little time", deadline.isOverdue()) + + } + + @Test + def stopTestTimeout = { + + val async = asyncRunner(s""" + setInterval(function() {}, 0); + """) + + async.start() + async.stop() + + try { + async.await(DefaultTimeout) + fail("Expected await to fail") + } catch { + case t: Throwable => // all is well + } + + } + +} diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala new file mode 100644 index 0000000..23d566a --- /dev/null +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala @@ -0,0 +1,132 @@ +package org.scalajs.jsenv.test + +import org.junit.Test +import org.junit.Assert._ + +import scala.concurrent.duration._ + +trait TimeoutTests extends JSEnvTest { + + @Test + def basicTimeoutTest = { + + val deadline = 300.millis.fromNow + + """ + setTimeout(function() { console.log("1"); }, 200); + setTimeout(function() { console.log("2"); }, 100); + setTimeout(function() { console.log("3"); }, 300); + setTimeout(function() { console.log("4"); }, 0); + """ hasOutput + """|4 + |2 + |1 + |3 + |""".stripMargin + + assertTrue("Execution took too little time", deadline.isOverdue()) + + } + + @Test + def clearTimeoutTest = { + + val deadline = 300.millis.fromNow + + """ + var c = setTimeout(function() { console.log("1"); }, 200); + setTimeout(function() { + console.log("2"); + clearTimeout(c); + }, 100); + setTimeout(function() { console.log("3"); }, 300); + setTimeout(function() { console.log("4"); }, 0); + """ hasOutput + """|4 + |2 + |3 + |""".stripMargin + + assertTrue("Execution took too little time", deadline.isOverdue()) + + } + + @Test + def timeoutArgTest = { + + val deadline = 300.millis.fromNow + + """ + setTimeout(function(a, b) { console.log("1" + a + b); }, 200, "foo", "bar"); + setTimeout(function() { console.log("2"); }, 100); + setTimeout(function(msg) { console.log(msg); }, 300, "Hello World"); + setTimeout(function() { console.log("4"); }, 0); + """ hasOutput + """|4 + |2 + |1foobar + |Hello World + |""".stripMargin + + assertTrue("Execution took too little time", deadline.isOverdue()) + + } + + @Test + def intervalTest = { + + val deadline = 1.second.fromNow + + """ + var i1 = setInterval(function() { console.log("each 203"); }, 203); + var i2 = setInterval(function() { console.log("each 301"); }, 301); + var i3 = setInterval(function() { console.log("each 103"); }, 103); + + setTimeout(function() { + clearInterval(i1); + clearInterval(i2); + clearInterval(i3); + }, 1000); + """ hasOutput + """|each 103 + |each 203 + |each 103 + |each 301 + |each 103 + |each 203 + |each 103 + |each 103 + |each 301 + |each 203 + |each 103 + |each 103 + |each 203 + |each 103 + |each 301 + |each 103 + |""".stripMargin + + assertTrue("Execution took too little time", deadline.isOverdue()) + + } + + @Test + def intervalSelfClearTest = { + + val deadline = 100.millis.fromNow + + """ + var c = 0; + var i = setInterval(function() { + c++; + console.log(c.toString()); + if (c >= 10) + clearInterval(i); + }, 10); + """ hasOutput (1 to 10).map(_ + "\n").mkString + + assertTrue("Execution took too little time", deadline.isOverdue()) + + } + +} From da14e870005794ba9602ea484d63c236dd95cff0 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 11 Dec 2014 22:38:44 +0100 Subject: [PATCH 048/133] Fix scala-js/scala-js#1377: Remove flakyness of JSEnv test The timeout tests rely on timing which is inherently unrealiable. By ensuring large enough intervals between (expected) events, we hopefully can eliminate flakyness. This commit increases intervals from the order of 1ms to 10ms. --- .../org/scalajs/jsenv/test/TimeoutTests.scala | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala index 23d566a..c1cfdc3 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala @@ -78,9 +78,9 @@ trait TimeoutTests extends JSEnvTest { val deadline = 1.second.fromNow """ - var i1 = setInterval(function() { console.log("each 203"); }, 203); - var i2 = setInterval(function() { console.log("each 301"); }, 301); - var i3 = setInterval(function() { console.log("each 103"); }, 103); + var i1 = setInterval(function() { console.log("each 230"); }, 230); + var i2 = setInterval(function() { console.log("each 310"); }, 310); + var i3 = setInterval(function() { console.log("each 130"); }, 130); setTimeout(function() { clearInterval(i1); @@ -88,22 +88,20 @@ trait TimeoutTests extends JSEnvTest { clearInterval(i3); }, 1000); """ hasOutput - """|each 103 - |each 203 - |each 103 - |each 301 - |each 103 - |each 203 - |each 103 - |each 103 - |each 301 - |each 203 - |each 103 - |each 103 - |each 203 - |each 103 - |each 301 - |each 103 + """|each 130 + |each 230 + |each 130 + |each 310 + |each 130 + |each 230 + |each 130 + |each 310 + |each 130 + |each 230 + |each 130 + |each 130 + |each 230 + |each 310 |""".stripMargin assertTrue("Execution took too little time", deadline.isOverdue()) From a9c4fe9dc1e42b49f7d6152d57d9de7b2c067652 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 12 Dec 2014 08:36:44 +0100 Subject: [PATCH 049/133] Add source map support detection to NodeJSEnv --- .../org/scalajs/jsenv/ExternalJSEnv.scala | 14 ++++++++++++-- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 19 +++++++++++++++++++ .../org/scalajs/jsenv/test/AsyncTests.scala | 2 +- .../org/scalajs/jsenv/test/JSEnvTest.scala | 4 ++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 78c7625..c8ce61f 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -14,6 +14,8 @@ abstract class ExternalJSEnv( final protected val additionalArgs: Seq[String], final protected val additionalEnv: Map[String, String]) extends AsyncJSEnv { + import ExternalJSEnv._ + /** Printable name of this VM */ protected def vmName: String @@ -73,7 +75,10 @@ abstract class ExternalJSEnv( finally { errSrc.close } } - /** Wait for the VM to terminate, verify exit code */ + /** Wait for the VM to terminate, verify exit code + * + * @throws NonZeroExitException if VM returned a non-zero code + */ final protected def waitForVM(vmInst: Process): Unit = { // Make sure we are done. vmInst.waitFor() @@ -81,7 +86,7 @@ abstract class ExternalJSEnv( // Get return value and return val retVal = vmInst.exitValue if (retVal != 0) - sys.error(s"$vmName exited with code $retVal") + throw new NonZeroExitException(vmName, retVal) } protected def startVM(): Process = { @@ -173,3 +178,8 @@ abstract class ExternalJSEnv( } } + +object ExternalJSEnv { + final case class NonZeroExitException(vmName: String, retVal: Int) + extends Exception(s"$vmName exited with code $retVal") +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 87409c4..620b0a3 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -38,6 +38,25 @@ class NodeJSEnv private ( def withSourceMap(sourceMap: Boolean): NodeJSEnv = new NodeJSEnv(nodejsPath, addArgs, addEnv, sourceMap) + /** True, if the installed node executable supports source mapping. + * + * Do `npm install source-map-support` if you need source maps. + */ + lazy val hasSourceMapSupport: Boolean = { + val code = new MemVirtualJSFile("source-map-support-probe.js") + .withContent("""require('source-map-support').install();""") + val runner = + jsRunner(CompleteClasspath.empty, code, NullLogger, NullJSConsole) + + try { + runner.run() + true + } catch { + case t: ExternalJSEnv.NonZeroExitException => + false + } + } + protected def vmName: String = "node.js" protected def executable: String = nodejsPath diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index c764497..83e1f75 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -19,7 +19,7 @@ trait AsyncTests { protected def newJSEnv: AsyncJSEnv - protected def emptyCP: CompleteClasspath = PartialClasspath.empty.resolve() + protected def emptyCP: CompleteClasspath = CompleteClasspath.empty protected def asyncRunner(code: String): AsyncJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala index d4d2080..210753b 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -3,7 +3,7 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ import org.scalajs.core.tools.io.MemVirtualJSFile -import org.scalajs.core.tools.classpath.PartialClasspath +import org.scalajs.core.tools.classpath.CompleteClasspath import org.scalajs.core.tools.logging._ import org.junit.Assert._ @@ -16,7 +16,7 @@ abstract class JSEnvTest { implicit class RunMatcher(codeStr: String) { - val emptyCP = PartialClasspath.empty.resolve() + val emptyCP = CompleteClasspath.empty val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) def hasOutput(expectedOut: String): Unit = { From a3869824dd67909f4bc0e0a8286fd988cf8d6f17 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 11 Dec 2014 23:56:19 +0100 Subject: [PATCH 050/133] Automatically set correct tags for tests --- js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 620b0a3..a91cd01 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -27,7 +27,7 @@ class NodeJSEnv private ( nodejsPath: String, addArgs: Seq[String], addEnv: Map[String, String], - sourceMap: Boolean + val sourceMap: Boolean ) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv { def this(nodejsPath: String = "node", addArgs: Seq[String] = Seq.empty, From 87cab2a59b23e3e337087d7f7f5c62ada99bcba6 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 15 Dec 2014 12:39:42 +0100 Subject: [PATCH 051/133] Fix scala-js/scala-js#1377 (again): Stabilize JSEnv timeout tests By sufficiently increasing the absolute time difference between events we test, we hopefully remove flakyness. --- .../org/scalajs/jsenv/test/TimeoutTests.scala | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala index c1cfdc3..8a0afaf 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala @@ -78,30 +78,30 @@ trait TimeoutTests extends JSEnvTest { val deadline = 1.second.fromNow """ - var i1 = setInterval(function() { console.log("each 230"); }, 230); - var i2 = setInterval(function() { console.log("each 310"); }, 310); - var i3 = setInterval(function() { console.log("each 130"); }, 130); + var i1 = setInterval(function() { console.log("each 2200"); }, 2200); + var i2 = setInterval(function() { console.log("each 3100"); }, 3100); + var i3 = setInterval(function() { console.log("each 1300"); }, 1300); setTimeout(function() { clearInterval(i1); clearInterval(i2); clearInterval(i3); - }, 1000); + }, 10000); """ hasOutput - """|each 130 - |each 230 - |each 130 - |each 310 - |each 130 - |each 230 - |each 130 - |each 310 - |each 130 - |each 230 - |each 130 - |each 130 - |each 230 - |each 310 + """|each 1300 + |each 2200 + |each 1300 + |each 3100 + |each 1300 + |each 2200 + |each 1300 + |each 3100 + |each 1300 + |each 2200 + |each 1300 + |each 2200 + |each 1300 + |each 3100 |""".stripMargin assertTrue("Execution took too little time", deadline.isOverdue()) From f31e1b7b90ccb860f15c0954c8028c670c670bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 20 Jan 2015 06:51:32 +0100 Subject: [PATCH 052/133] Add support for timeouts in ComJSRunner.receive(). --- .../scala/org/scalajs/jsenv/ComJSRunner.scala | 19 +++++++++-- .../main/scala/org/scalajs/jsenv/Utils.scala | 34 +++++++++++++++++++ .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 23 +++++++++++-- .../scalajs/jsenv/test/TimeoutComTests.scala | 28 +++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala index 9bdf01c..2d222c8 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala @@ -1,14 +1,27 @@ package org.scalajs.jsenv +import scala.concurrent.duration.Duration + trait ComJSRunner extends AsyncJSRunner { /** Send a message to the JS VM. Throws if the message cannot be sent. */ def send(msg: String): Unit - /** Block until a message is received. Throws a [[ComClosedExcpetion]] - * if the channel is closed before a message is received. + /** Blocks until a message is received and returns it. + * + * @throws ComClosedException if the channel is closed before a message + * is received + */ + final def receive(): String = receive(Duration.Inf) + + /** Blocks until a message is received and returns it. + * + * @throws ComClosedException if the channel is closed before a message + * is received + * @throws scala.concurrent.TimeoutException if the timeout expires + * before a message is received and the channel is still open */ - def receive(): String + def receive(timeout: Duration): String /** Close the communication channel. Allows the VM to terminate if it is * still waiting for callback. The JVM side **must** call close in diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala new file mode 100644 index 0000000..8d84d4a --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala @@ -0,0 +1,34 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js JS environments ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import scala.concurrent.duration._ + +private[jsenv] object Utils { + final class OptDeadline private ( + val deadline: Deadline /* nullable */) extends AnyVal { + def millisLeft: Long = + if (deadline == null) 0 + else (deadline.timeLeft.toMillis max 1L) + + def isOverdue: Boolean = + if (deadline == null) false + else deadline.isOverdue + } + + object OptDeadline { + def apply(timeout: Duration): OptDeadline = { + new OptDeadline(timeout match { + case timeout: FiniteDuration => timeout.fromNow + case _ => null + }) + } + } +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index a91cd01..20e954f 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -10,6 +10,7 @@ package org.scalajs.jsenv.nodejs import org.scalajs.jsenv._ +import org.scalajs.jsenv.Utils.OptDeadline import org.scalajs.core.ir.Utils.escapeJS @@ -21,6 +22,8 @@ import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } import java.net._ +import scala.concurrent.TimeoutException +import scala.concurrent.duration._ import scala.io.Source class NodeJSEnv private ( @@ -174,16 +177,32 @@ class NodeJSEnv private ( } } - def receive(): String = { + def receive(timeout: Duration): String = { if (!awaitConnection()) throw new ComJSEnv.ComClosedException("Node.js isn't connected") + + js2jvm.mark(Int.MaxValue) + val savedSoTimeout = comSocket.getSoTimeout() try { + val optDeadline = OptDeadline(timeout) + + comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) val len = js2jvm.readInt() - val carr = Array.fill(len)(js2jvm.readChar()) + val carr = Array.fill(len) { + comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) + js2jvm.readChar() + } + + js2jvm.mark(0) String.valueOf(carr) } catch { case e: EOFException => throw new ComJSEnv.ComClosedException(e) + case e: SocketTimeoutException => + js2jvm.reset() + throw new TimeoutException("Timeout expired") + } finally { + comSocket.setSoTimeout(savedSoTimeout) } } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index de28bcd..74db80a 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -3,6 +3,7 @@ package org.scalajs.jsenv.test import org.junit.Test import org.junit.Assert._ +import scala.concurrent.TimeoutException import scala.concurrent.duration._ trait TimeoutComTests extends TimeoutTests with ComTests { @@ -56,6 +57,33 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } + @Test + def receiveTimeoutTest = { + + val com = comRunner(s""" + scalajsCom.init(function(msg) { + setTimeout(scalajsCom.send, 2000, "Got: " + msg); + }); + """) + + com.start() + + for (i <- 1 to 2) { + com.send(s"Hello World: $i") + try { + com.receive(900.millis) + fail("Expected TimeoutException to be thrown") + } catch { + case _: TimeoutException => + } + assertEquals(s"Got: Hello World: $i", com.receive(3000.millis)) + } + + com.close() + com.await(DefaultTimeout) + + } + @Test def intervalSendTest = { From c5f19e56e79763dcce7c8e254c1c32847e001a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 21 Jan 2015 22:38:39 +0100 Subject: [PATCH 053/133] Strengthen the guarantees of AsyncJSRunner.stop(). It is now guaranteed that stop() can be called, and is a no-op, when the runner has already terminated, or when stop() has already been called. --- .../org/scalajs/jsenv/AsyncJSRunner.scala | 12 +++++++++- .../org/scalajs/jsenv/test/AsyncTests.scala | 12 +++++++++- .../org/scalajs/jsenv/test/ComTests.scala | 4 ++++ .../scalajs/jsenv/test/TimeoutComTests.scala | 22 +++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 977f6c7..7ea5f3a 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -15,7 +15,17 @@ trait AsyncJSRunner { */ def start(): Future[Unit] - /** Abort the associated run */ + /** Aborts the associated run. + * + * There is no guarantee that the runner will be effectively terminated + * by the time this method returns. If necessary, this call can be followed + * by a call to `await()`. + * + * If the run has already completed, this does nothing. Similarly, + * subsequent calls to `stop()` will do nothing. + * + * This method cannot be called before `start()` has been called. + */ def stop(): Unit /** diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index 83e1f75..bcc6f97 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -12,7 +12,7 @@ import org.junit.Assert._ import scala.concurrent.Await import scala.concurrent.duration._ -/** A couple of tests that test communication for mix-in into a test suite */ +/** A couple of tests that test async runners for mix-in into a test suite */ trait AsyncTests { protected final val DefaultTimeout: Duration = 10.seconds @@ -37,4 +37,14 @@ trait AsyncTests { assertFalse("VM should be terminated", runner.isRunning) } + @Test + def stopAfterTerminatedTest = { + val runner = asyncRunner("") + val fut = runner.start() + + Await.result(fut, DefaultTimeout) + + runner.stop() // should do nothing, and not fail + } + } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 7c0b066..1fa5010 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -53,6 +53,8 @@ trait ComTests extends AsyncTests { com.close() com.await(DefaultTimeout) + + com.stop() // should do nothing, and not fail } def comCloseJSTestCommon(timeout: Long) = { @@ -75,6 +77,8 @@ trait ComTests extends AsyncTests { com.close() com.await(DefaultTimeout) + + com.stop() // should do nothing, and not fail } @Test diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 74db80a..53c36aa 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -124,6 +124,28 @@ trait TimeoutComTests extends TimeoutTests with ComTests { case t: Throwable => // all is well } + async.stop() // should do nothing, and not fail + + } + + @Test + def doubleStopTest = { + val async = asyncRunner(s""" + setInterval(function() {}, 0); + """) + + async.start() + async.stop() + async.stop() // should do nothing, and not fail + + try { + async.await(DefaultTimeout) + fail("Expected await to fail") + } catch { + case t: Throwable => // all is well + } + + async.stop() // should do nothing, and not fail } } From 84f63ebb7e319829890ad23f9d9a41bbae584cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 20 Jan 2015 06:51:45 +0100 Subject: [PATCH 054/133] Fix scala-js/scala-js#1452: PhantomJS sometimes doesn't shut down properly. In fact, I believe this could have applied as well to other environments. Anyway, now the whole shutdown process protects against any exception that is being thrown, and still proceeds with terminating all the VMs. Any thrown exception is recorded and rethrown at the end. --- .../org/scalajs/jsenv/AsyncJSRunner.scala | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 7ea5f3a..146c6bf 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -48,12 +48,31 @@ trait AsyncJSRunner { /** Await completion of the started Run for the duration specified * by [[atMost]]. Strictly equivalent to: - * + * * {{{ * Await.result(future, atMost) * }}} - * + * */ final def await(atMost: Duration): Unit = Await.result(future, atMost) + /** Awaits completion of the started Run for the duration specified by + * [[atMost]], or force it to stop. + * + * If any exception is thrown while awaiting completion (including a + * [[scala.concurrent.TimeoutException TimeoutException]], forces the runner + * to stop by calling `stop()` before rethrowing the exception. + * + * Strictly equivalent to: + * + * {{{ + * try await(atMost) + * finally stop() + * }}} + */ + final def awaitOrStop(atMost: Duration): Unit = { + try await(atMost) + finally stop() + } + } From 50b5b676a3a417e6b70797a70319fe372102baab Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 9 Feb 2015 08:22:55 -0800 Subject: [PATCH 055/133] Fix scala-js/scala-js#1482: Node.js throws if JVM closes connection too fast --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 18 +++++++++++++++--- .../scalajs/jsenv/test/TimeoutComTests.scala | 11 +++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 20e954f..773ba1a 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -144,10 +144,22 @@ class NodeJSEnv private ( socket = net.connect(${serverSocket.getLocalPort}); socket.on('data', onData); socket.on('error', function(err) { + // Whatever happens, this closes the Com socket.end(); - // EPIPE on write is expected if the JVM closes - if (err.syscall !== "write" || err.code !== "EPIPE") - throw err; + + // Expected errors: + // - EPIPE on write: JVM closes + // - ECONNREFUSED on connect: JVM closes before JS opens + var expected = ( + err.syscall === "write" && err.code === "EPIPE" || + err.syscall === "connect" && err.code === "ECONNREFUSED" + ); + + if (!expected) { + console.error("Scala.js Com failed: " + err); + // We must terminate with an error + process.exit(-1); + } }); }, send: function(msg) { diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 53c36aa..23af5bd 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -107,6 +107,17 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } + @Test + def noMessageTest = { + val com = comRunner(s""" + // Make sure JVM has already closed when we init + setTimeout(scalajsCom.init, 1000, function(msg) {}); + """) + com.start() + com.close() + com.await(DefaultTimeout) + } + @Test def stopTestTimeout = { From 15feede85eca365d08515639cc3d4ecce02b38db Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 15 Mar 2015 23:52:38 -0700 Subject: [PATCH 056/133] Fix scala-js/scala-js#1536: NodeJSEnv is unable to send characters larger than 0x7FFF --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 2 +- .../scala/org/scalajs/jsenv/test/ComTests.scala | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 773ba1a..7e02ce5 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -169,7 +169,7 @@ class NodeJSEnv private ( var buf = new Buffer(4 + len * 2); buf.writeInt32BE(len, 0); for (var i = 0; i < len; ++i) - buf.writeInt16BE(msg.charCodeAt(i), 4 + i * 2); + buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2); socket.write(buf); }, close: function() { diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 1fa5010..021deb4 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -177,6 +177,23 @@ trait ComTests extends AsyncTests { com.await(DefaultTimeout) } + @Test + def highCharTest = { // #1536 + val com = comRunner(""" + scalajsCom.init(scalajsCom.send); + """) + + com.start() + + val msg = "\uC421\u8F10\u0112\uFF32" + + com.send(msg) + assertEquals(msg, com.receive()) + + com.close() + com.await(DefaultTimeout) + } + @Test def noInitTest = { val com = comRunner("") From 4606eecf9222dd62a71711913e9e0f94dd21d763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 16 Mar 2015 13:18:09 +0100 Subject: [PATCH 057/133] Increase Node.js' connection timeout to 5 s. This hopefully fixes scala-js/scala-js#1546. --- js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 7e02ce5..cd10e84 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -94,7 +94,7 @@ class NodeJSEnv private ( with ComJSRunner { /** Retry-timeout to wait for the JS VM to connect */ - private final val acceptTimeout = 1000 + private final val acceptTimeout = 5000 private[this] val serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address From a82b8c3fc12b56081f94b0e7f92853eaa08da871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 16 Mar 2015 14:34:51 +0100 Subject: [PATCH 058/133] Move NodeJSEnv.acceptTimeout as a protected val. So that it can be tinkered with if absolutely necessary. --- .../src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index cd10e84..97e8d07 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -63,6 +63,9 @@ class NodeJSEnv private ( protected def vmName: String = "node.js" protected def executable: String = nodejsPath + /** Retry-timeout to wait for the JS VM to connect */ + protected val acceptTimeout = 5000 + override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): JSRunner = { new NodeRunner(classpath, code, logger, console) @@ -93,9 +96,6 @@ class NodeJSEnv private ( ) extends AsyncNodeRunner(classpath, code, logger, console) with ComJSRunner { - /** Retry-timeout to wait for the JS VM to connect */ - private final val acceptTimeout = 5000 - private[this] val serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address private[this] var comSocket: Socket = _ From 01418893de00cdde0503f599d69954b1cb8b1d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 1 May 2015 20:34:53 +0200 Subject: [PATCH 059/133] Add a customInitFiles() hook in ExternalJSEnv. This will simplify the user-space workaround for scala-js/scala-js#1555. It can also be generally useful. --- .../org/scalajs/jsenv/ExternalJSEnv.scala | 9 ++++- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 1 + .../jsenv/test/CustomInitFilesTest.scala | 40 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index c8ce61f..d0e97e8 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -22,6 +22,9 @@ abstract class ExternalJSEnv( /** Command to execute (on shell) for this VM */ protected def executable: String + /** Custom initialization scripts. */ + protected def customInitFiles(): Seq[VirtualJSFile] = Nil + protected class AbstractExtRunner(protected val classpath: CompleteClasspath, protected val code: VirtualJSFile, protected val logger: Logger, protected val console: JSConsole) { @@ -29,6 +32,10 @@ abstract class ExternalJSEnv( /** JS files used to setup VM */ protected def initFiles(): Seq[VirtualJSFile] = Nil + /** Custom initialization scripts, defined by the environment. */ + final protected def customInitFiles(): Seq[VirtualJSFile] = + ExternalJSEnv.this.customInitFiles() + /** Sends required data to VM Stdin (can throw) */ protected def sendVMStdin(out: OutputStream): Unit = {} @@ -46,7 +53,7 @@ abstract class ExternalJSEnv( /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(): Seq[VirtualJSFile] = - initFiles() ++ classpath.allCode + initFiles() ++ customInitFiles() ++ classpath.allCode /** Get all files that are passed to VM (libraries and code) */ protected def getJSFiles(): Seq[VirtualJSFile] = diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 97e8d07..8915c08 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -317,6 +317,7 @@ class NodeJSEnv private ( /** Libraries are loaded via require in Node.js */ override protected def getLibJSFiles(): Seq[VirtualJSFile] = { initFiles() ++ + customInitFiles() ++ classpath.jsLibs.map(requireLibrary) :+ classpath.scalaJSCode } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala new file mode 100644 index 0000000..452dac6 --- /dev/null +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala @@ -0,0 +1,40 @@ +package org.scalajs.jsenv.test + +import org.scalajs.jsenv._ +import org.scalajs.jsenv.nodejs.NodeJSEnv +import org.scalajs.jsenv.phantomjs.PhantomJSEnv + +import org.scalajs.core.tools.io._ + +import org.junit.Test + +abstract class CustomInitFilesTest extends JSEnvTest { + def makeCustomInitFiles(): Seq[VirtualJSFile] = { + Seq(new MemVirtualJSFile("custominit.js").withContent(""" + function customPrint(s) { + console.log("custom: " + s); + } + """)) + } + + @Test + def customInitFilesTest = { + """ + customPrint("hello"); + """ hasOutput + """|custom: hello + |""".stripMargin + } +} + +class NodeJSWithCustomInitFilesTest extends CustomInitFilesTest { + protected def newJSEnv = new NodeJSEnv { + override def customInitFiles() = makeCustomInitFiles() + } +} + +class PhantomJSWithCustomInitFilesTest extends CustomInitFilesTest { + protected def newJSEnv = new PhantomJSEnv { + override def customInitFiles() = makeCustomInitFiles() + } +} From 593e07b9b391cf233cd1e8da997f15074b2174b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 29 May 2015 18:35:06 +0200 Subject: [PATCH 060/133] Fix a bunch of style issues discovered by Scalastyle. --- .../org/scalajs/jsenv/test/AsyncTests.scala | 4 ++-- .../org/scalajs/jsenv/test/ComTests.scala | 22 +++++++++---------- .../jsenv/test/CustomInitFilesTest.scala | 6 ++--- .../org/scalajs/jsenv/test/NodeJSTest.scala | 8 +++---- .../scalajs/jsenv/test/TimeoutComTests.scala | 14 ++++++------ .../org/scalajs/jsenv/test/TimeoutTests.scala | 10 ++++----- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index bcc6f97..e305a4d 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -28,7 +28,7 @@ trait AsyncTests { } @Test - def futureTest = { + def futureTest: Unit = { val runner = asyncRunner("") val fut = runner.start() @@ -38,7 +38,7 @@ trait AsyncTests { } @Test - def stopAfterTerminatedTest = { + def stopAfterTerminatedTest: Unit = { val runner = asyncRunner("") val fut = runner.start() diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 021deb4..2294b88 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -36,7 +36,7 @@ trait ComTests extends AsyncTests { } @Test - def comCloseJVMTest = { + def comCloseJVMTest: Unit = { val com = comRunner(s""" scalajsCom.init(function(msg) { scalajsCom.send("received: " + msg); }); scalajsCom.send("Hello World"); @@ -57,7 +57,7 @@ trait ComTests extends AsyncTests { com.stop() // should do nothing, and not fail } - def comCloseJSTestCommon(timeout: Long) = { + def comCloseJSTestCommon(timeout: Long): Unit = { val com = comRunner(s""" scalajsCom.init(function(msg) {}); for (var i = 0; i < 10; ++i) @@ -82,13 +82,13 @@ trait ComTests extends AsyncTests { } @Test - def comCloseJSTest = comCloseJSTestCommon(0) + def comCloseJSTest: Unit = comCloseJSTestCommon(0) @Test - def comCloseJSTestDelayed = comCloseJSTestCommon(1000) + def comCloseJSTestDelayed: Unit = comCloseJSTestCommon(1000) @Test - def doubleCloseTest = { + def doubleCloseTest: Unit = { val n = 10 val com = pingPongRunner(n) @@ -104,7 +104,7 @@ trait ComTests extends AsyncTests { } @Test - def multiEnvTest = { + def multiEnvTest: Unit = { val n = 10 val envs = List.fill(5)(pingPongRunner(10)) @@ -137,7 +137,7 @@ trait ComTests extends AsyncTests { } @Test - def largeMessageTest = { + def largeMessageTest: Unit = { // 1KB data val baseMsg = new String(Array.tabulate(512)(_.toChar)) val baseLen = baseMsg.length @@ -178,7 +178,7 @@ trait ComTests extends AsyncTests { } @Test - def highCharTest = { // #1536 + def highCharTest: Unit = { // #1536 val com = comRunner(""" scalajsCom.init(scalajsCom.send); """) @@ -195,7 +195,7 @@ trait ComTests extends AsyncTests { } @Test - def noInitTest = { + def noInitTest: Unit = { val com = comRunner("") com.start() @@ -205,7 +205,7 @@ trait ComTests extends AsyncTests { } @Test - def stopTestCom = { + def stopTestCom: Unit = { val com = comRunner(s"""scalajsCom.init(function(msg) {});""") com.start() @@ -227,7 +227,7 @@ trait ComTests extends AsyncTests { } @Test - def futureStopTest = { + def futureStopTest: Unit = { val com = comRunner(s"""scalajsCom.init(function(msg) {});""") val fut = com.start() diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala index 452dac6..7f5cd2f 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala @@ -18,7 +18,7 @@ abstract class CustomInitFilesTest extends JSEnvTest { } @Test - def customInitFilesTest = { + def customInitFilesTest: Unit = { """ customPrint("hello"); """ hasOutput @@ -28,13 +28,13 @@ abstract class CustomInitFilesTest extends JSEnvTest { } class NodeJSWithCustomInitFilesTest extends CustomInitFilesTest { - protected def newJSEnv = new NodeJSEnv { + protected def newJSEnv: NodeJSEnv = new NodeJSEnv { override def customInitFiles() = makeCustomInitFiles() } } class PhantomJSWithCustomInitFilesTest extends CustomInitFilesTest { - protected def newJSEnv = new PhantomJSEnv { + protected def newJSEnv: PhantomJSEnv = new PhantomJSEnv { override def customInitFiles() = makeCustomInitFiles() } } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index 0066984..74db64f 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -7,11 +7,11 @@ import org.junit.Assert._ class NodeJSTest extends TimeoutComTests { - protected def newJSEnv = new NodeJSEnv + protected def newJSEnv: NodeJSEnv = new NodeJSEnv /** Node.js strips double percentage signs - #500 */ @Test - def percentageTest = { + def percentageTest: Unit = { val counts = 1 to 15 val argcs = 1 to 3 val strings = counts.map("%" * _) @@ -35,7 +35,7 @@ class NodeJSTest extends TimeoutComTests { /** Node.js console.log hack didn't allow to log non-Strings - #561 */ @Test - def nonStringTest = { + def nonStringTest: Unit = { """ console.log(1); @@ -53,7 +53,7 @@ class NodeJSTest extends TimeoutComTests { } @Test - def slowJSEnvTest = { + def slowJSEnvTest: Unit = { val com = comRunner(""" setTimeout(function() { scalajsCom.init(function(msg) { diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 23af5bd..0a3a47f 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -9,7 +9,7 @@ import scala.concurrent.duration._ trait TimeoutComTests extends TimeoutTests with ComTests { @Test - def delayedInitTest = { + def delayedInitTest: Unit = { val com = comRunner(s""" setTimeout(function() { @@ -35,7 +35,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } @Test - def delayedReplyTest = { + def delayedReplyTest: Unit = { val com = comRunner(s""" scalajsCom.init(function(msg) { @@ -58,7 +58,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } @Test - def receiveTimeoutTest = { + def receiveTimeoutTest: Unit = { val com = comRunner(s""" scalajsCom.init(function(msg) { @@ -85,7 +85,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } @Test - def intervalSendTest = { + def intervalSendTest: Unit = { val com = comRunner(s""" scalajsCom.init(function(msg) {}); @@ -108,7 +108,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } @Test - def noMessageTest = { + def noMessageTest: Unit = { val com = comRunner(s""" // Make sure JVM has already closed when we init setTimeout(scalajsCom.init, 1000, function(msg) {}); @@ -119,7 +119,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } @Test - def stopTestTimeout = { + def stopTestTimeout: Unit = { val async = asyncRunner(s""" setInterval(function() {}, 0); @@ -140,7 +140,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { } @Test - def doubleStopTest = { + def doubleStopTest: Unit = { val async = asyncRunner(s""" setInterval(function() {}, 0); """) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala index 8a0afaf..9dc3bc9 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala @@ -8,7 +8,7 @@ import scala.concurrent.duration._ trait TimeoutTests extends JSEnvTest { @Test - def basicTimeoutTest = { + def basicTimeoutTest: Unit = { val deadline = 300.millis.fromNow @@ -29,7 +29,7 @@ trait TimeoutTests extends JSEnvTest { } @Test - def clearTimeoutTest = { + def clearTimeoutTest: Unit = { val deadline = 300.millis.fromNow @@ -52,7 +52,7 @@ trait TimeoutTests extends JSEnvTest { } @Test - def timeoutArgTest = { + def timeoutArgTest: Unit = { val deadline = 300.millis.fromNow @@ -73,7 +73,7 @@ trait TimeoutTests extends JSEnvTest { } @Test - def intervalTest = { + def intervalTest: Unit = { val deadline = 1.second.fromNow @@ -109,7 +109,7 @@ trait TimeoutTests extends JSEnvTest { } @Test - def intervalSelfClearTest = { + def intervalSelfClearTest: Unit = { val deadline = 100.millis.fromNow From 27a842c31d51ff1ce6580bac7a3b8a9d40d1b433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 29 May 2015 18:35:24 +0200 Subject: [PATCH 061/133] Add Scalastyle with a basic configuration. At least, this should catch quite a few trivial errors from here on. --- js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala | 2 +- .../scala/org/scalajs/jsenv/VirtualFileMaterializer.scala | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala index 8d84d4a..b2431b4 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala @@ -13,7 +13,7 @@ import scala.concurrent.duration._ private[jsenv] object Utils { final class OptDeadline private ( - val deadline: Deadline /* nullable */) extends AnyVal { + val deadline: Deadline /* nullable */) extends AnyVal { // scalastyle:ignore def millisLeft: Long = if (deadline == null) 0 else (deadline.timeLeft.toMillis max 1L) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala index f6838ac..316f334 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala @@ -58,9 +58,11 @@ final class VirtualFileMaterializer(singleDir: Boolean = false) { cacheDir.delete() } - /** Taken from Guava: - * https://github.com/google/guava/blob/1c285fc8d289c43b46aa55e7f90ec0359be5b69a/guava/src/com/google/common/io/Files.java#L413-L426 + // scalastyle:off line.size.limit + /* Taken from Guava: + * https://github.com/google/guava/blob/1c285fc8d289c43b46aa55e7f90ec0359be5b69a/guava/src/com/google/common/io/Files.java#L413-L426 */ + // scalastyle:on line.size.limit private def createTempDir(): File = { val baseDir = new File(System.getProperty("java.io.tmpdir")) val baseName = System.currentTimeMillis() + "-" From e215281727c93ec6feadbeaa3ebbec8195137068 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 23 Oct 2015 16:14:36 +0200 Subject: [PATCH 062/133] Fix scala-js/scala-js#1917: Add -Xfatal-warnings for ScalaDoc generation Note that links to the JavaDoc are not yet correct (filed as scala-js/scala-js#1970). --- .../src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala | 4 ++-- .../src/main/scala/org/scalajs/jsenv/ComJSRunner.scala | 8 ++++---- .../src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 146c6bf..32e399f 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -47,7 +47,7 @@ trait AsyncJSRunner { final def await(): Unit = Await.result(future, Duration.Inf) /** Await completion of the started Run for the duration specified - * by [[atMost]]. Strictly equivalent to: + * by `atMost`. Strictly equivalent to: * * {{{ * Await.result(future, atMost) @@ -57,7 +57,7 @@ trait AsyncJSRunner { final def await(atMost: Duration): Unit = Await.result(future, atMost) /** Awaits completion of the started Run for the duration specified by - * [[atMost]], or force it to stop. + * `atMost`, or force it to stop. * * If any exception is thrown while awaiting completion (including a * [[scala.concurrent.TimeoutException TimeoutException]], forces the runner diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala index 2d222c8..66d24d7 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala @@ -9,15 +9,15 @@ trait ComJSRunner extends AsyncJSRunner { /** Blocks until a message is received and returns it. * - * @throws ComClosedException if the channel is closed before a message - * is received + * @throws ComJSEnv.ComClosedException if the channel is closed before a + * message is received */ final def receive(): String = receive(Duration.Inf) /** Blocks until a message is received and returns it. * - * @throws ComClosedException if the channel is closed before a message - * is received + * @throws ComJSEnv.ComClosedException if the channel is closed before a + * message is received * @throws scala.concurrent.TimeoutException if the timeout expires * before a message is received and the channel is still open */ diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index d0e97e8..b4b276c 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -84,7 +84,7 @@ abstract class ExternalJSEnv( /** Wait for the VM to terminate, verify exit code * - * @throws NonZeroExitException if VM returned a non-zero code + * @throws ExternalJSEnv.NonZeroExitException if VM returned a non-zero code */ final protected def waitForVM(vmInst: Process): Unit = { // Make sure we are done. From 0c85ccd64a55e46130afee660983dd825b28fc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 30 Oct 2015 17:03:43 +0100 Subject: [PATCH 063/133] Fix scala-js/scala-js#1990: Do not duplicate % for 1-arg console.log on Node.js >= 2.1.0. As of io.js v2.1.0 (which later merged back into Node.js v4), console.log() does not deduplicate % signs anymore if only one argument is given to console.log(). This commit detects the version of Node.js, and will only duplicate % signs on old versions, or if there is more than one argument given to log(). This precisely restores the previous behavior of our patching of console.log() on recent versions. --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 8915c08..50621ff 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -281,11 +281,24 @@ class NodeJSEnv private ( """ // Hack console log to duplicate double % signs (function() { + function startsWithAnyOf(s, prefixes) { + for (var i = 0; i < prefixes.length; i++) { + // ES5 does not have .startsWith() on strings + if (s.substring(0, prefixes[i].length) === prefixes[i]) + return true; + } + return false; + } + var nodeWillDeduplicateEvenForOneArgument = startsWithAnyOf( + process.version, ["v0.", "v1.", "v2.0."]); var oldLog = console.log; var newLog = function() { var args = arguments; if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { - args[0] = args[0].toString().replace(/%/g, "%%"); + var argStr = args[0].toString(); + if (args.length > 1 || nodeWillDeduplicateEvenForOneArgument) + argStr = argStr.replace(/%/g, "%%"); + args[0] = argStr; } oldLog.apply(console, args); }; From c1e37141449e6f0e7044d9782e0433cea8a16b89 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 27 Nov 2015 07:23:28 +0100 Subject: [PATCH 064/133] Factor basic JSEnv tests in trait --- .../org/scalajs/jsenv/test/AsyncTests.scala | 2 +- .../scalajs/jsenv/test/BasicJSEnvTests.scala | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index e305a4d..d46589e 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -13,7 +13,7 @@ import scala.concurrent.Await import scala.concurrent.duration._ /** A couple of tests that test async runners for mix-in into a test suite */ -trait AsyncTests { +trait AsyncTests extends BasicJSEnvTests { protected final val DefaultTimeout: Duration = 10.seconds diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala new file mode 100644 index 0000000..676e472 --- /dev/null +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala @@ -0,0 +1,28 @@ +package org.scalajs.jsenv.test + +import org.junit.Test +import org.junit.Assert._ + +/** Tests that should succeed on any JSEnv */ +trait BasicJSEnvTests extends JSEnvTest { + + @Test + def failureTest: Unit = { + + """ + var a = {}; + a.foo(); + """.fails() + + } + + @Test + def syntaxErrorTest: Unit = { + + """ + { + """.fails() + + } + +} From b4f5755467d0a6c72a0b091cfe8f5322d45b869c Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 27 Nov 2015 07:25:12 +0100 Subject: [PATCH 065/133] Fix scala-js/scala-js#2053: Always use UTF8 in PhantomJSEnv --- .../scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala index 676e472..26f4297 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala @@ -25,4 +25,13 @@ trait BasicJSEnvTests extends JSEnvTest { } + @Test // Failed in Phantom - #2053 + def utf8Test: Unit = { + + """ + console.log("\u1234"); + """ hasOutput "\u1234\n"; + + } + } From c3f97a3d7813dd02ec3b4015681e80206ec190b1 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 1 Dec 2015 15:01:19 +0100 Subject: [PATCH 066/133] Log exact command executed by ExternalJSEnv. --- js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index b4b276c..5b90a57 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -107,6 +107,8 @@ abstract class ExternalJSEnv( for ((name, value) <- vmEnv) pBuilder.environment().put(name, value) + logger.debug("Starting process: " + allArgs.mkString(" ")) + pBuilder.start() } From bcb242da1615d160531e10ca2700419cd0526ebb Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 3 Dec 2015 10:33:43 +0100 Subject: [PATCH 067/133] Fix minor typo in doc --- js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 32e399f..a8e553e 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -60,7 +60,7 @@ trait AsyncJSRunner { * `atMost`, or force it to stop. * * If any exception is thrown while awaiting completion (including a - * [[scala.concurrent.TimeoutException TimeoutException]], forces the runner + * [[scala.concurrent.TimeoutException TimeoutException]]), forces the runner * to stop by calling `stop()` before rethrowing the exception. * * Strictly equivalent to: From 43705681be9a16461abe8260a4b05f0889f4f54f Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 17 Jan 2016 11:58:33 +0100 Subject: [PATCH 068/133] Tools refactoring This commit substantially refactors Scala.js' linker. Notably: Linker pipeline --------------- The linker pipeline is divided into a frontend and a backend. The frontend links and optimizes IR files into a `LinkingUnit`. It paves the path for linker plugins in the future. The backend emits a linking unit into a JavaScript file. Currently there are two backends: * BasicLinkerBackend (basic JavaScript generation, aka fastOptJS) * ClosureBackend (optimization pass with the Google Closure compiler, aka fullOptJS) As a side effect of this change, Rhino now runs optimized code (unless this is explicitly disabled). This further has the side effect, that the `PreLinkStage` was entirely removed. Classpath traversal ------------------- Classpath traversal and abstraction is fully pushed to the build system. As a consequence, the whole `org.scalajs.core.tools.classpath` package is removed. Functionally is mostly replaced by a couple of lines of sbt code. This also fixes scala-js/scala-js#1635. IR caching ---------- In the past, each linker cached its own IR. This caused a lot of IR to be in memory multiple times. This commit replaces caching with a global IR cache. There are two main advantages: * IR for each class is only cached once in memory. * Linking other projects or configurations (e.g. compile v.s. test) can reuse the already cached IR. JSEnvs ------ Since classpaths have been removed, we need a way to re-enable lazy loading in Rhino. We therefore introduce the `LinkingUnitJSEnv` that can directly accept a `LinkingUnit`. Further, we enrich the `JSEnv`s with an API to load libraries. This allows for much nicer interfaces in a couple of spots, where you essentially just pass a `JSEnv` and then run your code in it, with all dependencies already present (e.g. FrameworkDetector). There are a couple of advantages: - Symbol requirements are made explicit - No duplicate code for linking in RhinoJSEnv - Rhino can now run with optimized code (and does so by default) --- .../scala/org/scalajs/jsenv/AsyncJSEnv.scala | 21 ++++-- .../org/scalajs/jsenv/AsyncJSRunner.scala | 4 +- .../scala/org/scalajs/jsenv/ComJSEnv.scala | 20 ++++-- .../org/scalajs/jsenv/ExternalJSEnv.scala | 57 ++++++++++----- .../main/scala/org/scalajs/jsenv/JSEnv.scala | 40 +++++++++-- .../scala/org/scalajs/jsenv/JSRunner.scala | 4 +- .../scalajs/jsenv/LinkingUnitAsyncJSEnv.scala | 42 +++++++++++ .../scalajs/jsenv/LinkingUnitComJSEnv.scala | 43 +++++++++++ .../org/scalajs/jsenv/LinkingUnitJSEnv.scala | 72 +++++++++++++++++++ .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 48 +++++-------- .../org/scalajs/jsenv/test/AsyncTests.scala | 16 ++--- .../org/scalajs/jsenv/test/ComTests.scala | 23 +++--- .../org/scalajs/jsenv/test/JSEnvTest.scala | 10 ++- .../org/scalajs/jsenv/test/NodeJSTest.scala | 2 +- .../scalajs/jsenv/test/TimeoutComTests.scala | 14 ++-- 15 files changed, 318 insertions(+), 98 deletions(-) create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala index 01c5922..c0ba9aa 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala @@ -9,11 +9,22 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath._ -import org.scalajs.core.tools.logging._ +import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.core.tools.jsdep.ResolvedJSDependency trait AsyncJSEnv extends JSEnv { - def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): AsyncJSRunner + def asyncRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): AsyncJSRunner + + final def asyncRunner(code: VirtualJSFile): AsyncJSRunner = + asyncRunner(Nil, code) + + override def loadLibs(libs: Seq[ResolvedJSDependency]): AsyncJSEnv = + new AsyncLoadedLibs { val loadedLibs = libs } + + private[jsenv] trait AsyncLoadedLibs extends LoadedLibs with AsyncJSEnv { + def asyncRunner(libs: Seq[ResolvedJSDependency], + code: VirtualJSFile): AsyncJSRunner = { + AsyncJSEnv.this.asyncRunner(loadedLibs ++ libs, code) + } + } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index a8e553e..06619bd 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -3,6 +3,8 @@ package org.scalajs.jsenv import scala.concurrent.{Future, Await} import scala.concurrent.duration.Duration +import org.scalajs.core.tools.logging.Logger + trait AsyncJSRunner { /** A future that completes when the associated run has terminated. */ @@ -13,7 +15,7 @@ trait AsyncJSRunner { * when the run terminates. The returned Future is equivalent to * the one returned by [[future]]. */ - def start(): Future[Unit] + def start(logger: Logger, console: JSConsole): Future[Unit] /** Aborts the associated run. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala index bd52341..52c6dcf 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala @@ -9,9 +9,8 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath._ -import org.scalajs.core.tools.logging._ +import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.core.tools.jsdep.ResolvedJSDependency /** An [[AsyncJSEnv]] that provides communication to and from the JS VM. * @@ -29,8 +28,19 @@ import org.scalajs.core.tools.logging._ * }}} */ trait ComJSEnv extends AsyncJSEnv { - def comRunner(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): ComJSRunner + def comRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): ComJSRunner + + final def comRunner(code: VirtualJSFile): ComJSRunner = comRunner(Nil, code) + + override def loadLibs(libs: Seq[ResolvedJSDependency]): ComJSEnv = + new ComLoadedLibs { val loadedLibs = libs } + + private[jsenv] trait ComLoadedLibs extends AsyncLoadedLibs with ComJSEnv { + def comRunner(libs: Seq[ResolvedJSDependency], + code: VirtualJSFile): ComJSRunner = { + ComJSEnv.this.comRunner(loadedLibs ++ libs, code) + } + } } object ComJSEnv { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 5b90a57..4123cf6 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -1,8 +1,8 @@ package org.scalajs.jsenv import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath._ -import org.scalajs.core.tools.logging._ +import org.scalajs.core.tools.logging.Logger +import org.scalajs.core.tools.jsdep.ResolvedJSDependency import java.io.{ Console => _, _ } import scala.io.Source @@ -16,6 +16,8 @@ abstract class ExternalJSEnv( import ExternalJSEnv._ + def name: String = s"ExternalJSEnv for $vmName" + /** Printable name of this VM */ protected def vmName: String @@ -25,9 +27,21 @@ abstract class ExternalJSEnv( /** Custom initialization scripts. */ protected def customInitFiles(): Seq[VirtualJSFile] = Nil - protected class AbstractExtRunner(protected val classpath: CompleteClasspath, - protected val code: VirtualJSFile, protected val logger: Logger, - protected val console: JSConsole) { + protected class AbstractExtRunner( + protected val libs: Seq[ResolvedJSDependency], + protected val code: VirtualJSFile) { + + private[this] var _logger: Logger = _ + private[this] var _console: JSConsole = _ + + protected def logger: Logger = _logger + protected def console: JSConsole = _console + + protected def setupLoggerAndConsole(logger: Logger, console: JSConsole) = { + require(_logger == null && _console == null) + _logger = logger + _console = console + } /** JS files used to setup VM */ protected def initFiles(): Seq[VirtualJSFile] = Nil @@ -53,7 +67,7 @@ abstract class ExternalJSEnv( /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(): Seq[VirtualJSFile] = - initFiles() ++ customInitFiles() ++ classpath.allCode + initFiles() ++ customInitFiles() ++ libs.map(_.lib) /** Get all files that are passed to VM (libraries and code) */ protected def getJSFiles(): Seq[VirtualJSFile] = @@ -133,12 +147,12 @@ abstract class ExternalJSEnv( } - protected class ExtRunner(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole - ) extends AbstractExtRunner(classpath, code, logger, console) - with JSRunner { + protected class ExtRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + extends AbstractExtRunner(libs, code) with JSRunner { + + def run(logger: Logger, console: JSConsole): Unit = { + setupLoggerAndConsole(logger, console) - def run(): Unit = { val vmInst = startVM() pipeVMData(vmInst) @@ -146,10 +160,8 @@ abstract class ExternalJSEnv( } } - protected class AsyncExtRunner(classpath: CompleteClasspath, - code: VirtualJSFile, logger: Logger, console: JSConsole - ) extends AbstractExtRunner(classpath, code, logger, console) - with AsyncJSRunner { + protected class AsyncExtRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + extends AbstractExtRunner(libs, code) with AsyncJSRunner { private[this] var vmInst: Process = null private[this] var ioThreadEx: Throwable = null @@ -173,11 +185,22 @@ abstract class ExternalJSEnv( def future: Future[Unit] = promise.future - def start(): Future[Unit] = { + def start(logger: Logger, console: JSConsole): Future[Unit] = { + setupLoggerAndConsole(logger, console) + startExternalJSEnv() + future + } + + /** Core functionality of [[start]]. + * + * Same as [[start]] but without a call to [[setupLoggerAndConsole]] and + * not returning [[future]]. + * Useful to be called in overrides of [[start]]. + */ + protected def startExternalJSEnv(): Unit = { require(vmInst == null, "start() may only be called once") vmInst = startVM() thread.start() - future } def stop(): Unit = { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index 38552bb..aa6c7a5 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -9,12 +9,42 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath._ -import org.scalajs.core.tools.logging._ +import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.core.tools.jsdep.ResolvedJSDependency trait JSEnv { + /** Human-readable name for this [[JSEnv]] */ + def name: String + /** Prepare a runner for the code in the virtual file. */ - def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): JSRunner + def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner + + /** Prepare a runner without any libraries. + * + * Strictly equivalent to: + * {{{ + * this.jsRunner(Nil, code) + * }}} + */ + final def jsRunner(code: VirtualJSFile): JSRunner = jsRunner(Nil, code) + + /** Return this [[JSEnv]] with the given libraries already loaded. + * + * The following two are equivalent: + * {{{ + * jsEnv.loadLibs(a).jsRunner(b, c) + * jsEnv.jsRunner(a ++ b, c) + * }}} + */ + def loadLibs(libs: Seq[ResolvedJSDependency]): JSEnv = + new LoadedLibs { val loadedLibs = libs } + + private[jsenv] trait LoadedLibs extends JSEnv { + val loadedLibs: Seq[ResolvedJSDependency] + + def name: String = JSEnv.this.name + + def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = + JSEnv.this.jsRunner(loadedLibs ++ libs, code) + } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala index 7a0b0b6..7d2b5b6 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala @@ -9,7 +9,9 @@ package org.scalajs.jsenv +import org.scalajs.core.tools.logging.Logger + trait JSRunner { /** Run the associated JS code. Throw if an error occurs. */ - def run(): Unit + def run(logger: Logger, console: JSConsole): Unit } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala new file mode 100644 index 0000000..dc22192 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala @@ -0,0 +1,42 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.core.tools.jsdep.ResolvedJSDependency +import org.scalajs.core.tools.linker.LinkingUnit + +trait LinkingUnitAsyncJSEnv extends LinkingUnitJSEnv with AsyncJSEnv { + def asyncRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, + postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): AsyncJSRunner + + override def loadLibs(libs: Seq[ResolvedJSDependency]): LinkingUnitAsyncJSEnv = + new LinkingUnitAsyncLoadedLibs { val loadedLibs = libs } + + override def loadLinkingUnit(linkingUnit: LinkingUnit): AsyncJSEnv = + new AsyncLoadedUnit { val loadedUnit = linkingUnit } + + private[jsenv] trait LinkingUnitAsyncLoadedLibs extends LinkingUnitLoadedLibs + with AsyncLoadedLibs with LinkingUnitAsyncJSEnv { + def asyncRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, + postLibs: Seq[ResolvedJSDependency], + code: VirtualJSFile): AsyncJSRunner = { + LinkingUnitAsyncJSEnv.this.asyncRunner(loadedLibs ++ preLibs, linkingUnit, + postLibs, code) + } + } + + private[jsenv] trait AsyncLoadedUnit extends LoadedUnit with AsyncJSEnv { + def asyncRunner(libs: Seq[ResolvedJSDependency], + code: VirtualJSFile): AsyncJSRunner = { + LinkingUnitAsyncJSEnv.this.asyncRunner(Nil, loadedUnit, libs, code) + } + } +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala new file mode 100644 index 0000000..dc39bbc --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala @@ -0,0 +1,43 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.core.tools.jsdep.ResolvedJSDependency +import org.scalajs.core.tools.linker.LinkingUnit + +trait LinkingUnitComJSEnv extends LinkingUnitAsyncJSEnv with ComJSEnv { + def comRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, + postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): ComJSRunner + + override def loadLibs(libs: Seq[ResolvedJSDependency]): LinkingUnitComJSEnv = + new LinkingUnitComLoadedLibs { val loadedLibs = libs } + + override def loadLinkingUnit(linkingUnit: LinkingUnit): ComJSEnv = + new ComLoadedUnit { val loadedUnit = linkingUnit } + + private[jsenv] trait LinkingUnitComLoadedLibs + extends LinkingUnitAsyncLoadedLibs with ComLoadedLibs + with LinkingUnitComJSEnv { + def comRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, + postLibs: Seq[ResolvedJSDependency], + code: VirtualJSFile): ComJSRunner = { + LinkingUnitComJSEnv.this.comRunner(loadedLibs ++ preLibs, linkingUnit, + postLibs, code) + } + } + + private[jsenv] trait ComLoadedUnit extends AsyncLoadedUnit with ComJSEnv { + def comRunner(libs: Seq[ResolvedJSDependency], + code: VirtualJSFile): ComJSRunner = { + LinkingUnitComJSEnv.this.comRunner(Nil, loadedUnit, libs, code) + } + } +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala new file mode 100644 index 0000000..6ba33e6 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala @@ -0,0 +1,72 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.core.tools.jsdep.ResolvedJSDependency +import org.scalajs.core.tools.linker.LinkingUnit +import org.scalajs.core.tools.linker.analyzer.SymbolRequirement + +trait LinkingUnitJSEnv extends JSEnv { + /** Symbols this [[LinkingUnitJSEnv]] needs present in the + * [[org.scalajs.core.tools.linker.LinkingUnit LinkingUnit]] it receives. + */ + val symbolRequirements: SymbolRequirement + + /** Prepare a runner for the code in the virtual file. */ + def jsRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, + postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner + + override def loadLibs(libs: Seq[ResolvedJSDependency]): LinkingUnitJSEnv = + new LinkingUnitLoadedLibs { val loadedLibs = libs } + + /** Returns a [[JSEnv]] with the given + * [[org.scalajs.core.tools.linker.LinkingUnit LinkingUnit]] already loaded. + * + * Note that any subsequent libraries will be inserted after the + * [[org.scalajs.core.tools.linker.LinkingUnit LinkingUnit]]. + * + * Hence, the following are equivalent: + * {{{ + * jsEnv.loadUnit(a).jsRunner(b, c) + * jsEnv.jsRunner(Nil, a, b, c) + * }}} + * + * If you need to load libraries before, you can use the [[loadLibs]] method: + * {{{ + * jsEnv.loadLibs(a).loadUnit(b).jsRunner(c, d) + * // equivalent to + * jsEnv.jsRunner(a, b, c, d) + * }}} + */ + def loadLinkingUnit(linkingUnit: LinkingUnit): JSEnv = + new LoadedUnit { val loadedUnit = linkingUnit } + + private[jsenv] trait LinkingUnitLoadedLibs + extends LoadedLibs with LinkingUnitJSEnv { + val symbolRequirements: SymbolRequirement = + LinkingUnitJSEnv.this.symbolRequirements + + def jsRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, + postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = { + LinkingUnitJSEnv.this.jsRunner(loadedLibs ++ preLibs, + linkingUnit, postLibs, code) + } + } + + private[jsenv] trait LoadedUnit extends JSEnv { + val loadedUnit: LinkingUnit + + def name: String = LinkingUnitJSEnv.this.name + + def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = + LinkingUnitJSEnv.this.jsRunner(Nil, loadedUnit, libs, code) + } +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 50621ff..b7dd659 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -15,8 +15,7 @@ import org.scalajs.jsenv.Utils.OptDeadline import org.scalajs.core.ir.Utils.escapeJS import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath._ -import org.scalajs.core.tools.jsdep._ +import org.scalajs.core.tools.jsdep.ResolvedJSDependency import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } @@ -48,11 +47,9 @@ class NodeJSEnv private ( lazy val hasSourceMapSupport: Boolean = { val code = new MemVirtualJSFile("source-map-support-probe.js") .withContent("""require('source-map-support').install();""") - val runner = - jsRunner(CompleteClasspath.empty, code, NullLogger, NullJSConsole) try { - runner.run() + jsRunner(code).run(NullLogger, NullJSConsole) true } catch { case t: ExternalJSEnv.NonZeroExitException => @@ -60,41 +57,35 @@ class NodeJSEnv private ( } } - protected def vmName: String = "node.js" + protected def vmName: String = "Node.js" protected def executable: String = nodejsPath /** Retry-timeout to wait for the JS VM to connect */ protected val acceptTimeout = 5000 - override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): JSRunner = { - new NodeRunner(classpath, code, logger, console) + override def jsRunner(libs: Seq[ResolvedJSDependency], + code: VirtualJSFile): JSRunner = { + new NodeRunner(libs, code) } - override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): AsyncJSRunner = { - new AsyncNodeRunner(classpath, code, logger, console) + override def asyncRunner(libs: Seq[ResolvedJSDependency], + code: VirtualJSFile): AsyncJSRunner = { + new AsyncNodeRunner(libs, code) } - override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile, - logger: Logger, console: JSConsole): ComJSRunner = { - new ComNodeRunner(classpath, code, logger, console) + override def comRunner(libs: Seq[ResolvedJSDependency], + code: VirtualJSFile): ComJSRunner = { + new ComNodeRunner(libs, code) } - protected class NodeRunner(classpath: CompleteClasspath, - code: VirtualJSFile, logger: Logger, console: JSConsole - ) extends ExtRunner(classpath, code, logger, console) - with AbstractNodeRunner + protected class NodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + extends ExtRunner(libs, code) with AbstractNodeRunner - protected class AsyncNodeRunner(classpath: CompleteClasspath, - code: VirtualJSFile, logger: Logger, console: JSConsole - ) extends AsyncExtRunner(classpath, code, logger, console) - with AbstractNodeRunner + protected class AsyncNodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + extends AsyncExtRunner(libs, code) with AbstractNodeRunner - protected class ComNodeRunner(classpath: CompleteClasspath, - code: VirtualJSFile, logger: Logger, console: JSConsole - ) extends AsyncNodeRunner(classpath, code, logger, console) - with ComJSRunner { + protected class ComNodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + extends AsyncNodeRunner(libs, code) with ComJSRunner { private[this] val serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address @@ -331,8 +322,7 @@ class NodeJSEnv private ( override protected def getLibJSFiles(): Seq[VirtualJSFile] = { initFiles() ++ customInitFiles() ++ - classpath.jsLibs.map(requireLibrary) :+ - classpath.scalaJSCode + libs.map(requireLibrary) } /** Rewrites a library virtual file to a require statement if possible */ diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala index d46589e..acb3830 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -3,13 +3,12 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.classpath._ import org.scalajs.core.tools.logging._ import org.junit.Test import org.junit.Assert._ -import scala.concurrent.Await +import scala.concurrent.{Future, Await} import scala.concurrent.duration._ /** A couple of tests that test async runners for mix-in into a test suite */ @@ -19,18 +18,19 @@ trait AsyncTests extends BasicJSEnvTests { protected def newJSEnv: AsyncJSEnv - protected def emptyCP: CompleteClasspath = CompleteClasspath.empty - protected def asyncRunner(code: String): AsyncJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) - newJSEnv.asyncRunner(emptyCP, codeVF, - new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) + newJSEnv.asyncRunner(codeVF) + } + + protected def start(runner: AsyncJSRunner): Future[Unit] = { + runner.start(new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) } @Test def futureTest: Unit = { val runner = asyncRunner("") - val fut = runner.start() + val fut = start(runner) Await.result(fut, DefaultTimeout) @@ -40,7 +40,7 @@ trait AsyncTests extends BasicJSEnvTests { @Test def stopAfterTerminatedTest: Unit = { val runner = asyncRunner("") - val fut = runner.start() + val fut = start(runner) Await.result(fut, DefaultTimeout) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala index 2294b88..9dc42a1 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala @@ -15,12 +15,9 @@ trait ComTests extends AsyncTests { protected def newJSEnv: ComJSEnv - protected def logger: Logger = - new ScalaConsoleLogger(Level.Warn) - protected def comRunner(code: String): ComJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) - newJSEnv.comRunner(emptyCP, codeVF, logger, ConsoleJSConsole) + newJSEnv.comRunner(codeVF) } private def assertThrowClosed(msg: String, body: => Unit): Unit = { @@ -42,7 +39,7 @@ trait ComTests extends AsyncTests { scalajsCom.send("Hello World"); """) - com.start() + start(com) assertEquals("Hello World", com.receive()) @@ -65,7 +62,7 @@ trait ComTests extends AsyncTests { scalajsCom.close(); """) - com.start() + start(com) Thread.sleep(timeout) @@ -92,7 +89,7 @@ trait ComTests extends AsyncTests { val n = 10 val com = pingPongRunner(n) - com.start() + start(com) for (i <- 0 until n) { com.send("ping") @@ -108,7 +105,7 @@ trait ComTests extends AsyncTests { val n = 10 val envs = List.fill(5)(pingPongRunner(10)) - envs.foreach(_.start()) + envs.foreach(start) val ops = List[ComJSRunner => Unit]( _.send("ping"), @@ -151,7 +148,7 @@ trait ComTests extends AsyncTests { }); """) - com.start() + start(com) com.send(baseMsg) @@ -183,7 +180,7 @@ trait ComTests extends AsyncTests { scalajsCom.init(scalajsCom.send); """) - com.start() + start(com) val msg = "\uC421\u8F10\u0112\uFF32" @@ -198,7 +195,7 @@ trait ComTests extends AsyncTests { def noInitTest: Unit = { val com = comRunner("") - com.start() + start(com) com.send("Dummy") com.close() com.await(DefaultTimeout) @@ -208,7 +205,7 @@ trait ComTests extends AsyncTests { def stopTestCom: Unit = { val com = comRunner(s"""scalajsCom.init(function(msg) {});""") - com.start() + start(com) // Make sure the VM doesn't terminate. Thread.sleep(1000) @@ -230,7 +227,7 @@ trait ComTests extends AsyncTests { def futureStopTest: Unit = { val com = comRunner(s"""scalajsCom.init(function(msg) {});""") - val fut = com.start() + val fut = start(com) // Make sure the VM doesn't terminate. Thread.sleep(1000) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala index 210753b..963da21 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -2,8 +2,7 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ -import org.scalajs.core.tools.io.MemVirtualJSFile -import org.scalajs.core.tools.classpath.CompleteClasspath +import org.scalajs.core.tools.io.{VirtualJSFile, MemVirtualJSFile} import org.scalajs.core.tools.logging._ import org.junit.Assert._ @@ -16,15 +15,14 @@ abstract class JSEnvTest { implicit class RunMatcher(codeStr: String) { - val emptyCP = CompleteClasspath.empty - val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) + val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) def hasOutput(expectedOut: String): Unit = { val console = new StoreJSConsole() val logger = new StoreLogger() - newJSEnv.jsRunner(emptyCP, code, logger, console).run() + newJSEnv.jsRunner(code).run(logger, console) val log = logger.getLog val hasBadLog = log exists { @@ -40,7 +38,7 @@ abstract class JSEnvTest { def fails(): Unit = { try { - newJSEnv.jsRunner(emptyCP, code, NullLogger, NullJSConsole).run() + newJSEnv.jsRunner(code).run(NullLogger, NullJSConsole) assertTrue("Code snipped should fail", false) } catch { case e: Exception => diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index 74db64f..f8fe024 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -64,7 +64,7 @@ class NodeJSTest extends TimeoutComTests { val n = 20 - com.start() + start(com) for (_ <- 1 to n) com.send("ping") diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 0a3a47f..57c41c0 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -21,7 +21,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { val deadline = 100.millis.fromNow - com.start() + start(com) com.send("Hello World") @@ -43,7 +43,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { }); """) - com.start() + start(com) for (i <- 1 to 10) { val deadline = 19.millis.fromNow // give some slack @@ -66,7 +66,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { }); """) - com.start() + start(com) for (i <- 1 to 2) { com.send(s"Hello World: $i") @@ -95,7 +95,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { val deadline = 245.millis.fromNow - com.start() + start(com) for (i <- 1 to 5) assertEquals("Hello", com.receive()) @@ -113,7 +113,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { // Make sure JVM has already closed when we init setTimeout(scalajsCom.init, 1000, function(msg) {}); """) - com.start() + start(com) com.close() com.await(DefaultTimeout) } @@ -125,7 +125,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { setInterval(function() {}, 0); """) - async.start() + start(async) async.stop() try { @@ -145,7 +145,7 @@ trait TimeoutComTests extends TimeoutTests with ComTests { setInterval(function() {}, 0); """) - async.start() + start(async) async.stop() async.stop() // should do nothing, and not fail From eac5177a97685a440ffe78c5eede5b44c0f7b7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 17 Jan 2016 21:06:32 +0100 Subject: [PATCH 069/133] Make private everything that can be in tools and js-envs. --- .../main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala index 316f334..1f25a92 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala @@ -86,6 +86,6 @@ final class VirtualFileMaterializer(singleDir: Boolean = false) { } -object VirtualFileMaterializer { +private object VirtualFileMaterializer { private final val TempDirAttempts = 10000 } From a814cc3bfdda1b2984cb9a1d80001e3e1ccc4c95 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 21 Dec 2015 16:34:09 +0100 Subject: [PATCH 070/133] Add DOMJSEnv based on 'jsdom' in Node.js. --- .../org/scalajs/jsenv/ExternalJSEnv.scala | 5 +- .../scala/org/scalajs/jsenv/JSInitFiles.scala | 8 + .../jsenv/nodejs/AbstractNodeJSEnv.scala | 310 ++++++++++++++++++ .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 298 +---------------- 4 files changed, 331 insertions(+), 290 deletions(-) create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 4123cf6..a19ed6e 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -29,7 +29,7 @@ abstract class ExternalJSEnv( protected class AbstractExtRunner( protected val libs: Seq[ResolvedJSDependency], - protected val code: VirtualJSFile) { + protected val code: VirtualJSFile) extends JSInitFiles { private[this] var _logger: Logger = _ private[this] var _console: JSConsole = _ @@ -43,9 +43,6 @@ abstract class ExternalJSEnv( _console = console } - /** JS files used to setup VM */ - protected def initFiles(): Seq[VirtualJSFile] = Nil - /** Custom initialization scripts, defined by the environment. */ final protected def customInitFiles(): Seq[VirtualJSFile] = ExternalJSEnv.this.customInitFiles() diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala new file mode 100644 index 0000000..fad9e12 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala @@ -0,0 +1,8 @@ +package org.scalajs.jsenv + +import org.scalajs.core.tools.io.VirtualJSFile + +trait JSInitFiles { + /** JS files used to setup VM */ + protected def initFiles(): Seq[VirtualJSFile] = Nil +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala new file mode 100644 index 0000000..d0047c5 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -0,0 +1,310 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv.nodejs + +import java.io.{Console => _, _} +import java.net._ + +import org.scalajs.core.ir.Utils.escapeJS +import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.jsdep.ResolvedJSDependency +import org.scalajs.core.tools.logging.NullLogger +import org.scalajs.jsenv._ +import org.scalajs.jsenv.Utils.OptDeadline + +import scala.concurrent.TimeoutException +import scala.concurrent.duration._ + +abstract class AbstractNodeJSEnv(nodejsPath: String, addArgs: Seq[String], + addEnv: Map[String, String], val sourceMap: Boolean) + extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv { + + /** True, if the installed node executable supports source mapping. + * + * Do `npm install source-map-support` if you need source maps. + */ + lazy val hasSourceMapSupport: Boolean = { + val code = new MemVirtualJSFile("source-map-support-probe.js") + .withContent("""require('source-map-support').install();""") + + try { + jsRunner(code).run(NullLogger, NullJSConsole) + true + } catch { + case t: ExternalJSEnv.NonZeroExitException => + false + } + } + + protected def executable: String = nodejsPath + + /** Retry-timeout to wait for the JS VM to connect */ + protected val acceptTimeout = 5000 + + protected trait AbstractNodeRunner extends AbstractExtRunner with JSInitFiles { + + protected[this] val libCache = new VirtualFileMaterializer(true) + + /** File(s) to automatically install source-map-support. + * Is used by [[initFiles]], override to change/disable. + */ + protected def installSourceMap(): Seq[VirtualJSFile] = { + if (sourceMap) Seq( + new MemVirtualJSFile("sourceMapSupport.js").withContent( + """ + |try { + | require('source-map-support').install(); + |} catch (e) {} + """.stripMargin + ) + ) else Seq() + } + + /** File(s) to hack console.log to prevent if from changing `%%` to `%`. + * Is used by [[initFiles]], override to change/disable. + */ + protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq( + new MemVirtualJSFile("nodeConsoleHack.js").withContent( + """ + |// Hack console log to duplicate double % signs + |(function() { + | function startsWithAnyOf(s, prefixes) { + | for (var i = 0; i < prefixes.length; i++) { + | // ES5 does not have .startsWith() on strings + | if (s.substring(0, prefixes[i].length) === prefixes[i]) + | return true; + | } + | return false; + | } + | var nodeWillDeduplicateEvenForOneArgument = startsWithAnyOf( + | process.version, ["v0.", "v1.", "v2.0."]); + | var oldLog = console.log; + | var newLog = function() { + | var args = arguments; + | if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { + | var argStr = args[0].toString(); + | if (args.length > 1 || nodeWillDeduplicateEvenForOneArgument) + | argStr = argStr.replace(/%/g, "%%"); + | args[0] = argStr; + | } + | oldLog.apply(console, args); + | }; + | console.log = newLog; + |})(); + """.stripMargin + ) + ) + + + /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`. + * Is used by [[initFiles]], override to change/disable. + */ + protected def runtimeEnv(): Seq[VirtualJSFile] = Seq( + new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( + """ + |__ScalaJSEnv = { + | exitFunction: function(status) { process.exit(status); } + |}; + """.stripMargin + ) + ) + + override protected def initFiles(): Seq[VirtualJSFile] = + installSourceMap() ++ fixPercentConsole() ++ runtimeEnv() + + /** write a single JS file to a writer using an include fct if appropriate + * uses `require` if the file exists on the filesystem + */ + override protected def writeJSFile(file: VirtualJSFile, + writer: Writer): Unit = { + file match { + case file: FileVirtualJSFile => + val fname = file.file.getAbsolutePath + writer.write(s"""require("${escapeJS(fname)}");\n""") + case _ => + super.writeJSFile(file, writer) + } + } + + // Node.js specific (system) environment + override protected def getVMEnv(): Map[String, String] = { + val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) + val nodePath = libCache.cacheDir.getAbsolutePath + + baseNodePath.fold("")(p => File.pathSeparator + p) + + sys.env ++ Seq( + "NODE_MODULE_CONTEXTS" -> "0", + "NODE_PATH" -> nodePath + ) ++ additionalEnv + } + } + + protected trait NodeComJSRunner extends ComJSRunner with JSInitFiles { + + private[this] val serverSocket = + new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address + private var comSocket: Socket = _ + private var jvm2js: DataOutputStream = _ + private var js2jvm: DataInputStream = _ + + abstract override protected def initFiles(): Seq[VirtualJSFile] = + super.initFiles :+ comSetup + + private def comSetup(): VirtualJSFile = { + new MemVirtualJSFile("comSetup.js").withContent( + s""" + |(function() { + | // The socket for communication + | var socket = null; + | // The callback where received messages go + | var recvCallback = null; + | + | // Buffers received data + | var inBuffer = new Buffer(0); + | + | function onData(data) { + | inBuffer = Buffer.concat([inBuffer, data]); + | tryReadMsg(); + | } + | + | function tryReadMsg() { + | while (inBuffer.length >= 4) { + | var msgLen = inBuffer.readInt32BE(0); + | var byteLen = 4 + msgLen * 2; + | + | if (inBuffer.length < byteLen) return; + | var res = ""; + | + | for (var i = 0; i < msgLen; ++i) + | res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); + | + | inBuffer = inBuffer.slice(byteLen); + | + | recvCallback(res); + | } + | } + | + | global.scalajsCom = { + | init: function(recvCB) { + | if (socket !== null) throw new Error("Com already open"); + | + | var net = require('net'); + | recvCallback = recvCB; + | socket = net.connect(${serverSocket.getLocalPort}); + | socket.on('data', onData); + | socket.on('error', function(err) { + | // Whatever happens, this closes the Com + | socket.end(); + | + | // Expected errors: + | // - EPIPE on write: JVM closes + | // - ECONNREFUSED on connect: JVM closes before JS opens + | var expected = ( + | err.syscall === "write" && err.code === "EPIPE" || + | err.syscall === "connect" && err.code === "ECONNREFUSED" + | ); + | + | if (!expected) { + | console.error("Scala.js Com failed: " + err); + | // We must terminate with an error + | process.exit(-1); + | } + | }); + | }, + | send: function(msg) { + | if (socket === null) throw new Error("Com not open"); + | + | var len = msg.length; + | var buf = new Buffer(4 + len * 2); + | buf.writeInt32BE(len, 0); + | for (var i = 0; i < len; ++i) + | buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2); + | socket.write(buf); + | }, + | close: function() { + | if (socket === null) throw new Error("Com not open"); + | socket.end(); + | } + | } + |}).call(this); + """.stripMargin) + } + + def send(msg: String): Unit = { + if (awaitConnection()) { + jvm2js.writeInt(msg.length) + jvm2js.writeChars(msg) + jvm2js.flush() + } + } + + def receive(timeout: Duration): String = { + if (!awaitConnection()) + throw new ComJSEnv.ComClosedException("Node.js isn't connected") + + js2jvm.mark(Int.MaxValue) + val savedSoTimeout = comSocket.getSoTimeout() + try { + val optDeadline = OptDeadline(timeout) + + comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) + val len = js2jvm.readInt() + val carr = Array.fill(len) { + comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) + js2jvm.readChar() + } + + js2jvm.mark(0) + String.valueOf(carr) + } catch { + case e: EOFException => + throw new ComJSEnv.ComClosedException(e) + case e: SocketTimeoutException => + js2jvm.reset() + throw new TimeoutException("Timeout expired") + } finally { + comSocket.setSoTimeout(savedSoTimeout) + } + } + + def close(): Unit = { + serverSocket.close() + if (jvm2js != null) + jvm2js.close() + if (js2jvm != null) + js2jvm.close() + if (comSocket != null) + comSocket.close() + } + + /** Waits until the JS VM has established a connection or terminates + * + * @return true if the connection was established + */ + private def awaitConnection(): Boolean = { + serverSocket.setSoTimeout(acceptTimeout) + while (comSocket == null && isRunning) { + try { + comSocket = serverSocket.accept() + jvm2js = new DataOutputStream( + new BufferedOutputStream(comSocket.getOutputStream())) + js2jvm = new DataInputStream( + new BufferedInputStream(comSocket.getInputStream())) + } catch { + case to: SocketTimeoutException => + } + } + + comSocket != null + } + + override protected def finalize(): Unit = close() + } +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index b7dd659..5f01785 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -10,7 +10,6 @@ package org.scalajs.jsenv.nodejs import org.scalajs.jsenv._ -import org.scalajs.jsenv.Utils.OptDeadline import org.scalajs.core.ir.Utils.escapeJS @@ -19,18 +18,14 @@ import org.scalajs.core.tools.jsdep.ResolvedJSDependency import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } -import java.net._ -import scala.concurrent.TimeoutException -import scala.concurrent.duration._ -import scala.io.Source class NodeJSEnv private ( nodejsPath: String, addArgs: Seq[String], addEnv: Map[String, String], - val sourceMap: Boolean -) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv { + sourceMap: Boolean +) extends AbstractNodeJSEnv(nodejsPath, addArgs, addEnv, sourceMap) { def this(nodejsPath: String = "node", addArgs: Seq[String] = Seq.empty, addEnv: Map[String, String] = Map.empty) = { @@ -40,28 +35,10 @@ class NodeJSEnv private ( def withSourceMap(sourceMap: Boolean): NodeJSEnv = new NodeJSEnv(nodejsPath, addArgs, addEnv, sourceMap) - /** True, if the installed node executable supports source mapping. - * - * Do `npm install source-map-support` if you need source maps. - */ - lazy val hasSourceMapSupport: Boolean = { - val code = new MemVirtualJSFile("source-map-support-probe.js") - .withContent("""require('source-map-support').install();""") - - try { - jsRunner(code).run(NullLogger, NullJSConsole) - true - } catch { - case t: ExternalJSEnv.NonZeroExitException => - false - } - } - protected def vmName: String = "Node.js" - protected def executable: String = nodejsPath - /** Retry-timeout to wait for the JS VM to connect */ - protected val acceptTimeout = 5000 + // For binary compatibility, now `executable` is defined in AbstractNodeJSEnv + override protected def executable: String = super.executable override def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = { @@ -79,244 +56,15 @@ class NodeJSEnv private ( } protected class NodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) - extends ExtRunner(libs, code) with AbstractNodeRunner + extends ExtRunner(libs, code) with AbstractBasicNodeRunner protected class AsyncNodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) - extends AsyncExtRunner(libs, code) with AbstractNodeRunner + extends AsyncExtRunner(libs, code) with AbstractBasicNodeRunner protected class ComNodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) - extends AsyncNodeRunner(libs, code) with ComJSRunner { - - private[this] val serverSocket = - new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address - private[this] var comSocket: Socket = _ - private[this] var jvm2js: DataOutputStream = _ - private[this] var js2jvm: DataInputStream = _ - - private def comSetup = new MemVirtualJSFile("comSetup.js").withContent( - s""" - (function() { - // The socket for communication - var socket = null; - // The callback where received messages go - var recvCallback = null; - - // Buffers received data - var inBuffer = new Buffer(0); - - function onData(data) { - inBuffer = Buffer.concat([inBuffer, data]); - tryReadMsg(); - } - - function tryReadMsg() { - while (inBuffer.length >= 4) { - var msgLen = inBuffer.readInt32BE(0); - var byteLen = 4 + msgLen * 2; - - if (inBuffer.length < byteLen) return; - var res = ""; - - for (var i = 0; i < msgLen; ++i) - res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); - - inBuffer = inBuffer.slice(byteLen); - - recvCallback(res); - } - } - - global.scalajsCom = { - init: function(recvCB) { - if (socket !== null) throw new Error("Com already open"); - - var net = require('net'); - recvCallback = recvCB; - socket = net.connect(${serverSocket.getLocalPort}); - socket.on('data', onData); - socket.on('error', function(err) { - // Whatever happens, this closes the Com - socket.end(); - - // Expected errors: - // - EPIPE on write: JVM closes - // - ECONNREFUSED on connect: JVM closes before JS opens - var expected = ( - err.syscall === "write" && err.code === "EPIPE" || - err.syscall === "connect" && err.code === "ECONNREFUSED" - ); - - if (!expected) { - console.error("Scala.js Com failed: " + err); - // We must terminate with an error - process.exit(-1); - } - }); - }, - send: function(msg) { - if (socket === null) throw new Error("Com not open"); - - var len = msg.length; - var buf = new Buffer(4 + len * 2); - buf.writeInt32BE(len, 0); - for (var i = 0; i < len; ++i) - buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2); - socket.write(buf); - }, - close: function() { - if (socket === null) throw new Error("Com not open"); - socket.end(); - } - } - }).call(this); - """ - ) - - def send(msg: String): Unit = { - if (awaitConnection()) { - jvm2js.writeInt(msg.length) - jvm2js.writeChars(msg) - jvm2js.flush() - } - } - - def receive(timeout: Duration): String = { - if (!awaitConnection()) - throw new ComJSEnv.ComClosedException("Node.js isn't connected") - - js2jvm.mark(Int.MaxValue) - val savedSoTimeout = comSocket.getSoTimeout() - try { - val optDeadline = OptDeadline(timeout) - - comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) - val len = js2jvm.readInt() - val carr = Array.fill(len) { - comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) - js2jvm.readChar() - } - - js2jvm.mark(0) - String.valueOf(carr) - } catch { - case e: EOFException => - throw new ComJSEnv.ComClosedException(e) - case e: SocketTimeoutException => - js2jvm.reset() - throw new TimeoutException("Timeout expired") - } finally { - comSocket.setSoTimeout(savedSoTimeout) - } - } - - def close(): Unit = { - serverSocket.close() - if (jvm2js != null) - jvm2js.close() - if (js2jvm != null) - js2jvm.close() - if (comSocket != null) - comSocket.close() - } + extends AsyncNodeRunner(libs, code) with NodeComJSRunner - /** Waits until the JS VM has established a connection or terminates - * @return true if the connection was established - */ - private def awaitConnection(): Boolean = { - serverSocket.setSoTimeout(acceptTimeout) - while (comSocket == null && isRunning) { - try { - comSocket = serverSocket.accept() - jvm2js = new DataOutputStream( - new BufferedOutputStream(comSocket.getOutputStream())) - js2jvm = new DataInputStream( - new BufferedInputStream(comSocket.getInputStream())) - } catch { - case to: SocketTimeoutException => - } - } - - comSocket != null - } - - override protected def initFiles(): Seq[VirtualJSFile] = - super.initFiles :+ comSetup - - override protected def finalize(): Unit = close() - } - - protected trait AbstractNodeRunner extends AbstractExtRunner { - - protected[this] val libCache = new VirtualFileMaterializer(true) - - /** File(s) to automatically install source-map-support. - * Is used by [[initFiles]], override to change/disable. - */ - protected def installSourceMap(): Seq[VirtualJSFile] = { - if (sourceMap) Seq( - new MemVirtualJSFile("sourceMapSupport.js").withContent( - """ - try { - require('source-map-support').install(); - } catch (e) {} - """ - ) - ) else Seq() - } - - /** File(s) to hack console.log to prevent if from changing `%%` to `%`. - * Is used by [[initFiles]], override to change/disable. - */ - protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("nodeConsoleHack.js").withContent( - """ - // Hack console log to duplicate double % signs - (function() { - function startsWithAnyOf(s, prefixes) { - for (var i = 0; i < prefixes.length; i++) { - // ES5 does not have .startsWith() on strings - if (s.substring(0, prefixes[i].length) === prefixes[i]) - return true; - } - return false; - } - var nodeWillDeduplicateEvenForOneArgument = startsWithAnyOf( - process.version, ["v0.", "v1.", "v2.0."]); - var oldLog = console.log; - var newLog = function() { - var args = arguments; - if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { - var argStr = args[0].toString(); - if (args.length > 1 || nodeWillDeduplicateEvenForOneArgument) - argStr = argStr.replace(/%/g, "%%"); - args[0] = argStr; - } - oldLog.apply(console, args); - }; - console.log = newLog; - })(); - """ - ) - ) - - /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`. - * Is used by [[initFiles]], override to change/disable. - */ - protected def runtimeEnv(): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( - """ - __ScalaJSEnv = { - exitFunction: function(status) { process.exit(status); } - }; - """ - ) - ) - - /** Concatenates results from [[installSourceMap]], [[fixPercentConsole]] and - * [[runtimeEnv]] (in this order). - */ - override protected def initFiles(): Seq[VirtualJSFile] = - installSourceMap() ++ fixPercentConsole() ++ runtimeEnv() + protected trait AbstractBasicNodeRunner extends AbstractNodeRunner { /** Libraries are loaded via require in Node.js */ override protected def getLibJSFiles(): Seq[VirtualJSFile] = { @@ -338,34 +86,12 @@ class NodeJSEnv private ( // Send code to Stdin override protected def sendVMStdin(out: OutputStream): Unit = { + /* Do not factor this method out into AbstractNodeRunner or when mixin in + * the traits it would use AbstractExtRunner.sendVMStdin due to + * linearization order. + */ sendJS(getJSFiles(), out) } - - /** write a single JS file to a writer using an include fct if appropriate - * uses `require` if the file exists on the filesystem - */ - override protected def writeJSFile(file: VirtualJSFile, - writer: Writer): Unit = { - file match { - case file: FileVirtualJSFile => - val fname = file.file.getAbsolutePath - writer.write(s"""require("${escapeJS(fname)}");\n""") - case _ => - super.writeJSFile(file, writer) - } - } - - // Node.js specific (system) environment - override protected def getVMEnv(): Map[String, String] = { - val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) - val nodePath = libCache.cacheDir.getAbsolutePath + - baseNodePath.fold("")(p => File.pathSeparator + p) - - sys.env ++ Seq( - "NODE_MODULE_CONTEXTS" -> "0", - "NODE_PATH" -> nodePath - ) ++ additionalEnv - } } } From 3ffff104745bd5fff209314a55e802ac51444f2e Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 25 Apr 2016 11:25:39 +0200 Subject: [PATCH 071/133] Fix scala-js/scala-js#2250: Split jsEnvs, creating jsEnvsTestKit, jsEnvsTestSuite. This will make it possible to publish jsEnvsTestKit (issue scala-js/scala-js#2250) to be able to test external jsEnvs like SeleniumJSEnv. jsEnvsTestSuite contains the test implementations of jsEnvs implemented in scala-js/scala-js. --- .../src/main}/scala/org/scalajs/jsenv/test/AsyncTests.scala | 0 .../main}/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala | 0 .../src/main}/scala/org/scalajs/jsenv/test/ComTests.scala | 1 - .../scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala | 1 - .../src/main}/scala/org/scalajs/jsenv/test/JSEnvTest.scala | 2 +- .../src/main}/scala/org/scalajs/jsenv/test/StoreJSConsole.scala | 0 .../src/main}/scala/org/scalajs/jsenv/test/StoreLogger.scala | 0 .../main}/scala/org/scalajs/jsenv/test/TimeoutComTests.scala | 0 .../src/main}/scala/org/scalajs/jsenv/test/TimeoutTests.scala | 0 .../src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala | 0 10 files changed, 1 insertion(+), 3 deletions(-) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/AsyncTests.scala (100%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala (100%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/ComTests.scala (99%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala (97%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/JSEnvTest.scala (94%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/StoreJSConsole.scala (100%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/StoreLogger.scala (100%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/TimeoutComTests.scala (100%) rename {js-envs/src/test => js-envs-test-kit/src/main}/scala/org/scalajs/jsenv/test/TimeoutTests.scala (100%) rename {js-envs => js-envs-test-suite}/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala (100%) diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala similarity index 100% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/AsyncTests.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala similarity index 100% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala similarity index 99% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 9dc42a1..cb6415d 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -3,7 +3,6 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.logging._ import org.junit.Test import org.junit.Assert._ diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala similarity index 97% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala index 7f5cd2f..bc8e643 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala @@ -1,6 +1,5 @@ package org.scalajs.jsenv.test -import org.scalajs.jsenv._ import org.scalajs.jsenv.nodejs.NodeJSEnv import org.scalajs.jsenv.phantomjs.PhantomJSEnv diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala similarity index 94% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala index 963da21..e488599 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -2,7 +2,7 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ -import org.scalajs.core.tools.io.{VirtualJSFile, MemVirtualJSFile} +import org.scalajs.core.tools.io.MemVirtualJSFile import org.scalajs.core.tools.logging._ import org.junit.Assert._ diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreJSConsole.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala similarity index 100% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/StoreJSConsole.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala similarity index 100% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/StoreLogger.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala similarity index 100% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutComTests.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala similarity index 100% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/TimeoutTests.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala similarity index 100% rename from js-envs/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala rename to js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala From 6f1be71b7bf44931829860e17b42abc87bc357a9 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 2 May 2016 13:59:55 +0200 Subject: [PATCH 072/133] Fix scala-js/scala-js#2375: Move Node and Phantom CustomInitFilesTest to jsEnvTestSuite. --- .../scalajs/jsenv/test/CustomInitFilesTest.scala | 15 --------------- .../test/NodeJSWithCustomInitFilesTest.scala | 9 +++++++++ 2 files changed, 9 insertions(+), 15 deletions(-) create mode 100644 js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala index bc8e643..9e2e5e0 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala @@ -1,8 +1,5 @@ package org.scalajs.jsenv.test -import org.scalajs.jsenv.nodejs.NodeJSEnv -import org.scalajs.jsenv.phantomjs.PhantomJSEnv - import org.scalajs.core.tools.io._ import org.junit.Test @@ -25,15 +22,3 @@ abstract class CustomInitFilesTest extends JSEnvTest { |""".stripMargin } } - -class NodeJSWithCustomInitFilesTest extends CustomInitFilesTest { - protected def newJSEnv: NodeJSEnv = new NodeJSEnv { - override def customInitFiles() = makeCustomInitFiles() - } -} - -class PhantomJSWithCustomInitFilesTest extends CustomInitFilesTest { - protected def newJSEnv: PhantomJSEnv = new PhantomJSEnv { - override def customInitFiles() = makeCustomInitFiles() - } -} diff --git a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala new file mode 100644 index 0000000..758a919 --- /dev/null +++ b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala @@ -0,0 +1,9 @@ +package org.scalajs.jsenv.test + +import org.scalajs.jsenv.nodejs.NodeJSEnv + +class NodeJSWithCustomInitFilesTest extends CustomInitFilesTest { + protected def newJSEnv: NodeJSEnv = new NodeJSEnv { + override def customInitFiles() = makeCustomInitFiles() + } +} From 03388d466ae13e191346fddbb0ac62b2ef5d1091 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 30 Apr 2016 10:58:19 +0200 Subject: [PATCH 073/133] Fix scala-js/scala-js#2368: Support single-arg setTimeout in Rhino --- .../main/scala/org/scalajs/jsenv/test/TimeoutTests.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala index 9dc3bc9..2191ef7 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala @@ -51,6 +51,13 @@ trait TimeoutTests extends JSEnvTest { } + @Test // #2368 + def timeoutSingleArgTest: Unit = { + """ + setTimeout(function() { console.log("ok"); }); + """ hasOutput "ok\n" + } + @Test def timeoutArgTest: Unit = { From ac662e691775eeb7c2a1afaa2a9fde8f4e116f40 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 1 May 2016 23:07:21 +0200 Subject: [PATCH 074/133] Fix scala-js/scala-js#2376: PhantomJSEnv does not escape JS code --- .../scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala index 26f4297..4404f18 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala @@ -34,4 +34,13 @@ trait BasicJSEnvTests extends JSEnvTest { } + @Test + def allowScriptTags: Unit = { + + """ + console.log(""); + """ hasOutput "\n"; + + } + } From b21dab5bd98ea1fc4d42e7f39469d0c35c112779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 25 Mar 2017 00:20:05 +0100 Subject: [PATCH 075/133] Remove LinkingUnitJSEnv. This API was only useful for Rhino's lazy loading. --- .../scalajs/jsenv/LinkingUnitAsyncJSEnv.scala | 42 ----------- .../scalajs/jsenv/LinkingUnitComJSEnv.scala | 43 ----------- .../org/scalajs/jsenv/LinkingUnitJSEnv.scala | 72 ------------------- 3 files changed, 157 deletions(-) delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala deleted file mode 100644 index dc22192..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala +++ /dev/null @@ -1,42 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -import org.scalajs.core.tools.io.VirtualJSFile -import org.scalajs.core.tools.jsdep.ResolvedJSDependency -import org.scalajs.core.tools.linker.LinkingUnit - -trait LinkingUnitAsyncJSEnv extends LinkingUnitJSEnv with AsyncJSEnv { - def asyncRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, - postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): AsyncJSRunner - - override def loadLibs(libs: Seq[ResolvedJSDependency]): LinkingUnitAsyncJSEnv = - new LinkingUnitAsyncLoadedLibs { val loadedLibs = libs } - - override def loadLinkingUnit(linkingUnit: LinkingUnit): AsyncJSEnv = - new AsyncLoadedUnit { val loadedUnit = linkingUnit } - - private[jsenv] trait LinkingUnitAsyncLoadedLibs extends LinkingUnitLoadedLibs - with AsyncLoadedLibs with LinkingUnitAsyncJSEnv { - def asyncRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, - postLibs: Seq[ResolvedJSDependency], - code: VirtualJSFile): AsyncJSRunner = { - LinkingUnitAsyncJSEnv.this.asyncRunner(loadedLibs ++ preLibs, linkingUnit, - postLibs, code) - } - } - - private[jsenv] trait AsyncLoadedUnit extends LoadedUnit with AsyncJSEnv { - def asyncRunner(libs: Seq[ResolvedJSDependency], - code: VirtualJSFile): AsyncJSRunner = { - LinkingUnitAsyncJSEnv.this.asyncRunner(Nil, loadedUnit, libs, code) - } - } -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala deleted file mode 100644 index dc39bbc..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala +++ /dev/null @@ -1,43 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -import org.scalajs.core.tools.io.VirtualJSFile -import org.scalajs.core.tools.jsdep.ResolvedJSDependency -import org.scalajs.core.tools.linker.LinkingUnit - -trait LinkingUnitComJSEnv extends LinkingUnitAsyncJSEnv with ComJSEnv { - def comRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, - postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): ComJSRunner - - override def loadLibs(libs: Seq[ResolvedJSDependency]): LinkingUnitComJSEnv = - new LinkingUnitComLoadedLibs { val loadedLibs = libs } - - override def loadLinkingUnit(linkingUnit: LinkingUnit): ComJSEnv = - new ComLoadedUnit { val loadedUnit = linkingUnit } - - private[jsenv] trait LinkingUnitComLoadedLibs - extends LinkingUnitAsyncLoadedLibs with ComLoadedLibs - with LinkingUnitComJSEnv { - def comRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, - postLibs: Seq[ResolvedJSDependency], - code: VirtualJSFile): ComJSRunner = { - LinkingUnitComJSEnv.this.comRunner(loadedLibs ++ preLibs, linkingUnit, - postLibs, code) - } - } - - private[jsenv] trait ComLoadedUnit extends AsyncLoadedUnit with ComJSEnv { - def comRunner(libs: Seq[ResolvedJSDependency], - code: VirtualJSFile): ComJSRunner = { - LinkingUnitComJSEnv.this.comRunner(Nil, loadedUnit, libs, code) - } - } -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala deleted file mode 100644 index 6ba33e6..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala +++ /dev/null @@ -1,72 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -import org.scalajs.core.tools.io.VirtualJSFile -import org.scalajs.core.tools.jsdep.ResolvedJSDependency -import org.scalajs.core.tools.linker.LinkingUnit -import org.scalajs.core.tools.linker.analyzer.SymbolRequirement - -trait LinkingUnitJSEnv extends JSEnv { - /** Symbols this [[LinkingUnitJSEnv]] needs present in the - * [[org.scalajs.core.tools.linker.LinkingUnit LinkingUnit]] it receives. - */ - val symbolRequirements: SymbolRequirement - - /** Prepare a runner for the code in the virtual file. */ - def jsRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, - postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner - - override def loadLibs(libs: Seq[ResolvedJSDependency]): LinkingUnitJSEnv = - new LinkingUnitLoadedLibs { val loadedLibs = libs } - - /** Returns a [[JSEnv]] with the given - * [[org.scalajs.core.tools.linker.LinkingUnit LinkingUnit]] already loaded. - * - * Note that any subsequent libraries will be inserted after the - * [[org.scalajs.core.tools.linker.LinkingUnit LinkingUnit]]. - * - * Hence, the following are equivalent: - * {{{ - * jsEnv.loadUnit(a).jsRunner(b, c) - * jsEnv.jsRunner(Nil, a, b, c) - * }}} - * - * If you need to load libraries before, you can use the [[loadLibs]] method: - * {{{ - * jsEnv.loadLibs(a).loadUnit(b).jsRunner(c, d) - * // equivalent to - * jsEnv.jsRunner(a, b, c, d) - * }}} - */ - def loadLinkingUnit(linkingUnit: LinkingUnit): JSEnv = - new LoadedUnit { val loadedUnit = linkingUnit } - - private[jsenv] trait LinkingUnitLoadedLibs - extends LoadedLibs with LinkingUnitJSEnv { - val symbolRequirements: SymbolRequirement = - LinkingUnitJSEnv.this.symbolRequirements - - def jsRunner(preLibs: Seq[ResolvedJSDependency], linkingUnit: LinkingUnit, - postLibs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = { - LinkingUnitJSEnv.this.jsRunner(loadedLibs ++ preLibs, - linkingUnit, postLibs, code) - } - } - - private[jsenv] trait LoadedUnit extends JSEnv { - val loadedUnit: LinkingUnit - - def name: String = LinkingUnitJSEnv.this.name - - def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = - LinkingUnitJSEnv.this.jsRunner(Nil, loadedUnit, libs, code) - } -} From 5c64e4d931910c68e50b1310c747df9ca204e02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 28 Mar 2017 16:23:13 +0200 Subject: [PATCH 076/133] Move the implemention of commonJSName to the sbt plugin. It was previously done inside `NodeJSEnv`, which was good from an Object-Oriented point of view. However, this required that the API of `JSEnv` knows the notion of `commonJSName`, which is going to be a serious issue when we isolate `jsDependencies` out of the core. This commit moves the implementation of the `commonJSName` behavior in the sbt plugin, whence it can be extracted to the `jsDependencies` plugin in the future. This has the disadvantage that we burn `NodeJSEnv` as the only `JSEnv` for which `commonJSName` is a thing. We can compensate for that by a user-overridable predicate in the future `jsDependencies` plugin if necessary. --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 5f01785..37cb682 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -11,8 +11,6 @@ package org.scalajs.jsenv.nodejs import org.scalajs.jsenv._ -import org.scalajs.core.ir.Utils.escapeJS - import org.scalajs.core.tools.io._ import org.scalajs.core.tools.jsdep.ResolvedJSDependency import org.scalajs.core.tools.logging._ @@ -66,24 +64,6 @@ class NodeJSEnv private ( protected trait AbstractBasicNodeRunner extends AbstractNodeRunner { - /** Libraries are loaded via require in Node.js */ - override protected def getLibJSFiles(): Seq[VirtualJSFile] = { - initFiles() ++ - customInitFiles() ++ - libs.map(requireLibrary) - } - - /** Rewrites a library virtual file to a require statement if possible */ - protected def requireLibrary(dep: ResolvedJSDependency): VirtualJSFile = { - dep.info.commonJSName.fold(dep.lib) { varname => - val fname = dep.lib.name - libCache.materialize(dep.lib) - new MemVirtualJSFile(s"require-$fname").withContent( - s"""$varname = require("${escapeJS(fname)}");""" - ) - } - } - // Send code to Stdin override protected def sendVMStdin(out: OutputStream): Unit = { /* Do not factor this method out into AbstractNodeRunner or when mixin in From 86fedd1b9576a36e711c7c18ab57b4e859ab72bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 28 Mar 2017 16:51:41 +0200 Subject: [PATCH 077/133] Use VirtualJSFiles instead of ResolvedJSDependency in the JSEnv API. This removes the annoying dependency (sic!) of the `JSEnv` API on `ResolvedJSDependency`. This was the only part of `tools.jsdep` that was used in the `JSEnv` API, so it decouples those. This will be very important as we separate `jsDependencies` in a separate plugin, as the `tools.jsdep` package should go with that, out of the core. --- .../main/scala/org/scalajs/jsenv/AsyncJSEnv.scala | 7 +++---- .../src/main/scala/org/scalajs/jsenv/ComJSEnv.scala | 7 +++---- .../scala/org/scalajs/jsenv/ExternalJSEnv.scala | 9 ++++----- .../src/main/scala/org/scalajs/jsenv/JSEnv.scala | 9 ++++----- .../scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 13 ++++++------- 5 files changed, 20 insertions(+), 25 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala index c0ba9aa..9b6c8df 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala @@ -10,19 +10,18 @@ package org.scalajs.jsenv import org.scalajs.core.tools.io.VirtualJSFile -import org.scalajs.core.tools.jsdep.ResolvedJSDependency trait AsyncJSEnv extends JSEnv { - def asyncRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): AsyncJSRunner + def asyncRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): AsyncJSRunner final def asyncRunner(code: VirtualJSFile): AsyncJSRunner = asyncRunner(Nil, code) - override def loadLibs(libs: Seq[ResolvedJSDependency]): AsyncJSEnv = + override def loadLibs(libs: Seq[VirtualJSFile]): AsyncJSEnv = new AsyncLoadedLibs { val loadedLibs = libs } private[jsenv] trait AsyncLoadedLibs extends LoadedLibs with AsyncJSEnv { - def asyncRunner(libs: Seq[ResolvedJSDependency], + def asyncRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): AsyncJSRunner = { AsyncJSEnv.this.asyncRunner(loadedLibs ++ libs, code) } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala index 52c6dcf..b20faad 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala @@ -10,7 +10,6 @@ package org.scalajs.jsenv import org.scalajs.core.tools.io.VirtualJSFile -import org.scalajs.core.tools.jsdep.ResolvedJSDependency /** An [[AsyncJSEnv]] that provides communication to and from the JS VM. * @@ -28,15 +27,15 @@ import org.scalajs.core.tools.jsdep.ResolvedJSDependency * }}} */ trait ComJSEnv extends AsyncJSEnv { - def comRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): ComJSRunner + def comRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): ComJSRunner final def comRunner(code: VirtualJSFile): ComJSRunner = comRunner(Nil, code) - override def loadLibs(libs: Seq[ResolvedJSDependency]): ComJSEnv = + override def loadLibs(libs: Seq[VirtualJSFile]): ComJSEnv = new ComLoadedLibs { val loadedLibs = libs } private[jsenv] trait ComLoadedLibs extends AsyncLoadedLibs with ComJSEnv { - def comRunner(libs: Seq[ResolvedJSDependency], + def comRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): ComJSRunner = { ComJSEnv.this.comRunner(loadedLibs ++ libs, code) } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index a19ed6e..fe7ead4 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -2,7 +2,6 @@ package org.scalajs.jsenv import org.scalajs.core.tools.io._ import org.scalajs.core.tools.logging.Logger -import org.scalajs.core.tools.jsdep.ResolvedJSDependency import java.io.{ Console => _, _ } import scala.io.Source @@ -28,7 +27,7 @@ abstract class ExternalJSEnv( protected def customInitFiles(): Seq[VirtualJSFile] = Nil protected class AbstractExtRunner( - protected val libs: Seq[ResolvedJSDependency], + protected val libs: Seq[VirtualJSFile], protected val code: VirtualJSFile) extends JSInitFiles { private[this] var _logger: Logger = _ @@ -64,7 +63,7 @@ abstract class ExternalJSEnv( /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(): Seq[VirtualJSFile] = - initFiles() ++ customInitFiles() ++ libs.map(_.lib) + initFiles() ++ customInitFiles() ++ libs /** Get all files that are passed to VM (libraries and code) */ protected def getJSFiles(): Seq[VirtualJSFile] = @@ -144,7 +143,7 @@ abstract class ExternalJSEnv( } - protected class ExtRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + protected class ExtRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) extends AbstractExtRunner(libs, code) with JSRunner { def run(logger: Logger, console: JSConsole): Unit = { @@ -157,7 +156,7 @@ abstract class ExternalJSEnv( } } - protected class AsyncExtRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + protected class AsyncExtRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) extends AbstractExtRunner(libs, code) with AsyncJSRunner { private[this] var vmInst: Process = null diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index aa6c7a5..202a7ff 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -10,14 +10,13 @@ package org.scalajs.jsenv import org.scalajs.core.tools.io.VirtualJSFile -import org.scalajs.core.tools.jsdep.ResolvedJSDependency trait JSEnv { /** Human-readable name for this [[JSEnv]] */ def name: String /** Prepare a runner for the code in the virtual file. */ - def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner + def jsRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): JSRunner /** Prepare a runner without any libraries. * @@ -36,15 +35,15 @@ trait JSEnv { * jsEnv.jsRunner(a ++ b, c) * }}} */ - def loadLibs(libs: Seq[ResolvedJSDependency]): JSEnv = + def loadLibs(libs: Seq[VirtualJSFile]): JSEnv = new LoadedLibs { val loadedLibs = libs } private[jsenv] trait LoadedLibs extends JSEnv { - val loadedLibs: Seq[ResolvedJSDependency] + val loadedLibs: Seq[VirtualJSFile] def name: String = JSEnv.this.name - def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = + def jsRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): JSRunner = JSEnv.this.jsRunner(loadedLibs ++ libs, code) } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 37cb682..675cc56 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -12,7 +12,6 @@ package org.scalajs.jsenv.nodejs import org.scalajs.jsenv._ import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.jsdep.ResolvedJSDependency import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } @@ -38,28 +37,28 @@ class NodeJSEnv private ( // For binary compatibility, now `executable` is defined in AbstractNodeJSEnv override protected def executable: String = super.executable - override def jsRunner(libs: Seq[ResolvedJSDependency], + override def jsRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): JSRunner = { new NodeRunner(libs, code) } - override def asyncRunner(libs: Seq[ResolvedJSDependency], + override def asyncRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): AsyncJSRunner = { new AsyncNodeRunner(libs, code) } - override def comRunner(libs: Seq[ResolvedJSDependency], + override def comRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): ComJSRunner = { new ComNodeRunner(libs, code) } - protected class NodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + protected class NodeRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) extends ExtRunner(libs, code) with AbstractBasicNodeRunner - protected class AsyncNodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + protected class AsyncNodeRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) extends AsyncExtRunner(libs, code) with AbstractBasicNodeRunner - protected class ComNodeRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile) + protected class ComNodeRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) extends AsyncNodeRunner(libs, code) with NodeComJSRunner protected trait AbstractBasicNodeRunner extends AbstractNodeRunner { From a236c71eae3e5fca9c4c269fe67e5329b2bd614b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 8 Apr 2017 10:29:04 +0200 Subject: [PATCH 078/133] Unify the parameter names of external JS env constructors. They all standardize on `executable`, `args` en `env`, which are the names used by the `Initialize` constructors in `ScalaJSPlugin`. --- .../org/scalajs/jsenv/ExternalJSEnv.scala | 22 +++++++++---- .../jsenv/nodejs/AbstractNodeJSEnv.scala | 14 +++++--- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 32 +++++++++++-------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index a19ed6e..d58bc6c 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -11,8 +11,11 @@ import scala.concurrent.{Future, Promise} import scala.util.Try abstract class ExternalJSEnv( - final protected val additionalArgs: Seq[String], - final protected val additionalEnv: Map[String, String]) extends AsyncJSEnv { + @deprecatedName('additionalArgs) + final protected val args: Seq[String], + @deprecatedName('additionalEnv) + final protected val env: Map[String, String]) + extends AsyncJSEnv { import ExternalJSEnv._ @@ -24,6 +27,12 @@ abstract class ExternalJSEnv( /** Command to execute (on shell) for this VM */ protected def executable: String + @deprecated("Use `args` instead.", "0.6.16") + final protected def additionalArgs: Seq[String] = args + + @deprecated("Use `env` instead.", "0.6.16") + final protected def additionalEnv: Map[String, String] = env + /** Custom initialization scripts. */ protected def customInitFiles(): Seq[VirtualJSFile] = Nil @@ -51,16 +60,17 @@ abstract class ExternalJSEnv( protected def sendVMStdin(out: OutputStream): Unit = {} /** VM arguments excluding executable. Override to adapt. - * Overrider is responsible to add additionalArgs. + * + * The default value in `ExternalJSEnv` is `args`. */ - protected def getVMArgs(): Seq[String] = additionalArgs + protected def getVMArgs(): Seq[String] = args /** VM environment. Override to adapt. * - * Default is `sys.env` and [[additionalEnv]] + * The default value in `ExternalJSEnv` is `sys.env ++ env`. */ protected def getVMEnv(): Map[String, String] = - sys.env ++ additionalEnv + sys.env ++ env /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(): Seq[VirtualJSFile] = diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index d0047c5..a5304b6 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -22,9 +22,15 @@ import org.scalajs.jsenv.Utils.OptDeadline import scala.concurrent.TimeoutException import scala.concurrent.duration._ -abstract class AbstractNodeJSEnv(nodejsPath: String, addArgs: Seq[String], - addEnv: Map[String, String], val sourceMap: Boolean) - extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv { +abstract class AbstractNodeJSEnv( + @deprecatedName('nodejsPath) + protected val executable: String, + @deprecatedName('addArgs) + args: Seq[String], + @deprecatedName('addEnv) + env: Map[String, String], + val sourceMap: Boolean) + extends ExternalJSEnv(args, env) with ComJSEnv { /** True, if the installed node executable supports source mapping. * @@ -43,8 +49,6 @@ abstract class AbstractNodeJSEnv(nodejsPath: String, addArgs: Seq[String], } } - protected def executable: String = nodejsPath - /** Retry-timeout to wait for the JS VM to connect */ protected val acceptTimeout = 5000 diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 5f01785..8bc26f4 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -19,27 +19,31 @@ import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } - class NodeJSEnv private ( - nodejsPath: String, - addArgs: Seq[String], - addEnv: Map[String, String], - sourceMap: Boolean -) extends AbstractNodeJSEnv(nodejsPath, addArgs, addEnv, sourceMap) { - - def this(nodejsPath: String = "node", addArgs: Seq[String] = Seq.empty, - addEnv: Map[String, String] = Map.empty) = { - this(nodejsPath, addArgs, addEnv, sourceMap = true) + @deprecatedName('nodejsPath) + override protected val executable: String, // override val for bin compat + @deprecatedName('addArgs) + args: Seq[String], + @deprecatedName('addEnv) + env: Map[String, String], + sourceMap: Boolean) + extends AbstractNodeJSEnv(executable, args, env, sourceMap) { + + def this( + @deprecatedName('nodejsPath) + executable: String = "node", + @deprecatedName('addArgs) + args: Seq[String] = Seq.empty, + @deprecatedName('addEnv) + env: Map[String, String] = Map.empty) = { + this(executable, args, env, sourceMap = true) } def withSourceMap(sourceMap: Boolean): NodeJSEnv = - new NodeJSEnv(nodejsPath, addArgs, addEnv, sourceMap) + new NodeJSEnv(executable, args, env, sourceMap) protected def vmName: String = "Node.js" - // For binary compatibility, now `executable` is defined in AbstractNodeJSEnv - override protected def executable: String = super.executable - override def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = { new NodeRunner(libs, code) From c02c1bdd0948883bdf424eca9d8e88118ba1432d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 10 Apr 2017 18:15:17 +0200 Subject: [PATCH 079/133] Fix scala-js/scala-js#2874: Remove the distinction libs/code in the JSEnv API. `JSEnv#jsRunner` and friends `asyncRunner` and `comRunner` now only take a `Seq[VirtualJSFiles]`, containing all JS files to be given to the JS environment. The JS environments therefore make no distinction between "library" files and the "code" file. The disinction was arbitrary anyway, and unifying them makes sure that all the JS files are first-class. In particular, it forced to solve shortcomings in terms of error handling in `JSDOMNodeJSEnv`. --- .../org/scalajs/jsenv/test/AsyncTests.scala | 2 +- .../org/scalajs/jsenv/test/ComTests.scala | 2 +- .../org/scalajs/jsenv/test/JSEnvTest.scala | 4 +-- .../scala/org/scalajs/jsenv/AsyncJSEnv.scala | 10 ++----- .../scala/org/scalajs/jsenv/ComJSEnv.scala | 9 ++---- .../org/scalajs/jsenv/ExternalJSEnv.scala | 25 ++++++++-------- .../main/scala/org/scalajs/jsenv/JSEnv.scala | 21 ++++--------- .../jsenv/nodejs/AbstractNodeJSEnv.scala | 2 +- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 30 ++++++++----------- 9 files changed, 42 insertions(+), 63 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala index acb3830..a571573 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -20,7 +20,7 @@ trait AsyncTests extends BasicJSEnvTests { protected def asyncRunner(code: String): AsyncJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) - newJSEnv.asyncRunner(codeVF) + newJSEnv.asyncRunner(codeVF :: Nil) } protected def start(runner: AsyncJSRunner): Future[Unit] = { diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index cb6415d..5959156 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -16,7 +16,7 @@ trait ComTests extends AsyncTests { protected def comRunner(code: String): ComJSRunner = { val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) - newJSEnv.comRunner(codeVF) + newJSEnv.comRunner(codeVF :: Nil) } private def assertThrowClosed(msg: String, body: => Unit): Unit = { diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala index e488599..7d6adb7 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -22,7 +22,7 @@ abstract class JSEnvTest { val console = new StoreJSConsole() val logger = new StoreLogger() - newJSEnv.jsRunner(code).run(logger, console) + newJSEnv.jsRunner(code :: Nil).run(logger, console) val log = logger.getLog val hasBadLog = log exists { @@ -38,7 +38,7 @@ abstract class JSEnvTest { def fails(): Unit = { try { - newJSEnv.jsRunner(code).run(NullLogger, NullJSConsole) + newJSEnv.jsRunner(code :: Nil).run(NullLogger, NullJSConsole) assertTrue("Code snipped should fail", false) } catch { case e: Exception => diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala index 9b6c8df..9bf05fc 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala @@ -12,18 +12,14 @@ package org.scalajs.jsenv import org.scalajs.core.tools.io.VirtualJSFile trait AsyncJSEnv extends JSEnv { - def asyncRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): AsyncJSRunner - - final def asyncRunner(code: VirtualJSFile): AsyncJSRunner = - asyncRunner(Nil, code) + def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner override def loadLibs(libs: Seq[VirtualJSFile]): AsyncJSEnv = new AsyncLoadedLibs { val loadedLibs = libs } private[jsenv] trait AsyncLoadedLibs extends LoadedLibs with AsyncJSEnv { - def asyncRunner(libs: Seq[VirtualJSFile], - code: VirtualJSFile): AsyncJSRunner = { - AsyncJSEnv.this.asyncRunner(loadedLibs ++ libs, code) + def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner = { + AsyncJSEnv.this.asyncRunner(loadedLibs ++ files) } } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala index b20faad..f6a2345 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala @@ -27,17 +27,14 @@ import org.scalajs.core.tools.io.VirtualJSFile * }}} */ trait ComJSEnv extends AsyncJSEnv { - def comRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): ComJSRunner - - final def comRunner(code: VirtualJSFile): ComJSRunner = comRunner(Nil, code) + def comRunner(files: Seq[VirtualJSFile]): ComJSRunner override def loadLibs(libs: Seq[VirtualJSFile]): ComJSEnv = new ComLoadedLibs { val loadedLibs = libs } private[jsenv] trait ComLoadedLibs extends AsyncLoadedLibs with ComJSEnv { - def comRunner(libs: Seq[VirtualJSFile], - code: VirtualJSFile): ComJSRunner = { - ComJSEnv.this.comRunner(loadedLibs ++ libs, code) + def comRunner(files: Seq[VirtualJSFile]): ComJSRunner = { + ComJSEnv.this.comRunner(loadedLibs ++ files) } } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 872e7a9..7608cfb 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -36,8 +36,7 @@ abstract class ExternalJSEnv( protected def customInitFiles(): Seq[VirtualJSFile] = Nil protected class AbstractExtRunner( - protected val libs: Seq[VirtualJSFile], - protected val code: VirtualJSFile) extends JSInitFiles { + protected val files: Seq[VirtualJSFile]) extends JSInitFiles { private[this] var _logger: Logger = _ private[this] var _console: JSConsole = _ @@ -71,13 +70,15 @@ abstract class ExternalJSEnv( protected def getVMEnv(): Map[String, String] = sys.env ++ env - /** Get files that are a library (i.e. that do not run anything) */ - protected def getLibJSFiles(): Seq[VirtualJSFile] = - initFiles() ++ customInitFiles() ++ libs - - /** Get all files that are passed to VM (libraries and code) */ + /** All the JS files that are passed to the VM. + * + * This method can overridden to provide custom behavior in subclasses. + * + * The default value in `ExternalJSEnv` is + * `initFiles() ++ customInitFiles() ++ files`. + */ protected def getJSFiles(): Seq[VirtualJSFile] = - getLibJSFiles() :+ code + initFiles() ++ customInitFiles() ++ files /** write a single JS file to a writer using an include fct if appropriate */ protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = { @@ -153,8 +154,8 @@ abstract class ExternalJSEnv( } - protected class ExtRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) - extends AbstractExtRunner(libs, code) with JSRunner { + protected class ExtRunner(files: Seq[VirtualJSFile]) + extends AbstractExtRunner(files) with JSRunner { def run(logger: Logger, console: JSConsole): Unit = { setupLoggerAndConsole(logger, console) @@ -166,8 +167,8 @@ abstract class ExternalJSEnv( } } - protected class AsyncExtRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) - extends AbstractExtRunner(libs, code) with AsyncJSRunner { + protected class AsyncExtRunner(files: Seq[VirtualJSFile]) + extends AbstractExtRunner(files) with AsyncJSRunner { private[this] var vmInst: Process = null private[this] var ioThreadEx: Throwable = null diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index 202a7ff..c4e48eb 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -15,24 +15,15 @@ trait JSEnv { /** Human-readable name for this [[JSEnv]] */ def name: String - /** Prepare a runner for the code in the virtual file. */ - def jsRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): JSRunner - - /** Prepare a runner without any libraries. - * - * Strictly equivalent to: - * {{{ - * this.jsRunner(Nil, code) - * }}} - */ - final def jsRunner(code: VirtualJSFile): JSRunner = jsRunner(Nil, code) + /** Prepare a runner with the specified JavaScript files. */ + def jsRunner(files: Seq[VirtualJSFile]): JSRunner /** Return this [[JSEnv]] with the given libraries already loaded. * * The following two are equivalent: * {{{ - * jsEnv.loadLibs(a).jsRunner(b, c) - * jsEnv.jsRunner(a ++ b, c) + * jsEnv.loadLibs(a).jsRunner(b) + * jsEnv.jsRunner(a ++ b) * }}} */ def loadLibs(libs: Seq[VirtualJSFile]): JSEnv = @@ -43,7 +34,7 @@ trait JSEnv { def name: String = JSEnv.this.name - def jsRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile): JSRunner = - JSEnv.this.jsRunner(loadedLibs ++ libs, code) + def jsRunner(files: Seq[VirtualJSFile]): JSRunner = + JSEnv.this.jsRunner(loadedLibs ++ files) } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index a5304b6..aac1142 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -41,7 +41,7 @@ abstract class AbstractNodeJSEnv( .withContent("""require('source-map-support').install();""") try { - jsRunner(code).run(NullLogger, NullJSConsole) + jsRunner(Seq(code)).run(NullLogger, NullJSConsole) true } catch { case t: ExternalJSEnv.NonZeroExitException => diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 2845c84..ba8dc93 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -41,29 +41,23 @@ class NodeJSEnv private ( protected def vmName: String = "Node.js" - override def jsRunner(libs: Seq[VirtualJSFile], - code: VirtualJSFile): JSRunner = { - new NodeRunner(libs, code) - } + override def jsRunner(files: Seq[VirtualJSFile]): JSRunner = + new NodeRunner(files) - override def asyncRunner(libs: Seq[VirtualJSFile], - code: VirtualJSFile): AsyncJSRunner = { - new AsyncNodeRunner(libs, code) - } + override def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner = + new AsyncNodeRunner(files) - override def comRunner(libs: Seq[VirtualJSFile], - code: VirtualJSFile): ComJSRunner = { - new ComNodeRunner(libs, code) - } + override def comRunner(files: Seq[VirtualJSFile]): ComJSRunner = + new ComNodeRunner(files) - protected class NodeRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) - extends ExtRunner(libs, code) with AbstractBasicNodeRunner + protected class NodeRunner(files: Seq[VirtualJSFile]) + extends ExtRunner(files) with AbstractBasicNodeRunner - protected class AsyncNodeRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) - extends AsyncExtRunner(libs, code) with AbstractBasicNodeRunner + protected class AsyncNodeRunner(files: Seq[VirtualJSFile]) + extends AsyncExtRunner(files) with AbstractBasicNodeRunner - protected class ComNodeRunner(libs: Seq[VirtualJSFile], code: VirtualJSFile) - extends AsyncNodeRunner(libs, code) with NodeComJSRunner + protected class ComNodeRunner(files: Seq[VirtualJSFile]) + extends AsyncNodeRunner(files) with NodeComJSRunner protected trait AbstractBasicNodeRunner extends AbstractNodeRunner { From 65588023653e64faabb37c2fd6761e0938f74480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 12 Apr 2017 22:27:03 +0200 Subject: [PATCH 080/133] Fix scala-js/scala-js#2875: Remove JSEnv.loadLibs, resolvedJSEnv and loadedJSEnv. We remove `resolvedJSEnv` simply by initializing `jsEnv` by default in the project scope, and using `jsEnv` instead of `resolvedJSEnv`. We remove `loadedJSEnv` by using instead the pair `jsEnv` + `jsExecutionFiles` explicitly. Finally, we can remove `JSEnv.loadLibs`, as it was only useful for `loadedJSEnv`. --- .../scala/org/scalajs/jsenv/AsyncJSEnv.scala | 9 --------- .../scala/org/scalajs/jsenv/ComJSEnv.scala | 9 --------- .../main/scala/org/scalajs/jsenv/JSEnv.scala | 20 ------------------- 3 files changed, 38 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala index 9bf05fc..d389385 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala @@ -13,13 +13,4 @@ import org.scalajs.core.tools.io.VirtualJSFile trait AsyncJSEnv extends JSEnv { def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner - - override def loadLibs(libs: Seq[VirtualJSFile]): AsyncJSEnv = - new AsyncLoadedLibs { val loadedLibs = libs } - - private[jsenv] trait AsyncLoadedLibs extends LoadedLibs with AsyncJSEnv { - def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner = { - AsyncJSEnv.this.asyncRunner(loadedLibs ++ files) - } - } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala index f6a2345..b0fb295 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala @@ -28,15 +28,6 @@ import org.scalajs.core.tools.io.VirtualJSFile */ trait ComJSEnv extends AsyncJSEnv { def comRunner(files: Seq[VirtualJSFile]): ComJSRunner - - override def loadLibs(libs: Seq[VirtualJSFile]): ComJSEnv = - new ComLoadedLibs { val loadedLibs = libs } - - private[jsenv] trait ComLoadedLibs extends AsyncLoadedLibs with ComJSEnv { - def comRunner(files: Seq[VirtualJSFile]): ComJSRunner = { - ComJSEnv.this.comRunner(loadedLibs ++ files) - } - } } object ComJSEnv { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index c4e48eb..9d86183 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -17,24 +17,4 @@ trait JSEnv { /** Prepare a runner with the specified JavaScript files. */ def jsRunner(files: Seq[VirtualJSFile]): JSRunner - - /** Return this [[JSEnv]] with the given libraries already loaded. - * - * The following two are equivalent: - * {{{ - * jsEnv.loadLibs(a).jsRunner(b) - * jsEnv.jsRunner(a ++ b) - * }}} - */ - def loadLibs(libs: Seq[VirtualJSFile]): JSEnv = - new LoadedLibs { val loadedLibs = libs } - - private[jsenv] trait LoadedLibs extends JSEnv { - val loadedLibs: Seq[VirtualJSFile] - - def name: String = JSEnv.this.name - - def jsRunner(files: Seq[VirtualJSFile]): JSRunner = - JSEnv.this.jsRunner(loadedLibs ++ files) - } } From 67d998267fd881a66bbcd036cea5cc6d7d6e9adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 12 May 2017 23:17:44 +0200 Subject: [PATCH 081/133] Separate all jsDependencies-related things in separate projects. This is part of scala-js/scala-js#2841. * The contents of `org.scalajs.core.tools.jsdep._` are extracted in a separate project `jsdependencies-core`. * All the settings related to `jsDependencies` are extracted in a separate sbt plugin `JSDependenciesPlugin` in `jsdependencies-plugin`. --- .../main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index aac1142..0b5b808 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -14,7 +14,6 @@ import java.net._ import org.scalajs.core.ir.Utils.escapeJS import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.jsdep.ResolvedJSDependency import org.scalajs.core.tools.logging.NullLogger import org.scalajs.jsenv._ import org.scalajs.jsenv.Utils.OptDeadline From ba9e5a099ad3b133c0d0ccc56cdcd722eed1f117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 26 May 2017 22:12:22 +0200 Subject: [PATCH 082/133] Avoid using `sys.env`. Use `System.getenv` instead. --- .../src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 6 ++++-- .../scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index d58bc6c..858d64d 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -7,6 +7,7 @@ import org.scalajs.core.tools.jsdep.ResolvedJSDependency import java.io.{ Console => _, _ } import scala.io.Source +import scala.collection.JavaConverters._ import scala.concurrent.{Future, Promise} import scala.util.Try @@ -67,10 +68,11 @@ abstract class ExternalJSEnv( /** VM environment. Override to adapt. * - * The default value in `ExternalJSEnv` is `sys.env ++ env`. + * The default value in `ExternalJSEnv` is + * `System.getenv().asScala.toMap ++ env`. */ protected def getVMEnv(): Map[String, String] = - sys.env ++ env + System.getenv().asScala.toMap ++ env /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(): Seq[VirtualJSFile] = diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index a5304b6..9052e1c 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -19,6 +19,7 @@ import org.scalajs.core.tools.logging.NullLogger import org.scalajs.jsenv._ import org.scalajs.jsenv.Utils.OptDeadline +import scala.collection.JavaConverters._ import scala.concurrent.TimeoutException import scala.concurrent.duration._ @@ -139,14 +140,14 @@ abstract class AbstractNodeJSEnv( // Node.js specific (system) environment override protected def getVMEnv(): Map[String, String] = { - val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) + val baseNodePath = Option(System.getenv("NODE_PATH")).filter(_.nonEmpty) val nodePath = libCache.cacheDir.getAbsolutePath + baseNodePath.fold("")(p => File.pathSeparator + p) - sys.env ++ Seq( + System.getenv().asScala.toMap ++ Seq( "NODE_MODULE_CONTEXTS" -> "0", "NODE_PATH" -> nodePath - ) ++ additionalEnv + ) ++ env } } From 44cd5ab3a08b05a5e45effab744349687f54357e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 9 Jun 2017 16:08:31 +0200 Subject: [PATCH 083/133] Introduce a new surface API for JS envs with `Config` objects. This only changes the surface API of concrete JS envs. Their existing constructors are deprecated in favor of an overload with a `Config` object. This change provides in the 0.6.x series an API that can be used in a source-compatible way between 0.6.x and 1.x. In 1.x, deeper changes to the internal API of JS envs will be done. We also take this opportunity to "move" `JSDOMNodeJSEnv` in a different package `org.scalajs.jsenv.jsdomnodejs`. Since this JS env is scheduled to be moved in a different repository in 1.x, it should eventually be in a different package anyway. --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 76 ++++++++++++++++--- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 8bc26f4..5721647 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -19,16 +19,13 @@ import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } -class NodeJSEnv private ( - @deprecatedName('nodejsPath) - override protected val executable: String, // override val for bin compat - @deprecatedName('addArgs) - args: Seq[String], - @deprecatedName('addEnv) - env: Map[String, String], - sourceMap: Boolean) - extends AbstractNodeJSEnv(executable, args, env, sourceMap) { +class NodeJSEnv(config: NodeJSEnv.Config) + extends AbstractNodeJSEnv(config.executable, config.args, config.env, + config.sourceMap) { + def this() = this(NodeJSEnv.Config()) + + @deprecated("Use the overload with a NodeJSEnv.Config.", "0.6.18") def this( @deprecatedName('nodejsPath) executable: String = "node", @@ -36,14 +33,19 @@ class NodeJSEnv private ( args: Seq[String] = Seq.empty, @deprecatedName('addEnv) env: Map[String, String] = Map.empty) = { - this(executable, args, env, sourceMap = true) + this(NodeJSEnv.Config().withExecutable(executable).withArgs(args.toList).withEnv(env)) } + @deprecated("Use the overloaded constructor with a NodeJSEnv.Config.", + "0.6.18") def withSourceMap(sourceMap: Boolean): NodeJSEnv = - new NodeJSEnv(executable, args, env, sourceMap) + new NodeJSEnv(config.withSourceMap(sourceMap)) protected def vmName: String = "Node.js" + // For binary compatibility + override protected val executable: String = config.executable + override def jsRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile): JSRunner = { new NodeRunner(libs, code) @@ -99,3 +101,55 @@ class NodeJSEnv private ( } } + +object NodeJSEnv { + final class Config private ( + val executable: String, + val args: List[String], + val env: Map[String, String], + val sourceMap: Boolean + ) { + private def this() = { + this( + executable = "node", + args = Nil, + env = Map.empty, + sourceMap = true + ) + } + + def withExecutable(executable: String): Config = + copy(executable = executable) + + def withArgs(args: List[String]): Config = + copy(args = args) + + def withEnv(env: Map[String, String]): Config = + copy(env = env) + + def withSourceMap(sourceMap: Boolean): Config = + copy(sourceMap = sourceMap) + + private def copy( + executable: String = executable, + args: List[String] = args, + env: Map[String, String] = env, + sourceMap: Boolean = sourceMap + ): Config = { + new Config(executable, args, env, sourceMap) + } + } + + object Config { + /** Returns a default configuration for a [[NodeJSEnv]]. + * + * The defaults are: + * + * - `executable`: `"node"` + * - `args`: `Nil` + * - `env`: `Map.empty` + * - `sourceMap`: `true` + */ + def apply(): Config = new Config() + } +} From cedc3049d919296c556f00bc7e9c61a19d13117b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 16 Jun 2017 14:05:30 +0200 Subject: [PATCH 084/133] Remove the deprecated APIs in JS envs. In the process, the implementation of `org.scalajs.jsenv.nodejs.JSDOMNodeJSEnv` is transferred to the new `org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv`. --- .../scala/org/scalajs/jsenv/ExternalJSEnv.scala | 8 -------- .../scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 3 --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 16 ---------------- 3 files changed, 27 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index e8afe87..262e568 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -11,9 +11,7 @@ import scala.concurrent.{Future, Promise} import scala.util.Try abstract class ExternalJSEnv( - @deprecatedName('additionalArgs) final protected val args: Seq[String], - @deprecatedName('additionalEnv) final protected val env: Map[String, String]) extends AsyncJSEnv { @@ -27,12 +25,6 @@ abstract class ExternalJSEnv( /** Command to execute (on shell) for this VM */ protected def executable: String - @deprecated("Use `args` instead.", "0.6.16") - final protected def additionalArgs: Seq[String] = args - - @deprecated("Use `env` instead.", "0.6.16") - final protected def additionalEnv: Map[String, String] = env - /** Custom initialization scripts. */ protected def customInitFiles(): Seq[VirtualJSFile] = Nil diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 9f691dd..f0be72e 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -23,11 +23,8 @@ import scala.concurrent.TimeoutException import scala.concurrent.duration._ abstract class AbstractNodeJSEnv( - @deprecatedName('nodejsPath) protected val executable: String, - @deprecatedName('addArgs) args: Seq[String], - @deprecatedName('addEnv) env: Map[String, String], val sourceMap: Boolean) extends ExternalJSEnv(args, env) with ComJSEnv { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index e2b4dd5..c86a59d 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -22,22 +22,6 @@ class NodeJSEnv(config: NodeJSEnv.Config) def this() = this(NodeJSEnv.Config()) - @deprecated("Use the overload with a NodeJSEnv.Config.", "0.6.18") - def this( - @deprecatedName('nodejsPath) - executable: String = "node", - @deprecatedName('addArgs) - args: Seq[String] = Seq.empty, - @deprecatedName('addEnv) - env: Map[String, String] = Map.empty) = { - this(NodeJSEnv.Config().withExecutable(executable).withArgs(args.toList).withEnv(env)) - } - - @deprecated("Use the overloaded constructor with a NodeJSEnv.Config.", - "0.6.18") - def withSourceMap(sourceMap: Boolean): NodeJSEnv = - new NodeJSEnv(config.withSourceMap(sourceMap)) - protected def vmName: String = "Node.js" override def jsRunner(files: Seq[VirtualJSFile]): JSRunner = From 927b8b1b07c138e068afec781827368f6afe47ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 16 Jun 2017 14:20:18 +0200 Subject: [PATCH 085/133] Fix scala-js/scala-js#2877: Internal JS envs config amenable to bincompat evolution. The internal configuration of (abstract) classes in the JS env API is now `protected def`-based, rather than based on constructor parameters. This can be evolved in binary-compatible ways, as we can add new `protected def`s with default implementations in the superclasses. --- .../org/scalajs/jsenv/ExternalJSEnv.scala | 13 +++++++++---- .../jsenv/nodejs/AbstractNodeJSEnv.scala | 19 +++++++++---------- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 15 ++++++++++++--- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 262e568..e5a18a9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -1,5 +1,7 @@ package org.scalajs.jsenv +import scala.collection.immutable + import org.scalajs.core.tools.io._ import org.scalajs.core.tools.logging.Logger @@ -10,10 +12,7 @@ import scala.collection.JavaConverters._ import scala.concurrent.{Future, Promise} import scala.util.Try -abstract class ExternalJSEnv( - final protected val args: Seq[String], - final protected val env: Map[String, String]) - extends AsyncJSEnv { +abstract class ExternalJSEnv extends AsyncJSEnv { import ExternalJSEnv._ @@ -25,6 +24,12 @@ abstract class ExternalJSEnv( /** Command to execute (on shell) for this VM */ protected def executable: String + /** Command-line arguments to give to the external process. */ + protected def args: immutable.Seq[String] = Nil + + /** Environment in which to run the external process. */ + protected def env: Map[String, String] = Map.empty + /** Custom initialization scripts. */ protected def customInitFiles(): Seq[VirtualJSFile] = Nil diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index f0be72e..d12abe8 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -22,12 +22,9 @@ import scala.collection.JavaConverters._ import scala.concurrent.TimeoutException import scala.concurrent.duration._ -abstract class AbstractNodeJSEnv( - protected val executable: String, - args: Seq[String], - env: Map[String, String], - val sourceMap: Boolean) - extends ExternalJSEnv(args, env) with ComJSEnv { +abstract class AbstractNodeJSEnv extends ExternalJSEnv with ComJSEnv { + + protected def wantSourceMap: Boolean = true /** True, if the installed node executable supports source mapping. * @@ -57,15 +54,17 @@ abstract class AbstractNodeJSEnv( * Is used by [[initFiles]], override to change/disable. */ protected def installSourceMap(): Seq[VirtualJSFile] = { - if (sourceMap) Seq( - new MemVirtualJSFile("sourceMapSupport.js").withContent( + if (wantSourceMap) { + val content = """ |try { | require('source-map-support').install(); |} catch (e) {} """.stripMargin - ) - ) else Seq() + Seq(new MemVirtualJSFile("sourceMapSupport.js").withContent(content)) + } else { + Seq() + } } /** File(s) to hack console.log to prevent if from changing `%%` to `%`. diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index c86a59d..25392c0 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -9,6 +9,8 @@ package org.scalajs.jsenv.nodejs +import scala.collection.immutable + import org.scalajs.jsenv._ import org.scalajs.core.tools.io._ @@ -16,14 +18,21 @@ import org.scalajs.core.tools.logging._ import java.io.{ Console => _, _ } -class NodeJSEnv(config: NodeJSEnv.Config) - extends AbstractNodeJSEnv(config.executable, config.args, config.env, - config.sourceMap) { +class NodeJSEnv(config: NodeJSEnv.Config) extends AbstractNodeJSEnv { def this() = this(NodeJSEnv.Config()) protected def vmName: String = "Node.js" + protected def executable: String = config.executable + + override protected def args: immutable.Seq[String] = config.args + + override protected def env: Map[String, String] = config.env + + // TODO Our Build wants this to be public, but it does not seem clean + override def wantSourceMap: Boolean = config.sourceMap + override def jsRunner(files: Seq[VirtualJSFile]): JSRunner = new NodeRunner(files) From a2310a861e7af385cc19ffc5fce4b889bb7d1602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 22 Jun 2017 16:45:24 +0200 Subject: [PATCH 086/133] Extract JSDOMNodeJSEnv in its own project. So that we can move it to a different repository afterwards. In the process, we also move the standard `NodeJSEnv` in a dedicated project, although this one will obviously stay in the same repository. Now that all JS environments are in different artifacts, it makes sense that Node.js have its own artifact as well, even though the sbt plugin will depend on that artifact. The separation has a nice benefit that the `jsEnvsTestSuite` is not necessary anymore, since its last tests can be moved to the test directories of the relevant JS env implementations. --- .../scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 0 .../src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 0 .../src/test/scala/org/scalajs/jsenv/nodejs}/NodeJSTest.scala | 4 ++-- .../scalajs/jsenv/nodejs}/NodeJSWithCustomInitFilesTest.scala | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename {js-envs => nodejs-env}/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala (100%) rename {js-envs => nodejs-env}/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala (100%) rename {js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test => nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs}/NodeJSTest.scala (95%) rename {js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test => nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs}/NodeJSWithCustomInitFilesTest.scala (71%) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala similarity index 100% rename from js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala rename to nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala similarity index 100% rename from js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala rename to nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala diff --git a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSTest.scala similarity index 95% rename from js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala rename to nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSTest.scala index f8fe024..764d8ac 100644 --- a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSTest.scala @@ -1,6 +1,6 @@ -package org.scalajs.jsenv.test +package org.scalajs.jsenv.nodejs -import org.scalajs.jsenv.nodejs.NodeJSEnv +import org.scalajs.jsenv.test._ import org.junit.Test import org.junit.Assert._ diff --git a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSWithCustomInitFilesTest.scala similarity index 71% rename from js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala rename to nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSWithCustomInitFilesTest.scala index 758a919..d1841bd 100644 --- a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala +++ b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSWithCustomInitFilesTest.scala @@ -1,6 +1,6 @@ -package org.scalajs.jsenv.test +package org.scalajs.jsenv.nodejs -import org.scalajs.jsenv.nodejs.NodeJSEnv +import org.scalajs.jsenv.test._ class NodeJSWithCustomInitFilesTest extends CustomInitFilesTest { protected def newJSEnv: NodeJSEnv = new NodeJSEnv { From 52c8510eda731446858b8587e78b74ea4b71b048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 6 Dec 2017 15:59:20 +0100 Subject: [PATCH 087/133] Mitigate scala-js/scala-js#3206: Send stderr of Node.js to the `JSConsole`. This way, at least all JS environments agree on what they do with `console.error()`, which is to merge it with `console.log()` and send everything to the `console: JSConsole` (which is the standard output by default). A deeper fix would allow to separately configure stdout and stderr, but that can only be done in Scala.js 1.x in the context of the redesign of `JSEnv`s scala-js/scala-js#3033. --- .../src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 858d64d..87fff28 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -96,13 +96,8 @@ abstract class ExternalJSEnv( try { sendVMStdin(out) } finally { out.close() } - // Pipe stdout to console + // Pipe stdout (and stderr which is merged into it) to console pipeToConsole(vmInst.getInputStream(), console) - - // We are probably done (stdin is closed). Report any errors - val errSrc = Source.fromInputStream(vmInst.getErrorStream(), "UTF-8") - try { errSrc.getLines.foreach(err => logger.error(err)) } - finally { errSrc.close } } /** Wait for the VM to terminate, verify exit code @@ -125,6 +120,7 @@ abstract class ExternalJSEnv( val allArgs = executable +: vmArgs val pBuilder = new ProcessBuilder(allArgs: _*) + pBuilder.redirectErrorStream(true) // merge stderr into stdout pBuilder.environment().clear() for ((name, value) <- vmEnv) From f7f33df33ba8cdeb79e74445feab71e3f60de78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 27 Dec 2017 16:07:13 +0100 Subject: [PATCH 088/133] Make the constructors and underlying vals of AnyVals private. Now that our JS artifacts do not need to be compiled with 2.10 anymore, we can truly make the underlying `val`s of `AnyVal`s private. In the process, we also ensure that their constructors are private. For implicit classes, the constructor must be `private[Enclosing]` because it is accessed by the automatically generated `implicit def` that comes with them. For our artifacts that still support 2.10, we also standardize on a `val __private_foo` to make it clear that they are not part of the API, even if the language prevents us from marking them as actually private. --- js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala index b2431b4..6f921a5 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala @@ -13,7 +13,11 @@ import scala.concurrent.duration._ private[jsenv] object Utils { final class OptDeadline private ( - val deadline: Deadline /* nullable */) extends AnyVal { // scalastyle:ignore + val __private_deadline: Deadline /* nullable */) // scalastyle:ignore + extends AnyVal { + + @inline private def deadline: Deadline = __private_deadline + def millisLeft: Long = if (deadline == null) 0 else (deadline.timeLeft.toMillis max 1L) From 068d9d526163caebb4b80a708f04596d20e1e655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 9 Jan 2018 14:42:59 +0100 Subject: [PATCH 089/133] Move `ir.Utils.escapeJS` to `io.JSUtils`. This makes `js-envs/` independent of `ir.*`. However, we have to keep a `private[ir]` duplicate of `printEscapeJS` in `ir.Utils`, because it is used by `ir.Printers`. --- .../scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index d12abe8..3961c1d 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -12,9 +12,10 @@ package org.scalajs.jsenv.nodejs import java.io.{Console => _, _} import java.net._ -import org.scalajs.core.ir.Utils.escapeJS import org.scalajs.core.tools.io._ +import org.scalajs.core.tools.io.JSUtils.escapeJS import org.scalajs.core.tools.logging.NullLogger + import org.scalajs.jsenv._ import org.scalajs.jsenv.Utils.OptDeadline From 81b911d41a13b6c08bd22e746a78195e694f9a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 19 Jan 2018 16:52:44 +0100 Subject: [PATCH 090/133] Get rid of the `core` and `tools` package namespaces. Since the split into `io`, `logging` and `linker`, the `tools` namespaces does not mean anything anymore, so we lift its three components one level up. This is also a good opportunity to more generally get rid of the `core` namespace. That namespace does not convey any useful meaning, and only make our packages longer for our users and ourselves. Finally, we also rename `compiler` into `nscplugin`, since nsc is not the only Scala compiler anymore, and there will come a time when compiler plugins for other compilers will exist. Concretely, this commit introduces the following renamings: * `org.scalajs.core.ir` -> `org.scalajs.ir` * `org.scalajs.core.compiler` -> `org.scalajs.nscplugin` * `org.scalajs.core.tools.io` -> `org.scalajs.io` * `org.scalajs.core.tools.logging` -> `org.scalajs.logging` * `org.scalajs.core.tools.linker` -> `org.scalajs.linker` --- .../src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala | 4 ++-- .../src/main/scala/org/scalajs/jsenv/test/ComTests.scala | 2 +- .../scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala | 2 +- .../src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala | 4 ++-- .../src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala | 2 +- js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala | 2 +- .../src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala | 2 +- js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala | 2 +- .../src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 4 ++-- js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala | 2 +- js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala | 2 +- js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala | 2 +- .../scala/org/scalajs/jsenv/VirtualFileMaterializer.scala | 2 +- .../scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 6 +++--- .../src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 4 ++-- 15 files changed, 21 insertions(+), 21 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala index a571573..f3dc0bb 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -2,8 +2,8 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.logging._ +import org.scalajs.io._ +import org.scalajs.logging._ import org.junit.Test import org.junit.Assert._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 5959156..b8f7d44 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -2,7 +2,7 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ -import org.scalajs.core.tools.io._ +import org.scalajs.io._ import org.junit.Test import org.junit.Assert._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala index 9e2e5e0..2055dee 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala @@ -1,6 +1,6 @@ package org.scalajs.jsenv.test -import org.scalajs.core.tools.io._ +import org.scalajs.io._ import org.junit.Test diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala index 7d6adb7..61c6ce7 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -2,8 +2,8 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ -import org.scalajs.core.tools.io.MemVirtualJSFile -import org.scalajs.core.tools.logging._ +import org.scalajs.io.MemVirtualJSFile +import org.scalajs.logging._ import org.junit.Assert._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala index 5d97dc3..254703f 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala @@ -1,6 +1,6 @@ package org.scalajs.jsenv.test -import org.scalajs.core.tools.logging._ +import org.scalajs.logging._ import scala.collection.mutable.ListBuffer diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala index d389385..4777c46 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala @@ -9,7 +9,7 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.io.VirtualJSFile trait AsyncJSEnv extends JSEnv { def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 06619bd..37d45e4 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -3,7 +3,7 @@ package org.scalajs.jsenv import scala.concurrent.{Future, Await} import scala.concurrent.duration.Duration -import org.scalajs.core.tools.logging.Logger +import org.scalajs.logging.Logger trait AsyncJSRunner { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala index b0fb295..3aeeed2 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala @@ -9,7 +9,7 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.io.VirtualJSFile /** An [[AsyncJSEnv]] that provides communication to and from the JS VM. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index d8132ba..064ced4 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -2,8 +2,8 @@ package org.scalajs.jsenv import scala.collection.immutable -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.logging.Logger +import org.scalajs.io._ +import org.scalajs.logging.Logger import java.io.{ Console => _, _ } import scala.io.Source diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index 9d86183..c7fb0c5 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -9,7 +9,7 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.io.VirtualJSFile trait JSEnv { /** Human-readable name for this [[JSEnv]] */ diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala index fad9e12..118ce43 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala @@ -1,6 +1,6 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.io.VirtualJSFile +import org.scalajs.io.VirtualJSFile trait JSInitFiles { /** JS files used to setup VM */ diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala index 7d2b5b6..8981947 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala @@ -9,7 +9,7 @@ package org.scalajs.jsenv -import org.scalajs.core.tools.logging.Logger +import org.scalajs.logging.Logger trait JSRunner { /** Run the associated JS code. Throw if an error occurs. */ diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala index 1f25a92..604f2df 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala @@ -2,7 +2,7 @@ package org.scalajs.jsenv import scala.annotation.tailrec -import org.scalajs.core.tools.io._ +import org.scalajs.io._ import java.io.File diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 3961c1d..818ac44 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -12,9 +12,9 @@ package org.scalajs.jsenv.nodejs import java.io.{Console => _, _} import java.net._ -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.io.JSUtils.escapeJS -import org.scalajs.core.tools.logging.NullLogger +import org.scalajs.io._ +import org.scalajs.io.JSUtils.escapeJS +import org.scalajs.logging.NullLogger import org.scalajs.jsenv._ import org.scalajs.jsenv.Utils.OptDeadline diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 25392c0..9ff544e 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -13,8 +13,8 @@ import scala.collection.immutable import org.scalajs.jsenv._ -import org.scalajs.core.tools.io._ -import org.scalajs.core.tools.logging._ +import org.scalajs.io._ +import org.scalajs.logging._ import java.io.{ Console => _, _ } From 392e44a91408c938b059ca58842214af887764a1 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 10 Dec 2017 08:46:04 +0100 Subject: [PATCH 091/133] Fix scala-js/scala-js#3033: Rehaul JSEnv API This is a full rehaul of the JSEnv API. Highlights include: - No synchronous interface. - Proper stopping/closing semantics. - Proper message passing semantics. - Removal of JSConsole. - Reduce implementation inheritance. --- .../org/scalajs/jsenv/test/AsyncTests.scala | 50 --- .../scalajs/jsenv/test/BasicJSEnvTests.scala | 46 --- .../org/scalajs/jsenv/test/ComTests.scala | 253 +++++--------- .../jsenv/test/CustomInitFilesTest.scala | 24 -- .../org/scalajs/jsenv/test/JSEnvSuite.scala | 64 ++++ .../scalajs/jsenv/test/JSEnvSuiteConfig.scala | 71 ++++ .../org/scalajs/jsenv/test/JSEnvTest.scala | 49 --- .../org/scalajs/jsenv/test/RunTests.scala | 154 +++++++++ .../scalajs/jsenv/test/StoreJSConsole.scala | 14 - .../org/scalajs/jsenv/test/StoreLogger.scala | 29 -- .../org/scalajs/jsenv/test/TestComKit.scala | 65 ++++ .../org/scalajs/jsenv/test/TestKit.scala | 156 +++++++++ .../scalajs/jsenv/test/TimeoutComTests.scala | 165 +++------- ...meoutTests.scala => TimeoutRunTests.scala} | 14 +- .../scala/org/scalajs/jsenv/AsyncJSEnv.scala | 16 - .../org/scalajs/jsenv/AsyncJSRunner.scala | 80 ----- .../scala/org/scalajs/jsenv/ComJSEnv.scala | 42 --- .../scala/org/scalajs/jsenv/ComJSRunner.scala | 41 --- .../org/scalajs/jsenv/ConsoleJSConsole.scala | 17 - .../org/scalajs/jsenv/ExternalJSEnv.scala | 219 ------------ .../org/scalajs/jsenv/ExternalJSRun.scala | 196 +++++++++++ .../main/scala/org/scalajs/jsenv/Input.scala | 34 ++ .../scala/org/scalajs/jsenv/JSConsole.scala | 15 - .../main/scala/org/scalajs/jsenv/JSEnv.scala | 72 +++- .../scala/org/scalajs/jsenv/JSInitFiles.scala | 8 - .../scala/org/scalajs/jsenv/JSRunner.scala | 17 - .../main/scala/org/scalajs/jsenv/JSRuns.scala | 76 +++++ .../org/scalajs/jsenv/NullJSConsole.scala | 5 - .../scala/org/scalajs/jsenv/RunConfig.scala | 160 +++++++++ .../main/scala/org/scalajs/jsenv/Utils.scala | 38 --- .../org/scalajs/jsenv/RunConfigTest.scala | 88 +++++ .../jsenv/nodejs/AbstractNodeJSEnv.scala | 311 ------------------ .../org/scalajs/jsenv/nodejs/ComSupport.scala | 302 +++++++++++++++++ .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 105 ++++-- .../org/scalajs/jsenv/nodejs/Support.scala | 44 +++ .../scalajs/jsenv/nodejs/NodeJSSuite.scala | 11 + .../org/scalajs/jsenv/nodejs/NodeJSTest.scala | 79 ----- .../NodeJSWithCustomInitFilesTest.scala | 9 - 38 files changed, 1691 insertions(+), 1448 deletions(-) delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala rename js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/{TimeoutTests.scala => TimeoutRunTests.scala} (89%) delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/Input.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala create mode 100644 js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala delete mode 100644 nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala create mode 100644 nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala create mode 100644 nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala create mode 100644 nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala delete mode 100644 nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSTest.scala delete mode 100644 nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSWithCustomInitFilesTest.scala diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala deleted file mode 100644 index f3dc0bb..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ /dev/null @@ -1,50 +0,0 @@ -package org.scalajs.jsenv.test - -import org.scalajs.jsenv._ - -import org.scalajs.io._ -import org.scalajs.logging._ - -import org.junit.Test -import org.junit.Assert._ - -import scala.concurrent.{Future, Await} -import scala.concurrent.duration._ - -/** A couple of tests that test async runners for mix-in into a test suite */ -trait AsyncTests extends BasicJSEnvTests { - - protected final val DefaultTimeout: Duration = 10.seconds - - protected def newJSEnv: AsyncJSEnv - - protected def asyncRunner(code: String): AsyncJSRunner = { - val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) - newJSEnv.asyncRunner(codeVF :: Nil) - } - - protected def start(runner: AsyncJSRunner): Future[Unit] = { - runner.start(new ScalaConsoleLogger(Level.Warn), ConsoleJSConsole) - } - - @Test - def futureTest: Unit = { - val runner = asyncRunner("") - val fut = start(runner) - - Await.result(fut, DefaultTimeout) - - assertFalse("VM should be terminated", runner.isRunning) - } - - @Test - def stopAfterTerminatedTest: Unit = { - val runner = asyncRunner("") - val fut = start(runner) - - Await.result(fut, DefaultTimeout) - - runner.stop() // should do nothing, and not fail - } - -} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala deleted file mode 100644 index 4404f18..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala +++ /dev/null @@ -1,46 +0,0 @@ -package org.scalajs.jsenv.test - -import org.junit.Test -import org.junit.Assert._ - -/** Tests that should succeed on any JSEnv */ -trait BasicJSEnvTests extends JSEnvTest { - - @Test - def failureTest: Unit = { - - """ - var a = {}; - a.foo(); - """.fails() - - } - - @Test - def syntaxErrorTest: Unit = { - - """ - { - """.fails() - - } - - @Test // Failed in Phantom - #2053 - def utf8Test: Unit = { - - """ - console.log("\u1234"); - """ hasOutput "\u1234\n"; - - } - - @Test - def allowScriptTags: Unit = { - - """ - console.log(""); - """ hasOutput "\n"; - - } - -} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index b8f7d44..5eeed6f 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -2,134 +2,81 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv._ -import org.scalajs.io._ - -import org.junit.Test +import org.junit.{Before, Test} import org.junit.Assert._ +import org.junit.Assume._ import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global -/** A couple of tests that test communication for mix-in into a test suite */ -trait ComTests extends AsyncTests { - - protected def newJSEnv: ComJSEnv - - protected def comRunner(code: String): ComJSRunner = { - val codeVF = new MemVirtualJSFile("testScript.js").withContent(code) - newJSEnv.comRunner(codeVF :: Nil) - } - - private def assertThrowClosed(msg: String, body: => Unit): Unit = { - val thrown = try { - body - false - } catch { - case _: ComJSEnv.ComClosedException => - true - } +private[test] class ComTests(config: JSEnvSuiteConfig) { + private val kit = new TestComKit(config) - assertTrue(msg, thrown) + @Before + def before: Unit = { + assumeTrue("JSEnv needs com support", config.supportsCom) } @Test - def comCloseJVMTest: Unit = { - val com = comRunner(s""" + def basicTest: Unit = { + val run = kit.start(""" scalajsCom.init(function(msg) { scalajsCom.send("received: " + msg); }); scalajsCom.send("Hello World"); - """) - - start(com) - - assertEquals("Hello World", com.receive()) + """, RunConfig()) - for (i <- 0 to 10) { - com.send(i.toString) - assertEquals(s"received: $i", com.receive()) + try { + assertEquals("Hello World", run.waitNextMessage()) + + for (i <- 0 to 10) { + run.run.send(i.toString) + assertEquals(s"received: $i", run.waitNextMessage()) + } + } finally { + run.closeAndWait() } - - com.close() - com.await(DefaultTimeout) - - com.stop() // should do nothing, and not fail - } - - def comCloseJSTestCommon(timeout: Long): Unit = { - val com = comRunner(s""" - scalajsCom.init(function(msg) {}); - for (var i = 0; i < 10; ++i) - scalajsCom.send("msg: " + i); - scalajsCom.close(); - """) - - start(com) - - Thread.sleep(timeout) - - for (i <- 0 until 10) - assertEquals(s"msg: $i", com.receive()) - - assertThrowClosed("Expect receive to throw after closing of channel", - com.receive()) - - com.close() - com.await(DefaultTimeout) - - com.stop() // should do nothing, and not fail } @Test - def comCloseJSTest: Unit = comCloseJSTestCommon(0) + def jsExitsOnMessageTest: Unit = { + assumeTrue(config.terminateVMJSCode.isDefined) - @Test - def comCloseJSTestDelayed: Unit = comCloseJSTestCommon(1000) + val run = kit.start(s""" + scalajsCom.init(function(msg) { ${config.terminateVMJSCode.get}; }); + for (var i = 0; i < 10; ++i) + scalajsCom.send("msg: " + i); + """, RunConfig()) - @Test - def doubleCloseTest: Unit = { - val n = 10 - val com = pingPongRunner(n) + try { + for (i <- 0 until 10) + assertEquals(s"msg: $i", run.waitNextMessage()) - start(com) + run.run.send("quit") - for (i <- 0 until n) { - com.send("ping") - assertEquals("pong", com.receive()) + Await.result(run.run.future, config.awaitTimeout) + } finally { + run.run.close() } - - com.close() - com.await(DefaultTimeout) } @Test def multiEnvTest: Unit = { val n = 10 - val envs = List.fill(5)(pingPongRunner(10)) - - envs.foreach(start) - - val ops = List[ComJSRunner => Unit]( - _.send("ping"), - com => assertEquals("pong", com.receive()) - ) - - for { - i <- 0 until n - env <- envs - op <- ops - } op(env) - - envs.foreach(_.close()) - envs.foreach(_.await(DefaultTimeout)) - } - - private def pingPongRunner(count: Int) = { - comRunner(s""" - var seen = 0; + val runs = List.fill(5) { + kit.start(""" scalajsCom.init(function(msg) { scalajsCom.send("pong"); - if (++seen >= $count) - scalajsCom.close(); }); - """) + """, RunConfig()) + } + + try { + for (_ <- 0 until n) { + runs.foreach(_.run.send("ping")) + runs.foreach(r => assertEquals("pong", r.waitNextMessage())) + } + } finally { + runs.foreach(_.closeAndWait()) + } } @Test @@ -141,107 +88,57 @@ trait ComTests extends AsyncTests { // Max message size: 1KB * 2^(2*iters+1) = 1MB val iters = 4 - val com = comRunner(""" + val run = kit.start(""" scalajsCom.init(function(msg) { scalajsCom.send(msg + msg); }); - """) + """, RunConfig()) - start(com) + try { + run.run.send(baseMsg) - com.send(baseMsg) + def resultFactor(iters: Int) = Math.pow(2, 2 * iters + 1).toInt - def resultFactor(iters: Int) = Math.pow(2, 2 * iters + 1).toInt + for (i <- 0 until iters) { + val reply = run.waitNextMessage() - for (i <- 0 until iters) { - val reply = com.receive() + val factor = resultFactor(i) - val factor = resultFactor(i) + assertEquals(baseLen * factor, reply.length) - assertEquals(baseLen * factor, reply.length) + for (j <- 0 until factor) + assertEquals(baseMsg, reply.substring(j * baseLen, (j + 1) * baseLen)) - for (j <- 0 until factor) - assertEquals(baseMsg, reply.substring(j * baseLen, (j + 1) * baseLen)) + run.run.send(reply + reply) + } - com.send(reply + reply) + val lastLen = run.waitNextMessage().length + assertEquals(baseLen * resultFactor(iters), lastLen) + } finally { + run.closeAndWait() } - - val lastLen = com.receive().length - assertEquals(baseLen * resultFactor(iters), lastLen) - - com.close() - com.await(DefaultTimeout) } @Test def highCharTest: Unit = { // #1536 - val com = comRunner(""" - scalajsCom.init(scalajsCom.send); - """) - - start(com) - - val msg = "\uC421\u8F10\u0112\uFF32" - - com.send(msg) - assertEquals(msg, com.receive()) - - com.close() - com.await(DefaultTimeout) - } - - @Test - def noInitTest: Unit = { - val com = comRunner("") - - start(com) - com.send("Dummy") - com.close() - com.await(DefaultTimeout) - } - - @Test - def stopTestCom: Unit = { - val com = comRunner(s"""scalajsCom.init(function(msg) {});""") - - start(com) - - // Make sure the VM doesn't terminate. - Thread.sleep(1000) - - assertTrue("VM should still be running", com.isRunning) - - // Stop VM instead of closing channel - com.stop() + val run = kit.start("scalajsCom.init(scalajsCom.send);", RunConfig()) try { - com.await(DefaultTimeout) - fail("Stopped VM should be in failure state") - } catch { - case _: Throwable => + val msg = "\uC421\u8F10\u0112\uFF32" + run.run.send(msg) + assertEquals(msg, run.waitNextMessage()) + } finally { + run.closeAndWait() } } @Test - def futureStopTest: Unit = { - val com = comRunner(s"""scalajsCom.init(function(msg) {});""") - - val fut = start(com) - - // Make sure the VM doesn't terminate. - Thread.sleep(1000) - - assertTrue("VM should still be running", com.isRunning) - - // Stop VM instead of closing channel - com.stop() - + def noInitTest: Unit = { + val run = kit.start("", RunConfig()) try { - Await.result(fut, DefaultTimeout) - fail("Stopped VM should be in failure state") - } catch { - case _: Throwable => + run.run.send("Dummy") + } finally { + run.closeAndWait() } } - } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala deleted file mode 100644 index 2055dee..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala +++ /dev/null @@ -1,24 +0,0 @@ -package org.scalajs.jsenv.test - -import org.scalajs.io._ - -import org.junit.Test - -abstract class CustomInitFilesTest extends JSEnvTest { - def makeCustomInitFiles(): Seq[VirtualJSFile] = { - Seq(new MemVirtualJSFile("custominit.js").withContent(""" - function customPrint(s) { - console.log("custom: " + s); - } - """)) - } - - @Test - def customInitFilesTest: Unit = { - """ - customPrint("hello"); - """ hasOutput - """|custom: hello - |""".stripMargin - } -} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala new file mode 100644 index 0000000..e6a76be --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala @@ -0,0 +1,64 @@ +package org.scalajs.jsenv.test + +import org.scalajs.jsenv.JSEnv + +import scala.collection.JavaConverters._ +import scala.reflect.ClassTag + +import org.junit.runner.Runner +import org.junit.runners.Suite +import org.junit.runners.parameterized.{TestWithParameters, BlockJUnit4ClassRunnerWithParameters} +import org.junit.runners.model.TestClass + +/** Conformance test suite for any [[JSEnv]] implementation. + * + * Use with the [[JSEnvSuiteRunner]]. + * + * Example: + * {{{ + * import org.junit.runner.RunWith + * + * @RunWith(classOf[JSEnvSuiteRunner]) + * class MyJSEnvSuite extends JSEnvSuite(JSEnvSuiteConfig(new MyJSEnv)) + * }}} + * + * @see [[JSEnvSuiteConfig]] for details on the configuration. + */ +abstract class JSEnvSuite(private[test] val config: JSEnvSuiteConfig) + +/** Runner for a [[JSEnvSuite]]. May only be used on subclasses of [[JSEnvSuite]]. */ +final class JSEnvSuiteRunner(root: Class[_], config: JSEnvSuiteConfig) + extends Suite(root, JSEnvSuiteRunner.getRunners(config).asJava) { + + /** Constructor for reflective instantiation via `@RunWith`. */ + def this(suite: Class[_ <: JSEnvSuite]) = this(suite, suite.newInstance().config) + + /** Constructor for instantiation in a user defined Runner. */ + def this(config: JSEnvSuiteConfig) = this(null, config) +} + +private object JSEnvSuiteRunner { + private def r[T](config: JSEnvSuiteConfig, params: (String, AnyRef)*)(implicit t: ClassTag[T]) = { + val name = (("config" -> config.description) +: params) + .map { case (name, value) => s"$name = $value" } + .mkString("[", ", ", "]") + + val paramValues = config +: params.map(_._2) + + new BlockJUnit4ClassRunnerWithParameters( + new TestWithParameters(name, new TestClass(t.runtimeClass), paramValues.asJava)) + } + + private def getRunners(config: JSEnvSuiteConfig): List[Runner] = { + import java.lang.Boolean.{TRUE, FALSE} + + List( + r[RunTests](config, "withCom" -> FALSE), + r[RunTests](config, "withCom" -> TRUE), + r[TimeoutRunTests](config, "withCom" -> FALSE), + r[TimeoutRunTests](config, "withCom" -> TRUE), + r[ComTests](config), + r[TimeoutComTests](config) + ) + } +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala new file mode 100644 index 0000000..73323ad --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala @@ -0,0 +1,71 @@ +package org.scalajs.jsenv.test + +import org.scalajs.jsenv.JSEnv + +import scala.concurrent.duration._ + +/** Configuration for a [[JSEnvSuite]]. + * + * @see [[JSEnvSuite]] for usage. + * + * @param jsEnv [[JSEnv]] under test. + * @param terminateVMJSCode A JavaScript expression that terminates the VM. + * If set, proper handling of VM termination is tested. + * @param supportsCom Whether the [[JSEnv]] under test supports + * [[JSEnv#startWithCom]]. + * @param supportsTimeout Whether the [[JSEnv]] under test supports the + * JavaScript timeout methods (as defined in + * [[http://www.scala-js.org/api/scalajs-library/latest/#scala.scalajs.js.timers.RawTimers$ RawTimers]]). + * @param awaitTimeout Amount of time test cases wait for "things". This is + * deliberately not very well specified. Leave this as the default and + * increase it if your tests fail spuriously due to timeouts. + * @param description A human readable description of this configuration; + * defaults to [[JSEnv#name]]. This is only ever used in the parametrized + * JUnit test name. Can be customized if the same [[JSEnv]] is used with + * different configurations (e.g. Selenium with different browsers). + */ +final class JSEnvSuiteConfig private ( + val jsEnv: JSEnv, + val terminateVMJSCode: Option[String], + val supportsCom: Boolean, + val supportsTimeout: Boolean, + val awaitTimeout: FiniteDuration, + val description: String +) { + private def this(jsEnv: JSEnv) = this( + jsEnv = jsEnv, + terminateVMJSCode = None, + supportsCom = true, + supportsTimeout = true, + awaitTimeout = 1.minute, + description = jsEnv.name + ) + + def withTerminateVMJSCode(code: String): JSEnvSuiteConfig = + copy(terminateVMJSCode = Some(code)) + + def withSupportsCom(supportsCom: Boolean): JSEnvSuiteConfig = + copy(supportsCom = supportsCom) + + def withSupportsTimeout(supportsTimeout: Boolean): JSEnvSuiteConfig = + copy(supportsTimeout = supportsTimeout) + + def withAwaitTimepout(awaitTimeout: FiniteDuration): JSEnvSuiteConfig = + copy(awaitTimeout = awaitTimeout) + + def withDescription(description: String): JSEnvSuiteConfig = + copy(description = description) + + private def copy(terminateVMJSCode: Option[String] = terminateVMJSCode, + supportsCom: Boolean = supportsCom, + supportsTimeout: Boolean = supportsTimeout, + awaitTimeout: FiniteDuration = awaitTimeout, + description: String = description) = { + new JSEnvSuiteConfig(jsEnv, terminateVMJSCode, supportsCom, + supportsTimeout, awaitTimeout, description) + } +} + +object JSEnvSuiteConfig { + def apply(jsEnv: JSEnv): JSEnvSuiteConfig = new JSEnvSuiteConfig(jsEnv) +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala deleted file mode 100644 index 61c6ce7..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ /dev/null @@ -1,49 +0,0 @@ -package org.scalajs.jsenv.test - -import org.scalajs.jsenv._ - -import org.scalajs.io.MemVirtualJSFile -import org.scalajs.logging._ - -import org.junit.Assert._ - -import StoreLogger._ - -abstract class JSEnvTest { - - protected def newJSEnv: JSEnv - - implicit class RunMatcher(codeStr: String) { - - val code = new MemVirtualJSFile("testScript.js").withContent(codeStr) - - def hasOutput(expectedOut: String): Unit = { - - val console = new StoreJSConsole() - val logger = new StoreLogger() - - newJSEnv.jsRunner(code :: Nil).run(logger, console) - - val log = logger.getLog - val hasBadLog = log exists { - case Log(level, _) if level >= Level.Warn => true - case Trace(_) => true - case _ => false - } - - assertFalse("VM shouldn't log errors, warnings or traces. Log:\n" + - log.mkString("\n"), hasBadLog) - assertEquals("Output should match", expectedOut, console.getLog) - } - - def fails(): Unit = { - try { - newJSEnv.jsRunner(code :: Nil).run(NullLogger, NullJSConsole) - assertTrue("Code snipped should fail", false) - } catch { - case e: Exception => - } - } - } - -} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala new file mode 100644 index 0000000..ce584c6 --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -0,0 +1,154 @@ +package org.scalajs.jsenv.test + +import scala.concurrent.Await + +import org.scalajs.io.VirtualJSFile +import org.scalajs.jsenv._ + +import org.junit.Assert._ +import org.junit.Assume._ +import org.junit.{Test, Before} + +private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { + private val kit = new TestKit(config, withCom) + import kit._ + + @Test + def failureTest: Unit = { + """ + var a = {}; + a.foo(); + """.fails() + } + + @Test + def syntaxErrorTest: Unit = { + """ + { + """.fails() + } + + @Test // Failed in Phantom - #2053 + def utf8Test: Unit = { + """ + console.log("\u1234"); + """ hasOutput "\u1234\n"; + } + + @Test + def allowScriptTags: Unit = { + """ + console.log(""); + """ hasOutput "\n"; + } + + @Test + def jsExitsTest: Unit = { + assumeTrue(config.terminateVMJSCode.isDefined) + + val run = kit.start(config.terminateVMJSCode.get, RunConfig()) + try { + Await.result(run.future, config.awaitTimeout) + } finally { + run.close() + } + } + + @Test // Node.js strips double percentage signs - #500 + def percentageTest: Unit = { + val counts = 1 to 15 + val argcs = 1 to 3 + val strings = counts.map("%" * _) + + val strlists = for { + count <- argcs + string <- strings + } yield List.fill(count)(string) + + val codes = for { + strlist <- strlists + } yield { + val args = strlist.map(s => s""""$s"""").mkString(", ") + s"console.log($args);\n" + } + + val result = strlists.map(_.mkString(" ") + "\n").mkString("") + + codes.mkString("").hasOutput(result) + } + + @Test // Node.js console.log hack didn't allow to log non-Strings - #561 + def nonStringTest: Unit = { + """ + console.log(1); + console.log(undefined); + console.log(null); + console.log({}); + console.log([1,2]); + """ hasOutput + """|1 + |undefined + |null + |[object Object] + |1,2 + |""".stripMargin + } + + @Test + def fastCloseTest: Unit = { + /* This test also tests a failure mode where the ExternalJSRun is still + * piping output while the client calls close. + */ + val run = kit.start("", RunConfig()) + run.close() + awaitAfterClose(run) + } + + @Test + def multiCloseAfterTerminatedTest: Unit = { + val run = kit.start("", RunConfig()) + run.close() + awaitAfterClose(run) + + // Should be noops (and not fail). + run.close() + run.close() + run.close() + } + + @Test + def noThrowOnBadFileTest: Unit = { + val badFile = new VirtualJSFile { + def path: String = ??? + def exists: Boolean = ??? + def content: String = ??? + } + + // `start` may not throw but must fail asynchronously + val run = kit.start(badFile, RunConfig()) + try { + Await.ready(run.future, config.awaitTimeout) + assertTrue("Bad file should have made run fail", + run.future.value.get.isFailure) + } finally { + run.close() + } + } + + /* This test verifies that a [[JSEnv]] properly validates its [[RunConfig]] + * (through [[RunConfig.Validator]]). + * + * If you get here, because the test suite fails on your [[JSEnv]] you are not + * using [[RunConfig.Validator]] properly (or at all). See its documentation + * on how to use it properly. + * + * This test sets a private option on [[RunConfig]] that is only known + * internally. This ensures that [[JSEnv]]s reject options added in the future + * they cannot support. + */ + @Test(expected = classOf[IllegalArgumentException]) + def ensureValidate: Unit = { + val cfg = RunConfig().withEternallyUnsupportedOption(true) + kit.start("", cfg).close() + } +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala deleted file mode 100644 index f4e60da..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala +++ /dev/null @@ -1,14 +0,0 @@ -package org.scalajs.jsenv.test - -import org.scalajs.jsenv._ - -class StoreJSConsole extends JSConsole { - private[this] val buf = new StringBuilder() - - def log(msg: Any): Unit = { - buf.append(msg.toString) - buf.append('\n') - } - - def getLog: String = buf.toString -} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala deleted file mode 100644 index 254703f..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala +++ /dev/null @@ -1,29 +0,0 @@ -package org.scalajs.jsenv.test - -import org.scalajs.logging._ - -import scala.collection.mutable.ListBuffer - -class StoreLogger extends Logger { - import StoreLogger._ - - private[this] val buf = new ListBuffer[LogElem] - - def log(level: Level, message: => String): Unit = - buf += Log(level, message) - def success(message: => String): Unit = - buf += Success(message) - def trace(t: => Throwable): Unit = - buf += Trace(t) - - def getLog: List[LogElem] = buf.toList -} - -object StoreLogger { - - sealed trait LogElem - final case class Log(level: Level, message: String) extends LogElem - final case class Success(message: String) extends LogElem - final case class Trace(t: Throwable) extends LogElem - -} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala new file mode 100644 index 0000000..20eecd2 --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala @@ -0,0 +1,65 @@ +package org.scalajs.jsenv.test + +import java.util.concurrent.TimeoutException + +import org.scalajs.io.{VirtualJSFile, MemVirtualJSFile} + +import org.scalajs.jsenv._ + +import org.junit.Assert.fail + +import scala.collection.immutable +import scala.concurrent.Await +import scala.concurrent.duration.Duration + +private[test] final class TestComKit(config: JSEnvSuiteConfig) { + def start(code: String, runConfig: RunConfig): Run = { + val vf = new MemVirtualJSFile("testScript.js").withContent(code) + start(vf, runConfig) + } + + def start(vf: VirtualJSFile, runConfig: RunConfig): Run = { + val input = Input.ScriptsToLoad(List(vf)) + new Run(input, runConfig) + } + + final class Run(input: Input, runConfig: RunConfig) { + val run: JSComRun = config.jsEnv.startWithCom(input, runConfig, onMessage _) + + private var received = immutable.Queue.empty[String] + + def waitNextMessage(): String = synchronized { + val deadline = config.awaitTimeout.fromNow + + while (received.isEmpty) { + val m = deadline.timeLeft.toMillis + if (m > 0) + wait(m) + else + throw new TimeoutException("Timed out waiting for next message") + } + + val (msg, newReceived) = received.dequeue + received = newReceived + msg + } + + def closeAndWait(): Unit = { + run.close() + + // Run must complete successfully. + Await.result(run.future, config.awaitTimeout) + + synchronized { + if (received.nonEmpty) { + fail(s"There were unhandled messages: $received") + } + } + } + + private def onMessage(msg: String) = synchronized { + received = received.enqueue(msg) + notifyAll() + } + } +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala new file mode 100644 index 0000000..766f861 --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala @@ -0,0 +1,156 @@ +package org.scalajs.jsenv.test + +import java.io._ +import java.nio.CharBuffer +import java.nio.charset.StandardCharsets +import java.util.concurrent.TimeoutException + +import scala.annotation.tailrec +import scala.concurrent.Await +import scala.concurrent.duration.Deadline + +import org.scalajs.io.{VirtualJSFile, MemVirtualJSFile} + +import org.scalajs.jsenv._ + +import org.junit.Assert._ +import org.junit.Assume._ + +private[test] final class TestKit(config: JSEnvSuiteConfig, withCom: Boolean) { + assumeTrue("JSEnv needs com support", config.supportsCom || !withCom) + + def start(code: String, config: RunConfig): JSRun = { + val vf = new MemVirtualJSFile("testScript.js").withContent(code) + start(vf, config) + } + + def start(vf: VirtualJSFile, runConfig: RunConfig): JSRun = { + val input = Input.ScriptsToLoad(List(vf)) + if (withCom) + config.jsEnv.startWithCom(input, runConfig, _ => ()) + else + config.jsEnv.start(input, runConfig) + } + + /** Await a run started with [[start]] after it got closed. + * + * This expects successful termination depending on [[withCom]]: we are + * allowed to expect successful termination with a com. + */ + def awaitAfterClose(run: JSRun): Unit = { + if (withCom) + Await.result(run.future, config.awaitTimeout) + else + Await.ready(run.future, config.awaitTimeout) + } + + implicit final class RunMatcher private[TestKit] (codeStr: String) { + def hasOutput(expectedOut: String): Unit = { + val comparator = new OutputComparator(expectedOut) + val config = comparator.configure(RunConfig()) + val run = start(codeStr, config) + + try { + comparator.compare() + } finally { + run.close() + } + + awaitAfterClose(run) + } + + def fails(): Unit = { + // We do not want to spam the console with error output, so we ignore it. + def ignoreStreams(out: Option[InputStream], err: Option[InputStream]) = { + out.foreach(_.close()) + err.foreach(_.close()) + } + + val runConfig = RunConfig() + .withOnOutputStream(ignoreStreams) + .withInheritOut(false) + .withInheritErr(false) + + val run = start(codeStr, runConfig) + try { + Await.ready(run.future, config.awaitTimeout) + assertTrue("Code snipped should fail", run.future.value.get.isFailure) + } finally { + run.close() + } + } + } + + private class OutputComparator(expectedOut: String) { + private val waiter = new StreamWaiter + + def configure(config: RunConfig): RunConfig = { + config + .withOnOutputStream(waiter.onOutputStream _) + .withInheritOut(false) + .withInheritErr(true) + } + + def compare(): Unit = { + val deadline = config.awaitTimeout.fromNow + val stream = waiter.waitForStream(deadline) + + /* When reading, we use a CharBuffer for easy index tracking. However, we + * back it by an array so we can easily read partial results. + */ + val in = new InputStreamReader(stream, StandardCharsets.UTF_8) + val arr = new Array[Char](expectedOut.length) + val buf = CharBuffer.wrap(arr) + while (buf.hasRemaining && tryRead(in, buf, deadline) != -1) { + val len = buf.position + assertEquals("Partial check", + expectedOut.substring(0, len), + new String(arr, 0, len)) + } + + buf.flip() + assertEquals(expectedOut, buf.toString) + } + } + + @tailrec + private final def tryRead(in: Reader, buf: CharBuffer, deadline: Deadline): Int = { + if (deadline.isOverdue) { + buf.flip() + throw new TimeoutException("Timed out out waiting for output. Got so far: " + buf.toString) + } + + if (in.ready()) { + in.read(buf) + } else { + Thread.sleep(50) + tryRead(in, buf, deadline) + } + } + + private class StreamWaiter { + private[this] var stream: InputStream = _ + + def waitForStream(deadline: Deadline): InputStream = synchronized { + while (stream == null) { + val m = deadline.timeLeft.toMillis + if (m > 0) + wait(m) + else + throw new TimeoutException("Timed out waiting for stdout") + } + + stream + } + + def onOutputStream(out: Option[InputStream], + err: Option[InputStream]): Unit = synchronized { + require(err.isEmpty, "Got error stream, did not request it.") + require(stream == null, "Got called twice") + + stream = out.getOrElse(new ByteArrayInputStream(new Array[Byte](0))) + notifyAll() + } + } +} + diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 57c41c0..a707017 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -1,162 +1,89 @@ package org.scalajs.jsenv.test -import org.junit.Test +import org.scalajs.jsenv._ + +import org.junit.{Before, Test} import org.junit.Assert._ +import org.junit.Assume._ -import scala.concurrent.TimeoutException import scala.concurrent.duration._ -trait TimeoutComTests extends TimeoutTests with ComTests { +private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { + private val kit = new TestComKit(config) + + @Before + def before: Unit = { + assumeTrue("JSEnv needs timeout support", config.supportsTimeout) + assumeTrue("JSEnv needs com support", config.supportsCom) + } @Test def delayedInitTest: Unit = { - - val com = comRunner(s""" + val run = kit.start(s""" setTimeout(function() { scalajsCom.init(function(msg) { scalajsCom.send("Got: " + msg); }); }, 100); - """) - - val deadline = 100.millis.fromNow - - start(com) - - com.send("Hello World") + """, RunConfig()) - assertEquals("Got: Hello World", com.receive()) - - assertTrue("Execution took too little time", deadline.isOverdue()) - - com.close() - com.await(DefaultTimeout) - - } - - @Test - def delayedReplyTest: Unit = { - - val com = comRunner(s""" - scalajsCom.init(function(msg) { - setTimeout(scalajsCom.send, 20, "Got: " + msg); - }); - """) - - start(com) + try { + // Deadline only starts now. Execution must happen asynchronously. + val deadline = 100.millis.fromNow - for (i <- 1 to 10) { - val deadline = 19.millis.fromNow // give some slack - com.send(s"Hello World: $i") - assertEquals(s"Got: Hello World: $i", com.receive()) + run.run.send("Hello World") + assertEquals("Got: Hello World", run.waitNextMessage()) assertTrue("Execution took too little time", deadline.isOverdue()) + } finally { + run.closeAndWait() } - - com.close() - com.await(DefaultTimeout) - } @Test - def receiveTimeoutTest: Unit = { - - val com = comRunner(s""" + def delayedReplyTest: Unit = { + val run = kit.start(s""" scalajsCom.init(function(msg) { - setTimeout(scalajsCom.send, 2000, "Got: " + msg); + setTimeout(scalajsCom.send, 200, "Got: " + msg); }); - """) - - start(com) + """, RunConfig()) - for (i <- 1 to 2) { - com.send(s"Hello World: $i") - try { - com.receive(900.millis) - fail("Expected TimeoutException to be thrown") - } catch { - case _: TimeoutException => + try { + for (i <- 1 to 10) { + val deadline = 190.millis.fromNow // give some slack + run.run.send(s"Hello World: $i") + assertEquals(s"Got: Hello World: $i", run.waitNextMessage()) + assertTrue("Execution took too little time", deadline.isOverdue()) } - assertEquals(s"Got: Hello World: $i", com.receive(3000.millis)) + } finally { + run.closeAndWait() } - - com.close() - com.await(DefaultTimeout) - } @Test def intervalSendTest: Unit = { - - val com = comRunner(s""" + val run = kit.start(s""" scalajsCom.init(function(msg) {}); var interval = setInterval(scalajsCom.send, 50, "Hello"); setTimeout(clearInterval, 295, interval); - """) - - val deadline = 245.millis.fromNow - - start(com) - - for (i <- 1 to 5) - assertEquals("Hello", com.receive()) + """, RunConfig()) - com.close() - com.await(DefaultTimeout) - - assertTrue("Execution took too little time", deadline.isOverdue()) + try { + val deadline = 245.millis.fromNow + for (i <- 1 to 5) + assertEquals("Hello", run.waitNextMessage()) + assertTrue("Execution took too little time", deadline.isOverdue()) + } finally { + run.closeAndWait() + } } @Test def noMessageTest: Unit = { - val com = comRunner(s""" + val run = kit.start(s""" // Make sure JVM has already closed when we init setTimeout(scalajsCom.init, 1000, function(msg) {}); - """) - start(com) - com.close() - com.await(DefaultTimeout) + """, RunConfig()) + run.closeAndWait() } - - @Test - def stopTestTimeout: Unit = { - - val async = asyncRunner(s""" - setInterval(function() {}, 0); - """) - - start(async) - async.stop() - - try { - async.await(DefaultTimeout) - fail("Expected await to fail") - } catch { - case t: Throwable => // all is well - } - - async.stop() // should do nothing, and not fail - - } - - @Test - def doubleStopTest: Unit = { - val async = asyncRunner(s""" - setInterval(function() {}, 0); - """) - - start(async) - async.stop() - async.stop() // should do nothing, and not fail - - try { - async.await(DefaultTimeout) - fail("Expected await to fail") - } catch { - case t: Throwable => // all is well - } - - async.stop() // should do nothing, and not fail - } - } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala similarity index 89% rename from js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala rename to js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala index 2191ef7..3c36b2d 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala @@ -1,11 +1,21 @@ package org.scalajs.jsenv.test -import org.junit.Test +import org.scalajs.jsenv.JSEnv + +import org.junit.{Before, Test} import org.junit.Assert._ +import org.junit.Assume._ import scala.concurrent.duration._ -trait TimeoutTests extends JSEnvTest { +private[test] class TimeoutRunTests(config: JSEnvSuiteConfig, withCom: Boolean) { + private val kit = new TestKit(config, withCom) + import kit._ + + @Before + def before: Unit = { + assumeTrue("JSEnv needs timeout support", config.supportsTimeout) + } @Test def basicTimeoutTest: Unit = { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala deleted file mode 100644 index 4777c46..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala +++ /dev/null @@ -1,16 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -import org.scalajs.io.VirtualJSFile - -trait AsyncJSEnv extends JSEnv { - def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala deleted file mode 100644 index 37d45e4..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ /dev/null @@ -1,80 +0,0 @@ -package org.scalajs.jsenv - -import scala.concurrent.{Future, Await} -import scala.concurrent.duration.Duration - -import org.scalajs.logging.Logger - -trait AsyncJSRunner { - - /** A future that completes when the associated run has terminated. */ - def future: Future[Unit] - - /** - * Start the associated run and returns a Future that completes - * when the run terminates. The returned Future is equivalent to - * the one returned by [[future]]. - */ - def start(logger: Logger, console: JSConsole): Future[Unit] - - /** Aborts the associated run. - * - * There is no guarantee that the runner will be effectively terminated - * by the time this method returns. If necessary, this call can be followed - * by a call to `await()`. - * - * If the run has already completed, this does nothing. Similarly, - * subsequent calls to `stop()` will do nothing. - * - * This method cannot be called before `start()` has been called. - */ - def stop(): Unit - - /** - * Checks whether this async runner is still running. Strictly - * equivalent to - * - * {{{ - * !future.isCompleted - * }}} - */ - final def isRunning(): Boolean = !future.isCompleted - - /** Await completion of the started Run. Strictly equivalent to - * - * {{{ - * Await.result(future, Duration.Inf) - * }}} - */ - final def await(): Unit = Await.result(future, Duration.Inf) - - /** Await completion of the started Run for the duration specified - * by `atMost`. Strictly equivalent to: - * - * {{{ - * Await.result(future, atMost) - * }}} - * - */ - final def await(atMost: Duration): Unit = Await.result(future, atMost) - - /** Awaits completion of the started Run for the duration specified by - * `atMost`, or force it to stop. - * - * If any exception is thrown while awaiting completion (including a - * [[scala.concurrent.TimeoutException TimeoutException]]), forces the runner - * to stop by calling `stop()` before rethrowing the exception. - * - * Strictly equivalent to: - * - * {{{ - * try await(atMost) - * finally stop() - * }}} - */ - final def awaitOrStop(atMost: Duration): Unit = { - try await(atMost) - finally stop() - } - -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala deleted file mode 100644 index 3aeeed2..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala +++ /dev/null @@ -1,42 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -import org.scalajs.io.VirtualJSFile - -/** An [[AsyncJSEnv]] that provides communication to and from the JS VM. - * - * Inside the VM there is a global JavaScript object named `scalajsCom` that - * can be used to control the message channel. It's operations are: - * {{{ - * // initialize com (with callback) - * scalajsCom.init(function(msg) { console.log("Received: " + msg); }); - * - * // send a message to host system - * scalajsCom.send("my message"); - * - * // close com (releases callback, allowing VM to terminate) - * scalajsCom.close(); - * }}} - */ -trait ComJSEnv extends AsyncJSEnv { - def comRunner(files: Seq[VirtualJSFile]): ComJSRunner -} - -object ComJSEnv { - private final val defaultMsg = "JSCom has been closed" - - class ComClosedException(msg: String, - cause: Throwable) extends Exception(msg, cause) { - def this() = this(defaultMsg, null) - def this(cause: Throwable) = this(defaultMsg, cause) - def this(msg: String) = this(msg, null) - } -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala deleted file mode 100644 index 66d24d7..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala +++ /dev/null @@ -1,41 +0,0 @@ -package org.scalajs.jsenv - -import scala.concurrent.duration.Duration - -trait ComJSRunner extends AsyncJSRunner { - - /** Send a message to the JS VM. Throws if the message cannot be sent. */ - def send(msg: String): Unit - - /** Blocks until a message is received and returns it. - * - * @throws ComJSEnv.ComClosedException if the channel is closed before a - * message is received - */ - final def receive(): String = receive(Duration.Inf) - - /** Blocks until a message is received and returns it. - * - * @throws ComJSEnv.ComClosedException if the channel is closed before a - * message is received - * @throws scala.concurrent.TimeoutException if the timeout expires - * before a message is received and the channel is still open - */ - def receive(timeout: Duration): String - - /** Close the communication channel. Allows the VM to terminate if it is - * still waiting for callback. The JVM side **must** call close in - * order to be able to expect termination of the VM. - * - * Calling [[stop]] on a [ComJSRunner]] automatically closes the - * channel. - */ - def close(): Unit - - /** Abort the associated run. Also closes the communication channel. */ - abstract override def stop(): Unit = { - close() - super.stop() - } - -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala deleted file mode 100644 index 6426350..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala +++ /dev/null @@ -1,17 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -/** A JS console that prints on the console */ -object ConsoleJSConsole extends JSConsole { - override def log(msg: Any): Unit = { - Console.println(msg) - } -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala deleted file mode 100644 index 064ced4..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ /dev/null @@ -1,219 +0,0 @@ -package org.scalajs.jsenv - -import scala.collection.immutable - -import org.scalajs.io._ -import org.scalajs.logging.Logger - -import java.io.{ Console => _, _ } -import scala.io.Source - -import scala.collection.JavaConverters._ -import scala.concurrent.{Future, Promise} -import scala.util.Try - -abstract class ExternalJSEnv extends AsyncJSEnv { - - import ExternalJSEnv._ - - def name: String = s"ExternalJSEnv for $vmName" - - /** Printable name of this VM */ - protected def vmName: String - - /** Command to execute (on shell) for this VM */ - protected def executable: String - - /** Command-line arguments to give to the external process. */ - protected def args: immutable.Seq[String] = Nil - - /** Environment in which to run the external process. */ - protected def env: Map[String, String] = Map.empty - - /** Custom initialization scripts. */ - protected def customInitFiles(): Seq[VirtualJSFile] = Nil - - protected class AbstractExtRunner( - protected val files: Seq[VirtualJSFile]) extends JSInitFiles { - - private[this] var _logger: Logger = _ - private[this] var _console: JSConsole = _ - - protected def logger: Logger = _logger - protected def console: JSConsole = _console - - protected def setupLoggerAndConsole(logger: Logger, console: JSConsole) = { - require(_logger == null && _console == null) - _logger = logger - _console = console - } - - /** Custom initialization scripts, defined by the environment. */ - final protected def customInitFiles(): Seq[VirtualJSFile] = - ExternalJSEnv.this.customInitFiles() - - /** Sends required data to VM Stdin (can throw) */ - protected def sendVMStdin(out: OutputStream): Unit = {} - - /** VM arguments excluding executable. Override to adapt. - * - * The default value in `ExternalJSEnv` is `args`. - */ - protected def getVMArgs(): Seq[String] = args - - /** VM environment. Override to adapt. - * - * The default value in `ExternalJSEnv` is - * `System.getenv().asScala.toMap ++ env`. - */ - protected def getVMEnv(): Map[String, String] = - System.getenv().asScala.toMap ++ env - - /** All the JS files that are passed to the VM. - * - * This method can overridden to provide custom behavior in subclasses. - * - * The default value in `ExternalJSEnv` is - * `initFiles() ++ customInitFiles() ++ files`. - */ - protected def getJSFiles(): Seq[VirtualJSFile] = - initFiles() ++ customInitFiles() ++ files - - /** write a single JS file to a writer using an include fct if appropriate */ - protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = { - // The only platform-independent way to do this in JS is to dump the file. - writer.write(file.content) - writer.write('\n') - } - - /** Pipe stdin and stdout from/to VM */ - final protected def pipeVMData(vmInst: Process): Unit = { - // Send stdin to VM. - val out = vmInst.getOutputStream() - try { sendVMStdin(out) } - finally { out.close() } - - // Pipe stdout (and stderr which is merged into it) to console - pipeToConsole(vmInst.getInputStream(), console) - } - - /** Wait for the VM to terminate, verify exit code - * - * @throws ExternalJSEnv.NonZeroExitException if VM returned a non-zero code - */ - final protected def waitForVM(vmInst: Process): Unit = { - // Make sure we are done. - vmInst.waitFor() - - // Get return value and return - val retVal = vmInst.exitValue - if (retVal != 0) - throw new NonZeroExitException(vmName, retVal) - } - - protected def startVM(): Process = { - val vmArgs = getVMArgs() - val vmEnv = getVMEnv() - - val allArgs = executable +: vmArgs - val pBuilder = new ProcessBuilder(allArgs: _*) - pBuilder.redirectErrorStream(true) // merge stderr into stdout - - pBuilder.environment().clear() - for ((name, value) <- vmEnv) - pBuilder.environment().put(name, value) - - logger.debug("Starting process: " + allArgs.mkString(" ")) - - pBuilder.start() - } - - /** send a bunch of JS files to an output stream */ - final protected def sendJS(files: Seq[VirtualJSFile], - out: OutputStream): Unit = { - val writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")) - try sendJS(files, writer) - finally writer.close() - } - - /** send a bunch of JS files to a writer */ - final protected def sendJS(files: Seq[VirtualJSFile], out: Writer): Unit = - files.foreach { writeJSFile(_, out) } - - /** pipe lines from input stream to JSConsole */ - final protected def pipeToConsole(in: InputStream, console: JSConsole) = { - val source = Source.fromInputStream(in, "UTF-8") - try { source.getLines.foreach(console.log _) } - finally { source.close() } - } - - } - - protected class ExtRunner(files: Seq[VirtualJSFile]) - extends AbstractExtRunner(files) with JSRunner { - - def run(logger: Logger, console: JSConsole): Unit = { - setupLoggerAndConsole(logger, console) - - val vmInst = startVM() - - pipeVMData(vmInst) - waitForVM(vmInst) - } - } - - protected class AsyncExtRunner(files: Seq[VirtualJSFile]) - extends AbstractExtRunner(files) with AsyncJSRunner { - - private[this] var vmInst: Process = null - private[this] var ioThreadEx: Throwable = null - private[this] val promise = Promise[Unit] - - private[this] val thread = new Thread { - override def run(): Unit = { - // This thread should not be interrupted, so it is safe to use Trys - val pipeResult = Try(pipeVMData(vmInst)) - val vmComplete = Try(waitForVM(vmInst)) - - // Store IO exception - pipeResult recover { - case e => ioThreadEx = e - } - - // Chain Try's the other way: We want VM failure first, then IO failure - promise.complete(pipeResult orElse vmComplete) - } - } - - def future: Future[Unit] = promise.future - - def start(logger: Logger, console: JSConsole): Future[Unit] = { - setupLoggerAndConsole(logger, console) - startExternalJSEnv() - future - } - - /** Core functionality of [[start]]. - * - * Same as [[start]] but without a call to [[setupLoggerAndConsole]] and - * not returning [[future]]. - * Useful to be called in overrides of [[start]]. - */ - protected def startExternalJSEnv(): Unit = { - require(vmInst == null, "start() may only be called once") - vmInst = startVM() - thread.start() - } - - def stop(): Unit = { - require(vmInst != null, "start() must have been called") - vmInst.destroy() - } - } - -} - -object ExternalJSEnv { - final case class NonZeroExitException(vmName: String, retVal: Int) - extends Exception(s"$vmName exited with code $retVal") -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala new file mode 100644 index 0000000..215a188 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala @@ -0,0 +1,196 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js JS Envs ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import java.io.{IOException, OutputStream} + +import scala.concurrent.{Future, Promise} +import scala.util.control.NonFatal + +/** Support for creating a [[JSRun]] via an external process. */ +object ExternalJSRun { + /** Starts a [[JSRun]] in an external process. + * + * [[ExternalJSRun]] redirects the I/O of the external process according to + * [[Config#runConfig]]. + * + * @see [[supports]] for the exact options it currently supports. + * + * @param command Binary to execute including arguments. + * @param config Configuration. + * @param input Function to inform about creation of stdin for the external process. + * `input` should feed the required stdin to the passed + * [[java.io.OutputStream OutputStream]] and close it. + */ + def start(command: List[String], config: Config)( + input: OutputStream => Unit): JSRun = { + require(command.nonEmpty, "command may not be empty") + + try { + val process = startProcess(command, config.env, config.runConfig) + try { + notifyOutputStreams(config.runConfig, process) + + new ExternalJSRun(process, input, config.closingFails) + } catch { + case t: Throwable => + process.destroyForcibly() + throw t + } + } catch { + case NonFatal(t) => JSRun.failed(t) + } + } + + /** Informs the given [[RunConfig.Validator]] about the options an + * [[ExternalJSRun]] supports. + * + * Use this method to automatically benefit from improvements to + * [[ExternalJSRun]] without modifying the client [[JSEnv]]. + * + * Currently, this calls + * - [[RunConfig.Validator#supportsInheritIO supportsInheritIO]] + * - [[RunConfig.Validator#supportsOnOutputStream supportsOnOutputStream]] + * + * Note that in consequence, a [[JSEnv]] ''may not'' handle these options if + * it uses [[ExternalJSRun]]. + */ + def supports(validator: RunConfig.Validator): RunConfig.Validator = { + validator + .supportsInheritIO() + .supportsOnOutputStream() + } + + /** Configuration for a [[ExternalJSRun]] + * + * @param env Additional environment variables. The environment of the host + * JVM is inherited. + * @param runConfig Configuration for the run. See [[ExternalJSRun.supports]] + * for details about the currently supported configuration. + * @param closingFails Whether calling [[JSRun#close]] on a still running + * [[JSRun]] fails the run. While this defaults to true, [[JSEnv]]s that + * do not support automatic termination (and do not expect the JS program + * itself to explicitly terminate) typically want to set this to false + * (at least for non-com runs), since otherwise there is no successful + * way of terminating a [[JSRun]]. + */ + final class Config private ( + val env: Map[String, String], + val runConfig: RunConfig, + val closingFails: Boolean + ) { + private def this() = { + this( + env = Map.empty, + runConfig = RunConfig(), + closingFails = true) + } + + def withEnv(env: Map[String, String]): Config = + copy(env = env) + + def withRunConfig(runConfig: RunConfig): Config = + copy(runConfig = runConfig) + + def withClosingFails(closingFails: Boolean): Config = + copy(closingFails = closingFails) + + private def copy(env: Map[String, String] = env, + runConfig: RunConfig = runConfig, + closingFails: Boolean = closingFails) = { + new Config(env, runConfig, closingFails) + } + } + + object Config { + def apply(): Config = new Config() + } + + private def notifyOutputStreams(config: RunConfig, process: Process) = { + def opt[T](b: Boolean, v: => T) = if (b) Some(v) else None + + val out = opt(!config.inheritOutput, process.getInputStream()) + val err = opt(!config.inheritError, process.getErrorStream()) + + config.onOutputStream.foreach(f => f(out, err)) + } + + private def startProcess(command: List[String], env: Map[String, String], + config: RunConfig) = { + val builder = new ProcessBuilder(command: _*) + + if (config.inheritOutput) + builder.redirectOutput(ProcessBuilder.Redirect.INHERIT) + + if (config.inheritError) + builder.redirectError(ProcessBuilder.Redirect.INHERIT) + + for ((name, value) <- env) + builder.environment().put(name, value) + + config.logger.debug("Starting process: " + command.mkString(" ")) + + builder.start() + } + + final case class NonZeroExitException(retVal: Int) + extends Exception(s"exited with code $retVal") + + final case class ClosedException() + extends Exception("Termination was requested by user") +} + +private final class ExternalJSRun(process: Process, + input: OutputStream => Unit, closingFails: Boolean) extends JSRun { + + private[this] val promise = Promise[Unit]() + + @volatile + private[this] var closing = false + + def future: Future[Unit] = promise.future + + def close(): Unit = { + closing = true + process.destroyForcibly() + } + + private val waiter = new Thread { + setName("ExternalJSRun waiter") + + override def run(): Unit = { + try { + try { + input(process.getOutputStream()) + } catch { + case _: IOException if closing => + // We got closed while writing. Exception is expected. + } + + val retVal = process.waitFor() + if (retVal == 0 || closing && !closingFails) + promise.success(()) + else if (closing) + promise.failure(new ExternalJSRun.ClosedException) + else + promise.failure(new ExternalJSRun.NonZeroExitException(retVal)) + } catch { + case t: Throwable => + process.destroyForcibly() + promise.failure(t) + + if (!NonFatal(t)) + throw t + } + } + } + + waiter.start() +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala new file mode 100644 index 0000000..d760299 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -0,0 +1,34 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js JS Envs ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import org.scalajs.io._ + +/** Input to a [[JSEnv]]. + * + * Implementors of a [[JSEnv]] are expected to pattern match on this input + * type and handle the ones they support. + * + * Note that this type is not sealed, so future versions of Scala.js may add + * additional input types. Older [[JSEnv]]s are expected to fail in this case + * with an [[UnsupportedInputException]]. + */ +abstract class Input private () + +object Input { + /** All files are to be loaded as scripts into the global scope in the order given. */ + final case class ScriptsToLoad(scripts: List[VirtualJSFile]) extends Input +} + +case class UnsupportedInputException(msg: String, cause: Throwable) + extends IllegalArgumentException(msg, cause) { + def this(msg: String) = this(msg, null) + def this(input: Input) = this(s"Unsupported input: $input") +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala deleted file mode 100644 index c671ca1..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala +++ /dev/null @@ -1,15 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -/** Trait representing a JS console */ -trait JSConsole { - def log(msg: Any): Unit -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index c7fb0c5..7d60af7 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -1,6 +1,6 @@ /* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** ________ ___ / / ___ __ ____ Scala.js JS Envs ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** ** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** ** /____/\___/_/ |_/____/_/ | |__/ /____/ ** ** |/____/ ** @@ -11,10 +11,72 @@ package org.scalajs.jsenv import org.scalajs.io.VirtualJSFile +/** A JavaScript execution environment. + * + * This can run and interact with JavaScript code. + * + * Any implementation is expected to be fully thread-safe. + */ trait JSEnv { /** Human-readable name for this [[JSEnv]] */ - def name: String + val name: String - /** Prepare a runner with the specified JavaScript files. */ - def jsRunner(files: Seq[VirtualJSFile]): JSRunner + /** Starts a new (asynchronous) JS run. + * + * This may only throw if value of `input` is unknown or `config` cannot be + * supported. To verify whether a [[RunConfig]] can be supported in a forward + * compatible manner (i.e. when new options are added in later versions) + * implementations of [[JSEnv]]s must use [[RunConfig.Validator]]. + * + * This must not throw if the run cannot be started or there is a problem + * with the input's content (e.g. file does not exist, syntax error, etc.). + * In this case, [[JSRun#future]] should be failed instead. + * + * @throws java.lang.IllegalArgumentException if the value of `input` or + * `config` cannot be supported. + */ + def start(input: Input, config: RunConfig): JSRun + + /** Like [[start]], but initializes a communication channel. + * + * Inside the VM this is to provide a global JavaScript object named + * `scalajsCom` that can be used to interact with the message channel. Its + * operations are: + * {{{ + * // initialize com (with callback). May only be called once. + * scalajsCom.init(function(msg) { console.log("Received: " + msg); }); + * + * // send a message to host system + * scalajsCom.send("my message"); + * }}} + * + * We describe the expected message delivery guarantees by denoting the + * transmitter as `t` and the receiver as `r`. Both the JVM and the JS end + * act once as a transmitter and once as a receiver. These two + * transmitter/receiver pairs (JS/JVM and JVM/JS) are independent. + * + * For a pair `(t,r)`: + *
    + *
  • If `t` calls [[JSComRun#send]] exactly in the sequence + * {{{ + * send(m_1), ..., send(m_n) + * }}} + * + * and `r` observes `onMessage(m_k)` (k <= n) but not `onMessage(m_{k+1})`, + * `r` must observe + * {{{ + * onMessage(m_1), ..., onMessage(m_k) + * }}} + * exactly in this order. + *
  • If `t` and `r` keep running indefinitely and `t` sends n messages, + * `r` receives n messages. + *
+ * + * @param onMessage Callback invoked each time a message is received from the + * JS VM. The implementation may not call this anymore once + * [[JSRun#future]] of the returned [[JSComRun]] is completed. Further, + * [[JSRun#future]] may only complete with no callback in-flight. + */ + def startWithCom(input: Input, config: RunConfig, + onMessage: String => Unit): JSComRun } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala deleted file mode 100644 index 118ce43..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala +++ /dev/null @@ -1,8 +0,0 @@ -package org.scalajs.jsenv - -import org.scalajs.io.VirtualJSFile - -trait JSInitFiles { - /** JS files used to setup VM */ - protected def initFiles(): Seq[VirtualJSFile] = Nil -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala deleted file mode 100644 index 8981947..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala +++ /dev/null @@ -1,17 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -import org.scalajs.logging.Logger - -trait JSRunner { - /** Run the associated JS code. Throw if an error occurs. */ - def run(logger: Logger, console: JSConsole): Unit -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala new file mode 100644 index 0000000..cbdb123 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala @@ -0,0 +1,76 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js JS Envs ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import scala.concurrent.Future + +/** A launched instance of a [[JSEnv]]. + * + * This is the interface to actually running JS code (whether this is in + * process or not depens on the [[JSEnv]] that created the [[JSRun]]). + * + * Any implementation is expected to be fully thread-safe. + */ +trait JSRun extends AutoCloseable { + /** A [[scala.concurrent.Future Future]] that completes if the run completes. + * + * The future is failed if the run fails. + * + * Note that a [[JSRun]] is not required to ever terminate on it's own. That + * means even if all code is executed and the event loop is empty, the run + * may continue to run. As a consequence, it is *not* correct to rely on + * termination of a [[JSRun]] without any external means of stopping it + * (i.e. calling [[close]]). + */ + def future: Future[Unit] + + /** Stops the run and releases all the resources. + * + * This must be called to ensure the run's resources are + * released. + * + * Whether or not this makes the run fail or not is up to the implementation. + * However, in the following cases, calling [[close]] may not fail the run: + *
    + *
  • [[future]] is already completed when [[close]] is called. + *
  • This is a [[JSComRun]] and the event loop inside the VM is empty. + *
+ * + * Idempotent, async, nothrow. + */ + def close(): Unit +} + +object JSRun { + /** Creates a [[JSRun]] that has failed. */ + def failed(cause: Throwable): JSRun = new JSRun { + def close(): Unit = () + val future: Future[Unit] = Future.failed(cause) + } +} + +/** A [[JSRun]] that has a communication channel to the running JS code. */ +trait JSComRun extends JSRun { + /** Sends a message to the JS end. + * + * Async, nothrow. See [[JSEnv#startWithCom]] for expected message delivery + * guarantees. + */ + def send(msg: String): Unit +} + +object JSComRun { + /** Creates a [[JSComRun]] that has failed. */ + def failed(cause: Throwable): JSComRun = new JSComRun { + def close(): Unit = () + val future: Future[Unit] = Future.failed(cause) + def send(msg: String): Unit = () + } +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala deleted file mode 100644 index 24b677d..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.scalajs.jsenv - -object NullJSConsole extends JSConsole { - def log(msg: Any): Unit = {} -} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala new file mode 100644 index 0000000..83d7820 --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala @@ -0,0 +1,160 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js JS Envs ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv + +import java.io.InputStream + +import org.scalajs.logging._ + +/** Configuration provided when starting a [[JSEnv]]. + * + * @param onOutputStream Callback once output streams of the JS VM run become available. + * + * The callback receives the output and the error stream of the VM if they + * are available. If [[inheritOutput]] or [[inheritError]] are set to true, the + * respective streams must be `None`, in the invocation of + * [[onOutputStream]]. Note however, that if [[onOutputStream]] is present, + * it must be invoked by the JS VM. + * + * @param inheritOutput Whether the output stream of the VM should be inherited. + * + * The implementation may chose to redirect to the actual output stream of + * the parent JVM or simply [[scala.Console#out]]. + * + * If you set this value to `false` you must set [[onOutputStream]]. + * + * @param inheritError Whether the error stream of the VM should be inherited. + * + * The implementation may chose to redirect to the actual error stream of the + * parent JVM or simply [[scala.Console#err]]. + * + * If you set this value to `false` you must set [[onOutputStream]]. + * + * @param logger The logger to use in the run. A [[JSEnv]] is not required to + * log anything. + */ +final class RunConfig private ( + val onOutputStream: Option[RunConfig.OnOutputStream], + val inheritOutput: Boolean, + val inheritError: Boolean, + val logger: Logger, + /** An option that will never be supported by anything because it is not exposed. + * + * This is used to test that [[JSEnv]]s properly validate their configuration. + */ + private[jsenv] val eternallyUnsupportedOption: Boolean +) { + import RunConfig.OnOutputStream + + private def this() = { + this( + onOutputStream = None, + inheritOutput = true, + inheritError = true, + logger = NullLogger, + eternallyUnsupportedOption = false) + } + + def withOnOutputStream(onOutputStream: OnOutputStream): RunConfig = + copy(onOutputStream = Some(onOutputStream)) + + def withInheritOut(inheritOutput: Boolean): RunConfig = + copy(inheritOutput = inheritOutput) + + def withInheritErr(inheritError: Boolean): RunConfig = + copy(inheritError = inheritError) + + def withLogger(logger: Logger): RunConfig = + copy(logger = logger) + + private[jsenv] def withEternallyUnsupportedOption( + eternallyUnsupportedOption: Boolean): RunConfig = + copy(eternallyUnsupportedOption = eternallyUnsupportedOption) + + private def copy(onOutputStream: Option[OnOutputStream] = onOutputStream, + inheritOutput: Boolean = inheritOutput, + inheritError: Boolean = inheritError, + logger: Logger = logger, + eternallyUnsupportedOption: Boolean = eternallyUnsupportedOption + ): RunConfig = { + new RunConfig(onOutputStream, inheritOutput, inheritError, logger, + eternallyUnsupportedOption) + } + + /** Validates constraints on the config itself. */ + private def validate(): Unit = { + if (onOutputStream.isEmpty && (!inheritOutput || !inheritError)) { + throw new IllegalArgumentException("You may not set inheritOutput or " + + "inheritError to false without setting onOutputStream.") + } + } +} + +final object RunConfig { + type OnOutputStream = (Option[InputStream], Option[InputStream]) => Unit + def apply(): RunConfig = new RunConfig() + + /** Support validator for [[RunConfig]]. + * + * Validators allow us to add options to [[RunConfig]] in a forward + * compatible manner. + * + * Every [[JSEnv]] must + * + * 1. create a [[Validator]] + * 1. inform it of the [[JSEnv]]'s capabilities + * 1. invoke [[validate]] with every received [[RunConfig]] + * + * This ensures that enusre that all set config options are supported by the + * [[JSEnv]]. + */ + final class Validator private ( + inheritIO: Boolean, + onOutputStream: Boolean + ) { + private def this() = this(false, false) + + /** The caller supports [[RunConfig#inheritOutput]] and + * [[RunConfig#inheritError]]. + */ + def supportsInheritIO(): Validator = copy(inheritIO = true) + + /** The caller supports [[RunConfig#onOutputStream]]. */ + def supportsOnOutputStream(): Validator = copy(onOutputStream = true) + + /** Validates that `config` is valid and only sets supported options. + * + * @throws java.lang.IllegalArgumentException if there are unsupported options. + */ + def validate(config: RunConfig): Unit = { + def fail(msg: String) = throw new IllegalArgumentException(msg) + + config.validate() + + if (!inheritIO && (config.inheritOutput || config.inheritError)) + fail("inheritOutput / inheritError are not supported.") + + if (!onOutputStream && config.onOutputStream.isDefined) + fail("onOutputStream is not supported.") + + if (config.eternallyUnsupportedOption) + fail("eternallyUnsupportedOption is not supported.") + } + + private def copy(inheritIO: Boolean = inheritIO, + onOutputStream: Boolean = onOutputStream) = { + new Validator(inheritIO, onOutputStream) + } + } + + object Validator { + def apply(): Validator = new Validator() + } +} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala deleted file mode 100644 index 6f921a5..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala +++ /dev/null @@ -1,38 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js JS environments ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv - -import scala.concurrent.duration._ - -private[jsenv] object Utils { - final class OptDeadline private ( - val __private_deadline: Deadline /* nullable */) // scalastyle:ignore - extends AnyVal { - - @inline private def deadline: Deadline = __private_deadline - - def millisLeft: Long = - if (deadline == null) 0 - else (deadline.timeLeft.toMillis max 1L) - - def isOverdue: Boolean = - if (deadline == null) false - else deadline.isOverdue - } - - object OptDeadline { - def apply(timeout: Duration): OptDeadline = { - new OptDeadline(timeout match { - case timeout: FiniteDuration => timeout.fromNow - case _ => null - }) - } - } -} diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala new file mode 100644 index 0000000..a4734aa --- /dev/null +++ b/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala @@ -0,0 +1,88 @@ +package org.scalajs.jsenv + +import org.junit.Test + +class RunConfigTest { + @Test + def supportedInheritIO: Unit = { + val cfg = RunConfig() + .withInheritOut(true) + .withInheritErr(true) + RunConfig.Validator() + .supportsInheritIO() + .validate(cfg) + } + + @Test(expected = classOf[IllegalArgumentException]) + def unsupportedInheritOut: Unit = { + val cfg = RunConfig() + .withInheritOut(true) + .withInheritErr(false) + .withOnOutputStream((_, _) => ()) + RunConfig.Validator() + .supportsOnOutputStream() + .validate(cfg) + } + + @Test(expected = classOf[IllegalArgumentException]) + def unsupportedInheritErr: Unit = { + val cfg = RunConfig() + .withInheritOut(false) + .withInheritErr(true) + .withOnOutputStream((_, _) => ()) + RunConfig.Validator() + .supportsOnOutputStream() + .validate(cfg) + } + + @Test + def supportedOnOutputStream: Unit = { + val cfg = RunConfig() + .withInheritOut(false) + .withInheritErr(false) + .withOnOutputStream((_, _) => ()) + RunConfig.Validator() + .supportsOnOutputStream() + .validate(cfg) + } + + @Test(expected = classOf[IllegalArgumentException]) + def unsupportedOnOutputStream: Unit = { + val cfg = RunConfig() + .withInheritOut(false) + .withInheritErr(false) + .withOnOutputStream((_, _) => ()) + RunConfig.Validator() + .validate(cfg) + } + + @Test(expected = classOf[IllegalArgumentException]) + def missingOnOutputStreamNoInheritOut: Unit = { + val cfg = RunConfig() + .withInheritOut(false) + .withInheritErr(true) + RunConfig.Validator() + .supportsInheritIO() + .supportsOnOutputStream() + .validate(cfg) + } + + @Test(expected = classOf[IllegalArgumentException]) + def missingOnOutputStreamNoInheritErr: Unit = { + val cfg = RunConfig() + .withInheritOut(true) + .withInheritErr(false) + RunConfig.Validator() + .supportsInheritIO() + .supportsOnOutputStream() + .validate(cfg) + } + + @Test(expected = classOf[IllegalArgumentException]) + def failValidationForTest: Unit = { + val cfg = RunConfig() + .withEternallyUnsupportedOption(true) + RunConfig.Validator() + .validate(cfg) + } +} diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala deleted file mode 100644 index 818ac44..0000000 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ /dev/null @@ -1,311 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - - -package org.scalajs.jsenv.nodejs - -import java.io.{Console => _, _} -import java.net._ - -import org.scalajs.io._ -import org.scalajs.io.JSUtils.escapeJS -import org.scalajs.logging.NullLogger - -import org.scalajs.jsenv._ -import org.scalajs.jsenv.Utils.OptDeadline - -import scala.collection.JavaConverters._ -import scala.concurrent.TimeoutException -import scala.concurrent.duration._ - -abstract class AbstractNodeJSEnv extends ExternalJSEnv with ComJSEnv { - - protected def wantSourceMap: Boolean = true - - /** True, if the installed node executable supports source mapping. - * - * Do `npm install source-map-support` if you need source maps. - */ - lazy val hasSourceMapSupport: Boolean = { - val code = new MemVirtualJSFile("source-map-support-probe.js") - .withContent("""require('source-map-support').install();""") - - try { - jsRunner(Seq(code)).run(NullLogger, NullJSConsole) - true - } catch { - case t: ExternalJSEnv.NonZeroExitException => - false - } - } - - /** Retry-timeout to wait for the JS VM to connect */ - protected val acceptTimeout = 5000 - - protected trait AbstractNodeRunner extends AbstractExtRunner with JSInitFiles { - - protected[this] val libCache = new VirtualFileMaterializer(true) - - /** File(s) to automatically install source-map-support. - * Is used by [[initFiles]], override to change/disable. - */ - protected def installSourceMap(): Seq[VirtualJSFile] = { - if (wantSourceMap) { - val content = - """ - |try { - | require('source-map-support').install(); - |} catch (e) {} - """.stripMargin - Seq(new MemVirtualJSFile("sourceMapSupport.js").withContent(content)) - } else { - Seq() - } - } - - /** File(s) to hack console.log to prevent if from changing `%%` to `%`. - * Is used by [[initFiles]], override to change/disable. - */ - protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("nodeConsoleHack.js").withContent( - """ - |// Hack console log to duplicate double % signs - |(function() { - | function startsWithAnyOf(s, prefixes) { - | for (var i = 0; i < prefixes.length; i++) { - | // ES5 does not have .startsWith() on strings - | if (s.substring(0, prefixes[i].length) === prefixes[i]) - | return true; - | } - | return false; - | } - | var nodeWillDeduplicateEvenForOneArgument = startsWithAnyOf( - | process.version, ["v0.", "v1.", "v2.0."]); - | var oldLog = console.log; - | var newLog = function() { - | var args = arguments; - | if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { - | var argStr = args[0].toString(); - | if (args.length > 1 || nodeWillDeduplicateEvenForOneArgument) - | argStr = argStr.replace(/%/g, "%%"); - | args[0] = argStr; - | } - | oldLog.apply(console, args); - | }; - | console.log = newLog; - |})(); - """.stripMargin - ) - ) - - - /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`. - * Is used by [[initFiles]], override to change/disable. - */ - protected def runtimeEnv(): Seq[VirtualJSFile] = Seq( - new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( - """ - |__ScalaJSEnv = { - | exitFunction: function(status) { process.exit(status); } - |}; - """.stripMargin - ) - ) - - override protected def initFiles(): Seq[VirtualJSFile] = - installSourceMap() ++ fixPercentConsole() ++ runtimeEnv() - - /** write a single JS file to a writer using an include fct if appropriate - * uses `require` if the file exists on the filesystem - */ - override protected def writeJSFile(file: VirtualJSFile, - writer: Writer): Unit = { - file match { - case file: FileVirtualJSFile => - val fname = file.file.getAbsolutePath - writer.write(s"""require("${escapeJS(fname)}");\n""") - case _ => - super.writeJSFile(file, writer) - } - } - - // Node.js specific (system) environment - override protected def getVMEnv(): Map[String, String] = { - val baseNodePath = Option(System.getenv("NODE_PATH")).filter(_.nonEmpty) - val nodePath = libCache.cacheDir.getAbsolutePath + - baseNodePath.fold("")(p => File.pathSeparator + p) - - System.getenv().asScala.toMap ++ Seq( - "NODE_MODULE_CONTEXTS" -> "0", - "NODE_PATH" -> nodePath - ) ++ env - } - } - - protected trait NodeComJSRunner extends ComJSRunner with JSInitFiles { - - private[this] val serverSocket = - new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address - private var comSocket: Socket = _ - private var jvm2js: DataOutputStream = _ - private var js2jvm: DataInputStream = _ - - abstract override protected def initFiles(): Seq[VirtualJSFile] = - super.initFiles :+ comSetup - - private def comSetup(): VirtualJSFile = { - new MemVirtualJSFile("comSetup.js").withContent( - s""" - |(function() { - | // The socket for communication - | var socket = null; - | // The callback where received messages go - | var recvCallback = null; - | - | // Buffers received data - | var inBuffer = new Buffer(0); - | - | function onData(data) { - | inBuffer = Buffer.concat([inBuffer, data]); - | tryReadMsg(); - | } - | - | function tryReadMsg() { - | while (inBuffer.length >= 4) { - | var msgLen = inBuffer.readInt32BE(0); - | var byteLen = 4 + msgLen * 2; - | - | if (inBuffer.length < byteLen) return; - | var res = ""; - | - | for (var i = 0; i < msgLen; ++i) - | res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); - | - | inBuffer = inBuffer.slice(byteLen); - | - | recvCallback(res); - | } - | } - | - | global.scalajsCom = { - | init: function(recvCB) { - | if (socket !== null) throw new Error("Com already open"); - | - | var net = require('net'); - | recvCallback = recvCB; - | socket = net.connect(${serverSocket.getLocalPort}); - | socket.on('data', onData); - | socket.on('error', function(err) { - | // Whatever happens, this closes the Com - | socket.end(); - | - | // Expected errors: - | // - EPIPE on write: JVM closes - | // - ECONNREFUSED on connect: JVM closes before JS opens - | var expected = ( - | err.syscall === "write" && err.code === "EPIPE" || - | err.syscall === "connect" && err.code === "ECONNREFUSED" - | ); - | - | if (!expected) { - | console.error("Scala.js Com failed: " + err); - | // We must terminate with an error - | process.exit(-1); - | } - | }); - | }, - | send: function(msg) { - | if (socket === null) throw new Error("Com not open"); - | - | var len = msg.length; - | var buf = new Buffer(4 + len * 2); - | buf.writeInt32BE(len, 0); - | for (var i = 0; i < len; ++i) - | buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2); - | socket.write(buf); - | }, - | close: function() { - | if (socket === null) throw new Error("Com not open"); - | socket.end(); - | } - | } - |}).call(this); - """.stripMargin) - } - - def send(msg: String): Unit = { - if (awaitConnection()) { - jvm2js.writeInt(msg.length) - jvm2js.writeChars(msg) - jvm2js.flush() - } - } - - def receive(timeout: Duration): String = { - if (!awaitConnection()) - throw new ComJSEnv.ComClosedException("Node.js isn't connected") - - js2jvm.mark(Int.MaxValue) - val savedSoTimeout = comSocket.getSoTimeout() - try { - val optDeadline = OptDeadline(timeout) - - comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) - val len = js2jvm.readInt() - val carr = Array.fill(len) { - comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt) - js2jvm.readChar() - } - - js2jvm.mark(0) - String.valueOf(carr) - } catch { - case e: EOFException => - throw new ComJSEnv.ComClosedException(e) - case e: SocketTimeoutException => - js2jvm.reset() - throw new TimeoutException("Timeout expired") - } finally { - comSocket.setSoTimeout(savedSoTimeout) - } - } - - def close(): Unit = { - serverSocket.close() - if (jvm2js != null) - jvm2js.close() - if (js2jvm != null) - js2jvm.close() - if (comSocket != null) - comSocket.close() - } - - /** Waits until the JS VM has established a connection or terminates - * - * @return true if the connection was established - */ - private def awaitConnection(): Boolean = { - serverSocket.setSoTimeout(acceptTimeout) - while (comSocket == null && isRunning) { - try { - comSocket = serverSocket.accept() - jvm2js = new DataOutputStream( - new BufferedOutputStream(comSocket.getOutputStream())) - js2jvm = new DataInputStream( - new BufferedInputStream(comSocket.getInputStream())) - } catch { - case to: SocketTimeoutException => - } - } - - comSocket != null - } - - override protected def finalize(): Unit = close() - } -} diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala new file mode 100644 index 0000000..aa03c85 --- /dev/null +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala @@ -0,0 +1,302 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js Node.js env ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv.nodejs + +import java.io._ +import java.net._ + +import org.scalajs.io.{VirtualJSFile, MemVirtualJSFile} +import org.scalajs.jsenv._ + +import scala.collection.immutable +import scala.concurrent._ +import scala.util.{Failure, Success} +import scala.util.control.NonFatal + +// TODO Replace this by a better execution context on the RunConfig. +import scala.concurrent.ExecutionContext.Implicits.global + +private final class ComRun(run: JSRun, handleMessage: String => Unit, + serverSocket: ServerSocket) extends JSComRun { + import ComRun._ + + /** Promise that completes once the reciever thread is completed. */ + private[this] val promise = Promise[Unit]() + + @volatile + private[this] var state: State = AwaitingConnection(Nil) + + // If the run completes, make sure we also complete. + run.future.onComplete { + case Failure(t) => forceClose(t) + case Success(_) => onJSTerminated() + } + + // TODO replace this with scheduled tasks on the execution context. + private[this] val receiver = new Thread { + setName("ComRun receiver") + + override def run(): Unit = { + try { + try { + /* We need to await the connection unconditionally. Otherwise the JS end + * might try to connect indefinitely. + */ + awaitConnection() + + while (state != Closing) { + state match { + case s: AwaitingConnection => + throw new IllegalStateException(s"Unexpected state: $s") + + case Closing => + /* We can end up here if there is a race between the two read to + * state. Do nothing, loop will terminate. + */ + + case Connected(_, _, js2jvm) => + try { + val len = js2jvm.readInt() + val carr = Array.fill(len)(js2jvm.readChar()) + handleMessage(String.valueOf(carr)) + } catch { + case _: EOFException => + // JS end terminated gracefully. Close. + close() + } + } + } + } catch { + case _: IOException if state == Closing => + // We got interrupted by a graceful close. + // This is OK. + } + + /* Everything got closed. We wait for the run to terminate. + * We need to wait in order to make sure that closing the + * underlying run does not fail it. + */ + ComRun.this.run.future.foreach { _ => + ComRun.this.run.close() + promise.trySuccess(()) + } + } catch { + case t: Throwable => handleThrowable(t) + } + } + } + + receiver.start() + + def future: Future[Unit] = promise.future + + def send(msg: String): Unit = synchronized { + state match { + case AwaitingConnection(msgs) => + state = AwaitingConnection(msg :: msgs) + + case Connected(_, jvm2js, _) => + try { + writeMsg(jvm2js, msg) + jvm2js.flush() + } catch { + case t: Throwable => handleThrowable(t) + } + + case Closing => // ignore msg. + } + } + + def close(): Unit = synchronized { + val oldState = state + + // Signal receiver thread that it is OK if socket read fails. + state = Closing + + oldState match { + case c: Connected => + // Interrupts the receiver thread and signals the VM to terminate. + closeAll(c) + + case Closing | _:AwaitingConnection => + } + } + + private def onJSTerminated() = { + close() + + /* Interrupt receiver if we are still waiting for connection. + * Should only be relevant if we are still awaiting the connection. + * Note: We cannot do this in close(), otherwise if the JVM side closes + * before the JS side connected, the JS VM will fail instead of terminate + * normally. + */ + serverSocket.close() + } + + private def forceClose(cause: Throwable) = { + promise.tryFailure(cause) + close() + run.close() + serverSocket.close() + } + + private def handleThrowable(cause: Throwable) = { + forceClose(cause) + if (!NonFatal(cause)) + throw cause + } + + private def awaitConnection(): Unit = { + var comSocket: Socket = null + var jvm2js: DataOutputStream = null + var js2jvm: DataInputStream = null + + try { + comSocket = serverSocket.accept() + serverSocket.close() // we don't need it anymore. + jvm2js = new DataOutputStream( + new BufferedOutputStream(comSocket.getOutputStream())) + js2jvm = new DataInputStream( + new BufferedInputStream(comSocket.getInputStream())) + + onConnected(Connected(comSocket, jvm2js, js2jvm)) + } catch { + case t: Throwable => + closeAll(comSocket, jvm2js, js2jvm) + throw t + } + } + + private def onConnected(c: Connected): Unit = synchronized { + state match { + case AwaitingConnection(msgs) => + msgs.reverse.foreach(writeMsg(c.jvm2js, _)) + c.jvm2js.flush() + state = c + + case _: Connected => + throw new IllegalStateException(s"Unexpected state: $state") + + case Closing => + closeAll(c) + } + } +} + +object ComRun { + /** Starts a [[JSComRun]] using the provided [[JSRun]] launcher. + * + * @param config Configuration for the run. + * @param onMessage callback upon message reception. + * @param startRun [[JSRun]] launcher. Gets passed a + * [[org.scalajs.io.VirtualJSFile VirtualJSFile]] that + * initializes `scalaJSCom` on `global`. Requires Node.js libraries. + */ + def start(config: RunConfig, onMessage: String => Unit)( + startRun: VirtualJSFile => JSRun): JSComRun = { + try { + val serverSocket = + new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address + + val run = startRun(setupFile(serverSocket.getLocalPort)) + + new ComRun(run, onMessage, serverSocket) + } catch { + case NonFatal(t) => JSComRun.failed(t) + } + } + + private def closeAll(c: Closeable*): Unit = + c.withFilter(_ != null).foreach(_.close()) + + private def closeAll(c: Connected): Unit = + closeAll(c.comSocket, c.jvm2js, c.js2jvm) + + private sealed trait State + + private final case class AwaitingConnection( + sendQueue: List[String]) extends State + + private final case class Connected( + comSocket: Socket, + jvm2js: DataOutputStream, + js2jvm: DataInputStream) extends State + + private final case object Closing extends State + + private def writeMsg(s: DataOutputStream, msg: String): Unit = { + s.writeInt(msg.length) + s.writeChars(msg) + } + + private def setupFile(port: Int): VirtualJSFile = { + new MemVirtualJSFile("comSetup.js").withContent( + s""" + |(function() { + | // The socket for communication + | var socket = require('net').connect($port); + | + | // Buffers received data + | var inBuffer = new Buffer(0); + | + | // Buffers received messages + | var inMessages = []; + | + | // The callback where received messages go + | var recvCallback = function(msg) { inMessages.push(msg); }; + | + | socket.on('data', function(data) { + | inBuffer = Buffer.concat([inBuffer, data]); + | + | while (inBuffer.length >= 4) { + | var msgLen = inBuffer.readInt32BE(0); + | var byteLen = 4 + msgLen * 2; + | + | if (inBuffer.length < byteLen) return; + | var res = ""; + | + | for (var i = 0; i < msgLen; ++i) + | res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); + | + | inBuffer = inBuffer.slice(byteLen); + | + | recvCallback(res); + | } + | }); + | + | socket.on('error', function(err) { + | console.error("Scala.js Com failed: " + err); + | process.exit(-1); + | }); + | + | socket.on('close', function() { process.exit(0); }); + | + | global.scalajsCom = { + | init: function(recvCB) { + | if (inMessages === null) throw new Error("Com already initialized"); + | for (var i = 0; i < inMessages.length; ++i) + | recvCB(inMessages[i]); + | inMessages = null; + | recvCallback = recvCB; + | }, + | send: function(msg) { + | var len = msg.length; + | var buf = new Buffer(4 + len * 2); + | buf.writeInt32BE(len, 0); + | for (var i = 0; i < len; ++i) + | buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2); + | socket.write(buf); + | } + | } + |}).call(this); + """.stripMargin) + } +} diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 9ff544e..dfdf3e5 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -1,6 +1,6 @@ /* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** +** ________ ___ / / ___ __ ____ Scala.js Node.js env ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** ** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** ** /____/\___/_/ |_/____/_/ | |__/ /____/ ** ** |/____/ ** @@ -9,63 +9,98 @@ package org.scalajs.jsenv.nodejs +import java.nio.charset.StandardCharsets + import scala.collection.immutable +import scala.collection.JavaConverters._ import org.scalajs.jsenv._ import org.scalajs.io._ +import org.scalajs.io.JSUtils.escapeJS import org.scalajs.logging._ -import java.io.{ Console => _, _ } - -class NodeJSEnv(config: NodeJSEnv.Config) extends AbstractNodeJSEnv { +import java.io._ +final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { def this() = this(NodeJSEnv.Config()) - protected def vmName: String = "Node.js" + val name: String = "Node.js" - protected def executable: String = config.executable - - override protected def args: immutable.Seq[String] = config.args + def start(input: Input, runConfig: RunConfig): JSRun = { + NodeJSEnv.validator.validate(runConfig) + internalStart(initFiles ++ inputFiles(input), runConfig) + } - override protected def env: Map[String, String] = config.env + def startWithCom(input: Input, runConfig: RunConfig, + onMessage: String => Unit): JSComRun = { + NodeJSEnv.validator.validate(runConfig) + ComRun.start(runConfig, onMessage) { comLoader => + val files = initFiles ::: (comLoader :: inputFiles(input)) + internalStart(files, runConfig) + } + } - // TODO Our Build wants this to be public, but it does not seem clean - override def wantSourceMap: Boolean = config.sourceMap + private def internalStart(files: List[VirtualJSFile], + runConfig: RunConfig): JSRun = { + val command = config.executable :: config.args + val externalConfig = ExternalJSRun.Config() + .withEnv(env) + .withRunConfig(runConfig) + ExternalJSRun.start(command, externalConfig)(NodeJSEnv.write(files)) + } - override def jsRunner(files: Seq[VirtualJSFile]): JSRunner = - new NodeRunner(files) + private def initFiles: List[VirtualJSFile] = { + val base = List(NodeJSEnv.runtimeEnv, Support.fixPercentConsole) - override def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner = - new AsyncNodeRunner(files) + if (config.sourceMap) NodeJSEnv.installSourceMap :: base + else base + } - override def comRunner(files: Seq[VirtualJSFile]): ComJSRunner = - new ComNodeRunner(files) + private def inputFiles(input: Input) = input match { + case Input.ScriptsToLoad(scripts) => scripts + case _ => throw new UnsupportedInputException(input) + } - protected class NodeRunner(files: Seq[VirtualJSFile]) - extends ExtRunner(files) with AbstractBasicNodeRunner + private def env: Map[String, String] = + Map("NODE_MODULE_CONTEXTS" -> "0") ++ config.env +} - protected class AsyncNodeRunner(files: Seq[VirtualJSFile]) - extends AsyncExtRunner(files) with AbstractBasicNodeRunner +object NodeJSEnv { + private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) - protected class ComNodeRunner(files: Seq[VirtualJSFile]) - extends AsyncNodeRunner(files) with NodeComJSRunner + private lazy val installSourceMap = { + new MemVirtualJSFile("sourceMapSupport.js").withContent( + "require('source-map-support').install();") + } - protected trait AbstractBasicNodeRunner extends AbstractNodeRunner { + private lazy val runtimeEnv = { + new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( + """ + |__ScalaJSEnv = { + | exitFunction: function(status) { process.exit(status); } + |}; + """.stripMargin + ) + } - // Send code to Stdin - override protected def sendVMStdin(out: OutputStream): Unit = { - /* Do not factor this method out into AbstractNodeRunner or when mixin in - * the traits it would use AbstractExtRunner.sendVMStdin due to - * linearization order. - */ - sendJS(getJSFiles(), out) + private def write(files: List[VirtualJSFile])(out: OutputStream): Unit = { + val writer = new BufferedWriter( + new OutputStreamWriter(out, StandardCharsets.UTF_8)) + try { + files.foreach { + case file: FileVirtualJSFile => + val fname = file.file.getAbsolutePath + writer.write(s"""require("${escapeJS(fname)}");\n""") + case f => + IO.writeTo(f, writer) + writer.write('\n') + } + } finally { + writer.close() } } -} - -object NodeJSEnv { final class Config private ( val executable: String, val args: List[String], diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala new file mode 100644 index 0000000..f48bac1 --- /dev/null +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala @@ -0,0 +1,44 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js Node.js env ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package org.scalajs.jsenv.nodejs + +import org.scalajs.io._ + +object Support { + def fixPercentConsole: VirtualJSFile = { + new MemVirtualJSFile("nodeConsoleHack.js").withContent( + """ + |// Hack console log to duplicate double % signs + |(function() { + | function startsWithAnyOf(s, prefixes) { + | for (var i = 0; i < prefixes.length; i++) { + | // ES5 does not have .startsWith() on strings + | if (s.substring(0, prefixes[i].length) === prefixes[i]) + | return true; + | } + | return false; + | } + | var oldLog = console.log; + | var newLog = function() { + | var args = arguments; + | if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { + | var argStr = args[0].toString(); + | if (args.length > 1) + | argStr = argStr.replace(/%/g, "%%"); + | args[0] = argStr; + | } + | oldLog.apply(console, args); + | }; + | console.log = newLog; + |})(); + """.stripMargin + ) + } +} diff --git a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala new file mode 100644 index 0000000..6310447 --- /dev/null +++ b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala @@ -0,0 +1,11 @@ +package org.scalajs.jsenv.nodejs + +import org.scalajs.jsenv.test._ + +import org.junit.runner.RunWith + +@RunWith(classOf[JSEnvSuiteRunner]) +class NodeJSSuite extends JSEnvSuite( + JSEnvSuiteConfig(new NodeJSEnv) + .withTerminateVMJSCode("process.exit(0)") +) diff --git a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSTest.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSTest.scala deleted file mode 100644 index 764d8ac..0000000 --- a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSTest.scala +++ /dev/null @@ -1,79 +0,0 @@ -package org.scalajs.jsenv.nodejs - -import org.scalajs.jsenv.test._ - -import org.junit.Test -import org.junit.Assert._ - -class NodeJSTest extends TimeoutComTests { - - protected def newJSEnv: NodeJSEnv = new NodeJSEnv - - /** Node.js strips double percentage signs - #500 */ - @Test - def percentageTest: Unit = { - val counts = 1 to 15 - val argcs = 1 to 3 - val strings = counts.map("%" * _) - - val strlists = for { - count <- argcs - string <- strings - } yield List.fill(count)(string) - - val codes = for { - strlist <- strlists - } yield { - val args = strlist.map(s => s""""$s"""").mkString(", ") - s"console.log($args);\n" - } - - val result = strlists.map(_.mkString(" ") + "\n").mkString("") - - codes.mkString("").hasOutput(result) - } - - /** Node.js console.log hack didn't allow to log non-Strings - #561 */ - @Test - def nonStringTest: Unit = { - - """ - console.log(1); - console.log(undefined); - console.log(null); - console.log({}); - console.log([1,2]); - """ hasOutput - """|1 - |undefined - |null - |[object Object] - |1,2 - |""".stripMargin - } - - @Test - def slowJSEnvTest: Unit = { - val com = comRunner(""" - setTimeout(function() { - scalajsCom.init(function(msg) { - scalajsCom.send("pong: " + msg); - }); - }, 1000); - """) - - val n = 20 - - start(com) - - for (_ <- 1 to n) - com.send("ping") - - for (_ <- 1 to n) - assertEquals(com.receive(), "pong: ping") - - com.close() - com.await(DefaultTimeout) - } - -} diff --git a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSWithCustomInitFilesTest.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSWithCustomInitFilesTest.scala deleted file mode 100644 index d1841bd..0000000 --- a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSWithCustomInitFilesTest.scala +++ /dev/null @@ -1,9 +0,0 @@ -package org.scalajs.jsenv.nodejs - -import org.scalajs.jsenv.test._ - -class NodeJSWithCustomInitFilesTest extends CustomInitFilesTest { - protected def newJSEnv: NodeJSEnv = new NodeJSEnv { - override def customInitFiles() = makeCustomInitFiles() - } -} From 1135bac0856f34f48c1161f11b0110e3c31e9c68 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 26 Apr 2018 15:38:51 -0700 Subject: [PATCH 092/133] Reduce acceptTimeout I noticed that my tests were very slow to start running. While running with sbt | ts '%H:%M:%.S', I found that there was almost always exactly a 5 second delay between starting the process and the first output being printed. I grepped for 5000 and found this line. Changing this parameter eliminated the delay for me. The parameter seems to only be used in the awaitConnection method, which just loops until serverSocket.accept succeeds (with failure being a socket timeout). This makes me fairly confident that this change won't break anything. I also would expect any additional overhead of the extra thread wake ups to be negligible. --- .../main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 9052e1c..ae6b0fd 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -51,7 +51,7 @@ abstract class AbstractNodeJSEnv( } /** Retry-timeout to wait for the JS VM to connect */ - protected val acceptTimeout = 5000 + protected val acceptTimeout = 500 protected trait AbstractNodeRunner extends AbstractExtRunner with JSInitFiles { From 7c6e5fe9841512e4d0d432f8934271fdb6b4fa55 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 31 Mar 2018 09:20:11 +0200 Subject: [PATCH 093/133] Remove VirtualFileMaterializer It's usage was minimal and the API is too specific. With NIO's Files, the behavior can be easily replicated if necessary. As a side effect, this removes a usage of VirtualFile#name. --- .../jsenv/VirtualFileMaterializer.scala | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala deleted file mode 100644 index 604f2df..0000000 --- a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala +++ /dev/null @@ -1,91 +0,0 @@ -package org.scalajs.jsenv - -import scala.annotation.tailrec - -import org.scalajs.io._ - -import java.io.File - -/** A helper class to temporarily store virtual files to the filesystem. - * - * Can be used with tools that require real files. - * @param singleDir if true, forces files to be copied into - * [[cacheDir]]. Useful to setup include directories for - * example. - */ -final class VirtualFileMaterializer(singleDir: Boolean = false) { - import VirtualFileMaterializer._ - - val cacheDir = { - val dir = createTempDir() - dir.deleteOnExit() - dir - } - - /** Create a target file to write/copy to. Will also call - * deleteOnExit on the file. - */ - private def trgFile(name: String): File = { - val f = new File(cacheDir, name) - f.deleteOnExit() - f - } - - def materialize(vf: VirtualTextFile): File = vf match { - case vf: FileVirtualFile if !singleDir => - vf.file - case _ => - val trg = trgFile(vf.name) - IO.copyTo(vf, WritableFileVirtualTextFile(trg)) - trg - } - - def materialize(vf: VirtualBinaryFile): File = vf match { - case vf: FileVirtualFile if !singleDir => - vf.file - case _ => - val trg = trgFile(vf.name) - IO.copyTo(vf, WritableFileVirtualBinaryFile(trg)) - trg - } - - /** Removes the cache directory. Any operation on this - * VirtualFileMaterializer is invalid after [[close]] has been - * called. - */ - def close(): Unit = { - cacheDir.listFiles().foreach(_.delete) - cacheDir.delete() - } - - // scalastyle:off line.size.limit - /* Taken from Guava: - * https://github.com/google/guava/blob/1c285fc8d289c43b46aa55e7f90ec0359be5b69a/guava/src/com/google/common/io/Files.java#L413-L426 - */ - // scalastyle:on line.size.limit - private def createTempDir(): File = { - val baseDir = new File(System.getProperty("java.io.tmpdir")) - val baseName = System.currentTimeMillis() + "-" - - @tailrec - def loop(tries: Int): File = { - val tempDir = new File(baseDir, baseName + tries) - if (tempDir.mkdir()) - tempDir - else if (tries < TempDirAttempts) - loop(tries + 1) - else { - throw new IllegalStateException("Failed to create directory within " + - s"$TempDirAttempts attempts (tried ${baseName}0 to " + - s"${baseName}${TempDirAttempts - 1})") - } - } - - loop(0) - } - -} - -private object VirtualFileMaterializer { - private final val TempDirAttempts = 10000 -} From 050b9d7d99ecadf06ab77b5e24862b4c5b73ab1f Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 14 Apr 2018 07:25:33 +0200 Subject: [PATCH 094/133] Use VirtualBinaryFile in jsenv.Input --- .../org/scalajs/jsenv/test/RunTests.scala | 6 ++--- .../org/scalajs/jsenv/test/TestComKit.scala | 6 ++--- .../org/scalajs/jsenv/test/TestKit.scala | 6 ++--- .../main/scala/org/scalajs/jsenv/Input.scala | 2 +- .../main/scala/org/scalajs/jsenv/JSEnv.scala | 2 -- .../org/scalajs/jsenv/nodejs/ComSupport.scala | 10 ++++---- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 23 +++++++++---------- .../org/scalajs/jsenv/nodejs/Support.scala | 4 ++-- 8 files changed, 28 insertions(+), 31 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index ce584c6..69bf851 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -2,7 +2,7 @@ package org.scalajs.jsenv.test import scala.concurrent.Await -import org.scalajs.io.VirtualJSFile +import org.scalajs.io.VirtualBinaryFile import org.scalajs.jsenv._ import org.junit.Assert._ @@ -118,10 +118,10 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { @Test def noThrowOnBadFileTest: Unit = { - val badFile = new VirtualJSFile { + val badFile = new VirtualBinaryFile { def path: String = ??? def exists: Boolean = ??? - def content: String = ??? + def content: Array[Byte] = ??? } // `start` may not throw but must fail asynchronously diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala index 20eecd2..c33190b 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala @@ -2,7 +2,7 @@ package org.scalajs.jsenv.test import java.util.concurrent.TimeoutException -import org.scalajs.io.{VirtualJSFile, MemVirtualJSFile} +import org.scalajs.io.{VirtualBinaryFile, MemVirtualBinaryFile} import org.scalajs.jsenv._ @@ -14,11 +14,11 @@ import scala.concurrent.duration.Duration private[test] final class TestComKit(config: JSEnvSuiteConfig) { def start(code: String, runConfig: RunConfig): Run = { - val vf = new MemVirtualJSFile("testScript.js").withContent(code) + val vf = new MemVirtualBinaryFile("testScript.js").withStringUTF8(code) start(vf, runConfig) } - def start(vf: VirtualJSFile, runConfig: RunConfig): Run = { + def start(vf: VirtualBinaryFile, runConfig: RunConfig): Run = { val input = Input.ScriptsToLoad(List(vf)) new Run(input, runConfig) } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala index 766f861..934eebb 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala @@ -9,7 +9,7 @@ import scala.annotation.tailrec import scala.concurrent.Await import scala.concurrent.duration.Deadline -import org.scalajs.io.{VirtualJSFile, MemVirtualJSFile} +import org.scalajs.io.{VirtualBinaryFile, MemVirtualBinaryFile} import org.scalajs.jsenv._ @@ -20,11 +20,11 @@ private[test] final class TestKit(config: JSEnvSuiteConfig, withCom: Boolean) { assumeTrue("JSEnv needs com support", config.supportsCom || !withCom) def start(code: String, config: RunConfig): JSRun = { - val vf = new MemVirtualJSFile("testScript.js").withContent(code) + val vf = new MemVirtualBinaryFile("testScript.js").withStringUTF8(code) start(vf, config) } - def start(vf: VirtualJSFile, runConfig: RunConfig): JSRun = { + def start(vf: VirtualBinaryFile, runConfig: RunConfig): JSRun = { val input = Input.ScriptsToLoad(List(vf)) if (withCom) config.jsEnv.startWithCom(input, runConfig, _ => ()) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala index d760299..7ca7b83 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -24,7 +24,7 @@ abstract class Input private () object Input { /** All files are to be loaded as scripts into the global scope in the order given. */ - final case class ScriptsToLoad(scripts: List[VirtualJSFile]) extends Input + final case class ScriptsToLoad(scripts: List[VirtualBinaryFile]) extends Input } case class UnsupportedInputException(msg: String, cause: Throwable) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index 7d60af7..59956f9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -9,8 +9,6 @@ package org.scalajs.jsenv -import org.scalajs.io.VirtualJSFile - /** A JavaScript execution environment. * * This can run and interact with JavaScript code. diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala index aa03c85..34bb976 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala @@ -12,7 +12,7 @@ package org.scalajs.jsenv.nodejs import java.io._ import java.net._ -import org.scalajs.io.{VirtualJSFile, MemVirtualJSFile} +import org.scalajs.io.{VirtualBinaryFile, MemVirtualBinaryFile} import org.scalajs.jsenv._ import scala.collection.immutable @@ -197,11 +197,11 @@ object ComRun { * @param config Configuration for the run. * @param onMessage callback upon message reception. * @param startRun [[JSRun]] launcher. Gets passed a - * [[org.scalajs.io.VirtualJSFile VirtualJSFile]] that + * [[org.scalajs.io.VirtualBinaryFile VirtualBinaryFile]] that * initializes `scalaJSCom` on `global`. Requires Node.js libraries. */ def start(config: RunConfig, onMessage: String => Unit)( - startRun: VirtualJSFile => JSRun): JSComRun = { + startRun: VirtualBinaryFile => JSRun): JSComRun = { try { val serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address @@ -237,8 +237,8 @@ object ComRun { s.writeChars(msg) } - private def setupFile(port: Int): VirtualJSFile = { - new MemVirtualJSFile("comSetup.js").withContent( + private def setupFile(port: Int): VirtualBinaryFile = { + new MemVirtualBinaryFile("comSetup.js").withStringUTF8( s""" |(function() { | // The socket for communication diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index dfdf3e5..b7a5f1b 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -41,7 +41,7 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { } } - private def internalStart(files: List[VirtualJSFile], + private def internalStart(files: List[VirtualBinaryFile], runConfig: RunConfig): JSRun = { val command = config.executable :: config.args val externalConfig = ExternalJSRun.Config() @@ -50,7 +50,7 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { ExternalJSRun.start(command, externalConfig)(NodeJSEnv.write(files)) } - private def initFiles: List[VirtualJSFile] = { + private def initFiles: List[VirtualBinaryFile] = { val base = List(NodeJSEnv.runtimeEnv, Support.fixPercentConsole) if (config.sourceMap) NodeJSEnv.installSourceMap :: base @@ -70,12 +70,12 @@ object NodeJSEnv { private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) private lazy val installSourceMap = { - new MemVirtualJSFile("sourceMapSupport.js").withContent( + new MemVirtualBinaryFile("sourceMapSupport.js").withStringUTF8( "require('source-map-support').install();") } private lazy val runtimeEnv = { - new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( + new MemVirtualBinaryFile("scalaJSEnvInfo.js").withStringUTF8( """ |__ScalaJSEnv = { | exitFunction: function(status) { process.exit(status); } @@ -84,20 +84,19 @@ object NodeJSEnv { ) } - private def write(files: List[VirtualJSFile])(out: OutputStream): Unit = { - val writer = new BufferedWriter( - new OutputStreamWriter(out, StandardCharsets.UTF_8)) + private def write(files: List[VirtualBinaryFile])(out: OutputStream): Unit = { + val p = new PrintStream(out, false, "UTF8") try { files.foreach { - case file: FileVirtualJSFile => + case file: FileVirtualBinaryFile => val fname = file.file.getAbsolutePath - writer.write(s"""require("${escapeJS(fname)}");\n""") + p.println(s"""require("${escapeJS(fname)}");""") case f => - IO.writeTo(f, writer) - writer.write('\n') + IO.writeTo(f, p) + p.println() } } finally { - writer.close() + p.close() } } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala index f48bac1..5c847fa 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala @@ -12,8 +12,8 @@ package org.scalajs.jsenv.nodejs import org.scalajs.io._ object Support { - def fixPercentConsole: VirtualJSFile = { - new MemVirtualJSFile("nodeConsoleHack.js").withContent( + def fixPercentConsole: VirtualBinaryFile = { + new MemVirtualBinaryFile("nodeConsoleHack.js").withStringUTF8( """ |// Hack console log to duplicate double % signs |(function() { From 5c9a28cd04ef1b0c741af2267e8709c0a82b98b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 21 May 2018 15:26:53 +0200 Subject: [PATCH 095/133] Fix scala-js/scala-js#3351: Use Buffer.alloc and Buffer.from instead of new Buffer(). The latter was deprecated a long time ago, and has started printing noisy warnings to the console in Node.js v10. There is still one deprecation inside jszip, but there is nothing we can do about that one. --- .../scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index ae6b0fd..83a5b46 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -172,7 +172,7 @@ abstract class AbstractNodeJSEnv( | var recvCallback = null; | | // Buffers received data - | var inBuffer = new Buffer(0); + | var inBuffer = Buffer.alloc(0); | | function onData(data) { | inBuffer = Buffer.concat([inBuffer, data]); @@ -227,7 +227,7 @@ abstract class AbstractNodeJSEnv( | if (socket === null) throw new Error("Com not open"); | | var len = msg.length; - | var buf = new Buffer(4 + len * 2); + | var buf = Buffer.allocUnsafe(4 + len * 2); | buf.writeInt32BE(len, 0); | for (var i = 0; i < len; ++i) | buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2); From f48f51dba52bed9f75e5992832b994756b87ee06 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 16 Apr 2018 06:58:26 +0200 Subject: [PATCH 096/133] Remove most of the VirtualFile API that is now unused. --- .../org/scalajs/jsenv/test/RunTests.scala | 2 +- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 69bf851..53c298c 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -121,7 +121,7 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { val badFile = new VirtualBinaryFile { def path: String = ??? def exists: Boolean = ??? - def content: Array[Byte] = ??? + def inputStream: java.io.InputStream = ??? } // `start` may not throw but must fail asynchronously diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index b7a5f1b..3b523c7 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -11,7 +11,7 @@ package org.scalajs.jsenv.nodejs import java.nio.charset.StandardCharsets -import scala.collection.immutable +import scala.annotation.tailrec import scala.collection.JavaConverters._ import org.scalajs.jsenv._ @@ -92,7 +92,24 @@ object NodeJSEnv { val fname = file.file.getAbsolutePath p.println(s"""require("${escapeJS(fname)}");""") case f => - IO.writeTo(f, p) + val in = f.inputStream + try { + val buf = new Array[Byte](4096) + + @tailrec + def loop(): Unit = { + val read = in.read(buf) + if (read != -1) { + p.write(buf, 0, read) + loop() + } + } + + loop() + } finally { + in.close() + } + p.println() } } finally { From 4ccb51ade5eac26a86d4be1623b04b95dfa0fd09 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 29 Apr 2018 17:57:04 +0200 Subject: [PATCH 097/133] Make MemVirtualBinaryFile immutable As a consequence we decouple writable from readable files but provide a way to convert them. --- .../src/main/scala/org/scalajs/jsenv/test/TestComKit.scala | 2 +- .../src/main/scala/org/scalajs/jsenv/test/TestKit.scala | 2 +- .../src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala | 2 +- .../src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 4 ++-- .../src/main/scala/org/scalajs/jsenv/nodejs/Support.scala | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala index c33190b..7ca45b8 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala @@ -14,7 +14,7 @@ import scala.concurrent.duration.Duration private[test] final class TestComKit(config: JSEnvSuiteConfig) { def start(code: String, runConfig: RunConfig): Run = { - val vf = new MemVirtualBinaryFile("testScript.js").withStringUTF8(code) + val vf = MemVirtualBinaryFile.fromStringUTF8("testScript.js", code) start(vf, runConfig) } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala index 934eebb..0dfb473 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala @@ -20,7 +20,7 @@ private[test] final class TestKit(config: JSEnvSuiteConfig, withCom: Boolean) { assumeTrue("JSEnv needs com support", config.supportsCom || !withCom) def start(code: String, config: RunConfig): JSRun = { - val vf = new MemVirtualBinaryFile("testScript.js").withStringUTF8(code) + val vf = MemVirtualBinaryFile.fromStringUTF8("testScript.js", code) start(vf, config) } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala index 34bb976..d48f303 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala @@ -238,7 +238,7 @@ object ComRun { } private def setupFile(port: Int): VirtualBinaryFile = { - new MemVirtualBinaryFile("comSetup.js").withStringUTF8( + MemVirtualBinaryFile.fromStringUTF8("comSetup.js", s""" |(function() { | // The socket for communication diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 3b523c7..6071dae 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -70,12 +70,12 @@ object NodeJSEnv { private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) private lazy val installSourceMap = { - new MemVirtualBinaryFile("sourceMapSupport.js").withStringUTF8( + MemVirtualBinaryFile.fromStringUTF8("sourceMapSupport.js", "require('source-map-support').install();") } private lazy val runtimeEnv = { - new MemVirtualBinaryFile("scalaJSEnvInfo.js").withStringUTF8( + MemVirtualBinaryFile.fromStringUTF8("scalaJSEnvInfo.js", """ |__ScalaJSEnv = { | exitFunction: function(status) { process.exit(status); } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala index 5c847fa..05c3259 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala @@ -13,7 +13,7 @@ import org.scalajs.io._ object Support { def fixPercentConsole: VirtualBinaryFile = { - new MemVirtualBinaryFile("nodeConsoleHack.js").withStringUTF8( + MemVirtualBinaryFile.fromStringUTF8("nodeConsoleHack.js", """ |// Hack console log to duplicate double % signs |(function() { From dcd5ed3a8ad589b0cde6c0d890b6aece77c757c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 26 Jun 2018 14:02:55 +0200 Subject: [PATCH 098/133] Fix scala-js/scala-js#3393: Correctly report VM crashes as failed futures. The line in this commit was supposed to complete the future with a failure if either `vmComplete` or `pipeResult` was failed, giving precedence to `vmComplete`. However that line was bogus, and would complete with a success if `pipeResult` was a success, even if `vmComplete` was a failure. This caused the `TestAdapter` to erroneously interpret a VM failure as a normal exit without spawning the testing interface bridge. --- .../src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index 87fff28..d24e197 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -183,8 +183,11 @@ abstract class ExternalJSEnv( case e => ioThreadEx = e } - // Chain Try's the other way: We want VM failure first, then IO failure - promise.complete(pipeResult orElse vmComplete) + /* We want the VM failure to take precedence if there was one, + * otherwise the IO failure if there is one. We complete with a + * successful () only when both vmComplete and pipeResult were successful. + */ + promise.complete(vmComplete.flatMap(_ => pipeResult)) } } From ede2024afd4044eee8efac15db4d9657859fa548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 2 Jul 2018 13:50:54 +0200 Subject: [PATCH 099/133] Fix scala-js/scala-js#3400: Do not crash if source-map-support is unavailable by default. The `sourceMap` option of `NodeJSEnv.Config` is now a tri-state with `Disable`, `EnableIfAvailable` and `Enable`. Only the last one crashes if `source-map-support` is not available. The default is `EnableIfAvailable`, which should be the most appropriate option for many projects. --- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 6071dae..b59865f 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -23,6 +23,8 @@ import org.scalajs.logging._ import java.io._ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { + import NodeJSEnv._ + def this() = this(NodeJSEnv.Config()) val name: String = "Node.js" @@ -53,8 +55,11 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { private def initFiles: List[VirtualBinaryFile] = { val base = List(NodeJSEnv.runtimeEnv, Support.fixPercentConsole) - if (config.sourceMap) NodeJSEnv.installSourceMap :: base - else base + config.sourceMap match { + case SourceMap.Disable => base + case SourceMap.EnableIfAvailable => installSourceMapIfAvailable :: base + case SourceMap.Enable => installSourceMap :: base + } } private def inputFiles(input: Input) = input match { @@ -69,6 +74,17 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { object NodeJSEnv { private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) + private lazy val installSourceMapIfAvailable = { + MemVirtualBinaryFile.fromStringUTF8("sourceMapSupport.js", + """ + |try { + | require('source-map-support').install(); + |} catch (e) { + |}; + """.stripMargin + ) + } + private lazy val installSourceMap = { MemVirtualBinaryFile.fromStringUTF8("sourceMapSupport.js", "require('source-map-support').install();") @@ -117,18 +133,36 @@ object NodeJSEnv { } } + /** Requirements for source map support. */ + sealed abstract class SourceMap + + object SourceMap { + /** Disable source maps. */ + case object Disable extends SourceMap + + /** Enable source maps if `source-map-support` is available. */ + case object EnableIfAvailable extends SourceMap + + /** Always enable source maps. + * + * If `source-map-support` is not available, loading the .js code will + * fail. + */ + case object Enable extends SourceMap + } + final class Config private ( val executable: String, val args: List[String], val env: Map[String, String], - val sourceMap: Boolean + val sourceMap: SourceMap ) { private def this() = { this( executable = "node", args = Nil, env = Map.empty, - sourceMap = true + sourceMap = SourceMap.EnableIfAvailable ) } @@ -141,14 +175,23 @@ object NodeJSEnv { def withEnv(env: Map[String, String]): Config = copy(env = env) - def withSourceMap(sourceMap: Boolean): Config = + def withSourceMap(sourceMap: SourceMap): Config = copy(sourceMap = sourceMap) + /** Forces enabling (true) or disabling (false) source maps. + * + * `sourceMap = true` maps to [[SourceMap.Enable]]. `sourceMap = false` + * maps to [[SourceMap.Disable]]. [[SourceMap.EnableIfAvailable]] is never + * used by this method. + */ + def withSourceMap(sourceMap: Boolean): Config = + withSourceMap(if (sourceMap) SourceMap.Enable else SourceMap.Disable) + private def copy( executable: String = executable, args: List[String] = args, env: Map[String, String] = env, - sourceMap: Boolean = sourceMap + sourceMap: SourceMap = sourceMap ): Config = { new Config(executable, args, env, sourceMap) } @@ -162,7 +205,7 @@ object NodeJSEnv { * - `executable`: `"node"` * - `args`: `Nil` * - `env`: `Map.empty` - * - `sourceMap`: `true` + * - `sourceMap`: [[SourceMap.EnableIfAvailable]] */ def apply(): Config = new Config() } From 6401ae1bbc8831fc9c6d1a5be486c0a647c0310c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 13 Jul 2018 11:38:56 +0200 Subject: [PATCH 100/133] [no-master] Fix scala-js/scala-js#3408: Survive concurrent calls to `send` and `receive`. --- .../org/scalajs/jsenv/test/NodeJSTest.scala | 31 +++++++ .../jsenv/nodejs/AbstractNodeJSEnv.scala | 82 ++++++++++++++----- 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index f8fe024..4047286 100644 --- a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -76,4 +76,35 @@ class NodeJSTest extends TimeoutComTests { com.await(DefaultTimeout) } + @Test + def testConcurrentSendReceive_issue3408: Unit = { + for (_ <- 0 until 50) { + val com = comRunner(""" + scalajsCom.init(function(msg) { + scalajsCom.send("pong: " + msg); + }); + """) + + start(com) + + // Try very hard to send and receive at the same time + val lock = new AnyRef + val threadSend = new Thread { + override def run(): Unit = { + lock.synchronized(lock.wait()) + com.send("ping") + } + } + threadSend.start() + + Thread.sleep(200L) + lock.synchronized(lock.notifyAll()) + assertEquals(com.receive(), "pong: ping") + + threadSend.join() + com.close() + com.await(DefaultTimeout) + } + } + } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 83a5b46..8fde820 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -9,6 +9,8 @@ package org.scalajs.jsenv.nodejs +import scala.annotation.tailrec + import java.io.{Console => _, _} import java.net._ @@ -153,8 +155,17 @@ abstract class AbstractNodeJSEnv( protected trait NodeComJSRunner extends ComJSRunner with JSInitFiles { + /* Manipulation of the socket must be protected by synchronized, except + * calls to `close()`. + */ private[this] val serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address + + /* Those 3 fields are assigned *once* under synchronization in + * `awaitConnection()`. + * Read access must be protected by synchronized, or be done after a + * successful call to `awaitConnection()`. + */ private var comSocket: Socket = _ private var jvm2js: DataOutputStream = _ private var js2jvm: DataInputStream = _ @@ -280,34 +291,67 @@ abstract class AbstractNodeJSEnv( } def close(): Unit = { + /* Close the socket first. This will cause any existing and upcoming + * calls to `awaitConnection()` to be canceled and throw a + * `SocketException` (unless it has already successfully completed the + * `accept()` call). + */ serverSocket.close() - if (jvm2js != null) - jvm2js.close() - if (js2jvm != null) - js2jvm.close() - if (comSocket != null) - comSocket.close() + + /* Now wait for a possibly still-successful `awaitConnection()` to + * complete before closing the sockets. + */ + synchronized { + if (comSocket != null) { + jvm2js.close() + js2jvm.close() + comSocket.close() + } + } } /** Waits until the JS VM has established a connection or terminates * * @return true if the connection was established */ - private def awaitConnection(): Boolean = { - serverSocket.setSoTimeout(acceptTimeout) - while (comSocket == null && isRunning) { - try { - comSocket = serverSocket.accept() - jvm2js = new DataOutputStream( - new BufferedOutputStream(comSocket.getOutputStream())) - js2jvm = new DataInputStream( - new BufferedInputStream(comSocket.getInputStream())) - } catch { - case to: SocketTimeoutException => + private def awaitConnection(): Boolean = synchronized { + if (comSocket != null) { + true + } else { + @tailrec + def acceptLoop(): Option[Socket] = { + if (!isRunning) { + None + } else { + try { + Some(serverSocket.accept()) + } catch { + case to: SocketTimeoutException => acceptLoop() + } + } } - } - comSocket != null + serverSocket.setSoTimeout(acceptTimeout) + val optComSocket = acceptLoop() + + optComSocket.fold { + false + } { comSocket0 => + val jvm2js0 = new DataOutputStream( + new BufferedOutputStream(comSocket0.getOutputStream())) + val js2jvm0 = new DataInputStream( + new BufferedInputStream(comSocket0.getInputStream())) + + /* Assign those three fields together, without the possibility of + * an exception happening in the middle (see #3408). + */ + comSocket = comSocket0 + jvm2js = jvm2js0 + js2jvm = js2jvm0 + + true + } + } } override protected def finalize(): Unit = close() From 183409d69642edebd06c62254d21c287af123f2b Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 21 Jul 2018 12:00:28 +0200 Subject: [PATCH 101/133] Fix scala-js/scala-js#3410: Add supportsExit to JSEnvSuiteConfig. We also remove terminateVMJSCode. This is a source breaking change. However, leaving withTerminateVMJSCode would not relieve any burden of sub-projects as the Suite would fail anyways. --- .../scala/org/scalajs/jsenv/test/ComTests.scala | 6 +++--- .../org/scalajs/jsenv/test/JSEnvSuiteConfig.scala | 13 +++++++------ .../scala/org/scalajs/jsenv/test/RunTests.scala | 4 ++-- .../org/scalajs/jsenv/nodejs/NodeJSSuite.scala | 5 +---- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 5eeed6f..4e4e23a 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -38,10 +38,10 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { @Test def jsExitsOnMessageTest: Unit = { - assumeTrue(config.terminateVMJSCode.isDefined) + assumeTrue(config.supportsExit) - val run = kit.start(s""" - scalajsCom.init(function(msg) { ${config.terminateVMJSCode.get}; }); + val run = kit.start(""" + scalajsCom.init(function(msg) { __ScalaJSEnv.exitFunction(0); }); for (var i = 0; i < 10; ++i) scalajsCom.send("msg: " + i); """, RunConfig()) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala index 73323ad..9968a49 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala @@ -26,7 +26,7 @@ import scala.concurrent.duration._ */ final class JSEnvSuiteConfig private ( val jsEnv: JSEnv, - val terminateVMJSCode: Option[String], + val supportsExit: Boolean, val supportsCom: Boolean, val supportsTimeout: Boolean, val awaitTimeout: FiniteDuration, @@ -34,15 +34,15 @@ final class JSEnvSuiteConfig private ( ) { private def this(jsEnv: JSEnv) = this( jsEnv = jsEnv, - terminateVMJSCode = None, + supportsExit = true, supportsCom = true, supportsTimeout = true, awaitTimeout = 1.minute, description = jsEnv.name ) - def withTerminateVMJSCode(code: String): JSEnvSuiteConfig = - copy(terminateVMJSCode = Some(code)) + def withSupportsExit(supportsExit: Boolean): JSEnvSuiteConfig = + copy(supportsExit = supportsExit) def withSupportsCom(supportsCom: Boolean): JSEnvSuiteConfig = copy(supportsCom = supportsCom) @@ -56,12 +56,13 @@ final class JSEnvSuiteConfig private ( def withDescription(description: String): JSEnvSuiteConfig = copy(description = description) - private def copy(terminateVMJSCode: Option[String] = terminateVMJSCode, + private def copy( + supportsExit: Boolean = supportsExit, supportsCom: Boolean = supportsCom, supportsTimeout: Boolean = supportsTimeout, awaitTimeout: FiniteDuration = awaitTimeout, description: String = description) = { - new JSEnvSuiteConfig(jsEnv, terminateVMJSCode, supportsCom, + new JSEnvSuiteConfig(jsEnv, supportsExit, supportsCom, supportsTimeout, awaitTimeout, description) } } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 53c298c..1876c17 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -44,9 +44,9 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { @Test def jsExitsTest: Unit = { - assumeTrue(config.terminateVMJSCode.isDefined) + assumeTrue(config.supportsExit) - val run = kit.start(config.terminateVMJSCode.get, RunConfig()) + val run = kit.start("__ScalaJSEnv.exitFunction(0);", RunConfig()) try { Await.result(run.future, config.awaitTimeout) } finally { diff --git a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala index 6310447..34f0994 100644 --- a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala +++ b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala @@ -5,7 +5,4 @@ import org.scalajs.jsenv.test._ import org.junit.runner.RunWith @RunWith(classOf[JSEnvSuiteRunner]) -class NodeJSSuite extends JSEnvSuite( - JSEnvSuiteConfig(new NodeJSEnv) - .withTerminateVMJSCode("process.exit(0)") -) +class NodeJSSuite extends JSEnvSuite(JSEnvSuiteConfig(new NodeJSEnv)) From 40e8167b8b22cb3c3597c2c78305826134004445 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 21 Jul 2018 12:05:03 +0200 Subject: [PATCH 102/133] Fix scala-js/scala-js#3412: Throw an IOException in noThrowOnBadFileTest This makes sure no Scala version regard the failure as fatal (NotImplementedError was considered fatal in 2.10). Also it simply makes much more sense. --- .../src/main/scala/org/scalajs/jsenv/test/RunTests.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 1876c17..9c11131 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -118,10 +118,12 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { @Test def noThrowOnBadFileTest: Unit = { + def fail() = throw new java.io.IOException("exception for test") + val badFile = new VirtualBinaryFile { - def path: String = ??? - def exists: Boolean = ??? - def inputStream: java.io.InputStream = ??? + def path: String = fail() + def exists: Boolean = fail() + def inputStream: java.io.InputStream = fail() } // `start` may not throw but must fail asynchronously From 3a03cce261879dc3fa978c2c8b17f6cd4d3763ab Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 21 Jul 2018 12:20:52 +0200 Subject: [PATCH 103/133] Fix scala-js/scala-js#3411: Do not call onMessage immediately after init This change contains a fix to this in the Node.js Com support and a test in the test suite for this. --- .../scalajs/jsenv/test/TimeoutComTests.scala | 17 +++++++++++++++++ .../org/scalajs/jsenv/nodejs/ComSupport.scala | 19 +++++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index a707017..52307ac 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -86,4 +86,21 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { """, RunConfig()) run.closeAndWait() } + + @Test // #3411 + def noImmediateCallbackTest: Unit = { + val run = kit.start(s""" + setTimeout(function() { + var gotCalled = false; + scalajsCom.init(function(msg) { gotCalled = true; }); + if (gotCalled) throw "Buffered messages did not get deferred to the event loop"; + }, 100); + """, RunConfig()) + + try { + run.run.send("Hello World") + } finally { + run.closeAndWait() + } + } } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala index 5dadf32..7a5fe3f 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala @@ -251,7 +251,7 @@ object ComRun { | var inMessages = []; | | // The callback where received messages go - | var recvCallback = function(msg) { inMessages.push(msg); }; + | var onMessage = null; | | socket.on('data', function(data) { | inBuffer = Buffer.concat([inBuffer, data]); @@ -268,7 +268,8 @@ object ComRun { | | inBuffer = inBuffer.slice(byteLen); | - | recvCallback(res); + | if (inMessages !== null) inMessages.push(res); + | else onMessage(res); | } | }); | @@ -280,12 +281,14 @@ object ComRun { | socket.on('close', function() { process.exit(0); }); | | global.scalajsCom = { - | init: function(recvCB) { - | if (inMessages === null) throw new Error("Com already initialized"); - | for (var i = 0; i < inMessages.length; ++i) - | recvCB(inMessages[i]); - | inMessages = null; - | recvCallback = recvCB; + | init: function(onMsg) { + | if (onMessage !== null) throw new Error("Com already initialized"); + | onMessage = onMsg; + | process.nextTick(function() { + | for (var i = 0; i < inMessages.length; ++i) + | onMessage(inMessages[i]); + | inMessages = null; + | }); | }, | send: function(msg) { | var len = msg.length; From cf6a59b87457046d3a67e7cb5da048be35ff9fbc Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 22 Jul 2018 16:43:27 +0200 Subject: [PATCH 104/133] Backport exception tests from SeleniumJSEnv --- .../org/scalajs/jsenv/test/RunTests.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 9c11131..4029cff 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -28,6 +28,24 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { """.fails() } + @Test + def throwExceptionTest: Unit = { + """ + throw 1; + """.fails() + } + + @Test + def catchExceptionTest: Unit = { + """ + try { + throw "hello world"; + } catch (e) { + console.log(e); + } + """ hasOutput "hello world\n" + } + @Test // Failed in Phantom - #2053 def utf8Test: Unit = { """ From d3b0c92000207748bb4bd9ba7a59d95a06c5847b Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 22 Jul 2018 17:26:54 +0200 Subject: [PATCH 105/133] Fix a typo in a method name --- .../main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala index 9968a49..707912e 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala @@ -50,7 +50,7 @@ final class JSEnvSuiteConfig private ( def withSupportsTimeout(supportsTimeout: Boolean): JSEnvSuiteConfig = copy(supportsTimeout = supportsTimeout) - def withAwaitTimepout(awaitTimeout: FiniteDuration): JSEnvSuiteConfig = + def withAwaitTimeout(awaitTimeout: FiniteDuration): JSEnvSuiteConfig = copy(awaitTimeout = awaitTimeout) def withDescription(description: String): JSEnvSuiteConfig = From 4c7068f2d287ffe9eab0ab0419b9d13c75eab935 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 12 Aug 2018 20:07:33 +0200 Subject: [PATCH 106/133] Fix scala-js/scala-js#3420: Make timeout tests less restrictive Originally they were written to test our own implementation of `setTimeout` for Rhino. Now they are just a means to run things after returning to the event loop once, so we can reduce them significantly. --- .../scalajs/jsenv/test/TimeoutComTests.scala | 17 +-- .../scalajs/jsenv/test/TimeoutRunTests.scala | 104 ++---------------- 2 files changed, 18 insertions(+), 103 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 52307ac..6e546e6 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -19,6 +19,7 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { @Test def delayedInitTest: Unit = { + val deadline = 100.millis.fromNow val run = kit.start(s""" setTimeout(function() { scalajsCom.init(function(msg) { @@ -28,9 +29,6 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { """, RunConfig()) try { - // Deadline only starts now. Execution must happen asynchronously. - val deadline = 100.millis.fromNow - run.run.send("Hello World") assertEquals("Got: Hello World", run.waitNextMessage()) assertTrue("Execution took too little time", deadline.isOverdue()) @@ -49,7 +47,7 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { try { for (i <- 1 to 10) { - val deadline = 190.millis.fromNow // give some slack + val deadline = 200.millis.fromNow run.run.send(s"Hello World: $i") assertEquals(s"Got: Hello World: $i", run.waitNextMessage()) assertTrue("Execution took too little time", deadline.isOverdue()) @@ -61,14 +59,19 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { @Test def intervalSendTest: Unit = { + val deadline = 250.millis.fromNow + val run = kit.start(s""" scalajsCom.init(function(msg) {}); - var interval = setInterval(scalajsCom.send, 50, "Hello"); - setTimeout(clearInterval, 295, interval); + var sent = 0 + var interval = setInterval(function () { + scalajsCom.send("Hello"); + sent++; + if (sent >= 5) clearInterval(interval); + }, 50); """, RunConfig()) try { - val deadline = 245.millis.fromNow for (i <- 1 to 5) assertEquals("Hello", run.waitNextMessage()) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala index 3c36b2d..5a3e89e 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala @@ -38,110 +38,22 @@ private[test] class TimeoutRunTests(config: JSEnvSuiteConfig, withCom: Boolean) } - @Test - def clearTimeoutTest: Unit = { - - val deadline = 300.millis.fromNow - - """ - var c = setTimeout(function() { console.log("1"); }, 200); - setTimeout(function() { - console.log("2"); - clearTimeout(c); - }, 100); - setTimeout(function() { console.log("3"); }, 300); - setTimeout(function() { console.log("4"); }, 0); - """ hasOutput - """|4 - |2 - |3 - |""".stripMargin - - assertTrue("Execution took too little time", deadline.isOverdue()) - - } - - @Test // #2368 - def timeoutSingleArgTest: Unit = { - """ - setTimeout(function() { console.log("ok"); }); - """ hasOutput "ok\n" - } - - @Test - def timeoutArgTest: Unit = { - - val deadline = 300.millis.fromNow - - """ - setTimeout(function(a, b) { console.log("1" + a + b); }, 200, "foo", "bar"); - setTimeout(function() { console.log("2"); }, 100); - setTimeout(function(msg) { console.log(msg); }, 300, "Hello World"); - setTimeout(function() { console.log("4"); }, 0); - """ hasOutput - """|4 - |2 - |1foobar - |Hello World - |""".stripMargin - - assertTrue("Execution took too little time", deadline.isOverdue()) - - } - @Test def intervalTest: Unit = { + val deadline = 100.millis.fromNow - val deadline = 1.second.fromNow - + // We rely on the test kit to terminate the test after 5 iterations. """ - var i1 = setInterval(function() { console.log("each 2200"); }, 2200); - var i2 = setInterval(function() { console.log("each 3100"); }, 3100); - var i3 = setInterval(function() { console.log("each 1300"); }, 1300); - - setTimeout(function() { - clearInterval(i1); - clearInterval(i2); - clearInterval(i3); - }, 10000); + setInterval(function() { console.log("tick"); }, 20); """ hasOutput - """|each 1300 - |each 2200 - |each 1300 - |each 3100 - |each 1300 - |each 2200 - |each 1300 - |each 3100 - |each 1300 - |each 2200 - |each 1300 - |each 2200 - |each 1300 - |each 3100 + """|tick + |tick + |tick + |tick + |tick |""".stripMargin assertTrue("Execution took too little time", deadline.isOverdue()) } - - @Test - def intervalSelfClearTest: Unit = { - - val deadline = 100.millis.fromNow - - """ - var c = 0; - var i = setInterval(function() { - c++; - console.log(c.toString()); - if (c >= 10) - clearInterval(i); - }, 10); - """ hasOutput (1 to 10).map(_ + "\n").mkString - - assertTrue("Execution took too little time", deadline.isOverdue()) - - } - } From 6b3cc5fafc345770728fed96fc109b9ffd3d2e07 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 23 Sep 2018 19:32:11 +0200 Subject: [PATCH 107/133] Simplify largeMessage test Further, reduce some code duplication. --- .../org/scalajs/jsenv/test/ComTests.scala | 53 ++++--------------- 1 file changed, 11 insertions(+), 42 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 4e4e23a..9e9cc6f 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -79,57 +79,26 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { } } - @Test - def largeMessageTest: Unit = { - // 1KB data - val baseMsg = new String(Array.tabulate(512)(_.toChar)) - val baseLen = baseMsg.length - - // Max message size: 1KB * 2^(2*iters+1) = 1MB - val iters = 4 - - val run = kit.start(""" - scalajsCom.init(function(msg) { - scalajsCom.send(msg + msg); - }); - """, RunConfig()) + private def replyTest(msg: String) = { + val run = kit.start("scalajsCom.init(scalajsCom.send);", RunConfig()) try { - run.run.send(baseMsg) - - def resultFactor(iters: Int) = Math.pow(2, 2 * iters + 1).toInt - - for (i <- 0 until iters) { - val reply = run.waitNextMessage() - - val factor = resultFactor(i) - - assertEquals(baseLen * factor, reply.length) - - for (j <- 0 until factor) - assertEquals(baseMsg, reply.substring(j * baseLen, (j + 1) * baseLen)) - - run.run.send(reply + reply) - } - - val lastLen = run.waitNextMessage().length - assertEquals(baseLen * resultFactor(iters), lastLen) + run.run.send(msg) + assertEquals(msg, run.waitNextMessage()) } finally { run.closeAndWait() } } @Test - def highCharTest: Unit = { // #1536 - val run = kit.start("scalajsCom.init(scalajsCom.send);", RunConfig()) + def largeMessageTest: Unit = { + // 1MB data + replyTest(new String(Array.tabulate(1024 * 1024)(_.toChar))) + } - try { - val msg = "\uC421\u8F10\u0112\uFF32" - run.run.send(msg) - assertEquals(msg, run.waitNextMessage()) - } finally { - run.closeAndWait() - } + @Test + def highCharTest: Unit = { // #1536 + replyTest("\uC421\u8F10\u0112\uFF32") } @Test From c5631564cfa2f32b6a5ac93fa2c18ee38115c7f5 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 9 Oct 2018 10:09:34 +0200 Subject: [PATCH 108/133] Fix scala-js/scala-js#3457: Give timeout tests some slack --- .../org/scalajs/jsenv/test/TimeoutComTests.scala | 13 ++++++++++--- .../org/scalajs/jsenv/test/TimeoutRunTests.scala | 11 +++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 6e546e6..96d1a8f 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -17,9 +17,16 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { assumeTrue("JSEnv needs com support", config.supportsCom) } + /** Slack for timeout tests (see #3457) + * + * Empirically we can observe that timing can be off by ~0.1ms. By cutting + * 10ms slack, we definitely account for this without compromising the tests. + */ + private val slack = 10.millis + @Test def delayedInitTest: Unit = { - val deadline = 100.millis.fromNow + val deadline = (100.millis - slack).fromNow val run = kit.start(s""" setTimeout(function() { scalajsCom.init(function(msg) { @@ -47,7 +54,7 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { try { for (i <- 1 to 10) { - val deadline = 200.millis.fromNow + val deadline = (200.millis - slack).fromNow run.run.send(s"Hello World: $i") assertEquals(s"Got: Hello World: $i", run.waitNextMessage()) assertTrue("Execution took too little time", deadline.isOverdue()) @@ -59,7 +66,7 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { @Test def intervalSendTest: Unit = { - val deadline = 250.millis.fromNow + val deadline = (250.millis - slack).fromNow val run = kit.start(s""" scalajsCom.init(function(msg) {}); diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala index 5a3e89e..8832fb5 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala @@ -17,10 +17,17 @@ private[test] class TimeoutRunTests(config: JSEnvSuiteConfig, withCom: Boolean) assumeTrue("JSEnv needs timeout support", config.supportsTimeout) } + /** Slack for timeout tests (see #3457) + * + * Empirically we can observe that timing can be off by ~0.1ms. By cutting + * 10ms slack, we definitely account for this without compromising the tests. + */ + private val slack = 10.millis + @Test def basicTimeoutTest: Unit = { - val deadline = 300.millis.fromNow + val deadline = (300.millis - slack).fromNow """ setTimeout(function() { console.log("1"); }, 200); @@ -40,7 +47,7 @@ private[test] class TimeoutRunTests(config: JSEnvSuiteConfig, withCom: Boolean) @Test def intervalTest: Unit = { - val deadline = 100.millis.fromNow + val deadline = (100.millis - slack).fromNow // We rely on the test kit to terminate the test after 5 iterations. """ From e65a1d255bde0e6217b0ead8332239da61dc9af9 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 12 Aug 2018 20:08:42 +0200 Subject: [PATCH 109/133] Fix scala-js/scala-js#3414: Better test kit --- .../org/scalajs/jsenv/test/ComTests.scala | 78 +++-- .../org/scalajs/jsenv/test/RunTests.scala | 144 ++++----- .../org/scalajs/jsenv/test/TestComKit.scala | 65 ---- .../org/scalajs/jsenv/test/TestKit.scala | 156 ---------- .../scalajs/jsenv/test/TimeoutComTests.scala | 72 ++--- .../scalajs/jsenv/test/TimeoutRunTests.scala | 58 ++-- .../org/scalajs/jsenv/test/kit/ComRun.scala | 67 +++++ .../org/scalajs/jsenv/test/kit/IOReader.scala | 123 ++++++++ .../scalajs/jsenv/test/kit/MsgHandler.scala | 57 ++++ .../org/scalajs/jsenv/test/kit/Run.scala | 97 ++++++ .../org/scalajs/jsenv/test/kit/TestKit.scala | 146 +++++++++ .../org/scalajs/jsenv/test/kit/TestEnv.scala | 86 ++++++ .../scalajs/jsenv/test/kit/TestKitTest.scala | 284 ++++++++++++++++++ 13 files changed, 1043 insertions(+), 390 deletions(-) delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala delete mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala create mode 100644 js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala create mode 100644 js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala create mode 100644 js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 9e9cc6f..4e6f640 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -1,16 +1,13 @@ package org.scalajs.jsenv.test -import org.scalajs.jsenv._ - import org.junit.{Before, Test} -import org.junit.Assert._ import org.junit.Assume._ -import scala.concurrent.Await -import scala.concurrent.ExecutionContext.Implicits.global +import org.scalajs.jsenv._ +import org.scalajs.jsenv.test.kit.TestKit private[test] class ComTests(config: JSEnvSuiteConfig) { - private val kit = new TestComKit(config) + private val kit = new TestKit(config.jsEnv, config.awaitTimeout) @Before def before: Unit = { @@ -19,20 +16,21 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { @Test def basicTest: Unit = { - val run = kit.start(""" + kit.withComRun(""" scalajsCom.init(function(msg) { scalajsCom.send("received: " + msg); }); scalajsCom.send("Hello World"); - """, RunConfig()) + """) { run => - try { - assertEquals("Hello World", run.waitNextMessage()) + run.expectMsg("Hello World") for (i <- 0 to 10) { - run.run.send(i.toString) - assertEquals(s"received: $i", run.waitNextMessage()) + run + .send(i.toString) + .expectMsg(s"received: $i") } - } finally { - run.closeAndWait() + + run.expectNoMsgs() + .closeRun() } } @@ -40,21 +38,19 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { def jsExitsOnMessageTest: Unit = { assumeTrue(config.supportsExit) - val run = kit.start(""" + kit.withComRun(""" scalajsCom.init(function(msg) { __ScalaJSEnv.exitFunction(0); }); for (var i = 0; i < 10; ++i) scalajsCom.send("msg: " + i); - """, RunConfig()) + """) { run => - try { for (i <- 0 until 10) - assertEquals(s"msg: $i", run.waitNextMessage()) - - run.run.send("quit") + run.expectMsg(s"msg: $i") - Await.result(run.run.future, config.awaitTimeout) - } finally { - run.run.close() + run + .send("quit") + .expectNoMsgs() + .succeeds() } } @@ -62,31 +58,34 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { def multiEnvTest: Unit = { val n = 10 val runs = List.fill(5) { - kit.start(""" + kit.startWithCom(""" scalajsCom.init(function(msg) { scalajsCom.send("pong"); }); - """, RunConfig()) + """) } try { for (_ <- 0 until n) { - runs.foreach(_.run.send("ping")) - runs.foreach(r => assertEquals("pong", r.waitNextMessage())) + runs.foreach(_.send("ping")) + runs.foreach(_.expectMsg("pong")) + } + + runs.foreach { + _.expectNoMsgs() + .closeRun() } } finally { - runs.foreach(_.closeAndWait()) + runs.foreach(_.close()) } } private def replyTest(msg: String) = { - val run = kit.start("scalajsCom.init(scalajsCom.send);", RunConfig()) - - try { - run.run.send(msg) - assertEquals(msg, run.waitNextMessage()) - } finally { - run.closeAndWait() + kit.withComRun("scalajsCom.init(scalajsCom.send);") { + _.send(msg) + .expectMsg(msg) + .expectNoMsgs() + .closeRun() } } @@ -103,11 +102,10 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { @Test def noInitTest: Unit = { - val run = kit.start("", RunConfig()) - try { - run.run.send("Dummy") - } finally { - run.closeAndWait() + kit.withComRun("") { + _.send("Dummy") + .expectNoMsgs() + .closeRun() } } } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 4029cff..d27d3a5 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -1,74 +1,85 @@ package org.scalajs.jsenv.test -import scala.concurrent.Await +import org.junit.Assume._ +import org.junit.{Test, Before} import org.scalajs.io.VirtualBinaryFile import org.scalajs.jsenv._ - -import org.junit.Assert._ -import org.junit.Assume._ -import org.junit.{Test, Before} +import org.scalajs.jsenv.test.kit.{TestKit, Run} private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { - private val kit = new TestKit(config, withCom) - import kit._ + private val kit = new TestKit(config.jsEnv, config.awaitTimeout) + + private def withRun(input: Input)(body: Run => Unit) = { + if (withCom) kit.withComRun(input)(body) + else kit.withRun(input)(body) + } + + private def withRun(code: String, config: RunConfig = RunConfig())(body: Run => Unit) = { + if (withCom) kit.withComRun(code, config)(body) + else kit.withRun(code, config)(body) + } @Test def failureTest: Unit = { - """ - var a = {}; - a.foo(); - """.fails() + withRun(""" + var a = {}; + a.foo(); + """) { + _.fails() + } } @Test def syntaxErrorTest: Unit = { - """ - { - """.fails() + withRun("{") { + _.fails() + } } @Test def throwExceptionTest: Unit = { - """ - throw 1; - """.fails() + withRun("throw 1;") { + _.fails() + } } @Test def catchExceptionTest: Unit = { - """ - try { - throw "hello world"; - } catch (e) { - console.log(e); + withRun(""" + try { + throw "hello world"; + } catch (e) { + console.log(e); + } + """) { + _.expectOut("hello world\n") + .closeRun() } - """ hasOutput "hello world\n" } @Test // Failed in Phantom - #2053 def utf8Test: Unit = { - """ - console.log("\u1234"); - """ hasOutput "\u1234\n"; + withRun("""console.log("\u1234")""") { + _.expectOut("\u1234\n") + .closeRun() + } } @Test def allowScriptTags: Unit = { - """ - console.log(""); - """ hasOutput "\n"; + withRun("""console.log("");""") { + _.expectOut("\n") + .closeRun() + } } @Test def jsExitsTest: Unit = { assumeTrue(config.supportsExit) - val run = kit.start("__ScalaJSEnv.exitFunction(0);", RunConfig()) - try { - Await.result(run.future, config.awaitTimeout) - } finally { - run.close() + withRun("__ScalaJSEnv.exitFunction(0);") { + _.succeeds() } } @@ -92,24 +103,28 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { val result = strlists.map(_.mkString(" ") + "\n").mkString("") - codes.mkString("").hasOutput(result) + withRun(codes.mkString("")) { + _.expectOut(result) + .closeRun() + } } @Test // Node.js console.log hack didn't allow to log non-Strings - #561 def nonStringTest: Unit = { - """ - console.log(1); - console.log(undefined); - console.log(null); - console.log({}); - console.log([1,2]); - """ hasOutput - """|1 - |undefined - |null - |[object Object] - |1,2 - |""".stripMargin + withRun(""" + console.log(1); + console.log(undefined); + console.log(null); + console.log({}); + console.log([1,2]); + """) { + _.expectOut("1\n") + .expectOut("undefined\n") + .expectOut("null\n") + .expectOut("[object Object]\n") + .expectOut("1,2\n") + .closeRun() + } } @Test @@ -117,21 +132,21 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { /* This test also tests a failure mode where the ExternalJSRun is still * piping output while the client calls close. */ - val run = kit.start("", RunConfig()) - run.close() - awaitAfterClose(run) + withRun("") { + _.closeRun() + } } @Test def multiCloseAfterTerminatedTest: Unit = { - val run = kit.start("", RunConfig()) - run.close() - awaitAfterClose(run) - - // Should be noops (and not fail). - run.close() - run.close() - run.close() + withRun("") { run => + run.closeRun() + + // Should be noops (and not fail). + run.closeRun() + run.closeRun() + run.closeRun() + } } @Test @@ -145,13 +160,8 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { } // `start` may not throw but must fail asynchronously - val run = kit.start(badFile, RunConfig()) - try { - Await.ready(run.future, config.awaitTimeout) - assertTrue("Bad file should have made run fail", - run.future.value.get.isFailure) - } finally { - run.close() + withRun(Input.ScriptsToLoad(badFile :: Nil)) { + _.fails() } } @@ -169,6 +179,6 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { @Test(expected = classOf[IllegalArgumentException]) def ensureValidate: Unit = { val cfg = RunConfig().withEternallyUnsupportedOption(true) - kit.start("", cfg).close() + withRun("", cfg)(identity) } } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala deleted file mode 100644 index 7ca45b8..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestComKit.scala +++ /dev/null @@ -1,65 +0,0 @@ -package org.scalajs.jsenv.test - -import java.util.concurrent.TimeoutException - -import org.scalajs.io.{VirtualBinaryFile, MemVirtualBinaryFile} - -import org.scalajs.jsenv._ - -import org.junit.Assert.fail - -import scala.collection.immutable -import scala.concurrent.Await -import scala.concurrent.duration.Duration - -private[test] final class TestComKit(config: JSEnvSuiteConfig) { - def start(code: String, runConfig: RunConfig): Run = { - val vf = MemVirtualBinaryFile.fromStringUTF8("testScript.js", code) - start(vf, runConfig) - } - - def start(vf: VirtualBinaryFile, runConfig: RunConfig): Run = { - val input = Input.ScriptsToLoad(List(vf)) - new Run(input, runConfig) - } - - final class Run(input: Input, runConfig: RunConfig) { - val run: JSComRun = config.jsEnv.startWithCom(input, runConfig, onMessage _) - - private var received = immutable.Queue.empty[String] - - def waitNextMessage(): String = synchronized { - val deadline = config.awaitTimeout.fromNow - - while (received.isEmpty) { - val m = deadline.timeLeft.toMillis - if (m > 0) - wait(m) - else - throw new TimeoutException("Timed out waiting for next message") - } - - val (msg, newReceived) = received.dequeue - received = newReceived - msg - } - - def closeAndWait(): Unit = { - run.close() - - // Run must complete successfully. - Await.result(run.future, config.awaitTimeout) - - synchronized { - if (received.nonEmpty) { - fail(s"There were unhandled messages: $received") - } - } - } - - private def onMessage(msg: String) = synchronized { - received = received.enqueue(msg) - notifyAll() - } - } -} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala deleted file mode 100644 index 0dfb473..0000000 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TestKit.scala +++ /dev/null @@ -1,156 +0,0 @@ -package org.scalajs.jsenv.test - -import java.io._ -import java.nio.CharBuffer -import java.nio.charset.StandardCharsets -import java.util.concurrent.TimeoutException - -import scala.annotation.tailrec -import scala.concurrent.Await -import scala.concurrent.duration.Deadline - -import org.scalajs.io.{VirtualBinaryFile, MemVirtualBinaryFile} - -import org.scalajs.jsenv._ - -import org.junit.Assert._ -import org.junit.Assume._ - -private[test] final class TestKit(config: JSEnvSuiteConfig, withCom: Boolean) { - assumeTrue("JSEnv needs com support", config.supportsCom || !withCom) - - def start(code: String, config: RunConfig): JSRun = { - val vf = MemVirtualBinaryFile.fromStringUTF8("testScript.js", code) - start(vf, config) - } - - def start(vf: VirtualBinaryFile, runConfig: RunConfig): JSRun = { - val input = Input.ScriptsToLoad(List(vf)) - if (withCom) - config.jsEnv.startWithCom(input, runConfig, _ => ()) - else - config.jsEnv.start(input, runConfig) - } - - /** Await a run started with [[start]] after it got closed. - * - * This expects successful termination depending on [[withCom]]: we are - * allowed to expect successful termination with a com. - */ - def awaitAfterClose(run: JSRun): Unit = { - if (withCom) - Await.result(run.future, config.awaitTimeout) - else - Await.ready(run.future, config.awaitTimeout) - } - - implicit final class RunMatcher private[TestKit] (codeStr: String) { - def hasOutput(expectedOut: String): Unit = { - val comparator = new OutputComparator(expectedOut) - val config = comparator.configure(RunConfig()) - val run = start(codeStr, config) - - try { - comparator.compare() - } finally { - run.close() - } - - awaitAfterClose(run) - } - - def fails(): Unit = { - // We do not want to spam the console with error output, so we ignore it. - def ignoreStreams(out: Option[InputStream], err: Option[InputStream]) = { - out.foreach(_.close()) - err.foreach(_.close()) - } - - val runConfig = RunConfig() - .withOnOutputStream(ignoreStreams) - .withInheritOut(false) - .withInheritErr(false) - - val run = start(codeStr, runConfig) - try { - Await.ready(run.future, config.awaitTimeout) - assertTrue("Code snipped should fail", run.future.value.get.isFailure) - } finally { - run.close() - } - } - } - - private class OutputComparator(expectedOut: String) { - private val waiter = new StreamWaiter - - def configure(config: RunConfig): RunConfig = { - config - .withOnOutputStream(waiter.onOutputStream _) - .withInheritOut(false) - .withInheritErr(true) - } - - def compare(): Unit = { - val deadline = config.awaitTimeout.fromNow - val stream = waiter.waitForStream(deadline) - - /* When reading, we use a CharBuffer for easy index tracking. However, we - * back it by an array so we can easily read partial results. - */ - val in = new InputStreamReader(stream, StandardCharsets.UTF_8) - val arr = new Array[Char](expectedOut.length) - val buf = CharBuffer.wrap(arr) - while (buf.hasRemaining && tryRead(in, buf, deadline) != -1) { - val len = buf.position - assertEquals("Partial check", - expectedOut.substring(0, len), - new String(arr, 0, len)) - } - - buf.flip() - assertEquals(expectedOut, buf.toString) - } - } - - @tailrec - private final def tryRead(in: Reader, buf: CharBuffer, deadline: Deadline): Int = { - if (deadline.isOverdue) { - buf.flip() - throw new TimeoutException("Timed out out waiting for output. Got so far: " + buf.toString) - } - - if (in.ready()) { - in.read(buf) - } else { - Thread.sleep(50) - tryRead(in, buf, deadline) - } - } - - private class StreamWaiter { - private[this] var stream: InputStream = _ - - def waitForStream(deadline: Deadline): InputStream = synchronized { - while (stream == null) { - val m = deadline.timeLeft.toMillis - if (m > 0) - wait(m) - else - throw new TimeoutException("Timed out waiting for stdout") - } - - stream - } - - def onOutputStream(out: Option[InputStream], - err: Option[InputStream]): Unit = synchronized { - require(err.isEmpty, "Got error stream, did not request it.") - require(stream == null, "Got called twice") - - stream = out.getOrElse(new ByteArrayInputStream(new Array[Byte](0))) - notifyAll() - } - } -} - diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 96d1a8f..83cf460 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -1,15 +1,16 @@ package org.scalajs.jsenv.test -import org.scalajs.jsenv._ +import scala.concurrent.duration._ import org.junit.{Before, Test} import org.junit.Assert._ import org.junit.Assume._ -import scala.concurrent.duration._ +import org.scalajs.jsenv._ +import org.scalajs.jsenv.test.kit.TestKit private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { - private val kit = new TestComKit(config) + private val kit = new TestKit(config.jsEnv, config.awaitTimeout) @Before def before: Unit = { @@ -27,40 +28,43 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { @Test def delayedInitTest: Unit = { val deadline = (100.millis - slack).fromNow - val run = kit.start(s""" + kit.withComRun(""" setTimeout(function() { scalajsCom.init(function(msg) { scalajsCom.send("Got: " + msg); }); }, 100); - """, RunConfig()) + """) { run => + run.send("Hello World") + .expectMsg("Got: Hello World") - try { - run.run.send("Hello World") - assertEquals("Got: Hello World", run.waitNextMessage()) assertTrue("Execution took too little time", deadline.isOverdue()) - } finally { - run.closeAndWait() + + run + .expectNoMsgs() + .closeRun() } } @Test def delayedReplyTest: Unit = { - val run = kit.start(s""" + kit.withComRun(""" scalajsCom.init(function(msg) { setTimeout(scalajsCom.send, 200, "Got: " + msg); }); - """, RunConfig()) - - try { + """) { run => for (i <- 1 to 10) { val deadline = (200.millis - slack).fromNow - run.run.send(s"Hello World: $i") - assertEquals(s"Got: Hello World: $i", run.waitNextMessage()) + run + .send(s"Hello World: $i") + .expectMsg(s"Got: Hello World: $i") + assertTrue("Execution took too little time", deadline.isOverdue()) } - } finally { - run.closeAndWait() + + run + .expectNoMsgs() + .closeRun() } } @@ -68,7 +72,7 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { def intervalSendTest: Unit = { val deadline = (250.millis - slack).fromNow - val run = kit.start(s""" + kit.withComRun(""" scalajsCom.init(function(msg) {}); var sent = 0 var interval = setInterval(function () { @@ -76,41 +80,39 @@ private[test] class TimeoutComTests(config: JSEnvSuiteConfig) { sent++; if (sent >= 5) clearInterval(interval); }, 50); - """, RunConfig()) - - try { + """) { run => for (i <- 1 to 5) - assertEquals("Hello", run.waitNextMessage()) + run.expectMsg("Hello") assertTrue("Execution took too little time", deadline.isOverdue()) - } finally { - run.closeAndWait() + + run + .expectNoMsgs() + .closeRun() } } @Test def noMessageTest: Unit = { - val run = kit.start(s""" + kit.withComRun(s""" // Make sure JVM has already closed when we init setTimeout(scalajsCom.init, 1000, function(msg) {}); - """, RunConfig()) - run.closeAndWait() + """) { + _.closeRun() + } } @Test // #3411 def noImmediateCallbackTest: Unit = { - val run = kit.start(s""" + kit.withComRun(s""" setTimeout(function() { var gotCalled = false; scalajsCom.init(function(msg) { gotCalled = true; }); if (gotCalled) throw "Buffered messages did not get deferred to the event loop"; }, 100); - """, RunConfig()) - - try { - run.run.send("Hello World") - } finally { - run.closeAndWait() + """) { + _.send("Hello World") + .closeRun() } } } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala index 8832fb5..7b5e69c 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala @@ -1,16 +1,21 @@ package org.scalajs.jsenv.test -import org.scalajs.jsenv.JSEnv +import scala.concurrent.duration._ import org.junit.{Before, Test} import org.junit.Assert._ import org.junit.Assume._ -import scala.concurrent.duration._ +import org.scalajs.jsenv._ +import org.scalajs.jsenv.test.kit.{TestKit, Run} private[test] class TimeoutRunTests(config: JSEnvSuiteConfig, withCom: Boolean) { - private val kit = new TestKit(config, withCom) - import kit._ + private val kit = new TestKit(config.jsEnv, config.awaitTimeout) + + private def withRun(input: String)(body: Run => Unit) = { + if (withCom) kit.withComRun(input)(body) + else kit.withRun(input)(body) + } @Before def before: Unit = { @@ -29,38 +34,37 @@ private[test] class TimeoutRunTests(config: JSEnvSuiteConfig, withCom: Boolean) val deadline = (300.millis - slack).fromNow - """ - setTimeout(function() { console.log("1"); }, 200); - setTimeout(function() { console.log("2"); }, 100); - setTimeout(function() { console.log("3"); }, 300); - setTimeout(function() { console.log("4"); }, 0); - """ hasOutput - """|4 - |2 - |1 - |3 - |""".stripMargin + withRun(""" + setTimeout(function() { console.log("1"); }, 200); + setTimeout(function() { console.log("2"); }, 100); + setTimeout(function() { console.log("3"); }, 300); + setTimeout(function() { console.log("4"); }, 0); + """) { + _.expectOut("4\n") + .expectOut("2\n") + .expectOut("1\n") + .expectOut("3\n") + .closeRun() + } assertTrue("Execution took too little time", deadline.isOverdue()) - } @Test def intervalTest: Unit = { val deadline = (100.millis - slack).fromNow - // We rely on the test kit to terminate the test after 5 iterations. - """ - setInterval(function() { console.log("tick"); }, 20); - """ hasOutput - """|tick - |tick - |tick - |tick - |tick - |""".stripMargin + withRun(""" + setInterval(function() { console.log("tick"); }, 20); + """) { + _.expectOut("tick\n") + .expectOut("tick\n") + .expectOut("tick\n") + .expectOut("tick\n") + .expectOut("tick\n") + .closeRun() // Terminate after 5 iterations + } assertTrue("Execution took too little time", deadline.isOverdue()) - } } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala new file mode 100644 index 0000000..a29d8ee --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala @@ -0,0 +1,67 @@ +package org.scalajs.jsenv.test.kit + +import scala.concurrent.Await +import scala.concurrent.duration.FiniteDuration + +import org.junit.Assert._ + +import org.scalajs.jsenv._ + +/** A [[JSComRun]] instrumented for testing. + * + * Create an instance of this class through one of the overloads of + * `[[TestKit]].withComRun` or `[[TestKit]].startWithCom`. + */ +class ComRun private[kit] (run: JSComRun, out: IOReader, err: IOReader, + msgs: MsgHandler, timeout: FiniteDuration) + extends Run(run, out, err, timeout) { + private[this] var noMessages = false + + /** Calls [[JSComRun#send]] on the underlying run. */ + final def send(msg: String): this.type = { + run.send(msg) + this + } + + /** Waits until the given message is sent to the JVM. + * + * @throws java.lang.AssertionError if there is another message or the run terminates. + * @throws java.util.concurrent.TimeoutException if there is no message for too long. + */ + final def expectMsg(expected: String): this.type = { + require(!noMessages, "You may not call expectMsg after calling expectNoMsgs") + val actual = msgs.waitOnMessage(timeout.fromNow) + assertEquals("got bad message", expected, actual) + this + } + + /** Marks that no further messages are expected. + * + * This will make the methods [[closeRun]] / [[fails]] / [[succeeds]] fail if + * further messages are received. + * + * @note It is illegal to call [[expectMsg]] after [[expectNoMsgs]] has been + * called. + */ + final def expectNoMsgs(): this.type = { + noMessages = true + this + } + + override protected def postCloseRunWait(): Unit = { + try { + Await.result(run.future, timeout) + } catch { + case t: Throwable => + throw new AssertionError("closing a ComRun failed unexpectedly", t) + } + } + + override protected def postStopChecks(): Unit = { + super.postStopChecks() + if (noMessages) { + val rem = msgs.remainingMessages() + assertTrue(s"unhandled messages: $rem", rem.isEmpty) + } + } +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala new file mode 100644 index 0000000..1de46d9 --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala @@ -0,0 +1,123 @@ +package org.scalajs.jsenv.test.kit + +import scala.annotation.tailrec + +import scala.concurrent.Promise +import scala.concurrent.duration.Deadline + +import scala.util.Try + +import java.nio.ByteBuffer +import java.nio.channels.{Channels, ReadableByteChannel} + +import java.io.InputStream + +import java.util.concurrent._ + +private[kit] final class IOReader { + private val executor = Executors.newSingleThreadExecutor() + + private[this] var _closed = false + private[this] var _channel: ReadableByteChannel = _ + private[this] val run = Promise[Unit]() + + def read(len: Int, deadline: Deadline): ByteBuffer = { + val chan = try { + waitOnChannel(deadline) + } catch { + case t: TimeoutException => + throw new TimeoutException("timed out waiting on run to call onOutputStream") + } + + val task = executor.submit( + new Callable[ByteBuffer] { + def call(): ByteBuffer = readLoop(chan, ByteBuffer.allocate(len)) + } + ) + + try { + task.get(millisLeft(deadline), TimeUnit.MILLISECONDS) + } catch { + case e: ExecutionException => + throw e.getCause() + + case e: CancellationException => + throw new AssertionError("unexpected exception while running read task", e) + + case e: InterruptedException => + throw new AssertionError("unexpected exception while running read task", e) + + case e: TimeoutException => + task.cancel(true) + throw new TimeoutException("timed out reading from stream") + } + } + + def onInputStream(in: InputStream): Unit = synchronized { + require(_channel == null, "onInputStream called twice") + + if (_closed) { + in.close() + } else { + _channel = Channels.newChannel(in) + notifyAll() + } + } + + def onRunComplete(t: Try[Unit]): Unit = synchronized { + run.complete(t) + notifyAll() + } + + def close(): Unit = synchronized { + if (_channel != null) + _channel.close() + _closed = true + } + + private def waitOnChannel(deadline: Deadline) = synchronized { + while (_channel == null && !run.isCompleted) + wait(millisLeft(deadline)) + + if (_channel == null) { + throw new AssertionError( + "run completed and did not call onOutputStream", runFailureCause()) + } + + _channel + } + + private def runFailureCause() = { + require(run.isCompleted) + run.future.value.get.failed.getOrElse(null) + } + + @tailrec + private def readLoop(chan: ReadableByteChannel, buf: ByteBuffer): buf.type = { + if (chan.read(buf) == -1) { + // If we have reached the end of the stream, we wait for completion of the + // run so we can report a potential failure as a cause. + synchronized { + while (!run.isCompleted) + wait() + } + + throw new AssertionError("reached end of stream", runFailureCause()) + } else if (buf.hasRemaining()) { + readLoop(chan, buf) + } else { + buf.flip() + buf + } + } + + private def millisLeft(deadline: Deadline): Long = { + val millis = deadline.timeLeft.toMillis + + if (millis <= 0) { + throw new TimeoutException + } + + millis + } +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala new file mode 100644 index 0000000..0e742d9 --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala @@ -0,0 +1,57 @@ +package org.scalajs.jsenv.test.kit + +import scala.annotation.tailrec + +import scala.collection.immutable + +import scala.concurrent.Promise +import scala.concurrent.duration.Deadline + +import scala.util.Try + +import java.util.concurrent.TimeoutException + +private[kit] final class MsgHandler { + private[this] var msgs: immutable.Queue[String] = + immutable.Queue.empty[String] + private[this] val run = Promise[Unit] + + def onMessage(msg: String): Unit = synchronized { + if (run.isCompleted) { + throw new IllegalStateException( + "run already completed but still got a message") + } + + msgs = msgs.enqueue(msg) + notifyAll() + } + + def onRunComplete(t: Try[Unit]): Unit = synchronized { + run.complete(t) + notifyAll() + } + + @tailrec + def waitOnMessage(deadline: Deadline): String = synchronized { + if (msgs.nonEmpty) { + val (msg, newMsgs) = msgs.dequeue + msgs = newMsgs + msg + } else if (run.isCompleted) { + val cause = run.future.value.get.failed.getOrElse(null) + throw new AssertionError("no messages left and run has completed", cause) + } else { + val millis = deadline.timeLeft.toMillis + + if (millis <= 0) { + throw new TimeoutException("timed out waiting for next message") + } + + wait(millis) + waitOnMessage(deadline) + } + } + + /** @note may only be called once the run is completed. */ + def remainingMessages(): List[String] = synchronized(msgs.toList) +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala new file mode 100644 index 0000000..32984b3 --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala @@ -0,0 +1,97 @@ +package org.scalajs.jsenv.test.kit + +import scala.concurrent.Await +import scala.concurrent.duration.FiniteDuration + +import java.nio.charset.{CodingErrorAction, StandardCharsets} + +import org.junit.Assert._ + +import org.scalajs.jsenv._ + +/** A [[JSRun]] instrumented for testing. + * + * Create an instance of this class through one of the overloads of + * `[[TestKit]].withRun` or `[[TestKit]].start`. + */ +class Run private[kit] (run: JSRun, out: IOReader, err: IOReader, timeout: FiniteDuration) extends AutoCloseable { + private[this] val utf8decoder = { + StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + } + + /** Waits until the given string is output to stdout (in UTF8). + * + * @throws java.lang.AssertionError if there is some other output on stdout + * or the run terminates. + * @throws java.util.concurrent.TimeoutException if there is not enough output for too long. + */ + final def expectOut(v: String): this.type = expectIO(out, "stdout", v) + + /** Waits until the given string is output to stderr (in UTF8). + * + * @throws java.lang.AssertionError if there is some other output on stderr + * or the run terminates. + * @throws java.util.concurrent.TimeoutException if there is not enough output for too long. + */ + final def expectErr(v: String): this.type = expectIO(err, "stderr", v) + + /** Waits until the underlying [[JSRun]] terminates and asserts it failed. + * + * @throws java.lang.AssertionError if the [[JSRun]] succeeded. + * @throws java.util.concurrent.TimeoutException if the [[JSRun]] did not terminate in time. + */ + final def fails(): Unit = { + Await.ready(run.future, timeout) + assertTrue("run succeeded unexpectedly", run.future.value.get.isFailure) + postStopChecks() + } + + /** Waits until the underlying [[JSRun]] terminates and asserts it succeeded. + * + * @throws java.lang.AssertionError if the [[JSRun]] failed. + * @throws java.util.concurrent.TimeoutException if the [[JSRun]] did not terminate in time. + */ + final def succeeds(): Unit = { + try { + Await.result(run.future, timeout) + } catch { + case t: Throwable => + throw new AssertionError("run failed unexpectedly", t) + } + postStopChecks() + } + + /** Calls [[JSRun#close]] on the underlying [[JSRun]] and awaits termination. + * + * @throws java.lang.AssertionError if the [[JSRun]] behaves unexpectedly. + * @throws java.util.concurrent.TimeoutException if the [[JSRun]] does not terminate in time. + */ + final def closeRun(): Unit = { + run.close() + postCloseRunWait() + postStopChecks() + } + + /** Must be called to free all resources of this [[Run]]. Does not throw. */ + def close(): Unit = { + out.close() + err.close() + run.close() + } + + protected def postCloseRunWait(): Unit = Await.ready(run.future, timeout) + + protected def postStopChecks(): Unit = () + + private def expectIO(reader: IOReader, name: String, v: String): this.type = { + val len = v.getBytes(StandardCharsets.UTF_8).length + val buf = reader.read(len, timeout.fromNow) + val got = utf8decoder.decode(buf).toString + + assertEquals(s"bad output on $name", v, got) + + this + } +} diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala new file mode 100644 index 0000000..934c5dc --- /dev/null +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala @@ -0,0 +1,146 @@ +package org.scalajs.jsenv.test.kit + +import scala.concurrent.ExecutionContext +import scala.concurrent.duration.FiniteDuration + +import java.io.InputStream +import java.util.concurrent.Executors + +import org.scalajs.io.MemVirtualBinaryFile +import org.scalajs.jsenv._ + +/** TestKit is a utility class to simplify testing of [[JSEnv]]s. + * + * It is mostly used by Scala.js' provided [[JSEnv]] test suite but it may be + * used for additional tests specific to a particular [[JSEnv]]. + * + * @example + * {{{ + * import scala.concurrent.duration._ + * + * val kit = new TestKit(new MyEnv, 1.second) + * kit.withRun("""console.log("Hello World");""") { + * _.expectOut("Hello World\n") + * .closeRun() + * } + * }}} + * + * @note Methods in [[TestKit]] allow to take a string instead of an [[Input]]. + * The string is converted into an input form supported by the [[JSEnv]] to + * execute the code therein. + * + * @constructor Create a new [[TestKit]] for the given [[JSEnv]] and timeout. + * @param jsEnv The [[JSEnv]] to be tested. + * @param timeout Timeout for all `expect*` methods on [[Run]] / [[ComRun]]. + */ +final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { + import TestKit.codeToInput + + /** Starts a [[Run]] for testing. */ + def start(code: String): Run = + start(codeToInput(code)) + + /** Starts a [[Run]] for testing. */ + def start(input: Input): Run = + start(input, RunConfig()) + + /** Starts a [[Run]] for testing. */ + def start(code: String, config: RunConfig): Run = + start(codeToInput(code), config) + + /** Starts a [[Run]] for testing. */ + def start(input: Input, config: RunConfig): Run = { + val (run, out, err) = io(config)(jsEnv.start(input, _)) + new Run(run, out, err, timeout) + } + + /** Starts a [[ComRun]] for testing. */ + def startWithCom(code: String): ComRun = + startWithCom(codeToInput(code)) + + /** Starts a [[ComRun]] for testing. */ + def startWithCom(input: Input): ComRun = + startWithCom(input, RunConfig()) + + /** Starts a [[ComRun]] for testing. */ + def startWithCom(code: String, config: RunConfig): ComRun = + startWithCom(codeToInput(code), config) + + /** Starts a [[ComRun]] for testing. */ + def startWithCom(input: Input, config: RunConfig): ComRun = { + val msg = new MsgHandler + val (run, out, err) = io(config)(jsEnv.startWithCom(input, _, msg.onMessage _)) + run.future.onComplete(msg.onRunComplete _)(TestKit.completer) + + new ComRun(run, out, err, msg, timeout) + } + + /** Convenience method to start a [[Run]] and close it after usage. */ + def withRun[T](code: String)(body: Run => T): T = + withRun(codeToInput(code))(body) + + /** Convenience method to start a [[Run]] and close it after usage. */ + def withRun[T](input: Input)(body: Run => T): T = + withRun(input, RunConfig())(body) + + /** Convenience method to start a [[Run]] and close it after usage. */ + def withRun[T](code: String, config: RunConfig)(body: Run => T): T = + withRun(codeToInput(code), config)(body) + + /** Convenience method to start a [[Run]] and close it after usage. */ + def withRun[T](input: Input, config: RunConfig)(body: Run => T): T = { + val run = start(input, config) + try body(run) + finally run.close() + } + + /** Convenience method to start a [[ComRun]] and close it after usage. */ + def withComRun[T](code: String)(body: ComRun => T): T = withComRun(codeToInput(code))(body) + + /** Convenience method to start a [[ComRun]] and close it after usage. */ + def withComRun[T](input: Input)(body: ComRun => T): T = withComRun(input, RunConfig())(body) + + /** Convenience method to start a [[ComRun]] and close it after usage. */ + def withComRun[T](code: String, config: RunConfig)(body: ComRun => T): T = + withComRun(codeToInput(code), config)(body) + + /** Convenience method to start a [[ComRun]] and close it after usage. */ + def withComRun[T](input: Input, config: RunConfig)(body: ComRun => T): T = { + val run = startWithCom(input, config) + try body(run) + finally run.close() + } + + private def io[T <: JSRun](config: RunConfig)(start: RunConfig => T): (T, IOReader, IOReader) = { + val out = new IOReader + val err = new IOReader + + def onOutputStream(o: Option[InputStream], e: Option[InputStream]) = { + o.foreach(out.onInputStream _) + e.foreach(err.onInputStream _) + } + + val newConfig = config + .withOnOutputStream(onOutputStream) + .withInheritOut(false) + .withInheritErr(false) + + val run = start(newConfig) + + run.future.onComplete(out.onRunComplete _)(TestKit.completer) + run.future.onComplete(err.onRunComplete _)(TestKit.completer) + + (run, out, err) + } +} + +private object TestKit { + /** Execution context to run completion callbacks from runs under test. */ + private val completer = + ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor()) + + private def codeToInput(code: String): Input = { + val vf = MemVirtualBinaryFile.fromStringUTF8("testScript.js", code) + Input.ScriptsToLoad(List(vf)) + } +} diff --git a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala new file mode 100644 index 0000000..5d2ded3 --- /dev/null +++ b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala @@ -0,0 +1,86 @@ +package org.scalajs.jsenv.test.kit + +import scala.concurrent.Future + +import java.io._ +import java.nio.charset.StandardCharsets +import java.util.concurrent.atomic.AtomicInteger + +import org.scalajs.jsenv._ + +private[kit] class TestEnv private ( + result: Future[Unit], + outerr: Option[() => InputStream], + msgs: List[String]) extends JSEnv { + + // Interface for testing. + + def withSuccess(): TestEnv = copy(result = Future.unit) + + def withFailure(t: Throwable): TestEnv = copy(result = Future.failed(t)) + + def withHang(): TestEnv = copy(result = Future.never) + + def withOutErr(s: String): TestEnv = { + val bytes = s.getBytes(StandardCharsets.UTF_8) + copy(outerr = Some(() => new ByteArrayInputStream(bytes))) + } + + def withOutErrHang(): TestEnv = { + def hangStream() = new InputStream { + // read method that hangs indfinitely. + def read(): Int = synchronized { + while (true) wait() + throw new AssertionError("unreachable code") + } + } + + copy(outerr = Some(() => hangStream())) + } + + def withMsgs(msgs: String*): TestEnv = copy(msgs = msgs.toList) + + private def this() = this(Future.unit, None, Nil) + + private def copy( + result: Future[Unit] = result, + outerr: Option[() => InputStream] = outerr, + msgs: List[String] = msgs) = new TestEnv(result, outerr, msgs) + + // JSEnv interface + + val name: String = "TestEnv" + + def start(input: Input, config: RunConfig): JSRun = { + require(msgs.isEmpty) + callOnOutputStream(config) + new TestRun + } + + def startWithCom(input: Input, config: RunConfig, onMessage: String => Unit): JSComRun = { + callOnOutputStream(config) + msgs.foreach(onMessage) + new TestRun with JSComRun { + def send(msg: String): Unit = () + } + } + + private def callOnOutputStream(config: RunConfig): Unit = { + for { + factory <- outerr + onOutputStream <- config.onOutputStream + } { + def mkStream = Some(factory()) + onOutputStream(mkStream, mkStream) + } + } + + private class TestRun extends JSRun { + val future: Future[Unit] = result + def close(): Unit = () + } +} + +object TestEnv { + def apply(): TestEnv = new TestEnv() +} diff --git a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala new file mode 100644 index 0000000..be3662f --- /dev/null +++ b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala @@ -0,0 +1,284 @@ +package org.scalajs.jsenv.test.kit + +import scala.concurrent.duration._ + +import java.util.concurrent._ + +import org.junit.Assert._ +import org.junit.Test + +import org.scalajs.jsenv._ + +class TestKitTest { + import TestKit.codeToInput + import TestKitTest._ + + private def noHangTest(env: TestEnv, msg: String)(body: TestKit => Unit) = { + def test(e: JSEnv, cause: Throwable) = { + val timeout = 1.minute + val kit = new TestKit(e, timeout) + val deadline = timeout.fromNow + + expectAssert(msg, cause)(body(kit)) + + assertFalse("faster than timout", deadline.isOverdue) + } + + test(env.withSuccess(), null) + + val t = new Throwable + test(env.withFailure(t), t) + } + + @Test + def noHangExpectOutNoStream: Unit = { + noHangTest(TestEnv(), "run completed and did not call onOutputStream") { + _.withRun("") { + _.expectOut("a") + .closeRun() + } + } + } + + @Test + def noHangExpectErrNoStream: Unit = { + noHangTest(TestEnv(), "run completed and did not call onOutputStream") { + _.withRun("") { + _.expectErr("a") + .closeRun() + } + } + } + + @Test + def noHangExpectMsgOnFail: Unit = { + noHangTest(TestEnv(), "no messages left and run has completed") { + _.withComRun("") { + _.expectMsg("a") + .closeRun() + } + } + } + + @Test + def noHangExpectOutOnEOF: Unit = { + noHangTest(TestEnv().withOutErr(""), "reached end of stream") { + _.withRun("") { + _.expectOut("a") + .closeRun() + } + } + } + + @Test + def noHangExpectErrOnEOF: Unit = { + noHangTest(TestEnv().withOutErr(""), "reached end of stream") { + _.withRun("") { + _.expectErr("a") + .closeRun() + } + } + } + + @Test + def failOnUnexpectedSuccess: Unit = { + val kit = new TestKit(TestEnv().withSuccess(), 1.second) + expectAssert("run succeeded unexpectedly") { + kit.withRun("")(_.fails()) + } + } + + @Test + def failOnUnexpectedFailure: Unit = { + val t = new Throwable + val kit = new TestKit(TestEnv().withFailure(t), 1.second) + + expectAssert("run failed unexpectedly", t) { + kit.withRun("")(_.succeeds()) + } + } + + @Test + def ignoreRunFailOnClose: Unit = { + val kit = new TestKit(TestEnv().withFailure(new Throwable("dummy for test")), 1.second) + kit.withRun("")(_.closeRun()) + } + + @Test + def enforceSuccessComRunOnClose: Unit = { + val t = new Throwable + val kit = new TestKit(TestEnv().withFailure(t), 1.second) + + expectAssert("closing a ComRun failed unexpectedly", t) { + kit.withComRun("")(_.closeRun()) + } + } + + @Test + def failOnBadOut: Unit = { + val kit = new TestKit(TestEnv().withOutErr("a"), 1.second) + + expectAssert("bad output on stdout expected:<[b]> but was:<[a]>") { + kit.withRun("") { + _.expectOut("b") + .closeRun() + } + } + } + + @Test + def failOnBadErr: Unit = { + val kit = new TestKit(TestEnv().withOutErr("a"), 1.second) + + expectAssert("bad output on stderr expected:<[b]> but was:<[a]>") { + kit.withRun("") { + _.expectErr("b") + .closeRun() + } + } + } + + @Test + def ignoreExcessOut: Unit = { + val kit = new TestKit(TestEnv().withOutErr("abcdefg"), 1.second) + + kit.withRun("") { + _.expectOut("a") + .expectOut("b") + .closeRun() + } + } + + @Test + def ignoreExcessErr: Unit = { + val kit = new TestKit(TestEnv().withOutErr("abcdefg"), 1.second) + + kit.withRun("") { + _.expectErr("a") + .expectErr("b") + .closeRun() + } + } + + @Test + def failOnBadMsgErr: Unit = { + val kit = new TestKit(TestEnv().withMsgs("a"), 1.second) + + expectAssert("got bad message expected:<[b]> but was:<[a]>") { + kit.withComRun("") { + _.expectMsg("b") + .closeRun() + } + } + } + + @Test + def failOnExcessMsgs: Unit = { + val kit = new TestKit(TestEnv().withMsgs("a", "b", "c"), 1.second) + + expectAssert("unhandled messages: List(b, c)") { + kit.withComRun("") { + _.expectMsg("a") + .expectNoMsgs() + .closeRun() + } + } + } + + @Test + def ignoreExcessMsgs: Unit = { + val kit = new TestKit(TestEnv().withMsgs("a", "b", "c"), 1.second) + + kit.withComRun("") { + _.expectMsg("a") + .closeRun() + } + } + + @Test + def timeoutOutOnNoStream: Unit = { + val kit = new TestKit(TestEnv().withHang(), 10.millisecond) + + expectTimeout("timed out waiting on run to call onOutputStream") { + kit.withRun("") { + _.expectOut("b") + .closeRun() + } + } + } + + @Test + def timeoutErrOnNoStream: Unit = { + val kit = new TestKit(TestEnv().withHang(), 10.millisecond) + + expectTimeout("timed out waiting on run to call onOutputStream") { + kit.withRun("") { + _.expectErr("b") + .closeRun() + } + } + } + + @Test + def timeoutExpectMsg: Unit = { + val kit = new TestKit(TestEnv().withHang(), 10.millisecond) + + expectTimeout("timed out waiting for next message") { + kit.withComRun("") { + _.expectMsg("a") + .closeRun() + } + } + } + + @Test + def timeoutExpectOut: Unit = { + val kit = new TestKit(TestEnv().withOutErrHang(), 10.millisecond) + + expectTimeout("timed out reading from stream") { + kit.withRun("") { + _.expectOut("b") + .closeRun() + } + } + } + + @Test + def timeoutExpectErr: Unit = { + val kit = new TestKit(TestEnv().withOutErrHang(), 10.millisecond) + + expectTimeout("timed out reading from stream") { + kit.withRun("") { + _.expectErr("b") + .closeRun() + } + } + } +} + +private object TestKitTest { + def expectAssert(msg: String, cause: Throwable = null)(body: => Unit): Unit = { + val thrown = try { + body + false + } catch { + case e: AssertionError => + assertEquals("bad assertion error message", msg, e.getMessage()) + assertSame("should link cause", cause, e.getCause()) + true + } + + if (!thrown) + throw new AssertionError("expected AssertionError to be thrown") + } + + def expectTimeout(msg: String)(body: => Unit): Unit = { + try { + body + throw new AssertionError("expected TimeoutExeception to be thrown") + } catch { + case e: TimeoutException => + assertEquals("bad timeout error message", msg, e.getMessage()) + } + } +} From e057dfc5ff7af15220e47fc69feb799b4da98bd8 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 10 Oct 2018 16:18:45 +0200 Subject: [PATCH 110/133] Replace FrameworkDetector test with JSEnv test This test was originally designed to make sure FrameworkDetector doesn't fail with other `console.log` statements. However, since 22a8d7dc52669c537154f21519752a8b6680a174 FrameworkDetector does not exist anymore. We replace the specific test with a more generalized test that ensures that stdout and the scalajsCom do not interact for JSEnvs in general. --- .../org/scalajs/jsenv/test/ComTests.scala | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 4e6f640..b1a99a6 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -108,4 +108,26 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { .closeRun() } } + + @Test + def separateComStdoutTest: Unit = { + // Make sure that com and stdout do not interfere with each other. + kit.withComRun(""" + scalajsCom.init(function (msg) { + console.log("got: " + msg) + }); + console.log("a"); + scalajsCom.send("b"); + scalajsCom.send("c"); + console.log("d"); + """) { + _.expectOut("a\n") + .expectMsg("b") + .expectMsg("c") + .expectOut("d\n") + .send("foo") + .expectOut("got: foo\n") + .closeRun() + } + } } From fdad43213f594e0bbdd6b7e076d78d6686c238d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 11 Oct 2018 18:24:17 +0200 Subject: [PATCH 111/133] Fix scala-js/scala-js#3462: Apache License Version 2.0. This follows from the corresponding change of license in Scala at https://github.com/scala/scala/commit/2d9e6acce8c4246e6cba5a22ff9d965ec3bd117c We use sbt-header to enforce that the proper license header is present in all applicable files. Some files are excluded from the treatment because, as ports from other codebases, they have different licensing terms. --- .../org/scalajs/jsenv/test/AsyncTests.scala | 12 ++++++++++++ .../scalajs/jsenv/test/BasicJSEnvTests.scala | 12 ++++++++++++ .../org/scalajs/jsenv/test/ComTests.scala | 12 ++++++++++++ .../jsenv/test/CustomInitFilesTest.scala | 12 ++++++++++++ .../org/scalajs/jsenv/test/JSEnvTest.scala | 12 ++++++++++++ .../scalajs/jsenv/test/StoreJSConsole.scala | 12 ++++++++++++ .../org/scalajs/jsenv/test/StoreLogger.scala | 12 ++++++++++++ .../scalajs/jsenv/test/TimeoutComTests.scala | 12 ++++++++++++ .../org/scalajs/jsenv/test/TimeoutTests.scala | 12 ++++++++++++ .../org/scalajs/jsenv/test/NodeJSTest.scala | 12 ++++++++++++ .../test/NodeJSWithCustomInitFilesTest.scala | 12 ++++++++++++ .../scala/org/scalajs/jsenv/AsyncJSEnv.scala | 19 +++++++++++-------- .../org/scalajs/jsenv/AsyncJSRunner.scala | 12 ++++++++++++ .../scala/org/scalajs/jsenv/ComJSEnv.scala | 19 +++++++++++-------- .../scala/org/scalajs/jsenv/ComJSRunner.scala | 12 ++++++++++++ .../org/scalajs/jsenv/ConsoleJSConsole.scala | 19 +++++++++++-------- .../org/scalajs/jsenv/ExternalJSEnv.scala | 12 ++++++++++++ .../scala/org/scalajs/jsenv/JSConsole.scala | 19 +++++++++++-------- .../main/scala/org/scalajs/jsenv/JSEnv.scala | 19 +++++++++++-------- .../scala/org/scalajs/jsenv/JSInitFiles.scala | 12 ++++++++++++ .../scala/org/scalajs/jsenv/JSRunner.scala | 19 +++++++++++-------- .../scalajs/jsenv/LinkingUnitAsyncJSEnv.scala | 19 +++++++++++-------- .../scalajs/jsenv/LinkingUnitComJSEnv.scala | 19 +++++++++++-------- .../org/scalajs/jsenv/LinkingUnitJSEnv.scala | 19 +++++++++++-------- .../org/scalajs/jsenv/NullJSConsole.scala | 12 ++++++++++++ .../main/scala/org/scalajs/jsenv/Utils.scala | 19 +++++++++++-------- .../jsenv/VirtualFileMaterializer.scala | 12 ++++++++++++ .../jsenv/nodejs/AbstractNodeJSEnv.scala | 19 +++++++++++-------- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 19 +++++++++++-------- 29 files changed, 336 insertions(+), 96 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala index acb3830..076f90f 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/AsyncTests.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.jsenv._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala index 4404f18..46600b8 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/BasicJSEnvTests.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.junit.Test diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index cb6415d..45125ce 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.jsenv._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala index 9e2e5e0..be4ae13 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/CustomInitFilesTest.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.core.tools.io._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala index e488599..ca26440 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvTest.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.jsenv._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala index f4e60da..0de28ed 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreJSConsole.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.jsenv._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala index 5d97dc3..9738c93 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/StoreLogger.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.core.tools.logging._ diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index 57c41c0..1c2ce2f 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.junit.Test diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala index 2191ef7..cbe6dc9 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.junit.Test diff --git a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala index 4047286..73870a2 100644 --- a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala +++ b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSTest.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.jsenv.nodejs.NodeJSEnv diff --git a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala index 758a919..4ad2c68 100644 --- a/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala +++ b/js-envs-test-suite/src/test/scala/org/scalajs/jsenv/test/NodeJSWithCustomInitFilesTest.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv.test import org.scalajs.jsenv.nodejs.NodeJSEnv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala index c0ba9aa..08214d2 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala index 06619bd..14779fb 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/AsyncJSRunner.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv import scala.concurrent.{Future, Await} diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala index 52c6dcf..c8688dc 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala index 66d24d7..14954db 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ComJSRunner.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv import scala.concurrent.duration.Duration diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala index 6426350..4185fe0 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ConsoleJSConsole.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index d24e197..d23742e 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv import org.scalajs.core.tools.io._ diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala index c671ca1..b7f98b9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSConsole.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index aa6c7a5..52870da 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala index fad9e12..7ff78d1 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSInitFiles.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv import org.scalajs.core.tools.io.VirtualJSFile diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala index 7d2b5b6..5c9400e 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSRunner.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala index dc22192..e26ec3f 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitAsyncJSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala index dc39bbc..5761e0e 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitComJSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala index 6ba33e6..46020ee 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/LinkingUnitJSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala b/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala index 24b677d..4dbbcf5 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/NullJSConsole.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv object NullJSConsole extends JSConsole { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala index b2431b4..b2d5ff9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Utils.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js JS environments ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala index 1f25a92..de38666 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala @@ -1,3 +1,15 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package org.scalajs.jsenv import scala.annotation.tailrec diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 8fde820..39de10f 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv.nodejs diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 5721647..99e163e 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -1,11 +1,14 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ package org.scalajs.jsenv.nodejs From ccd71b645fb0e2f216faea3314efcf37826a4c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 24 Oct 2018 13:23:05 +0200 Subject: [PATCH 112/133] Fix scala-js/scala-js#3476: Specify that com messages must be valid UTF-16 strings. * Document it in the Scaladoc of `startWithCom` * Assert that all messages are valid in the `TestKit` * Fix `ComTests.largeMessageTest` not to use an invalid message --- .../org/scalajs/jsenv/test/ComTests.scala | 9 ++++-- .../org/scalajs/jsenv/test/kit/ComRun.scala | 30 +++++++++++++++++++ .../main/scala/org/scalajs/jsenv/JSEnv.scala | 5 ++++ .../main/scala/org/scalajs/jsenv/JSRuns.scala | 4 +++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 10103c9..2d32118 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -103,8 +103,13 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { @Test def largeMessageTest: Unit = { - // 1MB data - replyTest(new String(Array.tabulate(1024 * 1024)(_.toChar))) + /* 1MB data. + * (i & 0x7f) limits the input to the ASCII repertoire, which will use + * exactly 1 byte per Char in UTF-8. This restriction also ensures that we + * do not introduce surrogate characters and therefore no invalid UTF-16 + * strings. + */ + replyTest(new String(Array.tabulate(1024 * 1024)(i => (i & 0x7f).toChar))) } @Test diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala index 0a3770d..3a6d505 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala @@ -31,6 +31,7 @@ class ComRun private[kit] (run: JSComRun, out: IOReader, err: IOReader, /** Calls [[JSComRun#send]] on the underlying run. */ final def send(msg: String): this.type = { + requireValidMessage(msg) run.send(msg) this } @@ -41,12 +42,41 @@ class ComRun private[kit] (run: JSComRun, out: IOReader, err: IOReader, * @throws java.util.concurrent.TimeoutException if there is no message for too long. */ final def expectMsg(expected: String): this.type = { + requireValidMessage(expected) require(!noMessages, "You may not call expectMsg after calling expectNoMsgs") val actual = msgs.waitOnMessage(timeout.fromNow) assertEquals("got bad message", expected, actual) this } + private def requireValidMessage(msg: String): Unit = { + val len = msg.length + var i = 0 + while (i < len) { + val c = msg.charAt(i) + + def fail(lowOrHigh: String): Nothing = { + val msgDescription = + if (len > 128) s"Message (of length $len)" + else s"Message '$msg'" + throw new IllegalArgumentException( + s"$msgDescription is not a valid message because it contains an " + + s"unpaired $lowOrHigh surrogate 0x${c.toInt.toHexString} at index $i") + } + + if (Character.isSurrogate(c)) { + if (Character.isLowSurrogate(c)) + fail("low") + else if (i == len - 1 || !Character.isLowSurrogate(msg.charAt(i + 1))) + fail("high") + else + i += 2 + } else { + i += 1 + } + } + } + /** Marks that no further messages are expected. * * This will make the methods [[closeRun]] / [[fails]] / [[succeeds]] fail if diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index 2eb9ba9..091afeb 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -51,6 +51,11 @@ trait JSEnv { * scalajsCom.send("my message"); * }}} * + * All messages, sent in both directions, must be valid UTF-16 strings, + * i.e., they must not contain any unpaired surrogate character. The + * behavior of a communication channel is unspecified if this requirement is + * not met. + * * We describe the expected message delivery guarantees by denoting the * transmitter as `t` and the receiver as `r`. Both the JVM and the JS end * act once as a transmitter and once as a receiver. These two diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala index 7a7851e..57303d6 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala @@ -62,6 +62,10 @@ object JSRun { /** A [[JSRun]] that has a communication channel to the running JS code. */ trait JSComRun extends JSRun { /** Sends a message to the JS end. + * + * The `msg` must be a valid UTF-16 string, i.e., it must not contain any + * unpaired surrogate character. The behavior of the communication channel + * is unspecified if this requirement is not met. * * Async, nothrow. See [[JSEnv#startWithCom]] for expected message delivery * guarantees. From e2032ac55bbf92a4bf443da7d1553bf2c84ca679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 25 Oct 2018 14:49:50 +0200 Subject: [PATCH 113/133] [no-master] Fix scala-js/scala-js#2175: Add support for ECMAScript 2015 modules. This commit adds a third `ModuleKind` for ES modules, namely `ModuleKind.ESModule`. When emitting an ES module, `@JSImport`s and `@JSExportTopLevel`s straightforwardly map to ES `import` and `export` clauses, respectively. At the moment, imports are always implemented using a namespace import, then selecting fields inside the namespace. This is suboptimal because it can prevent advanced DCE across ES modules. Improving on this is left for future work. The Node.js-based environment is adapted to interpret files whose name ends with `.mjs` as ES modules rather than scripts. This aligns with how Node.js itself identifies ES modules as of version 10.x, although it is still experimental, so that could change in the future. For the 0.6.x branch, the `TestAdapter` assumes that it can force the `JSEnv` to interpret its launcher as an ES module by giving it the `.mjs` extension as well. This is wrong in general, but there does not seem to be a good way to deal with this issue. In 1.x, this will be a non-issue since the `TestAdapter` does not require any launcher. Although setting `scalaJSLinkerConfig.moduleKind` to `ModuleKind.ESModule` is enough for the Scala.js linker to emit a valid ES module, two additional settings are required to *run* or *test* using Node.js: artifactPath in (proj, Compile, fastOptJS) := (crossTarget in (proj, Compile)).value / "somename.mjs" jsEnv := { new org.scalajs.jsenv.NodeJSEnv( org.scalajs.jsenv.NODEJSEnv.Config() .withArguments(List("--experimental-modules")) ) } The first setting is necessary to give the `.mjs` extension to the file produced by Scala.js, which in turn is necessary for Node.js to accept it as an ES module. The second setting will be necessary until Node.js declares its support for ES module as non-experimental. ES modules are incompatible with a few features, which are all gone in Scala.js 1.x: * `EnvironmentInfo.exportsNamespace`: an ES module cannot read its own exports namespace, short of importing itself (which requires it to know its file name). The value of `exportsNamespace` is `undefined` in an ES module. * Generation of a `main` launcher script by the sbt plugin. Attempting to use one will throw an exception in the build. Moreover, the version of the Closure Compiler that we use does not support ES modules yet, so we deactivate GCC when emitting an ES module. At this point, the emission of ES modules can be considered stable, but the support in `NodeJSEnv` is experimental (since the support of ES modules in Node.js is itself experimental). Running the full test suite with ES modules requires Node.js 10.2.0 or later. It has been tested with v10.12.0. --- .../jsenv/nodejs/AbstractNodeJSEnv.scala | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 39de10f..a5f6303 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -134,12 +134,37 @@ abstract class AbstractNodeJSEnv( */ override protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = { + + def writeImport(file: File): Unit = { + val uri = file.toURI.toASCIIString + val importerFile = new MemVirtualJSFile("importer.js") + importerFile.content = { + s""" + |import("${escapeJS(uri)}").catch(e => { + | /* Make sure to fail the process, but give time to Node.js to + | * display a good stack trace before that. + | */ + | setTimeout(() => process.exit(1), 100); + | throw e; + |}); + """.stripMargin + } + val f = libCache.materialize(importerFile) + writer.write(s"""require("${escapeJS(f.getAbsolutePath)}");\n""") + } + file match { case file: FileVirtualJSFile => val fname = file.file.getAbsolutePath - writer.write(s"""require("${escapeJS(fname)}");\n""") + if (fname.endsWith(".mjs")) + writeImport(file.file) + else + writer.write(s"""require("${escapeJS(fname)}");\n""") case _ => - super.writeJSFile(file, writer) + if (file.path.endsWith(".mjs")) + writeImport(libCache.materialize(file)) + else + super.writeJSFile(file, writer) } } From 116600cfdc56a93aab39b0e6ac9cef76ea481099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 5 Dec 2018 23:38:40 +0100 Subject: [PATCH 114/133] Make `UnsupportedInputException` a normal (non-case) class. There seems to be no reason for it to be a case class. --- js-envs/src/main/scala/org/scalajs/jsenv/Input.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala index 26314eb..645ffe1 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -30,7 +30,7 @@ object Input { final case class ScriptsToLoad(scripts: List[VirtualBinaryFile]) extends Input } -case class UnsupportedInputException(msg: String, cause: Throwable) +class UnsupportedInputException(msg: String, cause: Throwable) extends IllegalArgumentException(msg, cause) { def this(msg: String) = this(msg, null) def this(input: Input) = this(s"Unsupported input: $input") From 802518a13de29d5502feed79550c09a8bedb6c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 16 Nov 2018 11:29:45 +0100 Subject: [PATCH 115/133] Add the distinction between Script and CommonJS module in Input. Scripts must be executed in the global scope, so that top-level declarations end up being available to other scripts, and also as members of the global object. Previously, `NodeJSEnv` would only load `Input.ScriptsToLoad` as true scripts if they were in-memory, by piping them to the standard input of Node.js. For actual files, it used `require`, which loads them as CommonJS modules, producing the wrong behavior for top-level declarations. We now introduce a separate `Input.CommonJSModulesToLoad`. For those, `NodeJSEnv` always uses `require`, even for in-memory ones. For `Input.ScriptsToLoad`, we use the `vm` module of Node.js and its method `runInThisContext` in order to actually run files as scripts, without losing source information. --- .../main/scala/org/scalajs/jsenv/Input.scala | 4 + .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 144 ++++++++++++++---- 2 files changed, 118 insertions(+), 30 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala index 645ffe1..6d6dc57 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -28,6 +28,10 @@ abstract class Input private () object Input { /** All files are to be loaded as scripts into the global scope in the order given. */ final case class ScriptsToLoad(scripts: List[VirtualBinaryFile]) extends Input + + /** All files are to be loaded as CommonJS modules, in the given order. */ + final case class CommonJSModulesToLoad(modules: List[VirtualBinaryFile]) + extends Input } class UnsupportedInputException(msg: String, cause: Throwable) diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 451a27f..8dc65b8 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -34,25 +34,36 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { def start(input: Input, runConfig: RunConfig): JSRun = { NodeJSEnv.validator.validate(runConfig) - internalStart(initFiles ++ inputFiles(input), runConfig) + validateInput(input) + internalStart(initFiles, input, runConfig) } def startWithCom(input: Input, runConfig: RunConfig, onMessage: String => Unit): JSComRun = { NodeJSEnv.validator.validate(runConfig) + validateInput(input) ComRun.start(runConfig, onMessage) { comLoader => - val files = initFiles ::: (comLoader :: inputFiles(input)) - internalStart(files, runConfig) + internalStart(initFiles :+ comLoader, input, runConfig) } } - private def internalStart(files: List[VirtualBinaryFile], + private def validateInput(input: Input): Unit = { + input match { + case _:Input.ScriptsToLoad | _:Input.CommonJSModulesToLoad => + // ok + case _ => + throw new UnsupportedInputException(input) + } + } + + private def internalStart(initFiles: List[VirtualBinaryFile], input: Input, runConfig: RunConfig): JSRun = { val command = config.executable :: config.args val externalConfig = ExternalJSRun.Config() .withEnv(env) .withRunConfig(runConfig) - ExternalJSRun.start(command, externalConfig)(NodeJSEnv.write(files)) + ExternalJSRun.start(command, externalConfig)( + NodeJSEnv.write(initFiles, input)) } private def initFiles: List[VirtualBinaryFile] = { @@ -103,39 +114,112 @@ object NodeJSEnv { ) } - private def write(files: List[VirtualBinaryFile])(out: OutputStream): Unit = { + private def write(initFiles: List[VirtualBinaryFile], input: Input)( + out: OutputStream): Unit = { val p = new PrintStream(out, false, "UTF8") try { - files.foreach { - case file: FileVirtualBinaryFile => - val fname = file.file.getAbsolutePath - p.println(s"""require("${escapeJS(fname)}");""") - case f => - val in = f.inputStream - try { - val buf = new Array[Byte](4096) - - @tailrec - def loop(): Unit = { - val read = in.read(buf) - if (read != -1) { - p.write(buf, 0, read) - loop() - } - } - - loop() - } finally { - in.close() - } - - p.println() + def writeRunScript(file: VirtualBinaryFile): Unit = { + file match { + case file: FileVirtualBinaryFile => + val pathJS = "\"" + escapeJS(file.file.getAbsolutePath) + "\"" + p.println(s""" + require('vm').runInThisContext( + require('fs').readFileSync($pathJS, { encoding: "utf-8" }), + { filename: $pathJS, displayErrors: true } + ); + """) + + case _ => + val code = readInputStreamToString(file.inputStream) + val codeJS = "\"" + escapeJS(code) + "\"" + val pathJS = "\"" + escapeJS(file.path) + "\"" + p.println(s""" + require('vm').runInThisContext( + $codeJS, + { filename: $pathJS, displayErrors: true } + ); + """) + } + } + + def writeRequire(file: VirtualBinaryFile): Unit = { + file match { + case file: FileVirtualBinaryFile => + p.println(s"""require("${escapeJS(file.file.getAbsolutePath)}")""") + + case _ => + val f = tmpFile(file.path, file.inputStream) + p.println(s"""require("${escapeJS(f.getAbsolutePath)}")""") + } + } + + for (initFile <- initFiles) + writeRunScript(initFile) + + input match { + case Input.ScriptsToLoad(scripts) => + for (script <- scripts) + writeRunScript(script) + + case Input.CommonJSModulesToLoad(modules) => + for (module <- modules) + writeRequire(module) } } finally { p.close() } } + private def readInputStreamToString(inputStream: InputStream): String = { + val baos = new java.io.ByteArrayOutputStream + val in = inputStream + try { + val buf = new Array[Byte](4096) + + @tailrec + def loop(): Unit = { + val read = in.read(buf) + if (read != -1) { + baos.write(buf, 0, read) + loop() + } + } + + loop() + } finally { + in.close() + } + new String(baos.toByteArray(), StandardCharsets.UTF_8) + } + + private def tmpFile(path: String, content: InputStream): File = { + import java.nio.file.{Files, StandardCopyOption} + + try { + val f = createTmpFile(path) + Files.copy(content, f.toPath(), StandardCopyOption.REPLACE_EXISTING) + f + } finally { + content.close() + } + } + + // tmpSuffixRE and createTmpFile copied from HTMLRunnerBuilder.scala + + private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r + + private def createTmpFile(path: String): File = { + /* - createTempFile requires a prefix of at least 3 chars + * - we use a safe part of the path as suffix so the extension stays (some + * browsers need that) and there is a clue which file it came from. + */ + val suffix = tmpSuffixRE.findFirstIn(path).orNull + + val f = File.createTempFile("tmp-", suffix) + f.deleteOnExit() + f + } + /** Requirements for source map support. */ sealed abstract class SourceMap From 2a81c37799ca5a2b85f229a442c9398f443813b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 25 Oct 2018 14:49:50 +0200 Subject: [PATCH 116/133] Fix scala-js/scala-js#2175: Add support for ECMAScript 2015 modules. This is a port of b744d12e0c6d8af74960e0ce5071c8e0011249a5. This commit adds a third `ModuleKind` for ES modules, namely `ModuleKind.ESModule`. When emitting an ES module, `@JSImport`s and `@JSExportTopLevel`s straightforwardly map to ES `import` and `export` clauses, respectively. At the moment, imports are always implemented using a namespace import, then selecting fields inside the namespace. This is suboptimal because it can prevent advanced DCE across ES modules. Improving on this is left for future work. A new `Input.ESModulesToLoad` instructs a JSEnv to load files as ES modules. The Node.js-based environment, however, will only *actually* interpret the files as ES modules if their name ends with `.mjs`. This happens because of how Node.js itself identifies ES modules as of version 10.x, although it is still experimental, so that could change in the future. `.js` files will be loaded as CommonJS modules instead. Although setting `scalaJSLinkerConfig.moduleKind` to `ModuleKind.ESModule` is enough for the Scala.js linker to emit a valid ES module, two additional settings are required to *run* or *test* using Node.js: artifactPath in (proj, Compile, fastOptJS) := (crossTarget in (proj, Compile)).value / "somename.mjs" jsEnv := { new org.scalajs.jsenv.NodeJSEnv( org.scalajs.jsenv.NODEJSEnv.Config() .withArguments(List("--experimental-modules")) ) } The first setting is necessary to give the `.mjs` extension to the file produced by Scala.js, which in turn is necessary for Node.js to accept it as an ES module. The second setting will be necessary until Node.js declares its support for ES module as non-experimental. The version of the Closure Compiler that we use does not support ES modules yet, so we deactivate GCC when emitting an ES module. At this point, the emission of ES modules can be considered stable, but the support in `NodeJSEnv` is experimental (since the support of ES modules in Node.js is itself experimental). Running the full test suite with ES modules requires Node.js 10.2.0 or later. It has been tested with v10.12.0. --- .../main/scala/org/scalajs/jsenv/Input.scala | 10 +++++ .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 42 ++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala index 6d6dc57..c2805bf 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -29,6 +29,16 @@ object Input { /** All files are to be loaded as scripts into the global scope in the order given. */ final case class ScriptsToLoad(scripts: List[VirtualBinaryFile]) extends Input + /** All files are to be loaded as ES modules, in the given order. + * + * Some environments may not be able to execute several ES modules in a + * deterministic order. If that is the case, they must reject an + * `ESModulesToLoad` input if the `modules` argument has more than one + * element. + */ + final case class ESModulesToLoad(modules: List[VirtualBinaryFile]) + extends Input + /** All files are to be loaded as CommonJS modules, in the given order. */ final case class CommonJSModulesToLoad(modules: List[VirtualBinaryFile]) extends Input diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 8dc65b8..0b1dcd7 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -49,7 +49,8 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { private def validateInput(input: Input): Unit = { input match { - case _:Input.ScriptsToLoad | _:Input.CommonJSModulesToLoad => + case _:Input.ScriptsToLoad | _:Input.ESModulesToLoad | + _:Input.CommonJSModulesToLoad => // ok case _ => throw new UnsupportedInputException(input) @@ -164,6 +165,34 @@ object NodeJSEnv { case Input.CommonJSModulesToLoad(modules) => for (module <- modules) writeRequire(module) + + case Input.ESModulesToLoad(modules) => + if (modules.nonEmpty) { + val uris = modules.map { + case module: FileVirtualBinaryFile => + module.file.toURI + case module => + tmpFile(module.path, module.inputStream).toURI + } + + val imports = uris.map { uri => + s"""import("${escapeJS(uri.toASCIIString)}")""" + } + val importChain = imports.reduceLeft { (prev, imprt) => + s"""$prev.then(_ => $imprt)""" + } + + val importerFileContent = { + s""" + |$importChain.catch(e => { + | console.error(e); + | process.exit(1); + |}); + """.stripMargin + } + val f = tmpFile("importer.js", importerFileContent) + p.println(s"""require("${escapeJS(f.getAbsolutePath)}");""") + } } } finally { p.close() @@ -192,6 +221,17 @@ object NodeJSEnv { new String(baos.toByteArray(), StandardCharsets.UTF_8) } + private def tmpFile(path: String, content: String): File = { + import java.nio.file.{Files, StandardOpenOption} + + val f = createTmpFile(path) + val contentList = new java.util.ArrayList[String]() + contentList.add(content) + Files.write(f.toPath(), contentList, StandardCharsets.UTF_8, + StandardOpenOption.TRUNCATE_EXISTING) + f + } + private def tmpFile(path: String, content: InputStream): File = { import java.nio.file.{Files, StandardCopyOption} From b4a627fe355986191cc4f07ba0616aff97d4ed14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 9 Dec 2018 13:01:58 +0100 Subject: [PATCH 117/133] Remove the hack to fix percentage signs in Node.js. Back when that hack was introduced in 1fc8d0526ba9b46049c0deefc3fe3fa93d3271b0, Node.js' `console.log` deduplicated consecutive `%` signs in its argument, even if it was a single argument. At the time we thought that was intentional, and we added our hack to have it behave in a more standard way for Scala.js. The deduplication with only 1 argument turned out to be a bug, recognized as such in https://github.com/nodejs/node/issues/3396, when it was accidentally fixed in Node.js 4. It is now the documented behavior, see https://nodejs.org/api/util.html#util_util_format_format_args which is referenced from https://nodejs.org/api/console.html#console_console_log_data_args. --- .../org/scalajs/jsenv/test/RunTests.scala | 43 +++-------------- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 2 +- .../org/scalajs/jsenv/nodejs/Support.scala | 47 ------------------- 3 files changed, 7 insertions(+), 85 deletions(-) delete mode 100644 nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index eed303f..67862a1 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -95,50 +95,19 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { } } - @Test // Node.js strips double percentage signs - #500 + // #500 Node.js used to strip double percentage signs even with only 1 argument + @Test def percentageTest: Unit = { - val counts = 1 to 15 - val argcs = 1 to 3 - val strings = counts.map("%" * _) - - val strlists = for { - count <- argcs - string <- strings - } yield List.fill(count)(string) - - val codes = for { - strlist <- strlists - } yield { - val args = strlist.map(s => s""""$s"""").mkString(", ") - s"console.log($args);\n" - } + val strings = (1 to 15).map("%" * _) + val code = strings.map(str => s"""console.log("$str");\n""").mkString("") + val result = strings.mkString("", "\n", "\n") - val result = strlists.map(_.mkString(" ") + "\n").mkString("") - - withRun(codes.mkString("")) { + withRun(code) { _.expectOut(result) .closeRun() } } - @Test // Node.js console.log hack didn't allow to log non-Strings - #561 - def nonStringTest: Unit = { - withRun(""" - console.log(1); - console.log(undefined); - console.log(null); - console.log({}); - console.log([1,2]); - """) { - _.expectOut("1\n") - .expectOut("undefined\n") - .expectOut("null\n") - .expectOut("[object Object]\n") - .expectOut("1,2\n") - .closeRun() - } - } - @Test def fastCloseTest: Unit = { /* This test also tests a failure mode where the ExternalJSRun is still diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 0b1dcd7..c869504 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -68,7 +68,7 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { } private def initFiles: List[VirtualBinaryFile] = { - val base = List(NodeJSEnv.runtimeEnv, Support.fixPercentConsole) + val base = List(NodeJSEnv.runtimeEnv) config.sourceMap match { case SourceMap.Disable => base diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala deleted file mode 100644 index 305379a..0000000 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/Support.scala +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Scala.js (https://www.scala-js.org/) - * - * Copyright EPFL. - * - * Licensed under Apache License 2.0 - * (https://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -package org.scalajs.jsenv.nodejs - -import org.scalajs.io._ - -object Support { - def fixPercentConsole: VirtualBinaryFile = { - MemVirtualBinaryFile.fromStringUTF8("nodeConsoleHack.js", - """ - |// Hack console log to duplicate double % signs - |(function() { - | function startsWithAnyOf(s, prefixes) { - | for (var i = 0; i < prefixes.length; i++) { - | // ES5 does not have .startsWith() on strings - | if (s.substring(0, prefixes[i].length) === prefixes[i]) - | return true; - | } - | return false; - | } - | var oldLog = console.log; - | var newLog = function() { - | var args = arguments; - | if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { - | var argStr = args[0].toString(); - | if (args.length > 1) - | argStr = argStr.replace(/%/g, "%%"); - | args[0] = argStr; - | } - | oldLog.apply(console, args); - | }; - | console.log = newLog; - |})(); - """.stripMargin - ) - } -} From a46ed82981dd3725d6966c59eb837110ea198d74 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 5 Dec 2018 21:16:42 +0100 Subject: [PATCH 118/133] Remove System.exit / __ScalaJSEnv.exitFunction This has been added way back in the days (scala-js/scala-js#958) as a poor man's way to support asynchronous testing. Since then, testing has substantially matured. Notably a JSEnv is not required anymore to be able to terminate automatically. Therefore, there is no need anymore to support exiting in a platform agnostic manner. Or if there is a need or such a thing, it should be put in an external library with JS-style dynamic platform detection. Unfortunately this makes the JSEnv test suite slightly more complicated again (and basically reverts 6746cf3a20d2497bbc6c520666a47f7ec1cd28dd). --- .../org/scalajs/jsenv/test/ComTests.scala | 9 ++++---- .../scalajs/jsenv/test/JSEnvSuiteConfig.scala | 16 +++++++------- .../org/scalajs/jsenv/test/RunTests.scala | 7 +++--- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 22 ++++--------------- .../scalajs/jsenv/nodejs/NodeJSSuite.scala | 5 ++++- 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 2d32118..1aebf7e 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -12,7 +12,7 @@ package org.scalajs.jsenv.test -import org.junit.{Before, Test} +import org.junit.{Before, Test, AssumptionViolatedException} import org.junit.Assume._ import org.scalajs.jsenv._ @@ -48,10 +48,11 @@ private[test] class ComTests(config: JSEnvSuiteConfig) { @Test def jsExitsOnMessageTest: Unit = { - assumeTrue(config.supportsExit) + val exitStat = config.exitJSStatement.getOrElse( + throw new AssumptionViolatedException("JSEnv needs exitJSStatement")) - kit.withComRun(""" - scalajsCom.init(function(msg) { __ScalaJSEnv.exitFunction(0); }); + kit.withComRun(s""" + scalajsCom.init(function(msg) { $exitStat }); for (var i = 0; i < 10; ++i) scalajsCom.send("msg: " + i); """) { run => diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala index 15f9b93..de5ba11 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala @@ -38,30 +38,30 @@ import scala.concurrent.duration._ */ final class JSEnvSuiteConfig private ( val jsEnv: JSEnv, - val supportsExit: Boolean, val supportsCom: Boolean, val supportsTimeout: Boolean, + val exitJSStatement: Option[String], val awaitTimeout: FiniteDuration, val description: String ) { private def this(jsEnv: JSEnv) = this( jsEnv = jsEnv, - supportsExit = true, supportsCom = true, supportsTimeout = true, + exitJSStatement = None, awaitTimeout = 1.minute, description = jsEnv.name ) - def withSupportsExit(supportsExit: Boolean): JSEnvSuiteConfig = - copy(supportsExit = supportsExit) - def withSupportsCom(supportsCom: Boolean): JSEnvSuiteConfig = copy(supportsCom = supportsCom) def withSupportsTimeout(supportsTimeout: Boolean): JSEnvSuiteConfig = copy(supportsTimeout = supportsTimeout) + def withExitJSStatement(code: String): JSEnvSuiteConfig = + copy(exitJSStatement = Some(code)) + def withAwaitTimeout(awaitTimeout: FiniteDuration): JSEnvSuiteConfig = copy(awaitTimeout = awaitTimeout) @@ -69,13 +69,13 @@ final class JSEnvSuiteConfig private ( copy(description = description) private def copy( - supportsExit: Boolean = supportsExit, supportsCom: Boolean = supportsCom, supportsTimeout: Boolean = supportsTimeout, + exitJSStatement: Option[String] = exitJSStatement, awaitTimeout: FiniteDuration = awaitTimeout, description: String = description) = { - new JSEnvSuiteConfig(jsEnv, supportsExit, supportsCom, - supportsTimeout, awaitTimeout, description) + new JSEnvSuiteConfig(jsEnv, supportsCom, supportsTimeout, + exitJSStatement, awaitTimeout, description) } } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 67862a1..e64f8ec 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -13,7 +13,7 @@ package org.scalajs.jsenv.test import org.junit.Assume._ -import org.junit.{Test, Before} +import org.junit.{Test, Before, AssumptionViolatedException} import org.scalajs.io.VirtualBinaryFile import org.scalajs.jsenv._ @@ -88,9 +88,10 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { @Test def jsExitsTest: Unit = { - assumeTrue(config.supportsExit) + val exitStat = config.exitJSStatement.getOrElse( + throw new AssumptionViolatedException("JSEnv needs exitJSStatement")) - withRun("__ScalaJSEnv.exitFunction(0);") { + withRun(exitStat) { _.succeeds() } } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index c869504..77bf442 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -67,14 +67,10 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { NodeJSEnv.write(initFiles, input)) } - private def initFiles: List[VirtualBinaryFile] = { - val base = List(NodeJSEnv.runtimeEnv) - - config.sourceMap match { - case SourceMap.Disable => base - case SourceMap.EnableIfAvailable => installSourceMapIfAvailable :: base - case SourceMap.Enable => installSourceMap :: base - } + private def initFiles: List[VirtualBinaryFile] = config.sourceMap match { + case SourceMap.Disable => Nil + case SourceMap.EnableIfAvailable => installSourceMapIfAvailable :: Nil + case SourceMap.Enable => installSourceMap :: Nil } private def inputFiles(input: Input) = input match { @@ -105,16 +101,6 @@ object NodeJSEnv { "require('source-map-support').install();") } - private lazy val runtimeEnv = { - MemVirtualBinaryFile.fromStringUTF8("scalaJSEnvInfo.js", - """ - |__ScalaJSEnv = { - | exitFunction: function(status) { process.exit(status); } - |}; - """.stripMargin - ) - } - private def write(initFiles: List[VirtualBinaryFile], input: Input)( out: OutputStream): Unit = { val p = new PrintStream(out, false, "UTF8") diff --git a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala index ea1aa2f..a3f1bf8 100644 --- a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala +++ b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala @@ -17,4 +17,7 @@ import org.scalajs.jsenv.test._ import org.junit.runner.RunWith @RunWith(classOf[JSEnvSuiteRunner]) -class NodeJSSuite extends JSEnvSuite(JSEnvSuiteConfig(new NodeJSEnv)) +class NodeJSSuite extends JSEnvSuite( + JSEnvSuiteConfig(new NodeJSEnv) + .withExitJSStatement("process.exit(0);") +) From 0d65d88e5cea765a06ba9aa55947f6bd3645d73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 25 Jan 2019 22:10:03 +0100 Subject: [PATCH 119/133] [no-master] Fix scala-js/scala-js#3528: Use `console.error(e)` to report ES module errors. Backported from master. --- .../scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index a5f6303..e1e63e9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -141,11 +141,8 @@ abstract class AbstractNodeJSEnv( importerFile.content = { s""" |import("${escapeJS(uri)}").catch(e => { - | /* Make sure to fail the process, but give time to Node.js to - | * display a good stack trace before that. - | */ - | setTimeout(() => process.exit(1), 100); - | throw e; + | console.error(e); + | process.exit(1); |}); """.stripMargin } From 5938bcb522b29195852e765d7df5370aecb50fbc Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 13 Apr 2019 18:08:46 +0200 Subject: [PATCH 120/133] Make JSEnvSuite compile warning free on Java 9 Class#newInstance is deprecated. --- .../src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala index 22320e1..17efd0a 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala @@ -43,7 +43,8 @@ final class JSEnvSuiteRunner(root: Class[_], config: JSEnvSuiteConfig) extends Suite(root, JSEnvSuiteRunner.getRunners(config).asJava) { /** Constructor for reflective instantiation via `@RunWith`. */ - def this(suite: Class[_ <: JSEnvSuite]) = this(suite, suite.newInstance().config) + def this(suite: Class[_ <: JSEnvSuite]) = + this(suite, suite.getDeclaredConstructor().newInstance().config) /** Constructor for instantiation in a user defined Runner. */ def this(config: JSEnvSuiteConfig) = this(null, config) From 628443b998e70deaa898100c4857ab1fd340ca82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 14 May 2019 11:40:04 +0200 Subject: [PATCH 121/133] Split `escapeJS` in `linker` and `js-envs`. And specialize them to the particular needs of each subproject: * `java.lang.StringBuilder` versus `java.io.Writer` instead of the generic `java.lang.Appendable`. * With or without tracking of how many characters are written out. Also review and specialize the one in `ir`. --- .../scala/org/scalajs/jsenv/JSUtils.scala | 92 +++++++++++++++++++ .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 2 +- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala new file mode 100644 index 0000000..a9f933f --- /dev/null +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala @@ -0,0 +1,92 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.jsenv + +object JSUtils { + + def escapeJS(str: String): String = { + // scalastyle:off return + val end = str.length + var i = 0 + while (i != end) { + val c = str.charAt(i) + if (c >= 32 && c <= 126 && c != '\\' && c != '"') + i += 1 + else + return createEscapeJSString(str) + } + str + // scalastyle:on return + } + + private def createEscapeJSString(str: String): String = { + val sb = new java.lang.StringBuilder(2 * str.length) + printEscapeJS(str, sb) + sb.toString + } + + /* !!! BEGIN CODE VERY SIMILAR TO ir/.../Utils.scala and + * linker/.../javascript/Utils.scala + */ + + private final val EscapeJSChars = "\\b\\t\\n\\v\\f\\r\\\"\\\\" + + private def printEscapeJS(str: String, out: java.lang.StringBuilder): Unit = { + /* Note that Java and JavaScript happen to use the same encoding for + * Unicode, namely UTF-16, which means that 1 char from Java always equals + * 1 char in JavaScript. */ + val end = str.length() + var i = 0 + /* Loop prints all consecutive ASCII printable characters starting + * from current i and one non ASCII printable character (if it exists). + * The new i is set at the end of the appended characters. + */ + while (i != end) { + val start = i + var c: Int = str.charAt(i) + // Find all consecutive ASCII printable characters from `start` + while (i != end && c >= 32 && c <= 126 && c != 34 && c != 92) { + i += 1 + if (i != end) + c = str.charAt(i) + } + // Print ASCII printable characters from `start` + if (start != i) { + out.append(str, start, i) + } + + // Print next non ASCII printable character + if (i != end) { + def escapeJSEncoded(c: Int): Unit = { + if (7 < c && c < 14) { + val i = 2 * (c - 8) + out.append(EscapeJSChars, i, i + 2) + } else if (c == 34) { + out.append(EscapeJSChars, 12, 14) + } else if (c == 92) { + out.append(EscapeJSChars, 14, 16) + } else { + out.append("\\u%04x".format(c)) + } + } + escapeJSEncoded(c) + i += 1 + } + } + } + + /* !!! END CODE VERY SIMILAR TO ir/.../Utils.scala and + * linker/.../javascript/Utils.scala + */ + +} diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 77bf442..71772c1 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -18,9 +18,9 @@ import scala.annotation.tailrec import scala.collection.JavaConverters._ import org.scalajs.jsenv._ +import org.scalajs.jsenv.JSUtils.escapeJS import org.scalajs.io._ -import org.scalajs.io.JSUtils.escapeJS import org.scalajs.logging._ import java.io._ From 6ece659f6f519029056c4f1828576a11c23bd88e Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 12 Apr 2019 17:51:55 +0200 Subject: [PATCH 122/133] Fix scala-js/scala-js#3589: Use java.nio.file.Path instead of VirtualBinaryFile --- .../org/scalajs/jsenv/test/RunTests.scala | 11 +- .../org/scalajs/jsenv/test/kit/TestKit.scala | 11 +- .../main/scala/org/scalajs/jsenv/Input.scala | 8 +- .../org/scalajs/jsenv/nodejs/ComSupport.scala | 29 ++-- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 129 ++++++------------ 5 files changed, 72 insertions(+), 116 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index e64f8ec..de76df0 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -12,10 +12,11 @@ package org.scalajs.jsenv.test +import com.google.common.jimfs.Jimfs + import org.junit.Assume._ import org.junit.{Test, Before, AssumptionViolatedException} -import org.scalajs.io.VirtualBinaryFile import org.scalajs.jsenv._ import org.scalajs.jsenv.test.kit.{TestKit, Run} @@ -133,13 +134,7 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { @Test def noThrowOnBadFileTest: Unit = { - def fail() = throw new java.io.IOException("exception for test") - - val badFile = new VirtualBinaryFile { - def path: String = fail() - def exists: Boolean = fail() - def inputStream: java.io.InputStream = fail() - } + val badFile = Jimfs.newFileSystem().getPath("nonexistent") // `start` may not throw but must fail asynchronously withRun(Input.ScriptsToLoad(badFile :: Nil)) { diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala index 262f978..889c3e5 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala @@ -16,9 +16,12 @@ import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.nio.file._ import java.util.concurrent.Executors -import org.scalajs.io.MemVirtualBinaryFile +import com.google.common.jimfs.Jimfs + import org.scalajs.jsenv._ /** TestKit is a utility class to simplify testing of [[JSEnv]]s. @@ -152,7 +155,9 @@ private object TestKit { ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor()) private def codeToInput(code: String): Input = { - val vf = MemVirtualBinaryFile.fromStringUTF8("testScript.js", code) - Input.ScriptsToLoad(List(vf)) + val p = Files.write( + Jimfs.newFileSystem().getPath("testScript.js"), + code.getBytes(StandardCharsets.UTF_8)) + Input.ScriptsToLoad(List(p)) } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala index c2805bf..3ad4a82 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -12,7 +12,7 @@ package org.scalajs.jsenv -import org.scalajs.io._ +import java.nio.file.Path /** Input to a [[JSEnv]]. * @@ -27,7 +27,7 @@ abstract class Input private () object Input { /** All files are to be loaded as scripts into the global scope in the order given. */ - final case class ScriptsToLoad(scripts: List[VirtualBinaryFile]) extends Input + final case class ScriptsToLoad(scripts: List[Path]) extends Input /** All files are to be loaded as ES modules, in the given order. * @@ -36,11 +36,11 @@ object Input { * `ESModulesToLoad` input if the `modules` argument has more than one * element. */ - final case class ESModulesToLoad(modules: List[VirtualBinaryFile]) + final case class ESModulesToLoad(modules: List[Path]) extends Input /** All files are to be loaded as CommonJS modules, in the given order. */ - final case class CommonJSModulesToLoad(modules: List[VirtualBinaryFile]) + final case class CommonJSModulesToLoad(modules: List[Path]) extends Input } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala index 346ead7..d387f93 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala @@ -12,12 +12,6 @@ package org.scalajs.jsenv.nodejs -import java.io._ -import java.net._ - -import org.scalajs.io.{VirtualBinaryFile, MemVirtualBinaryFile} -import org.scalajs.jsenv._ - import scala.collection.immutable import scala.concurrent._ import scala.util.{Failure, Success} @@ -26,6 +20,15 @@ import scala.util.control.NonFatal // TODO Replace this by a better execution context on the RunConfig. import scala.concurrent.ExecutionContext.Implicits.global +import java.io._ +import java.net._ +import java.nio.charset.StandardCharsets +import java.nio.file._ + +import com.google.common.jimfs.Jimfs + +import org.scalajs.jsenv._ + private final class ComRun(run: JSRun, handleMessage: String => Unit, serverSocket: ServerSocket) extends JSComRun { import ComRun._ @@ -200,11 +203,10 @@ object ComRun { * @param config Configuration for the run. * @param onMessage callback upon message reception. * @param startRun [[JSRun]] launcher. Gets passed a - * [[org.scalajs.io.VirtualBinaryFile VirtualBinaryFile]] that - * initializes `scalaJSCom` on `global`. Requires Node.js libraries. + * [[java.nio.file.Path Path]] that initializes `scalaJSCom` on + * `global`. Requires Node.js libraries. */ - def start(config: RunConfig, onMessage: String => Unit)( - startRun: VirtualBinaryFile => JSRun): JSComRun = { + def start(config: RunConfig, onMessage: String => Unit)(startRun: Path => JSRun): JSComRun = { try { val serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address @@ -240,8 +242,9 @@ object ComRun { s.writeChars(msg) } - private def setupFile(port: Int): VirtualBinaryFile = { - MemVirtualBinaryFile.fromStringUTF8("comSetup.js", + private def setupFile(port: Int): Path = { + Files.write( + Jimfs.newFileSystem().getPath("comSetup.js"), s""" |(function() { | // The socket for communication @@ -303,6 +306,6 @@ object ComRun { | } | } |}).call(this); - """.stripMargin) + """.stripMargin.getBytes(StandardCharsets.UTF_8)) } } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 71772c1..26ab9fe 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -12,18 +12,19 @@ package org.scalajs.jsenv.nodejs -import java.nio.charset.StandardCharsets import scala.annotation.tailrec -import scala.collection.JavaConverters._ + +import java.io._ +import java.nio.charset.StandardCharsets +import java.nio.file._ import org.scalajs.jsenv._ import org.scalajs.jsenv.JSUtils.escapeJS -import org.scalajs.io._ import org.scalajs.logging._ -import java.io._ +import com.google.common.jimfs.Jimfs final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { import NodeJSEnv._ @@ -57,8 +58,7 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { } } - private def internalStart(initFiles: List[VirtualBinaryFile], input: Input, - runConfig: RunConfig): JSRun = { + private def internalStart(initFiles: List[Path], input: Input, runConfig: RunConfig): JSRun = { val command = config.executable :: config.args val externalConfig = ExternalJSRun.Config() .withEnv(env) @@ -67,7 +67,7 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { NodeJSEnv.write(initFiles, input)) } - private def initFiles: List[VirtualBinaryFile] = config.sourceMap match { + private def initFiles: List[Path] = config.sourceMap match { case SourceMap.Disable => Nil case SourceMap.EnableIfAvailable => installSourceMapIfAvailable :: Nil case SourceMap.Enable => installSourceMap :: Nil @@ -83,43 +83,45 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { } object NodeJSEnv { + private lazy val fs = Jimfs.newFileSystem() + private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) private lazy val installSourceMapIfAvailable = { - MemVirtualBinaryFile.fromStringUTF8("sourceMapSupport.js", + Files.write( + fs.getPath("optionalSourceMapSupport.js"), """ |try { | require('source-map-support').install(); |} catch (e) { |}; - """.stripMargin - ) + """.stripMargin.getBytes(StandardCharsets.UTF_8)) } private lazy val installSourceMap = { - MemVirtualBinaryFile.fromStringUTF8("sourceMapSupport.js", - "require('source-map-support').install();") + Files.write( + fs.getPath("sourceMapSupport.js"), + "require('source-map-support').install();".getBytes(StandardCharsets.UTF_8)) } - private def write(initFiles: List[VirtualBinaryFile], input: Input)( - out: OutputStream): Unit = { + private def write(initFiles: List[Path], input: Input)(out: OutputStream): Unit = { val p = new PrintStream(out, false, "UTF8") try { - def writeRunScript(file: VirtualBinaryFile): Unit = { - file match { - case file: FileVirtualBinaryFile => - val pathJS = "\"" + escapeJS(file.file.getAbsolutePath) + "\"" - p.println(s""" - require('vm').runInThisContext( - require('fs').readFileSync($pathJS, { encoding: "utf-8" }), - { filename: $pathJS, displayErrors: true } - ); - """) - - case _ => - val code = readInputStreamToString(file.inputStream) + def writeRunScript(path: Path): Unit = { + try { + val f = path.toFile + val pathJS = "\"" + escapeJS(f.getAbsolutePath) + "\"" + p.println(s""" + require('vm').runInThisContext( + require('fs').readFileSync($pathJS, { encoding: "utf-8" }), + { filename: $pathJS, displayErrors: true } + ); + """) + } catch { + case _: UnsupportedOperationException => + val code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8) val codeJS = "\"" + escapeJS(code) + "\"" - val pathJS = "\"" + escapeJS(file.path) + "\"" + val pathJS = "\"" + escapeJS(path.toString) + "\"" p.println(s""" require('vm').runInThisContext( $codeJS, @@ -129,17 +131,6 @@ object NodeJSEnv { } } - def writeRequire(file: VirtualBinaryFile): Unit = { - file match { - case file: FileVirtualBinaryFile => - p.println(s"""require("${escapeJS(file.file.getAbsolutePath)}")""") - - case _ => - val f = tmpFile(file.path, file.inputStream) - p.println(s"""require("${escapeJS(f.getAbsolutePath)}")""") - } - } - for (initFile <- initFiles) writeRunScript(initFile) @@ -150,16 +141,11 @@ object NodeJSEnv { case Input.CommonJSModulesToLoad(modules) => for (module <- modules) - writeRequire(module) + p.println(s"""require("${escapeJS(toFile(module).getAbsolutePath)}")""") case Input.ESModulesToLoad(modules) => if (modules.nonEmpty) { - val uris = modules.map { - case module: FileVirtualBinaryFile => - module.file.toURI - case module => - tmpFile(module.path, module.inputStream).toURI - } + val uris = modules.map(m => toFile(m).toURI) val imports = uris.map { uri => s"""import("${escapeJS(uri.toASCIIString)}")""" @@ -176,7 +162,8 @@ object NodeJSEnv { |}); """.stripMargin } - val f = tmpFile("importer.js", importerFileContent) + val f = createTmpFile("importer.js") + Files.write(f.toPath, importerFileContent.getBytes(StandardCharsets.UTF_8)) p.println(s"""require("${escapeJS(f.getAbsolutePath)}");""") } } @@ -185,48 +172,14 @@ object NodeJSEnv { } } - private def readInputStreamToString(inputStream: InputStream): String = { - val baos = new java.io.ByteArrayOutputStream - val in = inputStream + private def toFile(path: Path): File = { try { - val buf = new Array[Byte](4096) - - @tailrec - def loop(): Unit = { - val read = in.read(buf) - if (read != -1) { - baos.write(buf, 0, read) - loop() - } - } - - loop() - } finally { - in.close() - } - new String(baos.toByteArray(), StandardCharsets.UTF_8) - } - - private def tmpFile(path: String, content: String): File = { - import java.nio.file.{Files, StandardOpenOption} - - val f = createTmpFile(path) - val contentList = new java.util.ArrayList[String]() - contentList.add(content) - Files.write(f.toPath(), contentList, StandardCharsets.UTF_8, - StandardOpenOption.TRUNCATE_EXISTING) - f - } - - private def tmpFile(path: String, content: InputStream): File = { - import java.nio.file.{Files, StandardCopyOption} - - try { - val f = createTmpFile(path) - Files.copy(content, f.toPath(), StandardCopyOption.REPLACE_EXISTING) - f - } finally { - content.close() + path.toFile + } catch { + case _: UnsupportedOperationException => + val f = createTmpFile(path.toString) + Files.copy(path, f.toPath(), StandardCopyOption.REPLACE_EXISTING) + f } } From 31259a2fab570327147ec4f000fe32db95d377e3 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 19 May 2019 11:27:25 +0200 Subject: [PATCH 123/133] Add a JSEnv test using the default filesystem This is a follow-up from scala-js/scala-js#3646. --- .../org/scalajs/jsenv/test/RunTests.scala | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index de76df0..63d8824 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -12,6 +12,10 @@ package org.scalajs.jsenv.test +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.Files + import com.google.common.jimfs.Jimfs import org.junit.Assume._ @@ -142,6 +146,24 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { } } + @Test + def defaultFilesystem: Unit = { + // Tests that a JSEnv works with files from the default filesystem. + + val tmpFile = File.createTempFile("sjs-run-test-defaultfile", ".js") + try { + val tmpPath = tmpFile.toPath + Files.write(tmpPath, "console.log(\"test\");".getBytes(StandardCharsets.UTF_8)) + + withRun(Input.ScriptsToLoad(tmpPath :: Nil)) { + _.expectOut("test\n") + .closeRun() + } + } finally { + tmpFile.delete() + } + } + /* This test verifies that a [[JSEnv]] properly validates its [[RunConfig]] * (through [[RunConfig.Validator]]). * From 94c5ec349678e5523261e3e9081e230890e30f72 Mon Sep 17 00:00:00 2001 From: Akhtyam Sakaev Date: Sat, 8 Jun 2019 10:12:28 +0300 Subject: [PATCH 124/133] Fix several typos in comments and debug strings --- .../src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala | 2 +- .../test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala | 2 +- js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala index 27ca31a..a9f4809 100644 --- a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala +++ b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala @@ -40,7 +40,7 @@ private[kit] class TestEnv private ( def withOutErrHang(): TestEnv = { def hangStream() = new InputStream { - // read method that hangs indfinitely. + // read method that hangs indefinitely. def read(): Int = synchronized { while (true) wait() throw new AssertionError("unreachable code") diff --git a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala index eaf2a5f..c098d21 100644 --- a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala +++ b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala @@ -33,7 +33,7 @@ class TestKitTest { expectAssert(msg, cause)(body(kit)) - assertFalse("faster than timout", deadline.isOverdue) + assertFalse("faster than timeout", deadline.isOverdue) } test(env.withSuccess(), null) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala index a82355e..17d6f17 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala @@ -115,8 +115,7 @@ final object RunConfig { * 1. inform it of the [[JSEnv]]'s capabilities * 1. invoke [[validate]] with every received [[RunConfig]] * - * This ensures that enusre that all set config options are supported by the - * [[JSEnv]]. + * This ensures that all set config options are supported by the [[JSEnv]]. */ final class Validator private ( inheritIO: Boolean, From 116901193af255048cafbb8cbe73f7cf3ac38d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 17 Jun 2019 17:46:45 +0200 Subject: [PATCH 125/133] [no-master] Remove `@deprecatedName`s that were introduced in 0.6.16. It is not possible to cross-compile `@deprecatedName`s without warnings between Scala 2.12- and 2.13+, because: * In 2.12, they require symbol literals as arguments * In 2.13, symbol literals are deprecated Since they were deprecated more than two years ago, and are part of an API that is not used much anyway (JS envs), I believe it is fair to drop the source compatibility in this case. --- js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 2 -- .../scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 3 --- .../src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 3 --- 3 files changed, 8 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index d23742e..d5c846f 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -24,9 +24,7 @@ import scala.concurrent.{Future, Promise} import scala.util.Try abstract class ExternalJSEnv( - @deprecatedName('additionalArgs) final protected val args: Seq[String], - @deprecatedName('additionalEnv) final protected val env: Map[String, String]) extends AsyncJSEnv { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index e1e63e9..2a37b2a 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -29,11 +29,8 @@ import scala.concurrent.TimeoutException import scala.concurrent.duration._ abstract class AbstractNodeJSEnv( - @deprecatedName('nodejsPath) protected val executable: String, - @deprecatedName('addArgs) args: Seq[String], - @deprecatedName('addEnv) env: Map[String, String], val sourceMap: Boolean) extends ExternalJSEnv(args, env) with ComJSEnv { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 99e163e..77a50de 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -30,11 +30,8 @@ class NodeJSEnv(config: NodeJSEnv.Config) @deprecated("Use the overload with a NodeJSEnv.Config.", "0.6.18") def this( - @deprecatedName('nodejsPath) executable: String = "node", - @deprecatedName('addArgs) args: Seq[String] = Seq.empty, - @deprecatedName('addEnv) env: Map[String, String] = Map.empty) = { this(NodeJSEnv.Config().withExecutable(executable).withArgs(args.toList).withEnv(env)) } From ffa58d24e9985ac5856b46fba2b4cf4c407a89da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 18 Jun 2019 14:54:02 +0200 Subject: [PATCH 126/133] [no-master] Enable Scala 2.13.0 in the tools and all the other artifacts. Partest is not yet enabled in this commit, because a dependency of `partest` 2.13.0, namely `testkit`, was not published. See https://github.com/scala/bug/issues/11529 upstream. --- .../org/scalajs/jsenv/test/TimeoutTests.scala | 2 +- .../scala/org/scalajs/jsenv/ExternalJSEnv.scala | 14 ++++++++++++-- .../scalajs/jsenv/VirtualFileMaterializer.scala | 2 +- .../scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 16 ++++++++++++---- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala index cbe6dc9..83c8b26 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutTests.scala @@ -140,7 +140,7 @@ trait TimeoutTests extends JSEnvTest { if (c >= 10) clearInterval(i); }, 10); - """ hasOutput (1 to 10).map(_ + "\n").mkString + """ hasOutput (1 to 10).mkString("", "\n", "\n") assertTrue("Execution took too little time", deadline.isOverdue()) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index d5c846f..bef59e9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -81,8 +81,18 @@ abstract class ExternalJSEnv( * The default value in `ExternalJSEnv` is * `System.getenv().asScala.toMap ++ env`. */ - protected def getVMEnv(): Map[String, String] = - System.getenv().asScala.toMap ++ env + protected def getVMEnv(): Map[String, String] = { + /* We use Java's `forEach` not to depend on Scala's JavaConverters, which + * are very difficult to cross-compile across 2.12- and 2.13+. + */ + val builder = Map.newBuilder[String, String] + System.getenv().forEach(new java.util.function.BiConsumer[String, String] { + def accept(key: String, value: String): Unit = + builder += key -> value + }) + builder ++= env + builder.result() + } /** Get files that are a library (i.e. that do not run anything) */ protected def getLibJSFiles(): Seq[VirtualJSFile] = diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala index de38666..455c0d9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/VirtualFileMaterializer.scala @@ -77,7 +77,7 @@ final class VirtualFileMaterializer(singleDir: Boolean = false) { // scalastyle:on line.size.limit private def createTempDir(): File = { val baseDir = new File(System.getProperty("java.io.tmpdir")) - val baseName = System.currentTimeMillis() + "-" + val baseName = "" + System.currentTimeMillis() + "-" @tailrec def loop(tries: Int): File = { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 2a37b2a..74eaa5c 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -168,10 +168,18 @@ abstract class AbstractNodeJSEnv( val nodePath = libCache.cacheDir.getAbsolutePath + baseNodePath.fold("")(p => File.pathSeparator + p) - System.getenv().asScala.toMap ++ Seq( - "NODE_MODULE_CONTEXTS" -> "0", - "NODE_PATH" -> nodePath - ) ++ env + /* We use Java's `forEach` not to depend on Scala's JavaConverters, which + * are very difficult to cross-compile across 2.12- and 2.13+. + */ + val builder = Map.newBuilder[String, String] + System.getenv().forEach(new java.util.function.BiConsumer[String, String] { + def accept(key: String, value: String): Unit = + builder += key -> value + }) + builder += "NODE_MODULE_CONTEXTS" -> "0" + builder += "NODE_PATH" -> nodePath + builder ++= env + builder.result() } } From df599a8262beec32d590c464264d7b94f9062296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 18 Jun 2019 16:19:30 +0200 Subject: [PATCH 127/133] Enable Scala 2.13.0 in the linker and all the other artifacts. Partest is not yet enabled in this commit, because a dependency of `partest` 2.13.0, namely `testkit`, was not published. See scala/bugscala-js/scala-js#11529 upstream. --- .../scala/org/scalajs/jsenv/test/JSEnvSuite.scala | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala index 17efd0a..4f5c120 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala @@ -14,7 +14,6 @@ package org.scalajs.jsenv.test import org.scalajs.jsenv.JSEnv -import scala.collection.JavaConverters._ import scala.reflect.ClassTag import org.junit.runner.Runner @@ -40,7 +39,7 @@ abstract class JSEnvSuite(private[test] val config: JSEnvSuiteConfig) /** Runner for a [[JSEnvSuite]]. May only be used on subclasses of [[JSEnvSuite]]. */ final class JSEnvSuiteRunner(root: Class[_], config: JSEnvSuiteConfig) - extends Suite(root, JSEnvSuiteRunner.getRunners(config).asJava) { + extends Suite(root, JSEnvSuiteRunner.getRunners(config)) { /** Constructor for reflective instantiation via `@RunWith`. */ def this(suite: Class[_ <: JSEnvSuite]) = @@ -56,16 +55,19 @@ private object JSEnvSuiteRunner { .map { case (name, value) => s"$name = $value" } .mkString("[", ", ", "]") - val paramValues = config +: params.map(_._2) + val paramValues = new java.util.LinkedList[AnyRef] + paramValues.add(config) + for (param <- params) + paramValues.add(param._2) new BlockJUnit4ClassRunnerWithParameters( - new TestWithParameters(name, new TestClass(t.runtimeClass), paramValues.asJava)) + new TestWithParameters(name, new TestClass(t.runtimeClass), paramValues)) } - private def getRunners(config: JSEnvSuiteConfig): List[Runner] = { + private def getRunners(config: JSEnvSuiteConfig): java.util.List[Runner] = { import java.lang.Boolean.{TRUE, FALSE} - List( + java.util.Arrays.asList( r[RunTests](config, "withCom" -> FALSE), r[RunTests](config, "withCom" -> TRUE), r[TimeoutRunTests](config, "withCom" -> FALSE), From 03edf0c8e5401ffcc159171379ece9c65d05200b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 14 Aug 2019 13:49:02 +0200 Subject: [PATCH 128/133] Remove two unused imports of `JavaConverters`. This removes the last references to `JavaConverters` in our codebase, excluding scalalib overrides and comments. --- js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala | 1 - .../main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala | 1 - 2 files changed, 2 deletions(-) diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala index bef59e9..0c2d73f 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala @@ -19,7 +19,6 @@ import org.scalajs.core.tools.jsdep.ResolvedJSDependency import java.io.{ Console => _, _ } import scala.io.Source -import scala.collection.JavaConverters._ import scala.concurrent.{Future, Promise} import scala.util.Try diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala index 74eaa5c..08b5287 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/nodejs/AbstractNodeJSEnv.scala @@ -24,7 +24,6 @@ import org.scalajs.core.tools.logging.NullLogger import org.scalajs.jsenv._ import org.scalajs.jsenv.Utils.OptDeadline -import scala.collection.JavaConverters._ import scala.concurrent.TimeoutException import scala.concurrent.duration._ From 23112c7a82be77657a33839996119d6b536c93ea Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 6 Aug 2019 20:54:46 +0200 Subject: [PATCH 129/133] Fix scala-js/scala-js#3616: Do not force all JSEnv inputs to be of the same type --- .../org/scalajs/jsenv/test/RunTests.scala | 6 +- .../org/scalajs/jsenv/test/kit/TestKit.scala | 20 +-- .../main/scala/org/scalajs/jsenv/Input.scala | 22 ++- .../main/scala/org/scalajs/jsenv/JSEnv.scala | 10 +- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 155 +++++++++--------- 5 files changed, 103 insertions(+), 110 deletions(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 63d8824..feee0fa 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -27,7 +27,7 @@ import org.scalajs.jsenv.test.kit.{TestKit, Run} private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { private val kit = new TestKit(config.jsEnv, config.awaitTimeout) - private def withRun(input: Input)(body: Run => Unit) = { + private def withRun(input: Seq[Input])(body: Run => Unit) = { if (withCom) kit.withComRun(input)(body) else kit.withRun(input)(body) } @@ -141,7 +141,7 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { val badFile = Jimfs.newFileSystem().getPath("nonexistent") // `start` may not throw but must fail asynchronously - withRun(Input.ScriptsToLoad(badFile :: Nil)) { + withRun(Input.Script(badFile) :: Nil) { _.fails() } } @@ -155,7 +155,7 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { val tmpPath = tmpFile.toPath Files.write(tmpPath, "console.log(\"test\");".getBytes(StandardCharsets.UTF_8)) - withRun(Input.ScriptsToLoad(tmpPath :: Nil)) { + withRun(Input.Script(tmpPath) :: Nil) { _.expectOut("test\n") .closeRun() } diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala index 889c3e5..7049dad 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala @@ -56,7 +56,7 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { start(codeToInput(code)) /** Starts a [[Run]] for testing. */ - def start(input: Input): Run = + def start(input: Seq[Input]): Run = start(input, RunConfig()) /** Starts a [[Run]] for testing. */ @@ -64,7 +64,7 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { start(codeToInput(code), config) /** Starts a [[Run]] for testing. */ - def start(input: Input, config: RunConfig): Run = { + def start(input: Seq[Input], config: RunConfig): Run = { val (run, out, err) = io(config)(jsEnv.start(input, _)) new Run(run, out, err, timeout) } @@ -74,7 +74,7 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { startWithCom(codeToInput(code)) /** Starts a [[ComRun]] for testing. */ - def startWithCom(input: Input): ComRun = + def startWithCom(input: Seq[Input]): ComRun = startWithCom(input, RunConfig()) /** Starts a [[ComRun]] for testing. */ @@ -82,7 +82,7 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { startWithCom(codeToInput(code), config) /** Starts a [[ComRun]] for testing. */ - def startWithCom(input: Input, config: RunConfig): ComRun = { + def startWithCom(input: Seq[Input], config: RunConfig): ComRun = { val msg = new MsgHandler val (run, out, err) = io(config)(jsEnv.startWithCom(input, _, msg.onMessage _)) run.future.onComplete(msg.onRunComplete _)(TestKit.completer) @@ -95,7 +95,7 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { withRun(codeToInput(code))(body) /** Convenience method to start a [[Run]] and close it after usage. */ - def withRun[T](input: Input)(body: Run => T): T = + def withRun[T](input: Seq[Input])(body: Run => T): T = withRun(input, RunConfig())(body) /** Convenience method to start a [[Run]] and close it after usage. */ @@ -103,7 +103,7 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { withRun(codeToInput(code), config)(body) /** Convenience method to start a [[Run]] and close it after usage. */ - def withRun[T](input: Input, config: RunConfig)(body: Run => T): T = { + def withRun[T](input: Seq[Input], config: RunConfig)(body: Run => T): T = { val run = start(input, config) try body(run) finally run.close() @@ -113,14 +113,14 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) { def withComRun[T](code: String)(body: ComRun => T): T = withComRun(codeToInput(code))(body) /** Convenience method to start a [[ComRun]] and close it after usage. */ - def withComRun[T](input: Input)(body: ComRun => T): T = withComRun(input, RunConfig())(body) + def withComRun[T](input: Seq[Input])(body: ComRun => T): T = withComRun(input, RunConfig())(body) /** Convenience method to start a [[ComRun]] and close it after usage. */ def withComRun[T](code: String, config: RunConfig)(body: ComRun => T): T = withComRun(codeToInput(code), config)(body) /** Convenience method to start a [[ComRun]] and close it after usage. */ - def withComRun[T](input: Input, config: RunConfig)(body: ComRun => T): T = { + def withComRun[T](input: Seq[Input], config: RunConfig)(body: ComRun => T): T = { val run = startWithCom(input, config) try body(run) finally run.close() @@ -154,10 +154,10 @@ private object TestKit { private val completer = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor()) - private def codeToInput(code: String): Input = { + private def codeToInput(code: String): Seq[Input] = { val p = Files.write( Jimfs.newFileSystem().getPath("testScript.js"), code.getBytes(StandardCharsets.UTF_8)) - Input.ScriptsToLoad(List(p)) + List(Input.Script(p)) } } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala index 3ad4a82..a3440e9 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -26,26 +26,24 @@ import java.nio.file.Path abstract class Input private () object Input { - /** All files are to be loaded as scripts into the global scope in the order given. */ - final case class ScriptsToLoad(scripts: List[Path]) extends Input + /** The file is to be loaded as a script into the global scope. */ + final case class Script(script: Path) extends Input - /** All files are to be loaded as ES modules, in the given order. + /** The file is to be loaded as an ES module. * - * Some environments may not be able to execute several ES modules in a + * Some environments may not be able to load several ES modules in a * deterministic order. If that is the case, they must reject an - * `ESModulesToLoad` input if the `modules` argument has more than one - * element. + * `ESModule` input if it appears with other Inputs such that loading + * in a deterministic order is not possible. */ - final case class ESModulesToLoad(modules: List[Path]) - extends Input + final case class ESModule(module: Path) extends Input - /** All files are to be loaded as CommonJS modules, in the given order. */ - final case class CommonJSModulesToLoad(modules: List[Path]) - extends Input + /** The file is to be loaded as a CommonJS module. */ + final case class CommonJSModule(module: Path) extends Input } class UnsupportedInputException(msg: String, cause: Throwable) extends IllegalArgumentException(msg, cause) { def this(msg: String) = this(msg, null) - def this(input: Input) = this(s"Unsupported input: $input") + def this(input: Seq[Input]) = this(s"Unsupported input: $input") } diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index 091afeb..1e7b400 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -33,10 +33,12 @@ trait JSEnv { * with the input's content (e.g. file does not exist, syntax error, etc.). * In this case, [[JSRun#future]] should be failed instead. * - * @throws java.lang.IllegalArgumentException if the value of `input` or - * `config` cannot be supported. + * @throws UnsupportedInputException if the value of `input` cannot be + * supported. + * @throws java.lang.IllegalArgumentException if the value of `config` cannot + * be supported. */ - def start(input: Input, config: RunConfig): JSRun + def start(input: Seq[Input], config: RunConfig): JSRun /** Like [[start]], but initializes a communication channel. * @@ -83,6 +85,6 @@ trait JSEnv { * [[JSRun#future]] of the returned [[JSComRun]] is completed. Further, * [[JSRun#future]] may only complete with no callback in-flight. */ - def startWithCom(input: Input, config: RunConfig, + def startWithCom(input: Seq[Input], config: RunConfig, onMessage: String => Unit): JSComRun } diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index 26ab9fe..c50fee5 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -33,49 +33,40 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv { val name: String = "Node.js" - def start(input: Input, runConfig: RunConfig): JSRun = { + def start(input: Seq[Input], runConfig: RunConfig): JSRun = { NodeJSEnv.validator.validate(runConfig) validateInput(input) - internalStart(initFiles, input, runConfig) + internalStart(initFiles ++ input, runConfig) } - def startWithCom(input: Input, runConfig: RunConfig, + def startWithCom(input: Seq[Input], runConfig: RunConfig, onMessage: String => Unit): JSComRun = { NodeJSEnv.validator.validate(runConfig) validateInput(input) ComRun.start(runConfig, onMessage) { comLoader => - internalStart(initFiles :+ comLoader, input, runConfig) + internalStart(initFiles ++ (Input.Script(comLoader) +: input), runConfig) } } - private def validateInput(input: Input): Unit = { - input match { - case _:Input.ScriptsToLoad | _:Input.ESModulesToLoad | - _:Input.CommonJSModulesToLoad => - // ok - case _ => - throw new UnsupportedInputException(input) - } + private def validateInput(input: Seq[Input]): Unit = input.foreach { + case _:Input.Script | _:Input.ESModule | _:Input.CommonJSModule => + // ok + case _ => + throw new UnsupportedInputException(input) } - private def internalStart(initFiles: List[Path], input: Input, runConfig: RunConfig): JSRun = { + private def internalStart(input: Seq[Input], runConfig: RunConfig): JSRun = { val command = config.executable :: config.args val externalConfig = ExternalJSRun.Config() .withEnv(env) .withRunConfig(runConfig) - ExternalJSRun.start(command, externalConfig)( - NodeJSEnv.write(initFiles, input)) + ExternalJSRun.start(command, externalConfig)(NodeJSEnv.write(input)) } - private def initFiles: List[Path] = config.sourceMap match { + private def initFiles: Seq[Input] = config.sourceMap match { case SourceMap.Disable => Nil - case SourceMap.EnableIfAvailable => installSourceMapIfAvailable :: Nil - case SourceMap.Enable => installSourceMap :: Nil - } - - private def inputFiles(input: Input) = input match { - case Input.ScriptsToLoad(scripts) => scripts - case _ => throw new UnsupportedInputException(input) + case SourceMap.EnableIfAvailable => Input.Script(installSourceMapIfAvailable) :: Nil + case SourceMap.Enable => Input.Script(installSourceMap) :: Nil } private def env: Map[String, String] = @@ -104,68 +95,70 @@ object NodeJSEnv { "require('source-map-support').install();".getBytes(StandardCharsets.UTF_8)) } - private def write(initFiles: List[Path], input: Input)(out: OutputStream): Unit = { - val p = new PrintStream(out, false, "UTF8") - try { - def writeRunScript(path: Path): Unit = { - try { - val f = path.toFile - val pathJS = "\"" + escapeJS(f.getAbsolutePath) + "\"" - p.println(s""" + private def write(input: Seq[Input])(out: OutputStream): Unit = { + def runScript(path: Path): String = { + try { + val f = path.toFile + val pathJS = "\"" + escapeJS(f.getAbsolutePath) + "\"" + s""" + require('vm').runInThisContext( + require('fs').readFileSync($pathJS, { encoding: "utf-8" }), + { filename: $pathJS, displayErrors: true } + ) + """ + } catch { + case _: UnsupportedOperationException => + val code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8) + val codeJS = "\"" + escapeJS(code) + "\"" + val pathJS = "\"" + escapeJS(path.toString) + "\"" + s""" require('vm').runInThisContext( - require('fs').readFileSync($pathJS, { encoding: "utf-8" }), + $codeJS, { filename: $pathJS, displayErrors: true } - ); - """) - } catch { - case _: UnsupportedOperationException => - val code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8) - val codeJS = "\"" + escapeJS(code) + "\"" - val pathJS = "\"" + escapeJS(path.toString) + "\"" - p.println(s""" - require('vm').runInThisContext( - $codeJS, - { filename: $pathJS, displayErrors: true } - ); - """) - } + ) + """ } + } - for (initFile <- initFiles) - writeRunScript(initFile) - - input match { - case Input.ScriptsToLoad(scripts) => - for (script <- scripts) - writeRunScript(script) - - case Input.CommonJSModulesToLoad(modules) => - for (module <- modules) - p.println(s"""require("${escapeJS(toFile(module).getAbsolutePath)}")""") - - case Input.ESModulesToLoad(modules) => - if (modules.nonEmpty) { - val uris = modules.map(m => toFile(m).toURI) - - val imports = uris.map { uri => - s"""import("${escapeJS(uri.toASCIIString)}")""" - } - val importChain = imports.reduceLeft { (prev, imprt) => - s"""$prev.then(_ => $imprt)""" - } - - val importerFileContent = { - s""" - |$importChain.catch(e => { - | console.error(e); - | process.exit(1); - |}); - """.stripMargin - } - val f = createTmpFile("importer.js") - Files.write(f.toPath, importerFileContent.getBytes(StandardCharsets.UTF_8)) - p.println(s"""require("${escapeJS(f.getAbsolutePath)}");""") - } + def requireCommonJSModule(module: Path): String = + s"""require("${escapeJS(toFile(module).getAbsolutePath)}")""" + + def importESModule(module: Path): String = + s"""import("${escapeJS(toFile(module).toURI.toASCIIString)}")""" + + def execInputExpr(input: Input): String = input match { + case Input.Script(script) => runScript(script) + case Input.CommonJSModule(module) => requireCommonJSModule(module) + case Input.ESModule(module) => importESModule(module) + } + + val p = new PrintStream(out, false, "UTF8") + try { + if (!input.exists(_.isInstanceOf[Input.ESModule])) { + /* If there is no ES module in the input, we can do everything + * synchronously, and directly on the standard input. + */ + for (item <- input) + p.println(execInputExpr(item) + ";") + } else { + /* If there is at least one ES module, we must asynchronous chain things, + * and we must use an actual file to feed code to Node.js (because + * `import()` cannot be used from the standard input). + */ + val importChain = input.foldLeft("Promise.resolve()") { (prev, item) => + s"$prev.\n then(${execInputExpr(item)})" + } + val importerFileContent = { + s""" + |$importChain.catch(e => { + | console.error(e); + | process.exit(1); + |}); + """.stripMargin + } + val f = createTmpFile("importer.js") + Files.write(f.toPath, importerFileContent.getBytes(StandardCharsets.UTF_8)) + p.println(s"""require("${escapeJS(f.getAbsolutePath)}");""") } } finally { p.close() From e7613ec0e4f4105b1a298ed663fc8191438cdba4 Mon Sep 17 00:00:00 2001 From: Brian Wignall Date: Fri, 6 Dec 2019 11:57:21 -0500 Subject: [PATCH 130/133] Fix typos --- .../src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala index d387f93..c150eb6 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala @@ -33,7 +33,7 @@ private final class ComRun(run: JSRun, handleMessage: String => Unit, serverSocket: ServerSocket) extends JSComRun { import ComRun._ - /** Promise that completes once the reciever thread is completed. */ + /** Promise that completes once the receiver thread is completed. */ private[this] val promise = Promise[Unit]() @volatile From 7c7c5090518a798241b3e0b5932fa599f22afdc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 13 May 2020 16:23:41 +0200 Subject: [PATCH 131/133] Merge '0.6.x' into 'master'. --- .../src/main/scala/org/scalajs/jsenv/test/RunTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index feee0fa..9a1a3ae 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -77,7 +77,7 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) { @Test // Failed in Phantom - #2053 def utf8Test: Unit = { - withRun("""console.log("\u1234")""") { + withRun("console.log('\u1234')") { _.expectOut("\u1234\n") .closeRun() } From 9e0a9c04ed737ce43087dadd02e58d549711eee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 4 Jun 2020 17:38:44 +0200 Subject: [PATCH 132/133] Add `()` at relevant call sites in the JS env test kit. --- .../src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala index f7df9c8..7c8ffac 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala @@ -26,7 +26,7 @@ import java.util.concurrent.TimeoutException private[kit] final class MsgHandler { private[this] var msgs: immutable.Queue[String] = immutable.Queue.empty[String] - private[this] val run = Promise[Unit] + private[this] val run = Promise[Unit]() def onMessage(msg: String): Unit = synchronized { if (run.isCompleted) { From f3a156c92e70b94b3af86140e92f6d680743d32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 15 Jun 2020 17:48:35 +0200 Subject: [PATCH 133/133] Import the JS Envs projects from the Scala.js core repository. This is the initial import of the JS Envs projects (scalajs-js-envs, scalajs-js-envs-test-kit and scalajs-env-nodejs) from the Scala.js core repository. The history of this commit reflects the entire history of relevant files from the core repo, filter-branch'ed to appear as if they had always been in this repo. This commit adds the specific setup of the build and tests. --- .gitignore | 1 + .travis.yml | 31 ++++ build.sbt | 143 ++++++++++++++++++ .../org/scalajs/jsenv/test/ComTests.scala | 2 +- .../org/scalajs/jsenv/test/JSEnvSuite.scala | 2 +- .../scalajs/jsenv/test/JSEnvSuiteConfig.scala | 2 +- .../org/scalajs/jsenv/test/RunTests.scala | 2 +- .../scalajs/jsenv/test/TimeoutComTests.scala | 2 +- .../scalajs/jsenv/test/TimeoutRunTests.scala | 2 +- .../org/scalajs/jsenv/test/kit/ComRun.scala | 2 +- .../org/scalajs/jsenv/test/kit/IOReader.scala | 2 +- .../scalajs/jsenv/test/kit/MsgHandler.scala | 2 +- .../org/scalajs/jsenv/test/kit/Run.scala | 2 +- .../org/scalajs/jsenv/test/kit/TestKit.scala | 2 +- .../org/scalajs/jsenv/test/kit/TestEnv.scala | 14 +- .../scalajs/jsenv/test/kit/TestKitTest.scala | 2 +- .../org/scalajs/jsenv/ExternalJSRun.scala | 2 +- .../main/scala/org/scalajs/jsenv/Input.scala | 2 +- .../main/scala/org/scalajs/jsenv/JSEnv.scala | 2 +- .../main/scala/org/scalajs/jsenv/JSRuns.scala | 2 +- .../scala/org/scalajs/jsenv/JSUtils.scala | 2 +- .../scala/org/scalajs/jsenv/RunConfig.scala | 2 +- .../org/scalajs/jsenv/RunConfigTest.scala | 2 +- .../org/scalajs/jsenv/nodejs/ComSupport.scala | 2 +- .../org/scalajs/jsenv/nodejs/NodeJSEnv.scala | 2 +- .../scalajs/jsenv/nodejs/NodeJSSuite.scala | 2 +- project/build.properties | 1 + project/plugins.sbt | 2 + 28 files changed, 207 insertions(+), 29 deletions(-) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 build.sbt create mode 100644 project/build.properties create mode 100644 project/plugins.sbt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6d67c42 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +sudo: false +language: scala +script: + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs/test + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs/doc + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs/mimaReportBinaryIssues + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs/headerCheck scalajs-js-envs/test:headerCheck + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs-test-kit/test + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs-test-kit/doc + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs-test-kit/mimaReportBinaryIssues + - sbt ++$TRAVIS_SCALA_VERSION scalajs-js-envs-test-kit/headerCheck scalajs-js-envs-test-kit/test:headerCheck + - sbt ++$TRAVIS_SCALA_VERSION scalajs-env-nodejs/test + - sbt ++$TRAVIS_SCALA_VERSION scalajs-env-nodejs/doc + - sbt ++$TRAVIS_SCALA_VERSION scalajs-env-nodejs/mimaReportBinaryIssues + - sbt ++$TRAVIS_SCALA_VERSION scalajs-env-nodejs/headerCheck scalajs-env-nodejs/test:headerCheck +scala: + - 2.11.12 + - 2.12.11 + - 2.13.2 +jdk: + - openjdk8 + +cache: + directories: + - "$HOME/.cache/coursier" + - "$HOME/.ivy2/cache" + - "$HOME/.sbt" +before_cache: + - rm -fv $HOME/.ivy2/.sbt.ivy.lock + - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete + - find $HOME/.sbt -name "*.lock" -print -delete diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..e75401b --- /dev/null +++ b/build.sbt @@ -0,0 +1,143 @@ +val previousVersion: Option[String] = Some("1.1.0") +val newScalaBinaryVersionsInThisRelease: Set[String] = Set() + +inThisBuild(Def.settings( + version := "1.1.1-SNAPSHOT", + organization := "org.scala-js", + scalaVersion := "2.12.11", + crossScalaVersions := Seq("2.11.12", "2.12.11", "2.13.2"), + scalacOptions ++= Seq( + "-deprecation", + "-feature", + "-Xfatal-warnings", + "-encoding", "utf-8", + ), + + // Licensing + homepage := Some(url("https://github.com/scala-js/scala-js-js-envs")), + startYear := Some(2013), + licenses += (("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0"))), + scmInfo := Some(ScmInfo( + url("https://github.com/scala-js/scala-js-js-envs"), + "scm:git:git@github.com:scala-js/scala-js-js-envs.git", + Some("scm:git:git@github.com:scala-js/scala-js-js-envs.git"))), + + // Publishing + publishMavenStyle := true, + publishTo := { + val nexus = "https://oss.sonatype.org/" + if (version.value.endsWith("-SNAPSHOT")) + Some("snapshots" at nexus + "content/repositories/snapshots") + else + Some("releases" at nexus + "service/local/staging/deploy/maven2") + }, + pomExtra := ( + + + sjrd + Sébastien Doeraene + https://github.com/sjrd/ + + + gzm0 + Tobias Schlatter + https://github.com/gzm0/ + + + nicolasstucki + Nicolas Stucki + https://github.com/nicolasstucki/ + + + ), + pomIncludeRepository := { _ => false }, +)) + +val commonSettings = Def.settings( + // Links to the JavaDoc do not work + Compile / doc / scalacOptions -= "-Xfatal-warnings", + + // Scaladoc linking + apiURL := { + val name = moduleName.value + val scalaBinVer = scalaBinaryVersion.value + val ver = version.value + Some(url(s"https://javadoc.io/doc/org.scala-js/${name}_$scalaBinVer/$ver/")) + }, + autoAPIMappings := true, + + // sbt-header configuration + headerLicense := Some(HeaderLicense.Custom( + s"""Scala.js JS Envs (${homepage.value.get}) + | + |Copyright EPFL. + | + |Licensed under Apache License 2.0 + |(https://www.apache.org/licenses/LICENSE-2.0). + | + |See the NOTICE file distributed with this work for + |additional information regarding copyright ownership. + |""".stripMargin + )), + + // MiMa auto-configuration + mimaPreviousArtifacts ++= { + val scalaV = scalaVersion.value + val scalaBinaryV = scalaBinaryVersion.value + val thisProjectID = projectID.value + previousVersion match { + case None => + Set.empty + case _ if newScalaBinaryVersionsInThisRelease.contains(scalaBinaryV) => + // New in this release, no binary compatibility to comply to + Set.empty + case Some(prevVersion) => + /* Filter out e:info.apiURL as it expects 1.1.1-SNAPSHOT, whereas the + * artifact we're looking for has 1.1.0 (for example). + */ + val prevExtraAttributes = + thisProjectID.extraAttributes.filterKeys(_ != "e:info.apiURL") + val prevProjectID = + (thisProjectID.organization % thisProjectID.name % prevVersion) + .cross(thisProjectID.crossVersion) + .extra(prevExtraAttributes.toSeq: _*) + Set(prevProjectID) + } + }, +) + +lazy val root = project + .in(file(".")) + +lazy val `scalajs-js-envs` = project + .in(file("js-envs")) + .settings( + commonSettings, + libraryDependencies ++= Seq( + "org.scala-js" %% "scalajs-logging" % "1.1.1", + "com.novocode" % "junit-interface" % "0.11" % "test", + ), + ) + +lazy val `scalajs-js-envs-test-kit` = project + .in(file("js-envs-test-kit")) + .settings( + commonSettings, + libraryDependencies ++= Seq( + "com.google.jimfs" % "jimfs" % "1.1", + "junit" % "junit" % "4.12", + "com.novocode" % "junit-interface" % "0.11" % "test" + ), + ) + .dependsOn(`scalajs-js-envs`) + +lazy val `scalajs-env-nodejs` = project + .in(file("nodejs-env")) + .settings( + commonSettings, + libraryDependencies ++= Seq( + "com.google.jimfs" % "jimfs" % "1.1", + "com.novocode" % "junit-interface" % "0.11" % "test" + ), + ) + .dependsOn(`scalajs-js-envs`, `scalajs-js-envs-test-kit` % "test") diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala index 1aebf7e..e42f0f7 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/ComTests.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala index 4f5c120..b296a0a 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuite.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala index de5ba11..3a5786d 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/JSEnvSuiteConfig.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala index 9a1a3ae..2abab5b 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala index a375769..e34adf6 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutComTests.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala index 67294a1..95dcff4 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/TimeoutRunTests.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala index 3a6d505..3b15bbe 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/ComRun.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala index e7a64ac..f5217f0 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/IOReader.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala index 7c8ffac..9825c76 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/MsgHandler.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala index 99b0f4c..75e23e1 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/Run.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala index 7049dad..196ab47 100644 --- a/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala +++ b/js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala index a9f4809..819d734 100644 --- a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala +++ b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestEnv.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * @@ -12,7 +12,7 @@ package org.scalajs.jsenv.test.kit -import scala.concurrent.Future +import scala.concurrent.{Future, Promise} import java.io._ import java.nio.charset.StandardCharsets @@ -27,11 +27,11 @@ private[kit] class TestEnv private ( // Interface for testing. - def withSuccess(): TestEnv = copy(result = Future.unit) + def withSuccess(): TestEnv = copy(result = Future.successful(())) def withFailure(t: Throwable): TestEnv = copy(result = Future.failed(t)) - def withHang(): TestEnv = copy(result = Future.never) + def withHang(): TestEnv = copy(result = Promise[Unit]().future) def withOutErr(s: String): TestEnv = { val bytes = s.getBytes(StandardCharsets.UTF_8) @@ -52,7 +52,7 @@ private[kit] class TestEnv private ( def withMsgs(msgs: String*): TestEnv = copy(msgs = msgs.toList) - private def this() = this(Future.unit, None, Nil) + private def this() = this(Future.successful(()), None, Nil) private def copy( result: Future[Unit] = result, @@ -63,13 +63,13 @@ private[kit] class TestEnv private ( val name: String = "TestEnv" - def start(input: Input, config: RunConfig): JSRun = { + def start(input: Seq[Input], config: RunConfig): JSRun = { require(msgs.isEmpty) callOnOutputStream(config) new TestRun } - def startWithCom(input: Input, config: RunConfig, onMessage: String => Unit): JSComRun = { + def startWithCom(input: Seq[Input], config: RunConfig, onMessage: String => Unit): JSComRun = { callOnOutputStream(config) msgs.foreach(onMessage) new TestRun with JSComRun { diff --git a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala index c098d21..ca436fd 100644 --- a/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala +++ b/js-envs-test-kit/src/test/scala/org/scalajs/jsenv/test/kit/TestKitTest.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala index cc034be..ae40578 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala index a3440e9..e97ad44 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/Input.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala index 1e7b400..c1b14d0 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSEnv.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala index 57303d6..2f54576 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSRuns.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala b/js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala index a9f933f..b140381 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/JSUtils.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala index 17d6f17..15af4db 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala index 163ec47..b57cc48 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala index c150eb6..31b514f 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala index c50fee5..dc968b7 100644 --- a/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala +++ b/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala index a3f1bf8..ede89e4 100644 --- a/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala +++ b/nodejs-env/src/test/scala/org/scalajs/jsenv/nodejs/NodeJSSuite.scala @@ -1,5 +1,5 @@ /* - * Scala.js (https://www.scala-js.org/) + * Scala.js JS Envs (https://github.com/scala-js/scala-js-js-envs) * * Copyright EPFL. * diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..654fe70 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.12 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..b20c845 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.7.0")