Skip to content
This repository was archived by the owner on Jul 12, 2024. It is now read-only.

Setup a testing infrastructure #7

Merged
merged 2 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,19 @@ $ deno run --allow-read run.mjs
- The WasmGC reference interpreter can be used to validate and convert between the binary and text form:
- https://github.com/WebAssembly/gc/tree/main/interpreter
- Use docker image for it https://github.com/tanishiking/wasmgc-docker

### Testing

Requires NodeJS >= 22 (for enough support of WasmGC).

```sh
$ NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly nvm install v22
```

- `tests/test` will
- Run `testSuite/run` to compile the Scala code under `test-suite` to WebAssembly
- Run the WebAssembly binary using NodeJS
- Each Scala program in `test-suite` should have a function that has no arguments and return a Boolean value. The test passes if the function returns `true`.
- When you add a test,
- Add a file under `test-suite`
- Add a test case to `cli/src/main/scala/TestSuites.scala` (`methodName` should be a exported function name).
59 changes: 50 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ lazy val cli = project
scalaJSUseMainModuleInitializer := true,
scalaJSLinkerConfig ~= {
_.withModuleKind(ModuleKind.CommonJSModule),
}
},
libraryDependencies ++= Seq(
"org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1"
),
)
.dependsOn(
wasm,
// tests // for TestSuites constant
)
.dependsOn(wasm)

lazy val wasm = project
.in(file("wasm"))
Expand All @@ -21,7 +27,6 @@ lazy val wasm = project
version := "0.1.0-SNAPSHOT",
scalaVersion := scalaV,
libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "0.7.29" % Test,
"org.scala-js" %%% "scalajs-linker" % "1.15.0"
),
scalaJSUseMainModuleInitializer := true,
Expand All @@ -41,14 +46,50 @@ lazy val sample = project
import org.scalajs.jsenv.nodejs.NodeJSEnv
val cp = Attributed
.data((Compile / fullClasspath).value)
// .filter { path =>
// val pathStr = path.toString()
// println(pathStr)
// pathStr.contains("sample/target")
// }
.mkString(";")
val env = Map("SCALAJS_CLASSPATH" -> cp, "SCALAJS_MODE" -> "sample")
val env = Map(
"SCALAJS_CLASSPATH" -> cp,
"SCALAJS_MODE" -> "sample",
)
new NodeJSEnv(NodeJSEnv.Config().withEnv(env).withArgs(List("--enable-source-maps")))
},
Compile / jsEnvInput := (`cli` / Compile / jsEnvInput).value
)

lazy val testSuite = project
.in(file("test-suite"))
.enablePlugins(ScalaJSPlugin)
.settings(
scalaVersion := scalaV,
scalaJSUseMainModuleInitializer := true,
Compile / jsEnv := {
import org.scalajs.jsenv.nodejs.NodeJSEnv
val cp = Attributed
.data((Compile / fullClasspath).value)
.mkString(";")
val env = Map(
"SCALAJS_CLASSPATH" -> cp,
"SCALAJS_MODE" -> "testsuite",
)
new NodeJSEnv(NodeJSEnv.Config().withEnv(env).withArgs(List("--enable-source-maps")))
},
Compile / jsEnvInput := (`cli` / Compile / jsEnvInput).value
)

lazy val tests = project
.in(file("tests"))
.enablePlugins(ScalaJSPlugin)
.settings(
scalaVersion := scalaV,
libraryDependencies ++= Seq(
"org.scalameta" %%% "munit" % "0.7.29" % Test,
"org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1" % Test
),
scalaJSLinkerConfig ~= {
_.withModuleKind(ModuleKind.CommonJSModule),
},
test := Def.sequential(
(testSuite / Compile / run).toTask(""),
(Test / test)
).value
).dependsOn(cli)
57 changes: 45 additions & 12 deletions cli/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,68 @@ package cli

import scala.scalajs.js

import scala.concurrent.ExecutionContext
import wasm.Compiler

import org.scalajs.linker.interface.ModuleInitializer
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._

import scala.concurrent.Future

