diff options
author | Andrew Richards <richards.aj@gmail.com> | 2018-07-18 08:50:09 +0100 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-07-18 15:50:09 +0800 |
commit | 1829391c0de0efcb96b1187fe35a0e9127e00d29 (patch) | |
tree | 4216bde1516e75fccbc8a8c6a0d1eebb5b929364 /scalanativelib | |
parent | 2d779b8ccd0a2f29a7d14dc193b4ad093142b521 (diff) | |
download | mill-1829391c0de0efcb96b1187fe35a0e9127e00d29.tar.gz mill-1829391c0de0efcb96b1187fe35a0e9127e00d29.tar.bz2 mill-1829391c0de0efcb96b1187fe35a0e9127e00d29.zip |
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
Diffstat (limited to 'scalanativelib')
10 files changed, 777 insertions, 0 deletions
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) + } +} |