-
-
Notifications
You must be signed in to change notification settings - Fork 337
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Kotest support for Kotlin/JS (#3723)
This PR brings [Kotest](https://github.com/kotest/kotest) test engine support for Kotlin/JS target test execution. Replaces #3710. --------- Co-authored-by: Li Haoyi <[email protected]>
- Loading branch information
Showing
10 changed files
with
251 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
*/ |
15 changes: 0 additions & 15 deletions
15
example/kotlinlib/web/3-hello-kotlinjs/foo/test/src/foo/HelloTests.kt
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
example/kotlinlib/web/3-hello-kotlinjs/test/src/foo/HelloTests.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>" | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 => | ||
|
@@ -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 = { | ||
|
@@ -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", "[email protected]"), | ||
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", "[email protected]"), | ||
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) | ||
|
@@ -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 | ||
|
16 changes: 16 additions & 0 deletions
16
kotlinlib/test/resources/kotlin-js/foo/test/src/foo/HelloKotestTests.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
}) |
File renamed without changes.
56 changes: 56 additions & 0 deletions
56
kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotestModuleTests.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:") | ||
) | ||
} | ||
} | ||
|
||
} |
55 changes: 55 additions & 0 deletions
55
kotlinlib/test/src/mill/kotlinlib/js/KotlinJSKotlinTestPackageModuleTests.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:") | ||
) | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.