From 1829391c0de0efcb96b1187fe35a0e9127e00d29 Mon Sep 17 00:00:00 2001 From: Andrew Richards Date: Wed, 18 Jul 2018 08:50:09 +0100 Subject: WIP: Scala native (#206) * add scala-native PR#1143 as submodule * first pass at integrating scala-native build into mill including worker/bridge * add the native libraries to the compile and run classpath * sssshhh don't be so noisy * update scala-native to latest build WIP * update mill to latest scala-native build-api code * add test interface from scala-native this code is not published ornot published at the correct scala version so copy it in for now * implement tests for scala-native very messy at the moment also correct bridge version as much as possible with out a scala-native release * update to scala-native/master scala-native #1143 now merged * Remove scala-native submodule * updates for scala-native 0.3.7 release * fixes after rebase * make test framework agnostic and tidy dependencies * add robust method of getting JVM classpath for running tests support for multiple test frameworks tidy up * rebase fixes for 0.2.0 * add SbtNativeModule and tidy * rebase fixes * fix building of compile / run Classpath (via transitiveIvyDeps) better method of loading JVM test frameworks * add tests for build, run, utest, scalatest * move native tests into it own trait which can be extended/overidden * change release mode to a sealed trait instead of boolean * add logLevel to ScalaNativeModule and plumb in propagate release and log levels to test projects * use test-runner from scala-native instead of including project source add ability easily compile against scala-native snapshots * add some docs * update to 0.3.8 --- .gitmodules | 0 build.sc | 47 +++- docs/pages/3 - Common Project Layouts.md | 24 ++ .../scalanativelib/bridge/ScalaNativeBridge.scala | 74 +++++ .../mill/scalanativelib/ScalaNativeBridge.scala | 77 +++++ .../mill/scalanativelib/ScalaNativeModule.scala | 312 +++++++++++++++++++++ .../hello-native-world/src/hello/ArgsParser.scala | 5 + .../hello-native-world/src/hello/Main.scala | 6 + .../test/src/scalatest/ArgsParserSpec.scala | 21 ++ .../test/src/scalatest/MainSpec.scala | 18 ++ .../test/src/utest/tests/ArgsParserTests.scala | 24 ++ .../test/src/utest/tests/MainTests.scala | 23 ++ .../scalanativelib/HelloNativeWorldTests.scala | 217 ++++++++++++++ 13 files changed, 843 insertions(+), 5 deletions(-) create mode 100644 .gitmodules create mode 100644 scalanativelib/scalanativebridges/0.3/src/mill/scalanativelib/bridge/ScalaNativeBridge.scala create mode 100644 scalanativelib/src/mill/scalanativelib/ScalaNativeBridge.scala create mode 100644 scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala create mode 100644 scalanativelib/test/resources/hello-native-world/src/hello/ArgsParser.scala create mode 100644 scalanativelib/test/resources/hello-native-world/src/hello/Main.scala create mode 100644 scalanativelib/test/resources/hello-native-world/test/src/scalatest/ArgsParserSpec.scala create mode 100644 scalanativelib/test/resources/hello-native-world/test/src/scalatest/MainSpec.scala create mode 100644 scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala create mode 100644 scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala create mode 100644 scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e69de29b diff --git a/build.sc b/build.sc index 5d9550dc..cc5115fa 100755 --- a/build.sc +++ b/build.sc @@ -156,7 +156,8 @@ object scalalib extends MillModule { genTask(core)() ++ genTask(main)() ++ genTask(scalalib)() ++ - genTask(scalajslib)() + genTask(scalajslib)() ++ + genTask(scalanativelib)() worker.testArgs() ++ main.graphviz.testArgs() ++ @@ -218,6 +219,39 @@ object twirllib extends MillModule { } +object scalanativelib extends MillModule { + def moduleDeps = Seq(scalalib) + + def scalacOptions = Seq[String]() // disable -P:acyclic:force + + def testArgs = T{ + val mapping = Map( + "MILL_SCALANATIVE_BRIDGE_0_3" -> + scalanativebridges("0.3").runClasspath() + .map(_.path) + .filter(_.toIO.exists) + .mkString(",") + ) + scalalib.worker.testArgs() ++ (for((k, v) <- mapping.toSeq) yield s"-D$k=$v") + } + + object scalanativebridges extends Cross[ScalaNativeBridgeModule]("0.3") + class ScalaNativeBridgeModule(scalaNativeBinary: String) extends MillModule { + def scalaNativeVersion = T{ "0.3.8" } + def moduleDeps = Seq(scalanativelib) + def ivyDeps = scalaNativeBinary match { + case "0.3" => + Agg( + ivy"org.scala-native::tools:${scalaNativeVersion()}", + ivy"org.scala-native::util:${scalaNativeVersion()}", + ivy"org.scala-native::nir:${scalaNativeVersion()}", + ivy"org.scala-native::nir:${scalaNativeVersion()}", + ivy"org.scala-native::test-runner:${scalaNativeVersion()}", + ) + } + } +} + def testRepos = T{ Seq( "MILL_ACYCLIC_REPO" -> @@ -238,10 +272,11 @@ def testRepos = T{ } object integration extends MillModule{ - def moduleDeps = Seq(moduledefs, scalalib, scalajslib) + def moduleDeps = Seq(moduledefs, scalalib, scalajslib, scalanativelib) def testArgs = T{ scalajslib.testArgs() ++ scalalib.worker.testArgs() ++ + scalanativelib.testArgs() ++ Seq( "-DMILL_TESTNG=" + testng.runClasspath().map(_.path).mkString(","), "-DMILL_VERSION=" + build.publishVersion()._2, @@ -290,12 +325,13 @@ def launcherScript(shellJvmArgs: Seq[String], } object dev extends MillModule{ - def moduleDeps = Seq(scalalib, scalajslib) + def moduleDeps = Seq(scalalib, scalajslib, scalanativelib) def forkArgs = ( scalalib.testArgs() ++ scalajslib.testArgs() ++ scalalib.worker.testArgs() ++ + scalanativelib.testArgs() ++ // Workaround for Zinc/JNA bug // https://github.com/sbt/sbt/blame/6718803ee6023ab041b045a6988fafcfae9d15b5/main/src/main/scala/sbt/Main.scala#L130 Seq( @@ -305,6 +341,7 @@ object dev extends MillModule{ ) ).distinct + // Pass dev.assembly VM options via file in Window due to small max args limit def windowsVmOptions(taskName: String, batch: Path, args: Seq[String])(implicit ctx: mill.util.Ctx) = { if (System.getProperty("java.specification.version").startsWith("1.")) { @@ -412,7 +449,7 @@ def gitHead = T.input{ def publishVersion = T.input{ val tag = try Option( - %%('git, 'describe, "--exact-match", "--tags", gitHead())(pwd).out.string.trim() + %%('git, 'describe, "--exact-match", "--tags", "--always", gitHead())(pwd).out.string.trim() ) catch{case e => None} @@ -424,7 +461,7 @@ def publishVersion = T.input{ tag match{ case Some(t) => (t, t) case None => - val latestTaggedVersion = %%('git, 'describe, "--abbrev=0", "--tags")(pwd).out.trim + val latestTaggedVersion = %%('git, 'describe, "--abbrev=0", "--always", "--tags")(pwd).out.trim val commitsSinceLastTag = %%('git, "rev-list", gitHead(), "--not", latestTaggedVersion, "--count")(pwd).out.trim.toInt diff --git a/docs/pages/3 - Common Project Layouts.md b/docs/pages/3 - Common Project Layouts.md index a8c55a10..01ec2e85 100644 --- a/docs/pages/3 - Common Project Layouts.md +++ b/docs/pages/3 - Common Project Layouts.md @@ -101,6 +101,30 @@ latter of which runs your code on Node.js, which must be pre-installed) `ScalaJSModule` also exposes the `foo.fastOpt` and `foo.fullOpt` tasks for generating the optimized Javascript file. +### Scala Native Modules + +``` +import mill._ +import mill.define._ +import mill.scalanativelib._ + +object nativehello extends ScalaNativeModule { + def scalaVersion = "2.11.12" + def scalaNativeVersion = "0.3.7" + def logLevel = NativeLogLevel.Info // optional + def releaseMode = ReleaseMode.Debug // optional +} +``` + +`ScalaNativeModule` builds scala sources to executable binaries using [Scala Native](http://www.scala-native.org). +You will need to have the [relevant parts](http://www.scala-native.org/en/latest/user/setup.html) +of the LLVM toolchain installed on your system. Optimized binaries can be built by setting +`releaseMode` (see above) and more verbose logging can be enabled using `logLevel`. +Currently two test frameworks are supported [utest](https://github.com/lihaoyi/utest) and [scalatest](http://www.scalatest.org/). +Support for [scalacheck](https://www.scalacheck.org/) should be possible when the relevant artifacts have been published +for scala native. + + ### SBT-Compatible Modules ```scala diff --git a/scalanativelib/scalanativebridges/0.3/src/mill/scalanativelib/bridge/ScalaNativeBridge.scala b/scalanativelib/scalanativebridges/0.3/src/mill/scalanativelib/bridge/ScalaNativeBridge.scala new file mode 100644 index 00000000..268e18ac --- /dev/null +++ b/scalanativelib/scalanativebridges/0.3/src/mill/scalanativelib/bridge/ScalaNativeBridge.scala @@ -0,0 +1,74 @@ +package mill.scalanativelib.bridge + +import java.io.File +import java.lang.System.{err, out} + +import scala.scalanative.build.{Build, Config, Discover, GC, Logger, Mode} +import ammonite.ops.Path +import mill.scalanativelib.{NativeConfig, NativeLogLevel, ReleaseMode} +import sbt.testing.Framework + +import scala.scalanative.testinterface.ScalaNativeFramework + + +class ScalaNativeBridge extends mill.scalanativelib.ScalaNativeBridge { + def logger(level: NativeLogLevel) = + Logger( + debugFn = msg => if (level >= NativeLogLevel.Debug) out.println(msg), + infoFn = msg => if (level >= NativeLogLevel.Info) out.println(msg), + warnFn = msg => if (level >= NativeLogLevel.Warn) out.println(msg), + errorFn = msg => if (level >= NativeLogLevel.Error) err.println(msg)) + + def discoverClang: Path = Path(Discover.clang()) + def discoverClangPP: Path = Path(Discover.clangpp()) + def discoverTarget(clang: Path, workdir: Path): String = Discover.targetTriple(clang.toNIO, workdir.toNIO) + def discoverCompileOptions: Seq[String] = Discover.compileOptions() + def discoverLinkingOptions: Seq[String] = Discover.linkingOptions() + def defaultGarbageCollector: String = GC.default.name + + def config(nativeLibJar: Path, + mainClass: String, + classpath: Seq[Path], + nativeWorkdir: Path, + nativeClang: Path, + nativeClangPP: Path, + nativeTarget: String, + nativeCompileOptions: Seq[String], + nativeLinkingOptions: Seq[String], + nativeGC: String, + nativeLinkStubs: Boolean, + releaseMode: ReleaseMode, + logLevel: NativeLogLevel): NativeConfig = + { + val entry = mainClass + "$" + + val config = + Config.empty + .withNativelib(nativeLibJar.toNIO) + .withMainClass(entry) + .withClassPath(classpath.map(_.toNIO)) + .withWorkdir(nativeWorkdir.toNIO) + .withClang(nativeClang.toNIO) + .withClangPP(nativeClangPP.toNIO) + .withTargetTriple(nativeTarget) + .withCompileOptions(nativeCompileOptions) + .withLinkingOptions(nativeLinkingOptions) + .withGC(GC(nativeGC)) + .withLinkStubs(nativeLinkStubs) + .withMode(Mode(releaseMode.name)) + .withLogger(logger(logLevel)) + NativeConfig(config) + } + + def nativeLink(nativeConfig: NativeConfig, outPath: Path): Path = { + val config = nativeConfig.config.asInstanceOf[Config] + Build.build(config, outPath.toNIO) + outPath + } + + override def newScalaNativeFrameWork(framework: Framework, id: Int, testBinary: File, + logLevel: NativeLogLevel, envVars: Map[String, String]): Framework = + { + new ScalaNativeFramework(framework, id, logger(logLevel), testBinary, envVars) + } +} diff --git a/scalanativelib/src/mill/scalanativelib/ScalaNativeBridge.scala b/scalanativelib/src/mill/scalanativelib/ScalaNativeBridge.scala new file mode 100644 index 00000000..a7777fad --- /dev/null +++ b/scalanativelib/src/mill/scalanativelib/ScalaNativeBridge.scala @@ -0,0 +1,77 @@ +package mill.scalanativelib + +import java.io.File +import java.net.URLClassLoader + +import ammonite.ops.Path +import mill.define.{Discover, Worker} +import mill.{Agg, T} +import sbt.testing.Framework + + +class ScalaNativeWorker { + private var scalaInstanceCache = Option.empty[(Long, ScalaNativeBridge)] + + def bridge(toolsClasspath: Agg[Path]): ScalaNativeBridge = { + val classloaderSig = toolsClasspath.map(p => p.toString().hashCode + p.mtime.toMillis).sum + scalaInstanceCache match { + case Some((sig, bridge)) if sig == classloaderSig => bridge + case _ => + val cl = new URLClassLoader( + toolsClasspath.map(_.toIO.toURI.toURL).toArray, + getClass.getClassLoader + ) + try { + val bridge = cl + .loadClass("mill.scalanativelib.bridge.ScalaNativeBridge") + .getDeclaredConstructor() + .newInstance() + .asInstanceOf[ScalaNativeBridge] + scalaInstanceCache = Some((classloaderSig, bridge)) + bridge + } + catch { + case e: Exception => + e.printStackTrace() + throw e + } + } + } +} + + +// result wrapper to preserve some type safety +case class NativeConfig(config: Any) + +trait ScalaNativeBridge { + def discoverClang: Path + def discoverClangPP: Path + def discoverTarget(clang: Path, workDir: Path): String + def discoverCompileOptions: Seq[String] + def discoverLinkingOptions: Seq[String] + + def config(nativeLibJar: Path, + mainClass: String, + classpath: Seq[Path], + nativeWorkdir: Path, + nativeClang: Path, + nativeClangPP: Path, + nativeTarget: String, + nativeCompileOptions: Seq[String], + nativeLinkingOptions: Seq[String], + nativeGC: String, + nativeLinkStubs: Boolean, + releaseMode: ReleaseMode, + logLevel: NativeLogLevel): NativeConfig + + def defaultGarbageCollector: String + def nativeLink(nativeConfig: NativeConfig, outPath: Path): Path + + def newScalaNativeFrameWork(framework: Framework, id: Int, testBinary: File, + logLevel: NativeLogLevel, envVars: Map[String, String]): Framework +} + +object ScalaNativeBridge extends mill.define.ExternalModule { + def scalaNativeBridge: Worker[ScalaNativeWorker] = T.worker { new ScalaNativeWorker() } + lazy val millDiscover = Discover[this.type] +} diff --git a/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala b/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala new file mode 100644 index 00000000..d4d2e050 --- /dev/null +++ b/scalanativelib/src/mill/scalanativelib/ScalaNativeModule.scala @@ -0,0 +1,312 @@ +package mill +package scalanativelib + +import java.net.URLClassLoader + +import ammonite.ops.Path +import coursier.Cache +import coursier.maven.MavenRepository +import mill.define.{Target, Task} +import mill.eval.Result +import mill.modules.Jvm +import mill.scalalib.{Dep, DepSyntax, Lib, SbtModule, ScalaModule, TestModule, TestRunner} +import mill.util.Loose.Agg +import sbt.testing.{AnnotatedFingerprint, SubclassFingerprint} +import sbt.testing.Fingerprint +import upickle.default.{ReadWriter => RW, macroRW} + + +sealed abstract class NativeLogLevel(val level: Int) extends Ordered[NativeLogLevel] { + def compare(that: NativeLogLevel) = this.level - that.level +} + +object NativeLogLevel { + case object Error extends NativeLogLevel(200) + case object Warn extends NativeLogLevel(300) + case object Info extends NativeLogLevel(400) + case object Debug extends NativeLogLevel(500) + case object Trace extends NativeLogLevel(600) + + implicit def rw: RW[NativeLogLevel] = macroRW +} + +sealed abstract class ReleaseMode(val name: String) + +object ReleaseMode { + case object Debug extends ReleaseMode("debug") + case object Release extends ReleaseMode("release") + + implicit def rw: RW[ReleaseMode] = macroRW +} + + +trait ScalaNativeModule extends ScalaModule { outer => + def scalaNativeVersion: T[String] + override def platformSuffix = s"_native${scalaNativeBinaryVersion()}" + override def artifactSuffix: T[String] = s"${platformSuffix()}_${artifactScalaVersion()}" + + trait Tests extends TestScalaNativeModule { + override def scalaWorker = outer.scalaWorker + override def scalaVersion = outer.scalaVersion() + override def scalaNativeVersion = outer.scalaNativeVersion() + override def releaseMode = outer.releaseMode() + override def logLevel = outer.logLevel() + override def moduleDeps = Seq(outer) + } + + def scalaNativeBinaryVersion = T{ scalaNativeVersion().split('.').take(2).mkString(".") } + + // This allows compilation and testing versus SNAPSHOT versions of scala-native + def scalaNativeToolsVersion = T{ + if (scalaNativeVersion().endsWith("-SNAPSHOT")) + scalaNativeVersion() + else + scalaNativeBinaryVersion() + } + + def bridge = T.task{ ScalaNativeBridge.scalaNativeBridge().bridge(bridgeFullClassPath()) } + + def scalaNativeBridgeClasspath = T { + val snBridgeKey = "MILL_SCALANATIVE_BRIDGE_" + scalaNativeBinaryVersion().replace('.', '_').replace('-', '_') + val snBridgePath = sys.props(snBridgeKey) + if (snBridgePath != null) + Result.Success(Agg(snBridgePath.split(',').map(p => PathRef(Path(p), quick = true)): _*)) + else + Lib.resolveDependencies( + Seq(Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2")), + Lib.depToDependency(_, "2.12.4", ""), + Seq(ivy"com.lihaoyi::mill-scalanativelib-scalanativebridges-${scalaNativeBinaryVersion()}:${sys.props("MILL_VERSION")}") + ) + } + + def toolsIvyDeps = T{ + Seq( + ivy"org.scala-native:tools_2.12:${scalaNativeVersion()}", + ivy"org.scala-native:util_2.12:${scalaNativeVersion()}", + ivy"org.scala-native:nir_2.12:${scalaNativeVersion()}" + ) + } + + override def transitiveIvyDeps: T[Agg[Dep]] = T{ + ivyDeps() ++ nativeIvyDeps() ++ Task.traverse(moduleDeps)(_.transitiveIvyDeps)().flatten + } + + def nativeLibIvy = T{ ivy"org.scala-native::nativelib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}" } + + def nativeIvyDeps = T{ + Seq(nativeLibIvy()) ++ + Seq( + ivy"org.scala-native::javalib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}", + ivy"org.scala-native::auxlib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}", + ivy"org.scala-native::scalalib_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}" + ) + } + + def bridgeFullClassPath = T { + Lib.resolveDependencies( + Seq(Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2")), + Lib.depToDependency(_, scalaVersion(), platformSuffix()), + toolsIvyDeps() + ).map(t => (scalaNativeBridgeClasspath().toSeq ++ t.toSeq).map(_.path)) + } + + override def scalacPluginIvyDeps = super.scalacPluginIvyDeps() ++ + Agg(ivy"org.scala-native:nscplugin_${scalaVersion()}:${scalaNativeVersion()}") + + def logLevel: Target[NativeLogLevel] = T{ NativeLogLevel.Info } + + def releaseMode: Target[ReleaseMode] = T { ReleaseMode.Debug } + + def nativeWorkdir = T{ T.ctx().dest } + + // Location of the clang compiler + def nativeClang = T{ bridge().discoverClang } + + // Location of the clang++ compiler + def nativeClangPP = T{ bridge().discoverClangPP } + + // GC choice, either "none", "boehm" or "immix" + def nativeGC = T{ + Option(System.getenv.get("SCALANATIVE_GC")) + .getOrElse(bridge().defaultGarbageCollector) + } + + def nativeTarget = T{ bridge().discoverTarget(nativeClang(), nativeWorkdir()) } + + // Options that are passed to clang during compilation + def nativeCompileOptions = T{ bridge().discoverCompileOptions } + + // Options that are passed to clang during linking + def nativeLinkingOptions = T{ bridge().discoverLinkingOptions } + + // Whether to link `@stub` methods, or ignore them + def nativeLinkStubs = T { false } + + + def nativeLibJar = T{ + resolveDeps(T.task{Agg(nativeLibIvy())})() + .filter{p => p.toString.contains("scala-native") && p.toString.contains("nativelib")} + .toList + .head + } + + def nativeConfig = T.task { + val classpath = runClasspath().map(_.path).filter(_.toIO.exists).toList + + bridge().config( + nativeLibJar().path, + finalMainClass(), + classpath, + nativeWorkdir(), + nativeClang(), + nativeClangPP(), + nativeTarget(), + nativeCompileOptions(), + nativeLinkingOptions(), + nativeGC(), + nativeLinkStubs(), + releaseMode(), + logLevel()) + } + + // Generates native binary + def nativeLink = T{ bridge().nativeLink(nativeConfig(), (T.ctx().dest / 'out)) } + + // Runs the native binary + override def run(args: String*) = T.command{ + Jvm.baseInteractiveSubprocess( + Vector(nativeLink().toString) ++ args, + forkEnv(), + workingDir = ammonite.ops.pwd) + } +} + + +trait TestScalaNativeModule extends ScalaNativeModule with TestModule { testOuter => + case class TestDefinition(framework: String, clazz: Class[_], fingerprint: Fingerprint) { + def name = clazz.getName.reverse.dropWhile(_ == '$').reverse + } + + override def testLocal(args: String*) = T.command { test(args:_*) } + + override def test(args: String*) = T.command{ + val outputPath = T.ctx().dest / "out.json" + + // The test frameworks run under the JVM and communicate with the native binary over a socket + // therefore the test framework is loaded from a JVM classloader + val testClassloader = + new URLClassLoader(testClasspathJvm().map(_.path.toIO.toURI.toURL).toArray, + this.getClass.getClassLoader) + val frameworkInstances = TestRunner.frameworks(testFrameworks())(testClassloader) + val testBinary = testRunnerNative.nativeLink().toIO + val envVars = forkEnv() + + val nativeFrameworks = (cl: ClassLoader) => + frameworkInstances.zipWithIndex.map { case (f, id) => + bridge().newScalaNativeFrameWork(f, id, testBinary, logLevel(), envVars) + } + + val (doneMsg, results) = TestRunner.runTests( + nativeFrameworks, + testClasspathJvm().map(_.path), + Agg(compile().classes.path), + args + ) + + TestModule.handleResults(doneMsg, results) + } + + private val supportedTestFrameworks = Set("utest", "scalatest") + + // get the JVM classpath entries for supported test frameworks + def testFrameworksJvmClasspath = T{ + Lib.resolveDependencies( + repositories, + Lib.depToDependency(_, scalaVersion(), ""), + transitiveIvyDeps().collect{ case x: Dep.Scala => x }.filter(d => supportedTestFrameworks(d.dep.module.name)) + ) + } + + def testClasspathJvm = T{ + localClasspath() ++ + transitiveLocalClasspath() ++ + unmanagedClasspath() ++ + testFrameworksJvmClasspath() + } + + // creates a specific binary used for running tests - has a different (generated) main class + // which knows the names of all the tests and references to invoke them + object testRunnerNative extends ScalaNativeModule { + override def scalaWorker = testOuter.scalaWorker + override def scalaVersion = testOuter.scalaVersion() + override def scalaNativeVersion = testOuter.scalaNativeVersion() + override def moduleDeps = Seq(testOuter) + override def releaseMode = testOuter.releaseMode() + override def logLevel = testOuter.logLevel() + override def nativeLinkStubs = true + + override def ivyDeps = testOuter.ivyDeps() ++ Agg( + ivy"org.scala-native::test-interface_native${scalaNativeToolsVersion()}:${scalaNativeVersion()}" + ) + + override def mainClass = Some("scala.scalanative.testinterface.TestMain") + + override def generatedSources = T { + val outDir = T.ctx().dest + ammonite.ops.write.over(outDir / "TestMain.scala", makeTestMain()) + Seq(PathRef(outDir)) + } + } + + // generate a main class for the tests + def makeTestMain = T{ + val frameworkInstances = TestRunner.frameworks(testFrameworks()) _ + + val testClasses = + Jvm.inprocess(testClasspathJvm().map(_.path), classLoaderOverrideSbtTesting = true, isolated = true, + cl => { + frameworkInstances(cl).flatMap { framework => + val df = Lib.discoverTests(cl, framework, Agg(compile().classes.path)) + df.map(d => TestDefinition(framework.getClass.getName, d._1, d._2)) + } + } + ) + + val frameworks = testClasses.map(_.framework).distinct + + val frameworksList = + if (frameworks.isEmpty) + "Nil" + else + frameworks.mkString("List(new _root_.", ", new _root_.", ")") + + val testsMap = makeTestsMap(testClasses) + + s"""package scala.scalanative.testinterface + |object TestMain extends TestMainBase { + | override val frameworks = $frameworksList + | override val tests = Map[String, AnyRef]($testsMap) + | def main(args: Array[String]): Unit = + | testMain(args) + |}""".stripMargin + } + + private def makeTestsMap(tests: Seq[TestDefinition]): String = { + tests + .map { t => + val isModule = t.fingerprint match { + case af: AnnotatedFingerprint => af.isModule + case sf: SubclassFingerprint => sf.isModule + } + + val inst = + if (isModule) s"_root_.${t.name}" else s"new _root_.${t.name}" + s""""${t.name}" -> $inst""" + } + .mkString(", ") + } +} + + +trait SbtNativeModule extends ScalaNativeModule with SbtModule + diff --git a/scalanativelib/test/resources/hello-native-world/src/hello/ArgsParser.scala b/scalanativelib/test/resources/hello-native-world/src/hello/ArgsParser.scala new file mode 100644 index 00000000..8ad93598 --- /dev/null +++ b/scalanativelib/test/resources/hello-native-world/src/hello/ArgsParser.scala @@ -0,0 +1,5 @@ +package hello + +object ArgsParser { + def parse(s:String): Seq[String] = s.split(":").toSeq +} diff --git a/scalanativelib/test/resources/hello-native-world/src/hello/Main.scala b/scalanativelib/test/resources/hello-native-world/src/hello/Main.scala new file mode 100644 index 00000000..5e04dbb3 --- /dev/null +++ b/scalanativelib/test/resources/hello-native-world/src/hello/Main.scala @@ -0,0 +1,6 @@ +package hello + +object Main extends App { + println("Hello " + vmName) + def vmName = sys.props("java.vm.name") +} diff --git a/scalanativelib/test/resources/hello-native-world/test/src/scalatest/ArgsParserSpec.scala b/scalanativelib/test/resources/hello-native-world/test/src/scalatest/ArgsParserSpec.scala new file mode 100644 index 00000000..dd160989 --- /dev/null +++ b/scalanativelib/test/resources/hello-native-world/test/src/scalatest/ArgsParserSpec.scala @@ -0,0 +1,21 @@ +package hellotest + +import hello._ +import org.scalatest._ + +class ArgsParserSpec extends FlatSpec with Matchers { + + behavior of "ArgsParser" + + "parse" should "one" in { + val result = ArgsParser.parse("hello:world") + result should have length 2 + result should contain only ("hello", "world") + } + + it should "two" in { + val result = ArgsParser.parse("hello:world") + result should have length 80 + } + +} diff --git a/scalanativelib/test/resources/hello-native-world/test/src/scalatest/MainSpec.scala b/scalanativelib/test/resources/hello-native-world/test/src/scalatest/MainSpec.scala new file mode 100644 index 00000000..582c3692 --- /dev/null +++ b/scalanativelib/test/resources/hello-native-world/test/src/scalatest/MainSpec.scala @@ -0,0 +1,18 @@ +package hellotest + +import hello._ +import org.scalatest._ + +class MainSpec extends FlatSpec with Matchers { + + behavior of "Main" + + "vmName" should "contain Native" in { + Main.vmName should include ("Native") + } + + it should "contain Scala" in { + Main.vmName should include ("Scala") + } + +} diff --git a/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala new file mode 100644 index 00000000..7929f947 --- /dev/null +++ b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/ArgsParserTests.scala @@ -0,0 +1,24 @@ +package hellotest + +import hello._ +import utest._ + +object ArgsParserTests extends TestSuite { + + def tests: Tests = Tests { + 'one - { + val result = ArgsParser.parse("hello:world") + assert( + result.length == 2, + result == Seq("hello", "world") + ) + } + 'two - { // we fail this test to check testing in scala.js + val result = ArgsParser.parse("hello:world") + assert( + result.length == 80 + ) + } + } + +} diff --git a/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala new file mode 100644 index 00000000..3a89f90c --- /dev/null +++ b/scalanativelib/test/resources/hello-native-world/test/src/utest/tests/MainTests.scala @@ -0,0 +1,23 @@ +package hellotest + +import hello._ +import utest._ + +object MainTests extends TestSuite { + + def tests: Tests = Tests { + 'vmName - { + 'containNative - { + assert( + Main.vmName.contains("Native") + ) + } + 'containScala - { + assert( + Main.vmName.contains("Scala") + ) + } + } + } + +} diff --git a/scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala b/scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala new file mode 100644 index 00000000..4c67b98f --- /dev/null +++ b/scalanativelib/test/src/mill/scalanativelib/HelloNativeWorldTests.scala @@ -0,0 +1,217 @@ +package mill.scalanativelib + +import java.util.jar.JarFile + +import ammonite.ops._ +import mill._ +import mill.define.Discover +import mill.eval.{Evaluator, Result} +import mill.scalalib.{CrossScalaModule, DepSyntax, Lib, PublishModule, TestRunner} +import mill.scalalib.publish.{Developer, License, PomSettings, VersionControl} +import mill.util.{TestEvaluator, TestUtil} +import utest._ + + +import scala.collection.JavaConverters._ + + +object HelloNativeWorldTests extends TestSuite { + val workspacePath = TestUtil.getOutPathStatic() / "hello-native-world" + + trait HelloNativeWorldModule extends CrossScalaModule with ScalaNativeModule with PublishModule { + override def millSourcePath = workspacePath + def publishVersion = "0.0.1-SNAPSHOT" + override def mainClass = Some("hello.Main") + } + + object HelloNativeWorld extends TestUtil.BaseModule { + val matrix = for { + scala <- Seq("2.11.12") + scalaNative <- Seq("0.3.8") + mode <- List(ReleaseMode.Debug, ReleaseMode.Release) + } yield (scala, scalaNative, mode) + + object helloNativeWorld extends Cross[BuildModule](matrix:_*) + class BuildModule(val crossScalaVersion: String, sNativeVersion: String, mode: ReleaseMode) extends HelloNativeWorldModule { + override def artifactName = "hello-native-world" + def scalaNativeVersion = sNativeVersion + def releaseMode = T{ mode } + def pomSettings = PomSettings( + organization = "com.lihaoyi", + description = "hello native world ready for real world publishing", + url = "https://github.com/lihaoyi/hello-world-publish", + licenses = Seq(License.Common.Apache2), + versionControl = VersionControl.github("lihaoyi", "hello-world-publish"), + developers = + Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi")) + ) + } + + object buildUTest extends Cross[BuildModuleUtest](matrix:_*) + class BuildModuleUtest(crossScalaVersion: String, sNativeVersion: String, mode: ReleaseMode) + extends BuildModule(crossScalaVersion, sNativeVersion, mode) { + object test extends super.Tests { + override def sources = T.sources{ millSourcePath / 'src / 'utest } + def testFrameworks = Seq("utest.runner.Framework") + override def ivyDeps = Agg( + ivy"com.lihaoyi::utest::0.6.4" + ) + } + } + + object buildScalaTest extends Cross[BuildModuleScalaTest](matrix:_*) + class BuildModuleScalaTest(crossScalaVersion: String, sNativeVersion: String, mode: ReleaseMode) + extends BuildModule(crossScalaVersion, sNativeVersion, mode) { + object test extends super.Tests { + override def sources = T.sources{ millSourcePath / 'src / 'scalatest } + def testFrameworks = Seq("org.scalatest.tools.Framework") + override def ivyDeps = Agg( + ivy"org.scalatest::scalatest::3.2.0-SNAP10" + ) + } + } + override lazy val millDiscover = Discover[this.type] + } + + val millSourcePath = pwd / 'scalanativelib / 'test / 'resources / "hello-native-world" + + val helloWorldEvaluator = TestEvaluator.static(HelloNativeWorld) + + + val mainObject = helloWorldEvaluator.outPath / 'src / "Main.scala" + + def tests: Tests = Tests { + prepareWorkspace() + 'compile - { + def testCompileFromScratch(scalaVersion: String, + scalaNativeVersion: String, + mode: ReleaseMode): Unit = { + val Right((result, evalCount)) = + helloWorldEvaluator(HelloNativeWorld.helloNativeWorld(scalaVersion, scalaNativeVersion, mode).compile) + + val outPath = result.classes.path + val outputFiles = ls.rec(outPath).filter(_.isFile) + val expectedClassfiles = compileClassfiles(outPath / 'hello) + assert( + outputFiles.toSet == expectedClassfiles, + evalCount > 0 + ) + + // don't recompile if nothing changed + val Right((_, unchangedEvalCount)) = + helloWorldEvaluator(HelloNativeWorld.helloNativeWorld(scalaVersion, scalaNativeVersion, mode).compile) + assert(unchangedEvalCount == 0) + } + + 'fromScratch_21112_037 - testCompileFromScratch("2.11.12", "0.3.8", ReleaseMode.Debug) + } + + 'jar - { + 'containsNirs - { + val Right((result, evalCount)) = + helloWorldEvaluator(HelloNativeWorld.helloNativeWorld("2.11.12", "0.3.8", ReleaseMode.Debug).jar) + val jar = result.path + val entries = new JarFile(jar.toIO).entries().asScala.map(_.getName) + assert(entries.contains("hello/Main$.nir")) + } + } + 'publish - { + def testArtifactId(scalaVersion: String, + scalaNativeVersion: String, + mode: ReleaseMode, + artifactId: String): Unit = { + val Right((result, evalCount)) = helloWorldEvaluator( + HelloNativeWorld.helloNativeWorld(scalaVersion, scalaNativeVersion, mode: ReleaseMode).artifactMetadata) + assert(result.id == artifactId) + } + 'artifactId_038 - testArtifactId("2.11.12", "0.3.8", ReleaseMode.Debug, "hello-native-world_native0.3_2.11") + } + 'test - { + def runTests(testTask: define.Command[(String, Seq[TestRunner.Result])]): Map[String, Map[String, TestRunner.Result]] = { + val Left(Result.Failure(_, Some(res))) = helloWorldEvaluator(testTask) + + val (doneMsg, testResults) = res + testResults + .groupBy(_.fullyQualifiedName) + .mapValues(_.map(e => e.selector -> e).toMap) + } + + def checkUtest(scalaVersion: String, scalaNativeVersion: String, mode: ReleaseMode) = { + val resultMap = runTests(HelloNativeWorld.buildUTest(scalaVersion, scalaNativeVersion, mode).test.test()) + + val mainTests = resultMap("hellotest.MainTests") + val argParserTests = resultMap("hellotest.ArgsParserTests") + + assert( + mainTests.size == 2, + mainTests("hellotest.MainTests.vmName.containNative").status == "Success", + mainTests("hellotest.MainTests.vmName.containScala").status == "Success", + + argParserTests.size == 2, + argParserTests("hellotest.ArgsParserTests.one").status == "Success", + argParserTests("hellotest.ArgsParserTests.two").status == "Failure" + ) + } + + def checkScalaTest(scalaVersion: String, scalaNativeVersion: String, mode: ReleaseMode) = { + val resultMap = runTests(HelloNativeWorld.buildScalaTest(scalaVersion, scalaNativeVersion, mode).test.test()) + + val mainSpec = resultMap("hellotest.MainSpec") + val argParserSpec = resultMap("hellotest.ArgsParserSpec") + + assert( + mainSpec.size == 2, + mainSpec("vmName should contain Native").status == "Success", + mainSpec("vmName should contain Scala").status == "Success", + + argParserSpec.size == 2, + argParserSpec("parse should one").status == "Success", + argParserSpec("parse should two").status == "Failure" + ) + } + + 'utest_21112_038_debug - (checkUtest("2.11.12", "0.3.8", ReleaseMode.Debug)) + 'utest_21112_038_release - (checkUtest("2.11.12", "0.3.8", ReleaseMode.Release)) + 'scalaTest_21112_038_debug - (checkScalaTest("2.11.12", "0.3.8", ReleaseMode.Debug)) + 'scalaTest_21112_038_release - (checkScalaTest("2.11.12", "0.3.8", ReleaseMode.Release)) + } + + def checkRun(scalaVersion: String, scalaNativeVersion: String, mode: ReleaseMode): Unit = { + val task = HelloNativeWorld.helloNativeWorld(scalaVersion, scalaNativeVersion, mode).run() + + val Right((_, evalCount)) = helloWorldEvaluator(task) + + val paths = Evaluator.resolveDestPaths( + helloWorldEvaluator.outPath, + task.ctx.segments + ) + val log = read(paths.log) + assert( + evalCount > 0, + log.contains("Scala Native") + ) + } + + 'run - { + 'run_21112_038_debug - (checkRun("2.11.12", "0.3.8", ReleaseMode.Debug)) + 'run_21112_038_release - (checkRun("2.11.12", "0.3.8", ReleaseMode.Release)) + } + } + + def compileClassfiles(parentDir: Path) = Set( + parentDir / "ArgsParser$.class", + parentDir / "ArgsParser$.nir", + parentDir / "ArgsParser.class", + parentDir / "Main.class", + parentDir / "Main$.class", + parentDir / "Main$delayedInit$body.class", + parentDir / "Main$.nir", + parentDir / "Main$delayedInit$body.nir" + ) + + def prepareWorkspace(): Unit = { + rm(workspacePath) + mkdir(workspacePath / up) + cp(millSourcePath, workspacePath) + } +} -- cgit v1.2.3