Skip to content

Emit Scala.js JUnit bootstrappers for JUnit test classes. #6304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 3, 2019

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Apr 15, 2019

This allows Scala.js for dotty to support normal JUnit tests.

See the equivalent scalac phase at https://github.com/scala-js/scala-js/blob/v1.0.0-M7/junit-plugin/src/main/scala/org/scalajs/junit/plugin/ScalaJSJUnitPlugin.scala

Copy link
Member

@dottybot dottybot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, and thank you for opening this PR! 🎉

All contributors have signed the CLA, thank you! ❤️

Commit Messages

We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).

Please stick to these guidelines for commit messages:

  1. Separate subject from body with a blank line
  2. When fixing an issue, start your commit message with Fix #<ISSUE-NBR>:
  3. Limit the subject line to 72 characters
  4. Capitalize the subject line
  5. Do not end the subject line with a period
  6. Use the imperative mood in the subject line ("Add" instead of "Added")
  7. Wrap the body at 80 characters
  8. Use the body to explain what and why vs. how

adapted from https://chris.beams.io/posts/git-commit

Have an awesome day! ☀️

@sjrd
Copy link
Member Author

sjrd commented Apr 15, 2019

@smarter Currently I get the following crash somewhere in sbt when I try sjsSandbox/test:compile:

sbt:dotty> sjsSandbox/test:compile
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] Compiling 1 Scala source to /localhome/doeraene/projects/dotty/sandbox/scalajs/../out/bootstrap/sjsSandbox/scala-0.15/test-classes ...
[info] Done compiling.
[error] ## Exception when compiling 1 sources to /localhome/doeraene/projects/dotty/sandbox/scalajs/../out/bootstrap/sjsSandbox/scala-0.15/test-classes
[error] Failed to find name hashes for hello.HelloTest$scalajs$junit$bootstrapper
[error] scala.sys.package$.error(package.scala:26)
[error] sbt.internal.inc.AnalysisCallback.nameHashesForCompanions(Compile.scala:337)
[error] sbt.internal.inc.AnalysisCallback.analyzeClass(Compile.scala:344)
[error] sbt.internal.inc.AnalysisCallback.$anonfun$addProductsAndDeps$4(Compile.scala:362)
[error] scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:233)
[error] scala.collection.mutable.HashSet.foreach(HashSet.scala:77)
[error] scala.collection.TraversableLike.map(TraversableLike.scala:233)
[error] scala.collection.TraversableLike.map$(TraversableLike.scala:226)
[error] scala.collection.mutable.AbstractSet.scala$collection$SetLike$$super$map(Set.scala:46)
[error] scala.collection.SetLike.map(SetLike.scala:100)
[error] scala.collection.SetLike.map$(SetLike.scala:100)
[error] scala.collection.mutable.AbstractSet.map(Set.scala:46)
[error] sbt.internal.inc.AnalysisCallback.$anonfun$addProductsAndDeps$1(Compile.scala:362)
[error] scala.collection.TraversableOnce.$anonfun$foldLeft$1(TraversableOnce.scala:156)
[error] scala.collection.TraversableOnce.$anonfun$foldLeft$1$adapted(TraversableOnce.scala:156)
[error] scala.collection.mutable.HashSet.foreach(HashSet.scala:77)
[error] scala.collection.TraversableOnce.foldLeft(TraversableOnce.scala:156)
[error] scala.collection.TraversableOnce.foldLeft$(TraversableOnce.scala:154)
[error] scala.collection.AbstractTraversable.foldLeft(Traversable.scala:104)
[error] scala.collection.TraversableOnce.$div$colon(TraversableOnce.scala:150)
[error] scala.collection.TraversableOnce.$div$colon$(TraversableOnce.scala:150)
[error] scala.collection.AbstractTraversable.$div$colon(Traversable.scala:104)
[error] sbt.internal.inc.AnalysisCallback.addProductsAndDeps(Compile.scala:358)
[error] sbt.internal.inc.AnalysisCallback.get(Compile.scala:302)
[error] sbt.internal.inc.Incremental$.doCompile(Incremental.scala:107)
[error] sbt.internal.inc.Incremental$.$anonfun$compile$4(Incremental.scala:87)
[error] sbt.internal.inc.IncrementalCommon.recompileClasses(IncrementalCommon.scala:116)
[error] sbt.internal.inc.IncrementalCommon.cycle(IncrementalCommon.scala:63)
[error] sbt.internal.inc.Incremental$.$anonfun$compile$3(Incremental.scala:89)
[error] sbt.internal.inc.Incremental$.manageClassfiles(Incremental.scala:134)
[error] sbt.internal.inc.Incremental$.compile(Incremental.scala:80)
[error] sbt.internal.inc.IncrementalCompile$.apply(Compile.scala:67)
[error] sbt.internal.inc.IncrementalCompilerImpl.compileInternal(IncrementalCompilerImpl.scala:311)
[error] sbt.internal.inc.IncrementalCompilerImpl.$anonfun$compileIncrementally$1(IncrementalCompilerImpl.scala:269)
[error] sbt.internal.inc.IncrementalCompilerImpl.handleCompilationError(IncrementalCompilerImpl.scala:159)
[error] sbt.internal.inc.IncrementalCompilerImpl.compileIncrementally(IncrementalCompilerImpl.scala:238)
[error] sbt.internal.inc.IncrementalCompilerImpl.compile(IncrementalCompilerImpl.scala:69)
[error] sbt.Defaults$.compileIncrementalTaskImpl(Defaults.scala:1549)
[error] sbt.Defaults$.$anonfun$compileIncrementalTask$1(Defaults.scala:1523)
[error] scala.Function1.$anonfun$compose$1(Function1.scala:44)
[error] sbt.internal.util.$tilde$greater.$anonfun$$u2219$1(TypeFunctions.scala:40)
[error] sbt.std.Transform$$anon$4.work(System.scala:67)
[error] sbt.Execute.$anonfun$submit$2(Execute.scala:269)
[error] sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:16)
[error] sbt.Execute.work(Execute.scala:278)
[error] sbt.Execute.$anonfun$submit$1(Execute.scala:269)
[error] sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:178)
[error] sbt.CompletionService$$anon$2.call(CompletionService.scala:37)
[error] java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error] java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[error] java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error] java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[error] java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[error] java.lang.Thread.run(Thread.java:748)
[error]            
[error] java.lang.RuntimeException: Failed to find name hashes for hello.HelloTest$scalajs$junit$bootstrapper
[error]         at scala.sys.package$.error(package.scala:26)
[error]         at sbt.internal.inc.AnalysisCallback.nameHashesForCompanions(Compile.scala:337)
[error]         at sbt.internal.inc.AnalysisCallback.analyzeClass(Compile.scala:344)
[error]         at sbt.internal.inc.AnalysisCallback.$anonfun$addProductsAndDeps$4(Compile.scala:362)
[error]         at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:233)
[error]         at scala.collection.mutable.HashSet.foreach(HashSet.scala:77)
[error]         at scala.collection.TraversableLike.map(TraversableLike.scala:233)
[error]         at scala.collection.TraversableLike.map$(TraversableLike.scala:226)
[error]         at scala.collection.mutable.AbstractSet.scala$collection$SetLike$$super$map(Set.scala:46)
[error]         at scala.collection.SetLike.map(SetLike.scala:100)
[error]         at scala.collection.SetLike.map$(SetLike.scala:100)
[error]         at scala.collection.mutable.AbstractSet.map(Set.scala:46)
[error]         at sbt.internal.inc.AnalysisCallback.$anonfun$addProductsAndDeps$1(Compile.scala:362)
[error]         at scala.collection.TraversableOnce.$anonfun$foldLeft$1(TraversableOnce.scala:156)
[error]         at scala.collection.TraversableOnce.$anonfun$foldLeft$1$adapted(TraversableOnce.scala:156)
[error]         at scala.collection.mutable.HashSet.foreach(HashSet.scala:77)
[error]         at scala.collection.TraversableOnce.foldLeft(TraversableOnce.scala:156)
[error]         at scala.collection.TraversableOnce.foldLeft$(TraversableOnce.scala:154)
[error]         at scala.collection.AbstractTraversable.foldLeft(Traversable.scala:104)
[error]         at scala.collection.TraversableOnce.$div$colon(TraversableOnce.scala:150)
[error]         at scala.collection.TraversableOnce.$div$colon$(TraversableOnce.scala:150)
[error]         at scala.collection.AbstractTraversable.$div$colon(Traversable.scala:104)
[error]         at sbt.internal.inc.AnalysisCallback.addProductsAndDeps(Compile.scala:358)
[error]         at sbt.internal.inc.AnalysisCallback.get(Compile.scala:302)
[error]         at sbt.internal.inc.Incremental$.doCompile(Incremental.scala:107)
[error]         at sbt.internal.inc.Incremental$.$anonfun$compile$4(Incremental.scala:87)
[error]         at sbt.internal.inc.IncrementalCommon.recompileClasses(IncrementalCommon.scala:116)
[error]         at sbt.internal.inc.IncrementalCommon.cycle(IncrementalCommon.scala:63)
[error]         at sbt.internal.inc.Incremental$.$anonfun$compile$3(Incremental.scala:89)
[error]         at sbt.internal.inc.Incremental$.manageClassfiles(Incremental.scala:134)
[error]         at sbt.internal.inc.Incremental$.compile(Incremental.scala:80)
[error]         at sbt.internal.inc.IncrementalCompile$.apply(Compile.scala:67)
[error]         at sbt.internal.inc.IncrementalCompilerImpl.compileInternal(IncrementalCompilerImpl.scala:311)
[error]         at sbt.internal.inc.IncrementalCompilerImpl.$anonfun$compileIncrementally$1(IncrementalCompilerImpl.scala:269)
[error]         at sbt.internal.inc.IncrementalCompilerImpl.handleCompilationError(IncrementalCompilerImpl.scala:159)
[error]         at sbt.internal.inc.IncrementalCompilerImpl.compileIncrementally(IncrementalCompilerImpl.scala:238)
[error]         at sbt.internal.inc.IncrementalCompilerImpl.compile(IncrementalCompilerImpl.scala:69)
[error]         at sbt.Defaults$.compileIncrementalTaskImpl(Defaults.scala:1549)
[error]         at sbt.Defaults$.$anonfun$compileIncrementalTask$1(Defaults.scala:1523)
[error]         at scala.Function1.$anonfun$compose$1(Function1.scala:44)
[error]         at sbt.internal.util.$tilde$greater.$anonfun$$u2219$1(TypeFunctions.scala:40)
[error]         at sbt.std.Transform$$anon$4.work(System.scala:67)
[error]         at sbt.Execute.$anonfun$submit$2(Execute.scala:269)
[error]         at sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:16)
[error]         at sbt.Execute.work(Execute.scala:278)
[error]         at sbt.Execute.$anonfun$submit$1(Execute.scala:269)
[error]         at sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:178)
[error]         at sbt.CompletionService$$anon$2.call(CompletionService.scala:37)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[error]         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[error]         at java.lang.Thread.run(Thread.java:748)
[error] (sjsSandbox / Test / compileIncremental) Failed to find name hashes for hello.HelloTest$scalajs$junit$bootstrapper
[error] Total time: 1 s, completed Apr 15, 2019 11:55:18 AM

