scala-js / scala-js-env-phantomjs

PhantomJS environment for Scala.js
BSD 3-Clause "New" or "Revised" License
2 stars 3 forks source link

Exceptions escape and crash the entire jsEnv #47

Open japgolly opened 4 years ago

japgolly commented 4 years ago

It's perfectly, completely and deterministically reproducible in my private code that the new SJS 1.0 PhantomJs jsEnv doesn't catch exceptions in some cases. I haven't been able to work out what "some cases" means exactly. I've got code that looks something like this:

      def x(f: => Any) = try Right(f) catch { case e: Throwable => Left(e) }
      x(???)

and instead of the error my test is expecting being caught, it seems to be intercepted by PhantomJs and causes this:

scala.MatchError: 2 (of class java.lang.Byte)

  phantomjs://code/phantomjs-launcher8807894677851635821.js:8 in onError

  phantomjs://code/phantomjs-launcher8807894677851635821.js:10 in onError
  file:///tmp/tmp-9624954527195675435webapp-base-test-test-fastopt.js:396685 (in function "$p_jl_JSConsoleBasedPrintStream__doWriteLine__T__V")

  phantomjs://code/phantomjs-launcher8807894677851635821.js:12
[error] stack trace is suppressed; run last Test / testOnly for the full output
[error] (Test / testOnly) org.scalajs.testing.common.RPCCore$ClosedException: org.scalajs.testing.adapter.JSEnvRPC$RunTerminatedException
[error] Total time: 9 s, completed 9 Jun. 2020, 9:03:20 pm

We actually went over this ages ago in https://github.com/scala-js/scala-js/issues/1555 and I came away with a local hack that avoided the problem for me consistently for years, until now.

japgolly commented 4 years ago

The fix is super easy so I've raised #48 .

japgolly commented 4 years ago

For reference: the fix I used to have is:

class PhantomJS2Env(c: PhantomJSEnv.Config) extends PhantomJSEnv(c) {

  override protected def vmName: String = "PhantomJS2"

  private val consoleNuker = new MemVirtualJSFile("consoleNuker.js")
    .withContent("console.error = console.log;")

  override protected def customInitFiles(): Seq[VirtualJSFile] =
    super.customInitFiles() :+ consoleNuker
}

which doesn't work anymore because PhantomJSEnv is final (which is a good thing).

gzm0 commented 4 years ago

The workaround could now be written as:

final class PhantomJS2Env(config: PhantomJSEnv.Config) extends JSEnv {

  private val innerEnv = new PhantomJSEnv(c)
  val name: String = "PhantomJS2"

  private val consoleNuker: Input = {
    val p = Files.write(
        Jimfs.newFileSystem().getPath("consoleNuker.js"),
        "console.error = console.log;".getBytes(StandardCharsets.UTF_8))
    Input.Script(p)
  }

  def start(input: Seq[Input], config: RunConfig): JSRun =
    innerEnv.start(consoleNuker :: input, config)

  def startWithCom(input: Seq[Input], config: RunConfig,
      onMessage: String => Unit): JSComRun =
    innerEnv.startWithCom(consoleNuker :: input, config, onMessage)
}
japgolly commented 4 years ago

Oh wow thanks @gzm0 ! I didn't think of doing that. Actually I came up with my own hack too :)

project/phantomjs-fix.js

console.error = console.log;

and then

Test / jsEnvInput := Input.Script(((ThisBuild / baseDirectory).value / "project/phantomjs-fix.js").toPath) +: (Test / jsEnvInput).value)