Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Kotest support for Kotlin/JS #3723

Merged
merged 4 commits into from
Oct 14, 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
35 changes: 18 additions & 17 deletions example/kotlinlib/web/3-hello-kotlinjs/build.mill
Original file line number Diff line number Diff line change
@@ -1,46 +1,47 @@
// KotlinJS support on Mill is still Work In Progress (WIP). As of time of writing it
// Node.js/Webpack test runners and reporting, etc.
// Kotlin/JS support on Mill is still Work In Progress (WIP). As of time of writing it
// supports Node.js, but lacks support of Browser, Webpack, test runners, reporting, etc.
//
// The example below demonstrates only the minimal compilation, running, and testing of
// a single KotlinJS module using a single third-party dependency. For more details in
// fully developing KotlinJS support, see the following ticket:
// a single Kotlin/JS module using a single third-party dependency. For more details in
// fully developing Kotlin/JS support, see the following ticket:
//
// * https://github.com/com-lihaoyi/mill/issues/3611

package build
import mill._, kotlinlib._, kotlinlib.js._

object foo extends KotlinJSModule {
object `package` extends RootModule with KotlinJSModule {
def moduleKind = ModuleKind.ESModule
def kotlinVersion = "1.9.25"
def kotlinJSRunTarget = Some(RunTarget.Node)
def ivyDeps = Agg(
ivy"org.jetbrains.kotlinx:kotlinx-html-js:0.11.0",
)
object test extends KotlinJSModule with KotlinJSKotlinXTests
object test extends KotlinJSModule with KotestTests
}


/** Usage

> mill foo.run
Compiling 1 Kotlin sources to .../out/foo/compile.dest/classes...
> mill run
Compiling 1 Kotlin sources to .../out/compile.dest/classes...
<h1>Hello World</h1>
stringifiedJsObject: ["hello","world","!"]

> mill foo.test # Test is incorrect, `foo.test`` fails
Compiling 1 Kotlin sources to .../out/foo/test/compile.dest/classes...
Linking IR to .../out/foo/test/linkBinary.dest/binaries
produce executable: .../out/foo/test/linkBinary.dest/binaries
> mill test # Test is incorrect, `test` fails
Compiling 1 Kotlin sources to .../out/test/compile.dest/classes...
Linking IR to .../out/test/linkBinary.dest/binaries
produce executable: .../out/test/linkBinary.dest/binaries
...
error: ...AssertionFailedError: expected:<"<h1>Hello World Wrong</h1>"> but was:<"<h1>Hello World</h1>...
...
error: AssertionError: Expected <<h1>Hello World</h1>>, actual <<h1>Hello World Wrong</h1>>.

> cat out/foo/test/linkBinary.dest/binaries/test.js # Generated javascript on disk
...assertEquals_0(..., '<h1>Hello World Wrong<\/h1>');...
> cat out/test/linkBinary.dest/binaries/test.js # Generated javascript on disk
...shouldBe(..., '<h1>Hello World Wrong<\/h1>');...
...

> sed -i.bak 's/Hello World Wrong/Hello World/g' foo/test/src/foo/HelloTests.kt
> sed -i.bak 's/Hello World Wrong/Hello World/g' test/src/foo/HelloTests.kt

> mill foo.test # passes after fixing test
> mill test # passes after fixing test

*/

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ fun main() {
}

fun hello(): String {
return createHTML().h1 { +"Hello World" }.toString()
}
return createHTML(prettyPrint = false).h1 { text("Hello World") }.toString()
}
11 changes: 11 additions & 0 deletions example/kotlinlib/web/3-hello-kotlinjs/test/src/foo/HelloTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package foo

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class HelloTests: FunSpec({
test("hello") {
val result = hello()
result shouldBe "<h1>Hello World Wrong</h1>"
}
})
109 changes: 93 additions & 16 deletions kotlinlib/src/mill/kotlinlib/js/KotlinJSModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,17 @@ trait KotlinJSModule extends KotlinModule { outer =>
}