IIUC, this happens because the new junitBootstrappers phase creates a new named (module) class late in the pipeline, and some data structure is not filled up with "name hashes" for those classes, which then confuses sbt.

Any idea how I can fix that?

Note that the strategy of creating those classes late is not new. We use exactly the same strategy in scalac, and no similar issue has popped up.

@smarter
Copy link
Member

smarter commented Apr 15, 2019

No idea. Can you describe what the phase your adding does in more details? Does it end up creating classfiles ? Does Scala code refers to the symbols in these classfiles somehow ? How does sbt incremental compilation handles deletion of these classfiles in Scala 2 ?

@sjrd
Copy link
Member Author

sjrd commented Apr 15, 2019

The phase the following thing: for each class with at least one method having a JUnit-related annotation, e.g.,

class FooTest {
  @Test
  def thisIsATest(): Unit = ...
}

it generates an accompanying module that looks like

object FooTest$scalajs$junit$bootstrapper extends org.scalajs.junit.Bootstrapper {
  def tests(): Array[org.scalajs.junit.TestMetadata] =
    Array(new org.scalajs.junit.TestMetadata("thisIsATest", new org.junit.Test()))

  def invokeTest(instance: Object, name: String): Unit =
    if ("thisIsATest" == name) instance.asInstanceOf[FooTest].thisIsATest()
    else throw new NoSuchMethodError(name)
}

