Skip to content

Commit e9d9677

Browse files
committed
Emit Scala.js JUnit bootstrappers for JUnit test classes.
This allows Scala.js for dotty to support normal JUnit tests.
1 parent bb5a921 commit e9d9677

File tree

5 files changed

+304
-1
lines changed

5 files changed

+304
-1
lines changed

.drone.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pipeline:
3131
image: lampepfl/dotty:2019-02-06
3232
commands:
3333
- cp -R . /tmp/2/ && cd /tmp/2/
34-
- ./project/scripts/sbt ";dotty-bootstrapped/compile ;dotty-bootstrapped/test; dotty-semanticdb/compile; dotty-semanticdb/test:compile;sjsSandbox/run"
34+
- ./project/scripts/sbt ";dotty-bootstrapped/compile ;dotty-bootstrapped/test; dotty-semanticdb/compile; dotty-semanticdb/test:compile;sjsSandbox/run;sjsSandbox/test"
3535
- ./project/scripts/bootstrapCmdTests
3636

3737
community_build:
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package dotty.tools.backend.sjs
2+
3+
import scala.annotation.tailrec
4+
5+
import dotty.tools.dotc._
6+
7+
import dotty.tools.dotc.core._
8+
import Constants._
9+
import Contexts._
10+
import Decorators._
11+
import Flags._
12+
import Names._
13+
import NameOps._
14+
import Phases._
15+
import Scopes._
16+
import Symbols._
17+
import StdNames._
18+
import Types._
19+
20+
import dotty.tools.dotc.transform.MegaPhase._
21+
22+
/** Generates JUnit bootstrapper classes for Scala.js. */
23+
class JUnitBootstrappers extends MiniPhase {
24+
import ast.tpd._
25+
26+
def phaseName: String = "junitBootstrappers"
27+
28+
override def isEnabled(implicit ctx: Context): Boolean =
29+
super.isEnabled && ctx.settings.scalajs.value
30+
31+
private object Names {
32+
val beforeClass: TermName = termName("beforeClass")
33+
val afterClass: TermName = termName("afterClass")
34+
val before: TermName = termName("before")
35+
val after: TermName = termName("after")
36+
val tests: TermName = termName("tests")
37+
val invokeTest: TermName = termName("invokeTest")
38+
val newInstance: TermName = termName("newInstance")
39+
40+
val instance: TermName = termName("instance")
41+
val name: TermName = termName("name")
42+
}
43+
44+
private class MyDefinitions()(implicit ctx: Context) {
45+
lazy val TestAnnotType: TypeRef = ctx.requiredClassRef("org.junit.Test")
46+
def TestAnnotClass(implicit ctx: Context): ClassSymbol = TestAnnotType.symbol.asClass
47+
48+
lazy val BeforeAnnotType: TypeRef = ctx.requiredClassRef("org.junit.Before")
49+
def BeforeAnnotClass(implicit ctx: Context): ClassSymbol = BeforeAnnotType.symbol.asClass
50+
51+
lazy val AfterAnnotType: TypeRef = ctx.requiredClassRef("org.junit.After")
52+
def AfterAnnotClass(implicit ctx: Context): ClassSymbol = AfterAnnotType.symbol.asClass
53+
54+
lazy val BeforeClassAnnotType: TypeRef = ctx.requiredClassRef("org.junit.BeforeClass")
55+
def BeforeClassAnnotClass(implicit ctx: Context): ClassSymbol = BeforeClassAnnotType.symbol.asClass
56+
57+
lazy val AfterClassAnnotType: TypeRef = ctx.requiredClassRef("org.junit.AfterClass")
58+
def AfterClassAnnotClass(implicit ctx: Context): ClassSymbol = AfterClassAnnotType.symbol.asClass
59+
60+
lazy val IgnoreAnnotType: TypeRef = ctx.requiredClassRef("org.junit.Ignore")
61+
def IgnoreAnnotClass(implicit ctx: Context): ClassSymbol = IgnoreAnnotType.symbol.asClass
62+
63+
lazy val BootstrapperType: TypeRef = ctx.requiredClassRef("org.scalajs.junit.Bootstrapper")
64+
65+
lazy val TestMetadataType: TypeRef = ctx.requiredClassRef("org.scalajs.junit.TestMetadata")
66+
67+
lazy val NoSuchMethodExceptionType: TypeRef = ctx.requiredClassRef("java.lang.NoSuchMethodException")
68+
69+
lazy val FutureType: TypeRef = ctx.requiredClassRef("scala.concurrent.Future")
70+
def FutureClass(implicit ctx: Context): ClassSymbol = FutureType.symbol.asClass
71+
72+
private lazy val FutureModule_successfulR = ctx.requiredModule("scala.concurrent.Future").requiredMethodRef("successful")
73+
def FutureModule_successful(implicit ctx: Context): Symbol = FutureModule_successfulR.symbol
74+
75+
private lazy val SuccessModule_applyR = ctx.requiredModule("scala.util.Success").requiredMethodRef(nme.apply)
76+
def SuccessModule_apply(implicit ctx: Context): Symbol = SuccessModule_applyR.symbol
77+
}
78+
79+
// The actual transform -------------------------------
80+
81+
override def transformPackageDef(tree: PackageDef)(implicit ctx: Context): Tree = {
82+
// TODO Can we cache this better?
83+
implicit val mydefn: MyDefinitions = new MyDefinitions()
84+
85+
@tailrec
86+
def hasTests(sym: ClassSymbol): Boolean = {
87+
sym.asClass.info.decls.exists(m => m.is(Method) && m.hasAnnotation(mydefn.TestAnnotClass)) ||
88+
sym.superClass.exists && hasTests(sym.superClass.asClass)
89+
}
90+
91+
def isTestClass(sym: Symbol): Boolean = {
92+
sym.isClass &&
93+
!sym.is(ModuleClass | Abstract | Trait) &&
94+
hasTests(sym.asClass)
95+
}
96+
97+
val bootstrappers = tree.stats.collect {
98+
case clDef: TypeDef if isTestClass(clDef.symbol) =>
99+
genBootstrapper(clDef.symbol.asClass)
100+
}
101+
102+
if (bootstrappers.isEmpty) tree
103+
else cpy.PackageDef(tree)(tree.pid, tree.stats ::: bootstrappers)
104+
}
105+
106+
private def genBootstrapper(testClass: ClassSymbol)(
107+
implicit ctx: Context, mydefn: MyDefinitions): TypeDef = {
108+
109+
val owner = testClass.owner
110+
val moduleSym = ctx.newCompleteModuleSymbol(owner,
111+
(testClass.name ++ "$scalajs$junit$bootstrapper").toTermName,
112+
Synthetic, Synthetic,
113+
List(defn.ObjectType, mydefn.BootstrapperType), newScope,
114+
coord = testClass.span, assocFile = testClass.assocFile).entered
115+
val classSym = moduleSym.moduleClass.asClass
116+
117+
val constr = genConstructor(classSym)
118+
119+
val testMethods = annotatedMethods(testClass, mydefn.TestAnnotClass)
120+
121+
val defs = List(
122+
genCallOnModule(classSym, Names.beforeClass, testClass.companionModule, mydefn.BeforeClassAnnotClass),
123+
genCallOnModule(classSym, Names.afterClass, testClass.companionModule, mydefn.AfterClassAnnotClass),
124+
genCallOnParam(classSym, Names.before, testClass, mydefn.BeforeAnnotClass),
125+
genCallOnParam(classSym, Names.after, testClass, mydefn.AfterAnnotClass),
126+
genTests(classSym, testMethods),
127+
genInvokeTest(classSym, testClass, testMethods),
128+
genNewInstance(classSym, testClass)
129+
)
130+
131+
if (ctx.sbtCallback != null)
132+
JUnitBootstrappers.ExtractAPI.createAndEmitSbtClassLike(classSym)
133+
134+
ClassDef(classSym, constr, defs)
135+
}
136+
137+
private def genConstructor(owner: ClassSymbol)(implicit ctx: Context): DefDef = {
138+
val sym = ctx.newConstructor(owner, Synthetic, Nil, Nil).entered
139+
DefDef(sym, {
140+
val objectType = defn.ObjectType
141+
Super(This(owner), nme.EMPTY.toTypeName, inConstrCall = true).select(defn.ObjectClass.primaryConstructor).appliedToNone
142+
})
143+
}
144+
145+
private def genCallOnModule(owner: ClassSymbol, name: TermName, module: Symbol, annot: Symbol)(implicit ctx: Context): DefDef = {
146+
val sym = ctx.newSymbol(owner, name, Synthetic | Method,
147+
MethodType(Nil, Nil, defn.UnitType)).entered
148+
149+
DefDef(sym, {
150+
if (module.exists) {
151+
val calls = annotatedMethods(module.moduleClass.asClass, annot)
152+
.map(m => Apply(ref(module).select(m), Nil))
153+
Block(calls, unitLiteral)
154+
} else {
155+
unitLiteral
156+
}
157+
})
158+
}
159+
160+
private def genCallOnParam(owner: ClassSymbol, name: TermName, testClass: ClassSymbol, annot: Symbol)(implicit ctx: Context): DefDef = {
161+
val sym = ctx.newSymbol(owner, name, Synthetic | Method,
162+
MethodType(Names.instance :: Nil, defn.ObjectType :: Nil, defn.UnitType)).entered
163+
164+
DefDef(sym, { (paramRefss: List[List[Tree]]) =>
165+
val List(List(instanceParamRef)) = paramRefss
166+
val calls = annotatedMethods(testClass, annot)
167+
.map(m => Apply(instanceParamRef.cast(testClass.typeRef).select(m), Nil))
168+
Block(calls, unitLiteral)
169+
})
170+
}
171+
172+
private def genTests(owner: ClassSymbol, tests: List[Symbol])(
173+
implicit ctx: Context, mydefn: MyDefinitions): DefDef = {
174+
175+
val sym = ctx.newSymbol(owner, Names.tests, Synthetic | Method,
176+
MethodType(Nil, defn.ArrayOf(mydefn.TestMetadataType))).entered
177+
178+
DefDef(sym, {
179+
val metadata = for (test <- tests) yield {
180+
val name = Literal(Constant(test.name.toString))
181+
val ignored = Literal(Constant(test.hasAnnotation(mydefn.IgnoreAnnotClass)))
182+
// TODO Handle @Test annotations with arguments
183+
// val reifiedAnnot = New(mydefn.TestAnnotType, test.getAnnotation(mydefn.TestAnnotClass).get.arguments)
184+
val testAnnot = test.getAnnotation(mydefn.TestAnnotClass).get
185+
if (testAnnot.arguments.nonEmpty)
186+
ctx.error("@Test annotations with arguments are not yet supported in Scala.js for dotty", testAnnot.tree.sourcePos)
187+
val noArgConstr = mydefn.TestAnnotType.member(nme.CONSTRUCTOR).suchThat(_.info.paramInfoss.head.isEmpty).symbol.asTerm
188+
val reifiedAnnot = New(mydefn.TestAnnotType, noArgConstr, Nil)
189+
New(mydefn.TestMetadataType, List(name, ignored, reifiedAnnot))
190+
}
191+
JavaSeqLiteral(metadata, TypeTree(mydefn.TestMetadataType))
192+
})
193+
}
194+
195+
private def genInvokeTest(owner: ClassSymbol, testClass: ClassSymbol, tests: List[Symbol])(
196+
implicit ctx: Context, mydefn: MyDefinitions): DefDef = {
197+
198+
val sym = ctx.newSymbol(owner, Names.invokeTest, Synthetic | Method,
199+
MethodType(List(Names.instance, Names.name), List(defn.ObjectType, defn.StringType), mydefn.FutureType)).entered
200+
201+
DefDef(sym, { (paramRefss: List[List[Tree]]) =>
202+
val List(List(instanceParamRef, nameParamRef)) = paramRefss
203+
tests.foldRight[Tree] {
204+
val tp = mydefn.NoSuchMethodExceptionType
205+
val constr = tp.member(nme.CONSTRUCTOR).suchThat { c =>
206+
c.info.paramInfoss.head.size == 1 &&
207+
c.info.paramInfoss.head.head.isRef(defn.StringClass)
208+
}.symbol.asTerm
209+
Throw(New(tp, constr, nameParamRef :: Nil))
210+
} { (test, next) =>
211+
If(Literal(Constant(test.name.toString)).select(defn.Any_equals).appliedTo(nameParamRef),
212+
genTestInvocation(testClass, test, instanceParamRef),
213+
next)
214+
}
215+
})
216+
}
217+
218+
private def genTestInvocation(testClass: ClassSymbol, testMethod: Symbol, instance: Tree)(
219+
implicit ctx: Context, mydefn: MyDefinitions): Tree = {
220+
221+
def castInstance = instance.cast(testClass.typeRef)
222+
223+
val resultType = testMethod.info.resultType
224+
if (resultType.isRef(defn.UnitClass)) {
225+
val newSuccess = ref(mydefn.SuccessModule_apply).appliedTo(ref(defn.BoxedUnit_UNIT))
226+
Block(
227+
castInstance.select(testMethod).appliedToNone :: Nil,
228+
ref(mydefn.FutureModule_successful).appliedTo(newSuccess)
229+
)
230+
} else if (resultType.isRef(mydefn.FutureClass)) {
231+
castInstance.select(testMethod).appliedToNone
232+
} else {
233+
// We lie in the error message to not expose that we support async testing.
234+
ctx.error("JUnit test must have Unit return type", testMethod.sourcePos)
235+
EmptyTree
236+
}
237+
}
238+
239+
private def genNewInstance(owner: ClassSymbol, testClass: ClassSymbol)(implicit ctx: Context): DefDef = {
240+
val sym = ctx.newSymbol(owner, Names.newInstance, Synthetic | Method,
241+
MethodType(Nil, defn.ObjectType)).entered
242+
243+
DefDef(sym, New(testClass.typeRef, Nil))
244+
}
245+
246+
private def castParam(param: Symbol, clazz: Symbol)(implicit ctx: Context): Tree =
247+
ref(param).cast(clazz.typeRef)
248+
249+
private def annotatedMethods(owner: ClassSymbol, annot: Symbol)(implicit ctx: Context): List[Symbol] =
250+
owner.info.decls.filter(m => m.is(Method) && m.hasAnnotation(annot))
251+
}
252+
253+
private object JUnitBootstrappers {
254+
private object ExtractAPI {
255+
import xsbti.api
256+
257+
def createAndEmitSbtClassLike(bootstrapperClass: ClassSymbol)(implicit ctx: Context): Unit = {
258+
// See computeClass() in ExtractAPI.scala
259+
val name = bootstrapperClass.fullName.stripModuleClassSuffix.toString
260+
val acc = api.Public.create()
261+
val mods = new api.Modifiers(false, false, /* final = */ true, false, false, false, false, false)
262+
val anns = Array[api.Annotation]()
263+
val defType = api.DefinitionType.Module
264+
def apiThis(sym: Symbol): api.Singleton = {
265+
val pathComponents = sym.ownersIterator.takeWhile(!_.isEffectiveRoot)
266+
.map(s => api.Id.of(s.name.toString))
267+
api.Singleton.of(api.Path.of(pathComponents.toArray.reverse ++ Array(api.This.create())))
268+
}
269+
val selfType = apiThis(bootstrapperClass)
270+
val structure = {
271+
// TODO the first parameter is wrong, it should contain java.lang.Object
272+
api.Structure.of(api.SafeLazy.strict(Array()), api.SafeLazy.strict(Array()), api.SafeLazy.strict(Array()))
273+
}
274+
val topLevel = true
275+
val childrenOfSealedClass = Array[api.Type]()
276+
val tparams = Array[api.TypeParameter]()
277+
val classLike =
278+
api.ClassLike.of(name, acc, mods, anns, defType, api.SafeLazy.strict(selfType), api.SafeLazy.strict(structure), Array[String](),
279+
childrenOfSealedClass, topLevel, tparams)
280+
ctx.sbtCallback.api(ctx.compilationUnit.source.file.file, classLike)
281+
}
282+
}
283+
}

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class Compiler {
112112
new ExpandPrivate, // Widen private definitions accessed from nested classes
113113
new RestoreScopes, // Repair scopes rendered invalid by moving definitions in prior phases of the group
114114
new SelectStatic, // get rid of selects that would be compiled into GetStatic
115+
new sjs.JUnitBootstrappers, // Generate JUnit-specific bootstrapper classes for Scala.js (not enabled by default)
115116
new CollectEntryPoints, // Find classes with main methods
116117
new CollectSuperCalls) :: // Find classes that are called with super
117118
Nil

project/Build.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,14 @@ object Build {
802802
},
803803
scalacOptions += "-scalajs",
804804

805+
libraryDependencies ~= {
806+
_.filter(!_.name.startsWith("junit-interface"))
807+
},
808+
//"com.novocode" % "junit-interface" % "0.11" % Test,
809+
810+
libraryDependencies +=
811+
"org.scala-js" % "scalajs-junit-test-runtime_2.12" % scalaJSVersion % "test",
812+
805813
// The main class cannot be found automatically due to the empty inc.Analysis
806814
mainClass in Compile := Some("hello.HelloWorld"),
807815

sandbox/scalajs/test/HelloTest.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package hello
2+
3+
import org.junit.Test
4+
import org.junit.Assert._
5+
6+
class HelloTest {
7+
@Test
8+
def simpleTest(): Unit = {
9+
assertEquals(1, 1)
10+
}
11+
}

0 commit comments

Comments
 (0)