kotlinJSRunTarget() match {
case Some(RunTarget.Node) => Jvm.runSubprocess(
case Some(RunTarget.Node) => {
val testBinaryPath = (linkResult.path / s"${moduleName()}.${moduleKind.extension}")
.toIO.getAbsolutePath
Jvm.runSubprocess(
commandArgs = Seq(
"node",
(linkResult.path / s"${moduleName()}.${moduleKind.extension}").toIO.getAbsolutePath
) ++ args().value,
"node"
) ++ args().value ++ Seq(testBinaryPath),
envArgs = T.env,
workingDir = T.dest
)
}
case Some(x) =>
T.log.error(s"Run target $x is not supported")
case None =>
Expand Down Expand Up @@ -379,16 +382,16 @@ trait KotlinJSModule extends KotlinModule { outer =>

// these 2 exist to ignore values added to the display name in case of the cross-modules
// we already have cross-modules in the paths, so we don't need them here
private def moduleName() = millModuleSegments.value
.filter(_.isInstanceOf[Segment.Label])
.map(_.asInstanceOf[Segment.Label])
.last
.value
private def fullModuleNameSegments() = {
millModuleSegments.value
.collect { case label: Segment.Label => label.value } match {
case Nil => Seq("root")
case segments => segments
}
}

private def fullModuleName() = millModuleSegments.value
.filter(_.isInstanceOf[Segment.Label])
.map(_.asInstanceOf[Segment.Label].value)
.mkString("-")
private def moduleName() = fullModuleNameSegments().last
private def fullModuleName() = fullModuleNameSegments().mkString("-")

// **NOTE**: This logic may (and probably is) be incomplete
private def isKotlinJsLibrary(path: os.Path)(implicit ctx: mill.api.Ctx): Boolean = {
Expand Down Expand Up @@ -417,8 +420,45 @@ trait KotlinJSModule extends KotlinModule { outer =>

// region Tests module

/**
* Generic trait to run tests for Kotlin/JS which doesn't specify test
* framework. For the particular implementation see [[KotlinTestPackageTests]] or [[KotestTests]].
*/
trait KotlinJSTests extends KotlinTests with KotlinJSModule {

// region private

// TODO may be optimized if there is a single folder for all modules
// but may be problematic if modules use different NPM packages versions
private def nodeModulesDir = Task(persistent = true) {
PathRef(T.dest)
}

// NB: for the packages below it is important to use specific version
// otherwise with random versions there is a possibility to have conflict
// between the versions of the shared transitive deps
private def mochaModule = Task {
val workingDir = nodeModulesDir().path
Jvm.runSubprocess(
commandArgs = Seq("npm", "install", "mocha@10.2.0"),
envArgs = T.env,
workingDir = workingDir
)
PathRef(workingDir / "node_modules" / "mocha" / "bin" / "mocha.js")
}

private def sourceMapSupportModule = Task {
val workingDir = nodeModulesDir().path
Jvm.runSubprocess(
commandArgs = Seq("npm", "install", "source-map-support@0.5.21"),
envArgs = T.env,
workingDir = nodeModulesDir().path
)
PathRef(workingDir / "node_modules" / "source-map-support" / "register.js")
}

// endregion

override def testFramework = ""

override def kotlinJSBinaryKind: T[Option[BinaryKind]] = Some(BinaryKind.Executable)
Expand All @@ -435,16 +475,53 @@ trait KotlinJSModule extends KotlinModule { outer =>
globSelectors: Task[Seq[String]]
): Task[(String, Seq[TestResult])] = Task.Anon {
// This is a terrible hack, but it works
run()()
run(Task.Anon {
Args(args() ++ Seq(
// TODO this is valid only for the NodeJS target. Once browser support is
// added, need to have different argument handling
"--require",
sourceMapSupportModule().path.toString(),
mochaModule().path.toString()
))
})()
("", Seq.empty[TestResult])
}

override def kotlinJSRunTarget: T[Option[RunTarget]] = Some(RunTarget.Node)
}

trait KotlinJSKotlinXTests extends KotlinJSTests {
/**
* Run tests for Kotlin/JS target using `kotlin.test` package.
*/
trait KotlinTestPackageTests extends KotlinJSTests {
override def ivyDeps = Agg(
ivy"org.jetbrains.kotlin:kotlin-test-js:${kotlinVersion()}"
)
override def kotlinJSRunTarget: T[Option[RunTarget]] = Some(RunTarget.Node)
}

/**
* Run tests for Kotlin/JS target using Kotest framework.
*/
trait KotestTests extends KotlinJSTests {

def kotestVersion: T[String] = "5.9.1"

private def kotestProcessor = Task {
defaultResolver().resolveDeps(
Agg(
ivy"io.kotest:kotest-framework-multiplatform-plugin-embeddable-compiler-jvm:${kotestVersion()}"
)
).head
}

override def kotlincOptions = super.kotlincOptions() ++ Seq(
s"-Xplugin=${kotestProcessor().path}"
)

override def ivyDeps = Agg(
ivy"io.kotest:kotest-framework-engine-js:${kotestVersion()}",
ivy"io.kotest:kotest-assertions-core-js:${kotestVersion()}"
)
}

// endregion
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo

import bar.getString
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class HelloTests: FunSpec({

test("success") {
getString() shouldBe "Hello, world"
}

test("failure") {
getString() shouldBe "Not hello, world"
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package mill
package kotlinlib.js

import mill.eval.EvaluatorPaths
import mill.testkit.{TestBaseModule, UnitTester}
import utest.{assert, TestSuite, Tests, test}

object KotlinJSKotestModuleTests extends TestSuite {

private val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "kotlin-js"

private val kotlinVersion = "1.9.25"

object module extends TestBaseModule {

object bar extends KotlinJSModule {
def kotlinVersion = KotlinJSKotestModuleTests.kotlinVersion
}

object foo extends KotlinJSModule {
def kotlinVersion = KotlinJSKotestModuleTests.kotlinVersion
override def moduleDeps = Seq(module.bar)

object test extends KotlinJSModule with KotestTests {
override def allSourceFiles = super.allSourceFiles()
.filter(!_.path.toString().endsWith("HelloKotlinTestPackageTests.kt"))
}
}
}

private def testEval() = UnitTester(module, resourcePath)

def tests: Tests = Tests {

test("run tests") {
val eval = testEval()

val command = module.foo.test.test()
val Left(_) = eval.apply(command)

// temporary, because we are running run() task, it won't be test.log, but run.log
val log =
os.read(EvaluatorPaths.resolveDestPaths(eval.outPath, command).log / ".." / "run.log")
assert(
log.contains(
"AssertionFailedError: expected:<\"Not hello, world\"> but was:<\"Hello, world\">"
),
log.contains("1 passing"),
log.contains("1 failing"),
// verify that source map is applied, otherwise all stack entries will point to .js
log.contains("HelloKotestTests.kt:")
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package mill
package kotlinlib
package js

import mill.eval.EvaluatorPaths
import mill.testkit.{TestBaseModule, UnitTester}
import utest.{assert, TestSuite, Tests, test}

object KotlinJSKotlinTestPackageModuleTests extends TestSuite {

private val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "kotlin-js"

private val kotlinVersion = "1.9.25"

object module extends TestBaseModule {

object bar extends KotlinJSModule {
def kotlinVersion = KotlinJSKotlinTestPackageModuleTests.kotlinVersion
}

object foo extends KotlinJSModule {
def kotlinVersion = KotlinJSKotlinTestPackageModuleTests.kotlinVersion
override def moduleDeps = Seq(module.bar)

object test extends KotlinJSModule with KotlinTestPackageTests {
override def allSourceFiles = super.allSourceFiles()
.filter(!_.path.toString().endsWith("HelloKotestTests.kt"))
}
}
}

private def testEval() = UnitTester(module, resourcePath)

def tests: Tests = Tests {

test("run tests") {
val eval = testEval()

val command = module.foo.test.test()
val Left(_) = eval.apply(command)

// temporary, because we are running run() task, it won't be test.log, but run.log
val log =
os.read(EvaluatorPaths.resolveDestPaths(eval.outPath, command).log / ".." / "run.log")
assert(
log.contains("AssertionError: Expected <Hello, world>, actual <Not hello, world>."),
log.contains("1 passing"),
log.contains("1 failing"),
// verify that source map is applied, otherwise all stack entries will point to .js
log.contains("HelloKotlinTestPackageTests.kt:")
)
}
}

}
Loading
Loading