That module is then emitted by the back-end, and hence it emits an .sjsir file and then a class file, yes.

No Scala code ever refers to the generated code. It is only instantiated through Scala.js' run-time reflection mechanism.

In Scala 2, I believe learns when to delete that .class file because the back-end records what compilation unit emits what .class file, and so when it recompiles a .scala file (or finds that a .scala file is deleted), it deletes all associated .class files.

@smarter
Copy link
Member

smarter commented Apr 15, 2019

In Scala 2, I believe learns when to delete that .class file because the back-end records what compilation unit emits what .class file, and so when it recompiles a .scala file (or finds that a .scala file is deleted), it deletes all associated .class files.

Dotty does the same, and off-topic for this PR but I think this won't be the case in sbt 1.3 / zinc 1.3 anymore due to sbt/zinc#582 moving the call to generatedNonLocalClass much earlier in the pipeline, /cc @jvican to confirm.

@sjrd sjrd force-pushed the scalajs-junit-tests branch from 2cc356e to ea4cc66 Compare April 15, 2019 11:15
@smarter
Copy link
Member

smarter commented Apr 15, 2019

I've looked into this a bit more and I really can't figure out why this doesn't crash in the exact same way in scalac. I guess we could add a call to ctx.sbtCallback.api with a dummy ClassLike which has the right name to work around this, or move generatedNonLocalClass earlier in the pipeline like zinc 1.3 does, although that means the classfiles won't be tracked correctly by sbt.

@sjrd sjrd force-pushed the scalajs-junit-tests branch 3 times, most recently from e376e2a to e9d9677 Compare April 16, 2019 06:57
@sjrd sjrd changed the title WiP Emit Scala.js JUnit bootstrappers for JUnit test classes. Emit Scala.js JUnit bootstrappers for JUnit test classes. Apr 16, 2019
@sjrd sjrd marked this pull request as ready for review April 16, 2019 06:57
@sjrd sjrd requested review from smarter and nicolasstucki April 16, 2019 06:58
@sjrd sjrd force-pushed the scalajs-junit-tests branch from e9d9677 to 90172ea Compare April 16, 2019 08:44
@jvican
Copy link
Member

jvican commented Apr 16, 2019

Dotty does the same, and off-topic for this PR but I think this won't be the case in sbt 1.3 / zinc 1.3 anymore due to sbt/zinc#582 moving the call to generatedNonLocalClass much earlier in the pipeline, /cc @jvican to confirm.

This is correct and it's important to have a list of all generated class files for the incremental compiler to work correctly. Generated class files can be used to know which class files need to be moved from one classes directory to another one and to disable the invalidation of previous symbols based on the relative path of the class file.

@sjrd
Copy link
Member Author

sjrd commented Apr 16, 2019

@jvican It seems that, in that case, it is now impossible for any compiler plugin that generates additional classes to be correct wrt incremental compilation. Should there be an API for compiler plugins to advertise what .class files they will generate, at the very least?

val name: TermName = termName("name")
}

private class MyDefinitions()(implicit ctx: Context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this stuff go into JSDefinitions instead ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now it could, but this phase really should go into a separate compiler plugin at some point. For scalac it is in its own nsc plugin. The reason is that you only want to enable it on a) the test configuration and b) only if you actually use JUnit tests; otherwise, it's a lot of work done for nothing.

So basically I wrote it in the compiler right now, but we should extract it in a compiler plugin at some point, and therefore I wrote it in a way that does not invade the rest of the codebase.

Copy link
Member

