Skip to content

Commit 2cc356e

Browse files
committed
WiP Emit Scala.js JUnit bootstrappers for JUnit test classes.
1 parent 52ad91c commit 2cc356e

File tree

5 files changed

+282
-0
lines changed

5 files changed

+282
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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 Phases._
14+
import Scopes._
15+
import Symbols._
16+
import StdNames._
17+
import Types._
18+
19+
import dotty.tools.dotc.transform.MegaPhase._
20+
21+
/** Generates JUnit bootstrapper classes for Scala.js. */
22+
class JUnitBootstrappers extends MiniPhase {
23+
import ast.tpd._
24+
25+
def phaseName: String = "junitBootstrappers"
26+
27+
override def isRunnable(implicit ctx: Context): Boolean =
28+
super.isRunnable && ctx.settings.scalajs.value
29+
30+
private object Names {
31+
val beforeClass: TermName = termName("beforeClass")
32+
val afterClass: TermName = termName("afterClass")
33+
val before: TermName = termName("before")
34+
val after: TermName = termName("after")
35+
val tests: TermName = termName("tests")
36+
val invokeTest: TermName = termName("invokeTest")
37+
val newInstance: TermName = termName("newInstance")
38+
39+
val instance: TermName = termName("instance")
40+
val name: TermName = termName("name")
41+
}
42+
43+
private class MyDefinitions()(implicit ctx: Context) {
44+
lazy val TestAnnotType: TypeRef = ctx.requiredClassRef("org.junit.Test")
45+
def TestAnnotClass(implicit ctx: Context): ClassSymbol = TestAnnotType.symbol.asClass
46+
47+
lazy val BeforeAnnotType: TypeRef = ctx.requiredClassRef("org.junit.Before")
48+
def BeforeAnnotClass(implicit ctx: Context): ClassSymbol = BeforeAnnotType.symbol.asClass
49+
50+
lazy val AfterAnnotType: TypeRef = ctx.requiredClassRef("org.junit.After")
51+
def AfterAnnotClass(implicit ctx: Context): ClassSymbol = AfterAnnotType.symbol.asClass
52+
53+
lazy val BeforeClassAnnotType: TypeRef = ctx.requiredClassRef("org.junit.BeforeClass")
54+
def BeforeClassAnnotClass(implicit ctx: Context): ClassSymbol = BeforeClassAnnotType.symbol.asClass
55+
56+
lazy val AfterClassAnnotType: TypeRef = ctx.requiredClassRef("org.junit.AfterClass")
57+
def AfterClassAnnotClass(implicit ctx: Context): ClassSymbol = AfterClassAnnotType.symbol.asClass
58+
59+
lazy val IgnoreAnnotType: TypeRef = ctx.requiredClassRef("org.junit.Ignore")
60+
def IgnoreAnnotClass(implicit ctx: Context): ClassSymbol = IgnoreAnnotType.symbol.asClass
61+
62+
lazy val BootstrapperType: TypeRef = ctx.requiredClassRef("org.scalajs.junit.Bootstrapper")
63+
def BootstrapperClass(implicit ctx: Context): ClassSymbol = BootstrapperType.symbol.asClass
64+
65+
lazy val TestMetadataType: TypeRef = ctx.requiredClassRef("org.scalajs.junit.TestMetadata")
66+
def TestMetadataClass(implicit ctx: Context): ClassSymbol = TestMetadataType.symbol.asClass
67+
68+
lazy val NoSuchMethodExceptionType: TypeRef = ctx.requiredClassRef("java.lang.NoSuchMethodException")
69+
70+
lazy val FutureType: TypeRef = ctx.requiredClassRef("scala.concurrent.Future")
71+
def FutureClass(implicit ctx: Context): ClassSymbol = FutureType.symbol.asClass
72+
73+
private lazy val FutureModule_successfulR = ctx.requiredModule("scala.concurrent.Future").requiredMethodRef("successful")
74+
def FutureModule_successful(implicit ctx: Context): Symbol = FutureModule_successfulR.symbol
75+
76+
private lazy val SuccessModule_applyR = ctx.requiredModule("scala.util.Success").requiredMethodRef(nme.apply)
77+
def SuccessModule_apply(implicit ctx: Context): Symbol = SuccessModule_applyR.symbol
78+
}
79+
80+
// The actual transform -------------------------------
81+
82+
override def transformPackageDef(tree: PackageDef)(implicit ctx: Context): Tree = {
83+
// TODO Can we cache this better?
84+
implicit val mydefn: MyDefinitions = new MyDefinitions()
85+
86+
@tailrec
87+
def hasTests(sym: ClassSymbol): Boolean = {
88+
sym.asClass.info.decls.exists(m => m.is(Method) && m.hasAnnotation(mydefn.TestAnnotClass)) ||
89+
sym.superClass.exists && hasTests(sym.superClass.asClass)
90+
}
91+
92+
def isTestClass(sym: Symbol): Boolean = {
93+
sym.isClass &&
94+
!sym.is(ModuleClass | Abstract | Trait) &&
95+
hasTests(sym.asClass)
96+
}
97+
98+
val bootstrappers = tree.stats.collect {
99+
case clDef: TypeDef if isTestClass(clDef.symbol) =>
100+
genBootstrapper(clDef.symbol.asClass)
101+
}
102+
103+
if (bootstrappers.isEmpty) tree
104+
else cpy.PackageDef(tree)(tree.pid, tree.stats ::: bootstrappers)
105+
}
106+
107+
private def genBootstrapper(testClass: ClassSymbol)(
108+
implicit ctx: Context, mydefn: MyDefinitions): TypeDef = {
109+
110+
val owner = testClass.owner
111+
val moduleSym = ctx.newCompleteModuleSymbol(owner,
112+
(testClass.name ++ "$scalajs$junit$bootstrapper").toTermName,
113+
Synthetic, Synthetic,
114+
List(defn.ObjectType, mydefn.BootstrapperType), newScope,
115+
coord = testClass.span, assocFile = testClass.assocFile).entered
116+
val classSym = moduleSym.moduleClass.asClass
117+
118+
val constr = genConstructor(classSym)
119+
120+
val testMethods = annotatedMethods(testClass, mydefn.TestAnnotClass)
121+
122+
val defs = List(
123+
genCallOnModule(classSym, Names.beforeClass, testClass.companionModule, mydefn.BeforeClassAnnotClass),
124+
genCallOnModule(classSym, Names.afterClass, testClass.companionModule, mydefn.AfterClassAnnotClass),
125+
genCallOnParam(classSym, Names.before, testClass, mydefn.BeforeAnnotClass),
126+
genCallOnParam(classSym, Names.after, testClass, mydefn.AfterAnnotClass),
127+
genTests(classSym, testMethods),
128+
genInvokeTest(classSym, testClass, testMethods),
129+
genNewInstance(classSym, testClass)
130+
)
131+
132+
ClassDef(classSym, constr, defs)
133+
}
134+
135+
private def genConstructor(owner: ClassSymbol)(implicit ctx: Context): DefDef = {
136+
val sym = ctx.newConstructor(owner, Synthetic, Nil, Nil).entered
137+
DefDef(sym, {
138+
val objectType = defn.ObjectType
139+
Super(This(owner), nme.EMPTY.toTypeName, inConstrCall = true).select(defn.ObjectClass.primaryConstructor).appliedToNone
140+
})
141+
}
142+
143+
private def genCallOnModule(owner: ClassSymbol, name: TermName, module: Symbol, annot: Symbol)(implicit ctx: Context): DefDef = {
144+
val sym = ctx.newSymbol(owner, name, Synthetic | Method,
145+
MethodType(Nil, Nil, defn.UnitType)).entered
146+
147+
DefDef(sym, {
148+
if (module.exists) {
149+
val calls = annotatedMethods(module.moduleClass.asClass, annot)
150+
.map(m => Apply(ref(module).select(m), Nil))
151+
Block(calls, unitLiteral)
152+
} else {
153+
unitLiteral
154+
}
155+
})
156+
}
157+
158+
private def genCallOnParam(owner: ClassSymbol, name: TermName, testClass: ClassSymbol, annot: Symbol)(implicit ctx: Context): DefDef = {
159+
val sym = ctx.newSymbol(owner, name, Synthetic | Method,
160+
MethodType(Names.instance :: Nil, defn.ObjectType :: Nil, defn.UnitType)).entered
161+
162+
DefDef(sym, { (paramRefss: List[List[Tree]]) =>
163+
val List(List(instanceParamRef)) = paramRefss
164+
val calls = annotatedMethods(testClass, annot)
165+
.map(m => Apply(instanceParamRef.cast(testClass.typeRef).select(m), Nil))
166+
Block(calls, unitLiteral)
167+
})
168+
}
169+
170+
private def genTests(owner: ClassSymbol, tests: List[Symbol])(
171+
implicit ctx: Context, mydefn: MyDefinitions): DefDef = {
172+
173+
val sym = ctx.newSymbol(owner, Names.tests, Synthetic | Method,
174+
MethodType(Nil, defn.ArrayOf(mydefn.TestMetadataType))).entered
175+
176+
DefDef(sym, {
177+
val metadata = for (test <- tests) yield {
178+
val name = Literal(Constant(test.name.toString))
179+
val ignored = Literal(Constant(test.hasAnnotation(mydefn.IgnoreAnnotClass)))
180+
//val reifiedAnnot = New(mydefn.TestAnnotType, test.getAnnotation(mydefn.TestAnnotClass).get.arguments)
181+
val reifiedAnnot = New(mydefn.TestAnnotType, mydefn.TestAnnotType.member(nme.CONSTRUCTOR).suchThat(_.info.paramInfoss.head.isEmpty).symbol.asTerm, Nil)
182+
New(mydefn.TestMetadataType, List(name, ignored, reifiedAnnot))
183+
}
184+
JavaSeqLiteral(metadata, TypeTree(mydefn.TestMetadataType))
185+
})
186+
}
187+
188+
private def genInvokeTest(owner: ClassSymbol, testClass: Symbol, tests: List[Symbol])(
189+
implicit ctx: Context, mydefn: MyDefinitions): DefDef = {
190+
191+
val sym = ctx.newSymbol(owner, Names.invokeTest, Synthetic | Method,
192+
MethodType(List(Names.instance, Names.name), List(defn.ObjectType, defn.StringType), mydefn.FutureType)).entered
193+
194+
DefDef(sym, { (paramRefss: List[List[Tree]]) =>
195+
val List(List(instanceParamRef, nameParamRef)) = paramRefss
196+
tests.foldRight[Tree] {
197+
val tp = mydefn.NoSuchMethodExceptionType
198+
val constr = tp.member(nme.CONSTRUCTOR).suchThat { c =>
199+
c.info.paramInfoss.head.size == 1 &&
200+
c.info.paramInfoss.head.head.isRef(defn.StringClass)
201+
}.symbol.asTerm
202+
Throw(New(tp, constr, nameParamRef :: Nil))
203+
} { (test, next) =>
204+
If(Literal(Constant(test.name.toString)).select(defn.Any_equals).appliedTo(nameParamRef),
205+
genTestInvocation(test, instanceParamRef),
206+
next)
207+
}
208+
})
209+
}
210+
211+
private def genTestInvocation(testMethod: Symbol, instance: Tree)(
212+
implicit ctx: Context, mydefn: MyDefinitions): Tree = {
213+
214+
val resultType = testMethod.info.resultType
215+
if (resultType.isRef(defn.UnitClass)) {
216+
val newSuccess = ref(mydefn.SuccessModule_apply).appliedTo(ref(defn.BoxedUnit_UNIT))
217+
Block(
218+
instance.select(testMethod).appliedToNone :: Nil,
219+
ref(mydefn.FutureModule_successful).appliedTo(newSuccess)
220+
)
221+
} else if (resultType.isRef(mydefn.FutureClass)) {
222+
instance.select(testMethod).appliedToNone
223+
} else {
224+
// We lie in the error message to not expose that we support async testing.
225+
ctx.error("JUnit test must have Unit return type", testMethod.sourcePos)
226+
EmptyTree
227+
}
228+
}
229+
230+
private def genNewInstance(owner: ClassSymbol, testClass: ClassSymbol)(implicit ctx: Context): DefDef = {
231+
val sym = ctx.newSymbol(owner, Names.tests, Synthetic | Method,
232+
MethodType(Nil, defn.ObjectType)).entered
233+
234+
DefDef(sym, New(testClass.typeRef, Nil))
235+
}
236+
237+
private def castParam(param: Symbol, clazz: Symbol)(implicit ctx: Context): Tree =
238+
ref(param).cast(clazz.typeRef)
239+
240+
private def annotatedMethods(owner: ClassSymbol, annot: Symbol)(implicit ctx: Context): List[Symbol] =
241+
owner.info.decls.filter(m => m.is(Method) && m.hasAnnotation(annot))
242+
}

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/src/hello.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package hello
22

33
import scala.scalajs.js
44

5+
@scala.scalajs.reflect.annotation.EnableReflectiveInstantiation
6+
class Foo
7+
58
trait MyTrait {
69
val x = 5
710
def foo(y: Int) = x
@@ -11,5 +14,14 @@ object HelloWorld extends MyTrait {
1114
def main(args: Array[String]): Unit = {
1215
println("hello dotty.js!")
1316
println(foo(4))
17+
println(classOf[scala.runtime.BoxedUnit])
18+
println(().getClass)
19+
20+
val optCls = scala.scalajs.reflect.Reflect.lookupInstantiatableClass("hello.Foo")
21+
println(optCls)
22+
for (cls <- optCls) {
23+
val obj = cls.newInstance()
24+
println(obj)
25+
}
1426
}
1527
}

sandbox/scalajs/test/HelloTest.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package hello
2+
3+
import org.junit.Test
4+
import org.junit.Assert._
5+
6+
object Foo {
7+
final val bar = 0L
8+
}
9+
10+
class HelloTest {
11+
@Test(classOf[ArithmeticException], Foo.bar)
12+
def simpleTest(): Unit = {
13+
assertEquals(1, 1)
14+
}
15+
}
16+
17+
object HelloTestBootstrap {
18+
val t = new Test(classOf[ArithmeticException])
19+
}

0 commit comments

Comments
 (0)