object Main {
private implicit val ec: ExecutionContext = ExecutionContext.global
def main(args: Array[String]): Unit = {
val modeEnvVar = js.Dynamic.global.process.env.SCALAJS_MODE

val cpEnvVar = js.Dynamic.global.process.env.SCALAJS_CLASSPATH

val classpath = (cpEnvVar: Any) match {
case cpEnvVar: String if cpEnvVar != "" =>
cpEnvVar.split(';').toList
case _ =>
throw new IllegalArgumentException("The classpath was not provided.")
}
println(classpath)

val result = for {
irFiles <- new CliReader(classpath).irFiles
_ <- Compiler.compileIRFiles(irFiles)
} yield {
println("Module successfully initialized")
()

val mode = (modeEnvVar: Any) match {
case modeEnvVar if modeEnvVar == "testsuite" => "testsuite"
case _ => "compile"
}

val result =
if (mode == "testsuite") {
val className = TestSuites.suites.map(_.className)
val moduleInitializers = className
.map { clazz =>
ModuleInitializer.mainMethod(clazz, "main")
}
.zip(className)

for {
irFiles <- new CliReader(classpath).irFiles
_ <- Future.sequence {
moduleInitializers.map { case (moduleInitializer, className) =>
Compiler.compileIRFiles(
irFiles,
List(moduleInitializer),
s"$className"
)
}
}
} yield {
println("Module successfully initialized")
()
}
} else {
for {
irFiles <- new CliReader(classpath).irFiles
_ <- Compiler.compileIRFiles(irFiles, Nil, s"output")
} yield {
println("Module successfully initialized")
()
}
}

result.recover { case th: Throwable =>
System.err.println("Module initialization failed:")
th.printStackTrace()
js.Dynamic.global.process.exit(1)
}

}
}
9 changes: 9 additions & 0 deletions cli/src/main/scala/TestSuites.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package cli

object TestSuites {
case class TestSuite(className: String, methodName: String)
val suites = List(
TestSuite("testsuite.core.simple.Simple", "simple"),
TestSuite("testsuite.core.add.Add", "add")
)
}
11 changes: 11 additions & 0 deletions test-suite/src/main/scala/testsuite/core/Add.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package testsuite.core.add

import scala.scalajs.js.annotation._

object Add {
def main(): Unit = { val _ = test() }
@JSExportTopLevel("add")
def test(): Boolean = {
1 + 1 == 2
}
}
11 changes: 11 additions & 0 deletions test-suite/src/main/scala/testsuite/core/Simple.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package testsuite.core.simple

import scala.scalajs.js.annotation._

object Simple {
def main(): Unit = { val _ = test() }
@JSExportTopLevel("simple")
def test(): Boolean = {
true
}
}
29 changes: 29 additions & 0 deletions tests/src/test/scala/tests/CoreTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package tests

import scala.scalajs.js
import scala.scalajs.js.annotation._
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._

class CoreTests extends munit.FunSuite {
cli.TestSuites.suites.map { suite =>
test(suite.className) {
val file = s"./target/${suite.className}.wasm"
val wasmBuffer = FS.readFileSync(file)
val wasmModule =
js.Dynamic.global.WebAssembly.instantiate(wasmBuffer).asInstanceOf[js.Promise[js.Dynamic]]
wasmModule.toFuture.map { module =>
val testFunction =
module.instance.exports
.selectDynamic(suite.methodName)
.asInstanceOf[js.Function0[Int]]
assert(testFunction() == 1)
}
}
}

}

private object FS {
@js.native @JSImport("fs")
def readFileSync(file: String): js.typedarray.Uint8Array = js.native
}
33 changes: 23 additions & 10 deletions wasm/src/main/scala/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.scalajs.ir.Trees._
import org.scalajs.ir.Types._

import org.scalajs.linker.frontend.LinkerFrontendImpl
import org.scalajs.linker.interface.IRFile
import org.scalajs.linker.interface.{IRFile, ModuleInitializer}
import org.scalajs.linker.standard.{LinkedClass, SymbolRequirement}

import org.scalajs.logging.{Level, ScalaConsoleLogger}
Expand All @@ -20,22 +20,32 @@ import scala.scalajs.js.annotation._
import scala.scalajs.js.typedarray._

object Compiler {
def compileIRFiles(irFiles: Seq[IRFile])(implicit ec: ExecutionContext): Future[Unit] = {
def compileIRFiles(
irFiles: Seq[IRFile],
moduleInitializers: List[ModuleInitializer],
outputName: String
)(implicit ec: ExecutionContext): Future[Unit] = {
val module = new WasmModule
val builder = new WasmBuilder()
implicit val context: WasmContext = new WasmContext(module)
println("compiling")
println("compiling... ")

val config = LinkerFrontendImpl.Config()
val config = LinkerFrontendImpl
.Config()
.withOptimizer(false)
val linkerFrontend = LinkerFrontendImpl(config)

val symbolRequirements = SymbolRequirement.factory("none").none()
val logger = new ScalaConsoleLogger(Level.Error)
val logger = new ScalaConsoleLogger(Level.Info)

for {
patchedIRFiles <- LibraryPatches.patchIRFiles(irFiles)
moduleSet <- linkerFrontend.link(patchedIRFiles, Nil, symbolRequirements, logger)
moduleSet <- linkerFrontend.link(
patchedIRFiles,
moduleInitializers,
symbolRequirements,
logger
)
} yield {
val onlyModule = moduleSet.modules.head

Expand All @@ -46,17 +56,18 @@ object Compiler {
filteredClasses.sortBy(_.className).foreach(showLinkedClass(_))

Preprocessor.preprocess(filteredClasses)(context)
println("preprocessed")
filteredClasses.foreach { clazz =>
builder.transformClassDef(clazz)
}
onlyModule.topLevelExports.foreach { tle =>
builder.transformTopLevelExport(tle)
}
val writer = new converters.WasmTextWriter()
println(writer.write(module))
val textOutput = new converters.WasmTextWriter().write(module)
FS.writeFileSync(s"./target/$outputName.wat", textOutput.getBytes().toTypedArray)

val binaryOutput = new converters.WasmBinaryWriter(module).write()
FS.writeFileSync("./target/output.wasm", binaryOutput.toTypedArray)
FS.writeFileSync(s"./target/$outputName.wasm", binaryOutput.toTypedArray)
}
}

Expand Down Expand Up @@ -112,7 +123,9 @@ object Compiler {
::: clazz.jsConstructorDef.toList
::: clazz.exportedMembers
::: clazz.jsNativeMembers,
"{", "", "}"
"{",
"",
"}"
)
}
}
Expand Down
5 changes: 3 additions & 2 deletions wasm/src/main/scala/ir2wasm/TypeTransformer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ object TypeTransformer {
t: IRTypes.Type
)(implicit ctx: ReadOnlyWasmContext): List[Types.WasmType] =
t match {
case IRTypes.NoType => Nil
case _ => List(transformType(t))
case IRTypes.NoType => Nil
case IRTypes.ClassType(className) if className == IRNames.BoxedUnitClass => Nil
case _ => List(transformType(t))
}
def transformType(t: IRTypes.Type)(implicit ctx: ReadOnlyWasmContext): Types.WasmType =
t match {
Expand Down
Loading