@smarter smarter Apr 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the compiler already has to handle JUnit tests in an earlier phase: https://github.com/lampepfl/dotty/blob/1c3b45a3d292fb97a3316cb3e6b118b2966d4d95/compiler/src/dotty/tools/dotc/transform/MixinOps.scala#L66
So it could persist this information somewhere to make this phase a no-op if the current compilation unit doesn't have any JUnit test. (And I'm not too worried about JUnit tests in non-test configuration, that's an edge-case).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsJUnit4Fix is only called for methods in traits, as a last resort of whether they need a mixin forwarder or not. Relying on this mechanism to store the information would miss a lot of methods, including @Test methods in classes, which are obviously the common case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still like to have these definitions in JSDefinitions since it's the simplest way to make sure they're only computed once, perhaps in their own inner object to keep them separate from the rest.


import dotty.tools.dotc.transform.MegaPhase._

/** Generates JUnit bootstrapper classes for Scala.js. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moar documentation of what is being generated and why please 🙇‍♂️

@sjrd
Copy link
Member Author

sjrd commented Apr 17, 2019

Documentation added.

@nicolasstucki nicolasstucki requested review from smarter and removed request for nicolasstucki April 17, 2019 09:28
@sjrd sjrd force-pushed the scalajs-junit-tests branch 3 times, most recently from cfe5ba7 to 54d6d30 Compare April 17, 2019 12:05
val name: TermName = termName("name")
}

private class MyDefinitions()(implicit ctx: Context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still like to have these definitions in JSDefinitions since it's the simplest way to make sure they're only computed once, perhaps in their own inner object to keep them separate from the rest.


DefDef(sym, {
val metadata = for (test <- tests) yield {
val name = Literal(Constant(test.name.toString))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct with respect to name-mangling ? More tests please :).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More tests will come from upstream Scala.js, notably coming from https://github.com/scala-js/scala-js/tree/v1.0.0-M7/test-suite/js/src/test/scala/org/scalajs/testsuite/junit, once #6365 is merged and I get to enable those tests. See for example JUnitNamesTest for the name-mangling aspect.

@sjrd sjrd force-pushed the scalajs-junit-tests branch from 775d5b8 to 6871ce6 Compare April 30, 2019 14:12
Copy link
Member Author

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with a new commit that addresses the review. Except the originalOwner thing as I'd like your input on where to factor it out.


DefDef(sym, {
val metadata = for (test <- tests) yield {
val name = Literal(Constant(test.name.toString))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More tests will come from upstream Scala.js, notably coming from https://github.com/scala-js/scala-js/tree/v1.0.0-M7/test-suite/js/src/test/scala/org/scalajs/testsuite/junit, once #6365 is merged and I get to enable those tests. See for example JUnitNamesTest for the name-mangling aspect.

@sjrd sjrd force-pushed the scalajs-junit-tests branch from 3433e3f to 9f6d249 Compare April 30, 2019 16:28
sjrd added 2 commits May 2, 2019 10:44
…ngClass.

The logic in `originalOwner` was not only looking up the original
owner, but also going further to the enclosing class of that owner,
so the name was pretty misleading.

This commit separates the two operations in two methods. The actual
`originalOwner` half of it is extracted in `SymDenotations`, as it
will be needed by the Scala.js back-end as well.
@sjrd sjrd force-pushed the scalajs-junit-tests branch from 9f6d249 to d51356b Compare May 2, 2019 08:50
This allows Scala.js for dotty to support normal JUnit tests.
@sjrd sjrd force-pushed the scalajs-junit-tests branch from 61bb2c4 to 16bd814 Compare May 2, 2019 09:18
@sjrd
Copy link
Member Author

sjrd commented May 2, 2019

Updated. I believe I've addressed all the comments.

Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise LGTM.

@sjrd
Copy link
Member Author

sjrd commented May 3, 2019

@smarter Hum you said "Otherwise" but there isn't anything associated with that. Did you forget to push a "Comment" button at some point? Or is it fine to merge as is?

Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, lost comment somehow, here it is.

@sjrd sjrd merged commit e1385a3 into scala:master May 3, 2019
@sjrd sjrd deleted the scalajs-junit-tests branch May 3, 2019 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants