Gradle: Basic Kotlin NodeJS tests runner

#KT-30531 Fixed
#KT-30528 Fixed
This commit is contained in:
Sergey Rostov
2019-02-20 15:14:05 +03:00
parent b6e675f934
commit d467e4209a
54 changed files with 3968 additions and 64 deletions
+3
View File
@@ -37,3 +37,6 @@ build/
.idea/.name .idea/.name
.idea/artifacts/dist_auto_* .idea/artifacts/dist_auto_*
kotlin-ultimate/ kotlin-ultimate/
node_modules/
.rpt2_cache/
libraries/tools/kotlin-test-nodejs-runner/lib/
@@ -23,6 +23,7 @@ dependencies {
testCompile(project(":kotlin-test:kotlin-test-jvm")) testCompile(project(":kotlin-test:kotlin-test-jvm"))
testCompile(projectRuntimeJar(":kotlin-compiler-embeddable")) testCompile(projectRuntimeJar(":kotlin-compiler-embeddable"))
testCompile(intellijCoreDep()) { includeJars("jdom") }
// testCompileOnly dependency on non-shaded artifacts is needed for IDE support // testCompileOnly dependency on non-shaded artifacts is needed for IDE support
// testRuntime on shaded artifact is needed for running tests with shaded compiler // testRuntime on shaded artifact is needed for running tests with shaded compiler
testCompileOnly(project(path = ":kotlin-gradle-plugin-test-utils-embeddable", configuration = "compile")) testCompileOnly(project(path = ":kotlin-gradle-plugin-test-utils-embeddable", configuration = "compile"))
@@ -1,9 +1,13 @@
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import com.intellij.testFramework.TestDataFile
import org.gradle.api.logging.LogLevel import org.gradle.api.logging.LogLevel
import org.gradle.tooling.GradleConnector import org.gradle.tooling.GradleConnector
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.gradle.util.VersionNumber import org.jdom.Element
import org.jdom.input.SAXBuilder
import org.jdom.output.Format
import org.jdom.output.XMLOutputter
import org.jetbrains.kotlin.gradle.model.ModelContainer import org.jetbrains.kotlin.gradle.model.ModelContainer
import org.jetbrains.kotlin.gradle.model.ModelFetcherBuildAction import org.jetbrains.kotlin.gradle.model.ModelFetcherBuildAction
import org.jetbrains.kotlin.gradle.util.* import org.jetbrains.kotlin.gradle.util.*
@@ -495,6 +499,23 @@ abstract class BaseGradleIT {
} }
} }
fun CompiledProject.assertTasksRegistered(vararg tasks: String) {
for (task in tasks) {
assertContains("'Register task $task'")
}
}
fun CompiledProject.assertTasksNotRealized(vararg tasks: String) {
for (task in tasks) {
assertNotContains("'Realize task $task'")
}
}
fun CompiledProject.assertTasksRegisteredAndNotRealized(vararg tasks: String) {
assertTasksRegistered(*tasks)
assertTasksNotRealized(*tasks)
}
fun CompiledProject.getOutputForTask(taskName: String): String { fun CompiledProject.getOutputForTask(taskName: String): String {
val taskOutputRegex = ("(?:\\[LIFECYCLE] \\[class org\\.gradle(?:\\.internal\\.buildevents)?\\.TaskExecutionLogger] :$taskName|" + val taskOutputRegex = ("(?:\\[LIFECYCLE] \\[class org\\.gradle(?:\\.internal\\.buildevents)?\\.TaskExecutionLogger] :$taskName|" +
"\\[org\\.gradle\\.execution\\.plan\\.DefaultPlanExecutor\\] :$taskName.*?started)" + "\\[org\\.gradle\\.execution\\.plan\\.DefaultPlanExecutor\\] :$taskName.*?started)" +
@@ -575,6 +596,71 @@ abstract class BaseGradleIT {
File(projectDir, it).takeIf(File::exists) File(projectDir, it).takeIf(File::exists)
}.single() }.single()
/**
* @param assertionFileName path to xml with expected test results, relative to test resources root
*/
fun CompiledProject.assertTestResults(
@TestDataFile assertionFileName: String,
testReportName: String
) {
val projectDir = project.projectDir
val testReportDir = projectDir.resolve("build/test-results/$testReportName")
if (!testReportDir.isDirectory) {
error("Test report dir was not created")
}
val actualTestResults = readAndCleanupTestResults(testReportDir, projectDir)
val expectedTestResults = resourcesRootFile.resolve(assertionFileName).readText()
assertEquals(
prettyPrintXml(expectedTestResults),
prettyPrintXml(actualTestResults)
)
}
private fun readAndCleanupTestResults(testReportDir: File, projectDir: File): String {
val files = testReportDir
.listFiles()
.filter { it.isFile && it.name.endsWith(".xml") }
.sortedBy {
// let containing test suite be first
it.name.replace(".xml", ".A.xml")
}
val xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<results>\n" +
files.joinToString("") {
it.readText()
.replace(projectDir.absolutePath, "/\$PROJECT_DIR$")
.replace(projectDir.name, "\$PROJECT_NAME$")
.replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "")
} +
"</results>"
val doc = SAXBuilder().build(xmlString.reader())
val skipAttrs = setOf("timestamp", "hostname", "time", "message")
val skipContentsOf = setOf("failure")
fun cleanup(e: Element) {
if (e.name in skipContentsOf) e.text = "..."
e.attributes.forEach {
if (it.name in skipAttrs) {
it.value = "..."
}
}
e.children.forEach {
cleanup(it)
}
}
cleanup(doc.rootElement)
return XMLOutputter(Format.getPrettyFormat()).outputString(doc)
}
private fun prettyPrintXml(uglyXml: String): String =
XMLOutputter(Format.getPrettyFormat()).outputString(SAXBuilder().build(uglyXml.reader()))
private fun Project.createGradleTailParameters(options: BuildOptions, params: Array<out String> = arrayOf()): List<String> = private fun Project.createGradleTailParameters(options: BuildOptions, params: Array<out String> = arrayOf()): List<String> =
params.toMutableList().apply { params.toMutableList().apply {
add("--stacktrace") add("--stacktrace")
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import com.intellij.testFramework.TestDataPath
import org.jetbrains.kotlin.gradle.plugin.EXPECTED_BY_CONFIG_NAME import org.jetbrains.kotlin.gradle.plugin.EXPECTED_BY_CONFIG_NAME
import org.jetbrains.kotlin.gradle.plugin.IMPLEMENT_CONFIG_NAME import org.jetbrains.kotlin.gradle.plugin.IMPLEMENT_CONFIG_NAME
import org.jetbrains.kotlin.gradle.plugin.IMPLEMENT_DEPRECATION_WARNING import org.jetbrains.kotlin.gradle.plugin.IMPLEMENT_DEPRECATION_WARNING
@@ -25,6 +26,7 @@ import org.junit.Test
import java.io.File import java.io.File
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestDataPath("\$CONTENT_ROOT/resources")
class MultiplatformGradleIT : BaseGradleIT() { class MultiplatformGradleIT : BaseGradleIT() {
@Test @Test
@@ -293,4 +295,81 @@ class MultiplatformGradleIT : BaseGradleIT() {
assertTasksExecuted(customSourceSetCompileTasks) assertTasksExecuted(customSourceSetCompileTasks)
} }
} }
@Test
fun testMppNodeJsTestRun() = with(Project("new-mpp-js-tests", GradleVersionRequired.AtLeast("4.10.2"))) {
setupWorkingDir()
gradleBuildScript().modify(::transformBuildScriptWithPluginsDsl)
gradleSettingsScript().modify(::transformBuildScriptWithPluginsDsl)
// just build without running tests to check configuration avoidance
build("assemble") {
assertSuccessful()
assertTasksRegisteredAndNotRealized(
":clientKotlinJsNodeModules",
":clientKotlinJsNodeModulesTestRuntime",
":clientTest",
":serverKotlinJsNodeModules",
":serverKotlinJsNodeModulesTestRuntime",
":serverTest"
)
}
build("check") {
assertSuccessful()
assertTasksExecuted(
":clientKotlinJsNodeModules",
":clientKotlinJsNodeModulesTestRuntime",
":clientTest",
":serverKotlinJsNodeModules",
":serverKotlinJsNodeModulesTestRuntime",
":serverTest"
)
assertTestResults("testProject/new-mpp-js-tests/TEST-CommonTest.xml", "clientTest")
assertTestResults("testProject/new-mpp-js-tests/TEST-CommonTest.xml", "serverTest")
}
// test all is up-to-date when no changes
build("check") {
assertSuccessful()
assertTasksUpToDate(
":clientKotlinJsNodeModules",
":clientKotlinJsNodeModulesTestRuntime",
":clientTest",
":serverKotlinJsNodeModules",
":serverKotlinJsNodeModulesTestRuntime",
":serverTest"
)
}
// change common file and check that all tasks executed
projectDir.resolve("src/commonMain/kotlin/common.kt").writeText("fun common() = 777")
build("check") {
assertSuccessful()
assertTasksUpToDate(
":clientKotlinJsNodeModulesTestRuntime",
":serverKotlinJsNodeModulesTestRuntime"
)
assertTasksExecuted(
":clientKotlinJsNodeModules",
":clientTest",
":serverKotlinJsNodeModules",
":serverTest"
)
assertTestResults("testProject/new-mpp-js-tests/TEST-CommonTest-2.xml", "clientTest")
assertTestResults("testProject/new-mpp-js-tests/TEST-CommonTest-2.xml", "serverTest")
}
}
} }
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<results>
<testsuite name="CommonTest" tests="3" skipped="1" failures="2" errors="0" timestamp="..." hostname="..." time="...">
<properties />
<testcase name="test1" classname="CommonTest" time="...">
<failure message="..." type="org.jetbrains.kotlin.gradle.targets.js.NodeJsTestFailure">...</failure>
</testcase>
<testcase name="test2" classname="CommonTest" time="...">
<skipped />
</testcase>
<testcase name="test3" classname="CommonTest" time="...">
<failure message="..." type="org.jetbrains.kotlin.gradle.targets.js.NodeJsTestFailure">...</failure>
</testcase>
<system-out />
<system-err />
</testsuite>
<testsuite name="InnerIgnored" tests="3" skipped="3" failures="0" errors="0" timestamp="..." hostname="..." time="...">
<properties />
<testcase name="test4" classname="InnerIgnored" time="...">
<skipped />
</testcase>
<testcase name="test5" classname="InnerIgnored" time="...">
<skipped />
</testcase>
<testcase name="test6" classname="InnerIgnored" time="...">
<skipped />
</testcase>
<system-out />
<system-err />
</testsuite>
</results>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<results>
<testsuite name="CommonTest" tests="3" skipped="1" failures="1" errors="0" timestamp="..." hostname="..." time="...">
<properties />
<testcase name="test1" classname="CommonTest" time="..." />
<testcase name="test2" classname="CommonTest" time="...">
<skipped />
</testcase>
<testcase name="test3" classname="CommonTest" time="...">
<failure message="..." type="org.jetbrains.kotlin.gradle.targets.js.NodeJsTestFailure">...</failure>
</testcase>
<system-out />
<system-err />
</testsuite>
<testsuite name="InnerIgnored" tests="3" skipped="3" failures="0" errors="0" timestamp="..." hostname="..." time="...">
<properties />
<testcase name="test4" classname="InnerIgnored" time="...">
<skipped />
</testcase>
<testcase name="test5" classname="InnerIgnored" time="...">
<skipped />
</testcase>
<testcase name="test6" classname="InnerIgnored" time="...">
<skipped />
</testcase>
<system-out />
<system-err />
</testsuite>
</results>
@@ -0,0 +1,42 @@
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinNodeJsTestTask
plugins {
kotlin("multiplatform").version("<pluginMarkerVersion>")
}
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
kotlin {
dependencies {
commonMainImplementation(kotlin("stdlib-common"))
commonTestApi(kotlin("test-common"))
}
val jsCommon = js("jsCommon") {
dependencies {
commonMainImplementation(kotlin("stdlib-js"))
commonTestApi(kotlin("test-js"))
}
}
js("server")
js("client")
}
tasks {
"jsCommonTest"(KotlinNodeJsTestTask::class) {
ignoreFailures = true
}
"clientTest"(KotlinNodeJsTestTask::class) {
ignoreFailures = true
}
"serverTest"(KotlinNodeJsTestTask::class) {
ignoreFailures = true
}
}
@@ -0,0 +1,8 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
rootProject.name = "new-mpp-js-tests"
@@ -0,0 +1,31 @@
package my.pack.name
import common
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
class CommonTest {
@Test
fun test1() = assertEquals(common(), 1)
@Test
@Ignore
fun test2() = assertEquals(common(), 2)
@Test
fun test3() = assertEquals(common(), 3)
@Ignore
class InnerIgnored {
@Test
fun test4() = assertEquals(common(), 1)
@Test
@Ignore
fun test5() = assertEquals(common(), 2)
@Test
fun test6() = assertEquals(common(), 3)
}
}
@@ -15,13 +15,14 @@ publish()
// todo: make lazy // todo: make lazy
val jar: Jar by tasks val jar: Jar by tasks
runtimeJar(rewriteDepsToShadedCompiler(jar)) val jarContents by configurations.creating
sourcesJar() sourcesJar()
javadocJar() javadocJar()
repositories { repositories {
google() google()
maven(url = "https://plugins.gradle.org/m2/")
} }
pill { pill {
@@ -61,6 +62,9 @@ dependencies {
runtime(projectRuntimeJar(":kotlin-scripting-compiler-embeddable")) runtime(projectRuntimeJar(":kotlin-scripting-compiler-embeddable"))
runtime(project(":kotlin-reflect")) runtime(project(":kotlin-reflect"))
jarContents(compileOnly(intellijDep()) { includeJars("serviceMessages") })
jarContents(projectArchives(":kotlin-test-nodejs-runner"))
// com.android.tools.build:gradle has ~50 unneeded transitive dependencies // com.android.tools.build:gradle has ~50 unneeded transitive dependencies
compileOnly("com.android.tools.build:gradle:3.0.0") { isTransitive = false } compileOnly("com.android.tools.build:gradle:3.0.0") { isTransitive = false }
compileOnly("com.android.tools.build:gradle-core:3.0.0") { isTransitive = false } compileOnly("com.android.tools.build:gradle-core:3.0.0") { isTransitive = false }
@@ -77,6 +81,17 @@ dependencies {
testCompileOnly(project(":kotlin-annotation-processing-gradle")) testCompileOnly(project(":kotlin-annotation-processing-gradle"))
} }
runtimeJar(rewriteDepsToShadedCompiler(jar)) {
dependsOn(jarContents)
from {
jarContents.asFileTree.map {
if (it.endsWith(".jar")) zipTree(it)
else it
}
}
}
tasks { tasks {
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions.jdkHome = rootProject.extra["JDK_18"] as String kotlinOptions.jdkHome = rootProject.extra["JDK_18"] as String
@@ -0,0 +1,310 @@
package org.jetbrains.kotlin.gradle.internal.testing
import jetbrains.buildServer.messages.serviceMessages.*
import org.gradle.api.internal.tasks.testing.*
import org.gradle.api.tasks.testing.TestOutputEvent.Destination.StdErr
import org.gradle.api.tasks.testing.TestOutputEvent.Destination.StdOut
import org.gradle.api.tasks.testing.TestResult
import org.gradle.api.tasks.testing.TestResult.ResultType.*
import org.gradle.internal.operations.OperationIdentifier
import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.gradle.targets.js.NodeJsTestFailure
import org.slf4j.Logger
import java.text.ParseException
import java.lang.System.currentTimeMillis as currentTimeMillis1
data class TCServiceMessagesClientSettings(
val rootNodeName: String,
val nameOfRootSuiteToAppend: String? = null,
val nameOfRootSuiteToReplace: String? = null,
val nameOfLeafTestToAppend: String? = null,
val skipRoots: Boolean = false
) {
init {
if (skipRoots) {
check(nameOfRootSuiteToReplace == null) { "nameOfRootSuiteToReplace makes no sense when skipRoots is set" }
check(nameOfRootSuiteToAppend == null) { "nameOfRootSuiteToAppend cannot work with skipRoots" }
}
}
}
internal class TCServiceMessagesClient(
private val results: TestResultProcessor,
val settings: TCServiceMessagesClientSettings,
val log: Logger
) : ServiceMessageParserCallback {
inline fun root(operation: OperationIdentifier, actions: () -> Unit) {
RootNode(operation.id).open { root ->
if (settings.nameOfRootSuiteToAppend != null) {
SuiteNode(root, settings.nameOfRootSuiteToAppend).open {
actions()
}
} else {
actions()
}
}
}
override fun parseException(e: ParseException, text: String) {
log.error("Failed to parse test process messages: \"$text\"", e)
}
override fun serviceMessage(message: ServiceMessage) {
log.kotlinDebug { "TCSM: $message" }
when (message) {
is TestSuiteStarted -> open(message.ts, SuiteNode(leaf, hookSuiteName(leaf, message.suiteName)))
is TestStarted -> beginTest(message.ts, message.testName)
is TestStdOut -> results.output(requireLeafTest().descriptor, DefaultTestOutputEvent(StdOut, message.stdOut))
is TestStdErr -> results.output(requireLeafTest().descriptor, DefaultTestOutputEvent(StdErr, message.stdErr))
is TestFailed -> requireLeafTest().failure(message)
is TestFinished -> endTest(message.ts, message.testName)
is TestIgnored -> {
if (message.attributes["suite"] == "true") {
// non standard property for dealing with ignored test suites without visiting all inner tests
SuiteNode(requireLeaf(), message.testName).open(message.ts) { message.ts }
} else {
beginTest(message.ts, message.testName, isIgnored = true)
endTest(message.ts, message.testName)
}
}
is TestSuiteFinished -> close(message.ts, hookSuiteName(leaf?.parent, message.suiteName))
else -> Unit
}
}
private fun hookSuiteName(parent: Node?, originalName: String) =
if (parent?.parent == null && settings.nameOfRootSuiteToReplace != null) settings.nameOfRootSuiteToReplace
else originalName
override fun regularText(text: String) {
log.kotlinDebug { "TCSM stdout captured: $text" }
val test = leaf as? TestNode
if (test != null) {
results.output(test.descriptor, DefaultTestOutputEvent(StdOut, text))
} else {
println(text)
}
}
private fun beginTest(ts: Long, testName: String, isIgnored: Boolean = false) {
val parent = requireLeafGroup()
val parsedName = ParsedTestName(testName, parent.localId)
if (settings.nameOfLeafTestToAppend != null) {
val group = open(ts, SuiteNode(parent, parsedName.methodName))
open(ts, TestNode(
group, parsedName.className, parsedName.classDisplayName, parsedName.methodName,
displayName = "${parsedName.methodName}.${settings.nameOfLeafTestToAppend}",
localId = "$testName.${settings.nameOfLeafTestToAppend}",
ignored = isIgnored
))
} else {
open(ts, TestNode(
parent, parsedName.className, parsedName.classDisplayName, parsedName.methodName,
displayName = parsedName.methodName,
localId = testName,
ignored = isIgnored
))
}
}
private fun endTest(ts: Long, testName: String) {
val parsedName = ParsedTestName(testName, leaf!!.parent!!.localId)
if (settings.nameOfLeafTestToAppend != null) {
close(ts, "$testName.${settings.nameOfLeafTestToAppend}")
close(ts, parsedName.methodName)
} else {
close(ts, testName)
}
}
private fun Node.failure(
message: TestFailed
) {
hasFailures = true
results.failure(descriptor.id, NodeJsTestFailure(message.messageName, message.stacktrace))
}
private inline fun <NodeType : Node> NodeType.open(contents: (NodeType) -> Unit) = open(System.currentTimeMillis()) {
contents(it)
System.currentTimeMillis()
}
private inline fun <NodeType : Node> NodeType.open(tsStart: Long, contents: (NodeType) -> Long) {
val child = open(tsStart, this@open)
val tsEnd = contents(child)
assert(close(tsEnd, child.localId) === child)
}
private fun <NodeType : Node> open(ts: Long, new: NodeType): NodeType = new.also {
log.kotlinDebug { "Test node opened: $it" }
if (!it.isSkippedRoot) {
results.started(it.descriptor, TestStartEvent(ts, it.reportingParent?.descriptor?.id))
}
push(it)
}
private fun close(ts: Long, assertLocalId: String?) = pop().also {
if (assertLocalId != null) {
check(it.localId == assertLocalId) {
"Bad TCSM: unexpected node to close: ${it.localId}, stack: ${
collectParents().joinToString("") { item -> "\n - ${item.localId}" }
}\n"
}
}
log.kotlinDebug { "Test node closed: $it" }
if (!it.isSkippedRoot) {
results.completed(it.descriptor.id, TestCompleteEvent(ts, it.resultType))
}
}
private fun collectParents(): MutableList<Node> {
var i = leaf
val items = mutableListOf<Node>()
while (i != null) {
items.add(i)
i = i.parent
}
return items
}
class ParsedTestName(testName: String, parentName: String) {
val hasClassName: Boolean
val className: String
val classDisplayName: String
val methodName: String
init {
val methodNameCut = testName.lastIndexOf('.')
hasClassName = methodNameCut != -1
if (hasClassName) {
className = testName.substring(0, methodNameCut)
classDisplayName = className.substringAfterLast('.')
methodName = testName.substring(methodNameCut + 1)
} else {
className = parentName
classDisplayName = parentName
methodName = testName
}
}
}
/**
* Node of tests tree
*/
abstract inner class Node(
var parent: Node? = null,
val localId: String
) {
val reportingParent: Node?
get() = when {
parent == null -> null
parent!!.isSkippedRoot -> parent!!.reportingParent
else -> parent
}
val isSkippedRoot: Boolean
get() = settings.skipRoots && parent != null && parent!!.parent == null
val id: String = if (parent != null) "${reportingParent?.descriptor?.id}.$localId" else localId
abstract val descriptor: TestDescriptorInternal
var hasFailures: Boolean = false
set(value) {
// traverse parents only on first failure
if (!field) {
field = value
parent?.hasFailures = true
}
}
/**
* If all tests in group are ignored, then group marked as skipped.
* This is workaround for absence of ignored test suite flag in TC service messages protocol.
*/
var containsNotIgnored: Boolean = false
set(value) {
// traverse parents only on first test
if (!field) {
field = value
parent?.containsNotIgnored = true
}
}
val resultType: TestResult.ResultType
get() = when {
containsNotIgnored -> when {
hasFailures -> FAILURE
else -> SUCCESS
}
else -> SKIPPED
}
override fun toString(): String = descriptor.toString()
}
inner class RootNode(val ownerBuildOperationId: Any) : Node(null, settings.rootNodeName) {
override val descriptor =
object : DefaultTestSuiteDescriptor(settings.rootNodeName, localId) {
override fun getOwnerBuildOperationId(): Any? = this@RootNode.ownerBuildOperationId
}
}
inner class SuiteNode(parent: Node? = null, name: String) : Node(parent, name) {
override val descriptor = object : DefaultTestSuiteDescriptor(id, name) {
override fun getParent(): TestDescriptorInternal? = this@SuiteNode.parent?.descriptor
}
}
inner class TestNode(
parent: Node,
className: String,
classDisplayName: String,
methodName: String,
displayName: String,
localId: String,
ignored: Boolean = false
) : Node(parent, localId) {
override val descriptor = object : DefaultTestDescriptor(id, className, methodName, classDisplayName, displayName) {
override fun getParent(): TestDescriptorInternal? = this@TestNode.parent?.descriptor
}
init {
if (!ignored) containsNotIgnored = true
}
}
private var leaf: Node? = null
private val ServiceMessage.ts: Long
get() = creationTimestamp?.timestamp?.time ?: System.currentTimeMillis()
private fun push(node: Node) = node.also { leaf = node }
private fun pop() = leaf!!.also { leaf = it.parent }
fun closeAll() {
val ts = System.currentTimeMillis()
while (leaf != null) {
close(ts, leaf!!.localId)
}
}
private fun requireLeaf() = leaf ?: error("test out of group")
private fun requireLeafGroup() = requireLeaf().also {
check(it !is TestNode) { "previous test `$it` not finished"}
}
private fun requireLeafTest() = leaf as? TestNode
?: error("no running test")
}
@@ -0,0 +1,86 @@
package org.jetbrains.kotlin.gradle.internal.testing
import jetbrains.buildServer.messages.serviceMessages.ServiceMessage
import org.gradle.api.internal.tasks.testing.TestExecuter
import org.gradle.api.internal.tasks.testing.TestExecutionSpec
import org.gradle.api.internal.tasks.testing.TestResultProcessor
import org.gradle.internal.operations.BuildOperationExecutor
import org.gradle.process.ProcessForkOptions
import org.gradle.process.internal.ExecHandle
import org.gradle.process.internal.ExecHandleFactory
import org.slf4j.LoggerFactory
import java.io.PipedInputStream
import java.io.PipedOutputStream
import kotlin.concurrent.thread
data class TCServiceMessagesTestExecutionSpec(
val forkOptions: ProcessForkOptions,
val args: List<String>,
val clientSettings: TCServiceMessagesClientSettings
) : TestExecutionSpec
private val log = LoggerFactory.getLogger("org.jetbrains.kotlin.gradle.tasks.testing")
class TCServiceMessagesTestExecutor(
val execHandleFactory: ExecHandleFactory,
val buildOperationExecutor: BuildOperationExecutor
) : TestExecuter<TCServiceMessagesTestExecutionSpec> {
var execHandle: ExecHandle? = null
var outputReaderThread: Thread? = null
var shouldStop = false
override fun execute(spec: TCServiceMessagesTestExecutionSpec, testResultProcessor: TestResultProcessor) {
val stdInPipe = PipedInputStream()
val rootOperation = buildOperationExecutor.currentOperation.parentId
outputReaderThread = thread(name = "${spec.forkOptions} output reader") {
try {
val client = TCServiceMessagesClient(testResultProcessor, spec.clientSettings, log)
client.root(rootOperation) {
stdInPipe.reader().useLines { lines ->
lines.forEach {
if (shouldStop) {
client.closeAll()
return@thread
}
try {
ServiceMessage.parse(it, client)
} catch (e: Exception) {
log.error(
"Error while processing test process output message \"$it\"",
e
)
}
}
}
}
} catch (t: Throwable) {
log.error("Error creating TCServiceMessagesClient", t)
}
}
val exec = execHandleFactory.newExec()
spec.forkOptions.copyTo(exec)
exec.args = spec.args
exec.standardOutput = PipedOutputStream(stdInPipe)
execHandle = exec.build()
execHandle!!.start()
val result = execHandle!!.waitForFinish()
outputReaderThread!!.join()
if (result.exitValue != 0) {
error("$execHandle exited with errors (exit code: ${result.exitValue})")
}
}
override fun stopNow() {
shouldStop = true
execHandle?.abort()
outputReaderThread?.join()
}
}
@@ -0,0 +1,118 @@
package org.jetbrains.kotlin.gradle.targets.js
import org.gradle.api.Project
import org.gradle.api.internal.plugins.DslObject
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.testing.base.plugins.TestingBasePlugin
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsSetupTask
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinJsNodeModulesTask
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinNodeJsTestRuntimeToNodeModulesTask
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinNodeJsTestTask
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import org.jetbrains.kotlin.gradle.tasks.registerTask
import java.io.File
internal class KotlinJsCompilationTestsConfigurator(
val compilation: KotlinCompilationToRunnableFiles<*>
) {
private val target get() = compilation.target
private val project get() = target.project
private val compileTestKotlin2Js get() = compilation.compileKotlinTask as Kotlin2JsCompile
private val isSinglePlatformProject get() = target is KotlinWithJavaTarget<*>
private val testTaskName: String
get() = if (isSinglePlatformProject) "testJs" else camelCaseTargetName("test")
private fun camelCaseTargetName(prefix: String): String {
return if (isSinglePlatformProject) prefix
else target.name + prefix.capitalize()
}
@Suppress("SameParameterValue")
private fun underscoredCompilationName(prefix: String): String {
return if (isSinglePlatformProject) prefix
else "${target.name}_${compilation.name}_$prefix"
}
private val nodeModulesDir
get() = project.buildDir.resolve(underscoredCompilationName("node_modules"))
@Suppress("UnstableApiUsage")
private val Project.testResults: File
get() = project.buildDir.resolve(TestingBasePlugin.TEST_RESULTS_DIR_NAME)
@Suppress("UnstableApiUsage")
private val Project.testReports: File
get() = project.buildDir.resolve(TestingBasePlugin.TESTS_DIR_NAME)
private val compileTask: Kotlin2JsCompile
get() = project.tasks.findByName(compileTestKotlin2Js.name) as Kotlin2JsCompile
private val Kotlin2JsCompile.jsRuntimeClasspath: FileCollection
get() = classpath.plus(project.files(destinationDir))
fun configure() {
val nodeModulesTask = registerTask(
project,
camelCaseTargetName("kotlinJsNodeModules"),
KotlinJsNodeModulesTask::class.java
) {
it.dependsOn(compileTestKotlin2Js)
it.nodeModulesDir = nodeModulesDir
it.classpath = compileTask.jsRuntimeClasspath
}
val nodeModulesTestRuntimeTask = registerTask(
project,
camelCaseTargetName("kotlinJsNodeModulesTestRuntime"),
KotlinNodeJsTestRuntimeToNodeModulesTask::class.java
) {
it.nodeModulesDir = nodeModulesDir
}
val projectWithNodeJsPlugin = NodeJsPlugin.ensureAppliedInHierarchy(target.project)
val testTask = registerTask(project, testTaskName, KotlinNodeJsTestTask::class.java) { testJs ->
testJs.group = "verification"
testJs.dependsOn(
nodeModulesTask.getTaskOrProvider(),
nodeModulesTestRuntimeTask.getTaskOrProvider()
)
if (!isSinglePlatformProject) {
testJs.targetName = target.name
}
testJs.nodeJsProcessOptions.workingDir = project.projectDir
testJs.nodeModulesDir = nodeModulesDir
testJs.nodeModulesToLoad = setOf(compileTestKotlin2Js.outputFile.name)
val htmlReport = DslObject(testJs.reports.html)
val xmlReport = DslObject(testJs.reports.junitXml)
xmlReport.conventionMapping.map("destination") { project.testResults.resolve(testJs.name) }
htmlReport.conventionMapping.map("destination") { project.testReports.resolve(testJs.name) }
testJs.conventionMapping.map("binResultsDir") { project.testResults.resolve(testJs.name + "/binary") }
}
project.afterEvaluate {
project.tasks.findByName(JavaBasePlugin.CHECK_TASK_NAME)?.dependsOn(testTask.getTaskOrProvider())
// defer nodeJs executable setup, as nodejs project settings may change during configuration
testTask.configure {
val nodeJsSetupTask = projectWithNodeJsPlugin.tasks.findByName(NodeJsSetupTask.NAME)
it.dependsOn(nodeJsSetupTask)
if (it.nodeJsProcessOptions.executable == null) {
it.nodeJsProcessOptions.executable = NodeJsExtension[projectWithNodeJsPlugin].buildEnv().nodeExec
}
}
}
}
}
@@ -5,21 +5,14 @@
package org.jetbrains.kotlin.gradle.targets.js package org.jetbrains.kotlin.gradle.targets.js
import org.gradle.api.plugins.JavaBasePlugin import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.Kotlin2JsSourceSetProcessor
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetProcessor
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetConfigurator
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinJsNodeModulesTask import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinNodeJsTestRuntimeToNodeModulesTask
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinNodeJsTestTask
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider
class KotlinJsTargetConfigurator(kotlinPluginVersion: String) : class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
KotlinTargetConfigurator<KotlinJsCompilation>(true, true, kotlinPluginVersion) { KotlinTargetConfigurator<KotlinJsCompilation>(true, true, kotlinPluginVersion) {
override fun buildCompilationProcessor(compilation: KotlinJsCompilation): KotlinSourceSetProcessor<*> { override fun buildCompilationProcessor(compilation: KotlinJsCompilation): KotlinSourceSetProcessor<*> {
val tasksProvider = KotlinTasksProvider(compilation.target.targetName) val tasksProvider = KotlinTasksProvider(compilation.target.targetName)
@@ -28,59 +21,11 @@ class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
override fun configureTest(target: KotlinOnlyTarget<KotlinJsCompilation>) { override fun configureTest(target: KotlinOnlyTarget<KotlinJsCompilation>) {
target.compilations.all { target.compilations.all {
configureTest(it) it.compileKotlinTask.kotlinOptions.moduleKind = "umd"
}
}
private fun configureTest(compilation: KotlinCompilationToRunnableFiles<*>) { if (it.name == KotlinCompilation.TEST_COMPILATION_NAME) {
val target = compilation.target KotlinJsCompilationTestsConfigurator(it).configure()
val project = target.project }
val name = compilation.name
val compileTestKotlin2Js = compilation.compileKotlinTask as Kotlin2JsCompile
val isDefaultTarget = compilation.name.isBlank()
fun camelCaseName(prefix: String): String {
return if (isDefaultTarget) prefix
else name + prefix.capitalize()
}
fun underscoredName(prefix: String): String {
return if (isDefaultTarget) prefix
else "${name}_$prefix"
}
val nodeModulesDir = project.buildDir.resolve(underscoredName("node_modules"))
val nodeModulesTask = project.tasks.create(
camelCaseName("kotlinJsNodeModules"),
KotlinJsNodeModulesTask::class.java
) {
it.dependsOn(compileTestKotlin2Js)
it.nodeModulesDir = nodeModulesDir
it.compile = compileTestKotlin2Js
}
val nodeModulesTestRuntimeTask = project.tasks.create(
camelCaseName("kotlinJsNodeModulesTestRuntime"),
KotlinNodeJsTestRuntimeToNodeModulesTask::class.java
) {
it.nodeModulesDir = nodeModulesDir
}
val testJs: KotlinNodeJsTestTask =
if (isDefaultTarget) project.tasks.create("testJs", KotlinNodeJsTestTask::class.java)
else project.tasks.replace(camelCaseName("test"), KotlinNodeJsTestTask::class.java)
testJs.also {
it.group = "verification"
it.dependsOn(nodeModulesTask, nodeModulesTestRuntimeTask)
it.nodeModulesDir = nodeModulesDir
it.nodeModulesToLoad = setOf(compileTestKotlin2Js.outputFile.name)
}
project.afterEvaluate {
project.tasks.findByName(JavaBasePlugin.CHECK_TASK_NAME)?.dependsOn(testJs)
} }
} }
} }
@@ -0,0 +1,15 @@
package org.jetbrains.kotlin.gradle.targets.js
/**
* Class to be shown in default Gradle tests console reporter.
*
* Example console output:
* ```
* clientTest.CommonTest.test1 FAILED
* org.jetbrains.kotlin.gradle.targets.js.NodeJsTestFailure
* ```
*/
class NodeJsTestFailure(message: String, val stackTrace: String?) : Throwable(message) {
override fun fillInStackTrace(): Throwable = this
override fun toString(): String = stackTrace ?: ""
}
@@ -0,0 +1,17 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs
import java.io.*
internal data class NodeJsEnv(
val nodeDir: File,
val nodeBinDir: File,
val nodeExec: String,
val npmExec: String,
val platformName: String,
val architectureName: String,
val ivyDependency: String
) {
val isWindows: Boolean
get() = platformName == "win"
}
@@ -0,0 +1,68 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import java.io.File
open class NodeJsExtension(project: Project) {
private val gradleHome = project.gradle.gradleUserHomeDir.also {
project.logger.kotlinInfo("Storing cached files in $it")
}
var installationDir = gradleHome.resolve("nodejs")
var distBaseUrl = "https://nodejs.org/dist"
var version = "10.15.3"
var npmVersion = ""
var nodeCommand = "node"
var npmCommand = "npm"
var download = true
internal fun buildEnv(): NodeJsEnv {
val platform = NodeJsPlatform.name
val architecture = NodeJsPlatform.architecture
val nodeDir = installationDir.resolve("node-v$version-$platform-$architecture")
val isWindows = NodeJsPlatform.name == NodeJsPlatform.WIN
val nodeBinDir = if (isWindows) nodeDir else nodeDir.resolve("bin")
fun getExecutable(command: String, customCommand: String, windowsExtension: String): String {
val finalCommand = if (isWindows && customCommand == command) "$command.$windowsExtension" else customCommand
return if (download) File(nodeBinDir, finalCommand).absolutePath else finalCommand
}
fun getIvyDependency(): String {
val type = if (isWindows) "zip" else "tar.gz"
return "org.nodejs:node:$version:$platform-$architecture@$type"
}
return NodeJsEnv(
nodeDir = nodeDir,
nodeBinDir = nodeBinDir,
nodeExec = getExecutable("node", nodeCommand, "exe"),
npmExec = getExecutable("npm", npmCommand, "cmd"),
platformName = platform,
architectureName = architecture,
ivyDependency = getIvyDependency()
)
}
companion object {
const val NODE_JS: String = "nodeJs"
operator fun get(project: Project): NodeJsExtension {
val extension = project.extensions.findByType(NodeJsExtension::class.java)
if (extension != null)
return extension
val parentProject = project.parent
if (parentProject != null)
return get(parentProject)
throw GradleException("NodeJsExtension is not installed")
}
}
}
@@ -0,0 +1,49 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs
/**
* Provides platform and architecture names that is used to download NodeJs.
* See [NodeJsEnv.ivyDependency] that is filled in [NodeJsExtension.buildEnv].
*/
internal object NodeJsPlatform {
private val props = System.getProperties()
private fun property(name: String) = props.getProperty(name) ?: System.getProperty(name)
const val WIN = "win"
const val DARWIN = "darwin"
const val LINUX = "linux"
const val SUNOS = "sunos"
val name: String = run {
val name = property("os.name").toLowerCase()
when {
name.contains("windows") -> WIN
name.contains("mac") -> DARWIN
name.contains("linux") -> LINUX
name.contains("freebsd") -> LINUX
name.contains("sunos") -> SUNOS
else -> throw IllegalArgumentException("Unsupported OS: $name")
}
}
const val X64 = "x64"
const val X86 = "x86"
const val ARM64 = "arm64"
val architecture: String = run {
val arch = property("os.arch").toLowerCase()
when {
arch.contains("64") -> X64
arch == "arm" -> {
// as Java just returns "arm" on all ARM variants, we need a system call to determine the exact arch
// the node binaries for 'armv8l' are called 'arm64', so we need to distinguish here
val process = Runtime.getRuntime().exec("uname -m")
val systemArch = process.inputStream.bufferedReader().use { it.readText() }
when (systemArch.trim()) {
"armv8l" -> ARM64
else -> systemArch
}
}
else -> X86
}
}
}
@@ -0,0 +1,29 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsExtension.Companion.NODE_JS
open class NodeJsPlugin : Plugin<Project> {
override fun apply(project: Project): Unit = project.run {
this.extensions.create(NODE_JS, NodeJsExtension::class.java, this)
tasks.create(NodeJsSetupTask.NAME, NodeJsSetupTask::class.java)
}
companion object {
fun ensureAppliedInHierarchy(myProject: Project): Project =
findInHeirarchy(myProject) ?: myProject.also {
it.pluginManager.apply(NodeJsPlugin::class.java)
}
fun findInHeirarchy(myProject: Project): Project? {
var project: Project? = myProject
while (project != null) {
if (myProject.plugins.hasPlugin(NodeJsPlugin::class.java)) return project
project = project.parent
}
return null
}
}
}
@@ -0,0 +1,85 @@
package org.jetbrains.kotlin.gradle.targets.js.nodejs
import org.gradle.api.DefaultTask
import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.logging.kotlinInfo
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
import java.io.File
import java.net.URI
open class NodeJsSetupTask : DefaultTask() {
private val settings = NodeJsExtension[project]
private val env by lazy { settings.buildEnv() }
init {
group = NodeJsExtension.NODE_JS
description = "Download and install a local node/npm version."
}
val input: Set<String>
@Input get() = setOf(settings.download.toString(), env.ivyDependency)
val destination: File
@OutputDirectory get() = env.nodeDir
@Suppress("unused")
@TaskAction
fun exec() {
if (!settings.download)
return
@Suppress("UnstableApiUsage", "DEPRECATION")
val repo = project.repositories.ivy { repo ->
repo.name = "Node Distributions at ${settings.distBaseUrl}"
repo.url = URI(settings.distBaseUrl)
repo.layout("pattern") { layout ->
layout as IvyPatternRepositoryLayout
layout.artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]")
layout.ivy("v[revision]/ivy.xml")
}
repo.metadataSources { it.artifact() }
if (isGradleVersionAtLeast(5, 1)) {
repo.content { it.includeModule("org.nodejs", "node") }
}
}
val dep = this.project.dependencies.create(env.ivyDependency)
val conf = this.project.configurations.detachedConfiguration(dep)
conf.isTransitive = false
val result = conf.resolve().single()
project.repositories.remove(repo)
project.logger.kotlinInfo("Using node distribution from '$result'")
unpackNodeArchive(result, destination.parentFile) // parent because archive contains name already
if (!env.isWindows) {
File(env.nodeExec).setExecutable(true)
}
}
private fun unpackNodeArchive(archive: File, destination: File) {
project.logger.kotlinInfo("Unpacking $archive to $destination")
when {
archive.name.endsWith("zip") -> project.copy {
it.from(project.zipTree(archive))
it.into(destination)
}
else -> {
project.copy {
it.from(project.tarTree(archive))
it.into(destination)
}
}
}
}
companion object {
const val NAME: String = "nodeJsSetup"
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.targets.js.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import java.io.File
open class KotlinJsNodeModulesTask : DefaultTask() {
@OutputDirectory
@SkipWhenEmpty
lateinit var nodeModulesDir: File
@Classpath
@InputFiles
@SkipWhenEmpty
lateinit var classpath: FileCollection
@TaskAction
fun copyFromRuntimeClasspath() {
project.copy { copy ->
copy.includeEmptyDirs = false
classpath.forEach {
if (it.isZip) copy.from(project.zipTree(it))
else copy.from(it)
}
copy.include { fileTreeElement ->
isKotlinJsRuntimeFile(fileTreeElement.file)
}
copy.into(nodeModulesDir)
}
}
}
val File.isZip
get() = isFile && name.endsWith(".jar")
fun isKotlinJsRuntimeFile(file: File): Boolean {
if (!file.isFile) return false
val name = file.name
return (name.endsWith(".js") && !name.endsWith(".meta.js"))
|| name.endsWith(".js.map")
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.targets.js.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.*
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
const val kotlinNodeJsTestRuntimeBin: String = "kotlin-js-test"
open class KotlinNodeJsTestRuntimeToNodeModulesTask : DefaultTask() {
@Input
public var resourceName: String = "/kotlin-js-test.js"
@OutputDirectory
@SkipWhenEmpty
public lateinit var nodeModulesDir: File
@TaskAction
fun copyRuntime() {
val testsRuntime = javaClass.getResourceAsStream(resourceName) ?: error("Cannot find `$resourceName` in resources")
val bin = nodeModulesDir.resolve(".bin").resolve(kotlinNodeJsTestRuntimeBin)
bin.parentFile.mkdirs()
Files.copy(
testsRuntime,
bin.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
}
data class KotlinNodeJsTestRunnerCliArgs(
val moduleNames: List<String>,
val include: Collection<String> = listOf(),
val exclude: Collection<String> = listOf(),
val ignoredTestSuites: IgnoredTestSuitesReporting = IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored
) {
fun toList(): List<String> = mutableListOf<String>().also { args ->
if (include.isNotEmpty()) {
args.add("--include")
args.add(include.joinToString(","))
}
if (exclude.isNotEmpty()) {
args.add("--exclude")
args.add(exclude.joinToString(","))
}
if (ignoredTestSuites !== IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored) {
args.add("--ignoredTestSuites")
args.add(ignoredTestSuites.name)
}
args.addAll(moduleNames)
}
@Suppress("EnumEntryName")
enum class IgnoredTestSuitesReporting {
skip, reportAsIgnoredTest, reportAllInnerTestsAsIgnored
}
}
@@ -0,0 +1,125 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.targets.js.tasks
import org.gradle.api.internal.file.FileResolver
import org.gradle.api.internal.tasks.testing.filter.DefaultTestFilter
import org.gradle.api.tasks.*
import org.gradle.api.tasks.testing.AbstractTestTask
import org.gradle.process.ProcessForkOptions
import org.gradle.process.internal.DefaultProcessForkOptions
import org.gradle.process.internal.ExecHandleFactory
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutor
import org.jetbrains.kotlin.gradle.testing.IgnoredTestSuites
import org.jetbrains.kotlin.gradle.testing.TestsGrouping
import org.jetbrains.kotlin.gradle.utils.injected
import java.io.File
import javax.inject.Inject
open class KotlinNodeJsTestTask : AbstractTestTask() {
@Input
var ignoredTestSuites: IgnoredTestSuites =
IgnoredTestSuites.showWithContents
@Input
var testsGrouping: TestsGrouping =
TestsGrouping.root
@Input
@Optional
var targetName: String? = null
@Input
var excludes = mutableSetOf<String>()
@InputDirectory
var nodeModulesDir: File? = null
@Input
@SkipWhenEmpty
var nodeModulesToLoad: Set<String> = setOf()
@InputFile
@Optional
var testRuntimeNodeModule: File? = null
@Suppress("UnstableApiUsage")
private val filterExt: DefaultTestFilter
get() = filter as DefaultTestFilter
init {
filterExt.isFailOnNoMatchingTests = false
}
@get:Inject
open val fileResolver: FileResolver
get() = injected
@Suppress("LeakingThis")
@Internal
val nodeJsProcessOptions: ProcessForkOptions = DefaultProcessForkOptions(fileResolver)
val nodeJsExecutable: String
@Input get() = nodeJsProcessOptions.executable
val nodeJsWorkingDirCanonicalPath: String
@Input get() = nodeJsProcessOptions.workingDir.canonicalPath
fun nodeJs(options: ProcessForkOptions.() -> Unit) {
options(nodeJsProcessOptions)
}
override fun createTestExecutionSpec(): TCServiceMessagesTestExecutionSpec {
val extendedForkOptions = DefaultProcessForkOptions(fileResolver)
nodeJsProcessOptions.copyTo(extendedForkOptions)
extendedForkOptions.environment.addPath("NODE_PATH", nodeModulesDir!!.canonicalPath)
val cliArgs = KotlinNodeJsTestRunnerCliArgs(
nodeModulesToLoad.toList(),
filterExt.includePatterns + filterExt.commandLineIncludePatterns,
excludes,
ignoredTestSuites.cli
)
val clientSettings = when (testsGrouping) {
TestsGrouping.none -> TCServiceMessagesClientSettings(rootNodeName = name, skipRoots = true)
TestsGrouping.root -> TCServiceMessagesClientSettings(rootNodeName = name, nameOfRootSuiteToReplace = targetName)
TestsGrouping.leaf -> TCServiceMessagesClientSettings(
rootNodeName = name,
skipRoots = true,
nameOfLeafTestToAppend = targetName
)
}
return TCServiceMessagesTestExecutionSpec(
extendedForkOptions,
listOf(finalTestRuntimeNodeModule.absolutePath) + cliArgs.toList(),
clientSettings
)
}
@get:Inject
open val execHandleFactory: ExecHandleFactory
get() = injected
private val finalTestRuntimeNodeModule: File
get() = testRuntimeNodeModule
?: nodeModulesDir!!.resolve(".bin").resolve(kotlinNodeJsTestRuntimeBin)
override fun createTestExecuter() = TCServiceMessagesTestExecutor(
execHandleFactory,
buildOperationExecutor
)
}
private fun MutableMap<String, Any>.addPath(key: String, path: String) {
val prev = get(key)
if (prev == null) set(key, path)
else set(key, prev as String + File.pathSeparator + path)
}
@@ -0,0 +1,18 @@
package org.jetbrains.kotlin.gradle.testing
import org.jetbrains.kotlin.gradle.targets.js.tasks.KotlinNodeJsTestRunnerCliArgs
@Suppress("EnumEntryName")
enum class IgnoredTestSuites(val cli: KotlinNodeJsTestRunnerCliArgs.IgnoredTestSuitesReporting) {
hide(KotlinNodeJsTestRunnerCliArgs.IgnoredTestSuitesReporting.skip),
showWithContents(KotlinNodeJsTestRunnerCliArgs.IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored),
showWithoutContents(KotlinNodeJsTestRunnerCliArgs.IgnoredTestSuitesReporting.reportAsIgnoredTest)
}
@Suppress("EnumEntryName")
enum class TestsGrouping {
none,
root,
leaf;
}
@@ -0,0 +1,11 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.utils
/**
* Utility for functions annotated with @Inject
*/
val injected: Nothing get() = error("Should be injected");
@@ -0,0 +1 @@
implementation-class=org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
@@ -0,0 +1,40 @@
package org.jetbrains.kotlin.gradle.internal.testing
import org.gradle.api.internal.tasks.testing.*
import org.gradle.api.tasks.testing.TestOutputEvent
class RecordingTestResultProcessor : TestResultProcessor {
val output = StringBuilder()
var indent = 0
fun line(line: String) {
repeat(indent) { output.append(" ") }
output.appendln(line)
}
override fun started(test: TestDescriptorInternal, event: TestStartEvent) {
val description = when (test) {
is DefaultTestSuiteDescriptor -> "SUITE ${test.displayName}"
is DefaultTestDescriptor -> "TEST displayName: ${test.displayName}, " +
"classDisplayName: ${test.classDisplayName}, " +
"className: ${test.className}, " +
"name: ${test.name}"
else -> error("Unknown test descriptor $test")
}
line("STARTED $description // ${test.id}")
indent++
}
override fun output(testId: Any, event: TestOutputEvent) {
line("${event.destination}[${event.message}] // $testId")
}
override fun failure(testId: Any, result: Throwable) {
line("FAILURE ${result.message} // $testId")
}
override fun completed(testId: Any, event: TestCompleteEvent) {
indent--
line("COMPLETED ${event.resultType} // $testId")
}
}
@@ -0,0 +1,116 @@
package org.jetbrains.kotlin.gradle.internal.testing.tcsmc
import jetbrains.buildServer.messages.serviceMessages.TestFinished
import jetbrains.buildServer.messages.serviceMessages.TestStarted
import jetbrains.buildServer.messages.serviceMessages.TestSuiteFinished
import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted
import org.junit.Test
class AppendLeaf : TCServiceMessagesClientTest() {
@Test
fun appendLeafWithFqn() {
skipRoots = true
nameOfLeafTestToAppend = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE myMethod // root.myMethod
STARTED TEST displayName: myMethod.js, classDisplayName: MyTest, className: my.company.product.MyTest, name: myMethod // root.myMethod.my.company.product.MyTest.myMethod.js
COMPLETED SUCCESS // root.myMethod.my.company.product.MyTest.myMethod.js
COMPLETED SUCCESS // root.myMethod
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestStarted("my.company.product.MyTest.myMethod", false, null))
serviceMessage(TestFinished("my.company.product.MyTest.myMethod", 1))
serviceMessage(TestSuiteFinished(""))
}
}
@Test
fun appendLeafWithClassName() {
skipRoots = true
nameOfLeafTestToAppend = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE myMethod // root.myMethod
STARTED TEST displayName: myMethod.js, classDisplayName: MyTest, className: MyTest, name: myMethod // root.myMethod.MyTest.myMethod.js
COMPLETED SUCCESS // root.myMethod.MyTest.myMethod.js
COMPLETED SUCCESS // root.myMethod
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestStarted("MyTest.myMethod", false, null))
serviceMessage(TestFinished("MyTest.myMethod", 1))
serviceMessage(TestSuiteFinished(""))
}
}
@Test
fun replaceRootWithSuite() {
nameOfRootSuiteToReplace = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE js // root.js
STARTED SUITE MyTest // root.js.MyTest
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: MyTest, name: myMethod // root.js.MyTest.myMethod
COMPLETED SUCCESS // root.js.MyTest.myMethod
COMPLETED SUCCESS // root.js.MyTest
COMPLETED SUCCESS // root.js
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestSuiteStarted("MyTest"))
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
serviceMessage(TestSuiteFinished("MyTest"))
serviceMessage(TestSuiteFinished(""))
}
}
@Test
fun appendLeaf() {
skipRoots = true
nameOfLeafTestToAppend = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE myMethod // root.myMethod
STARTED TEST displayName: myMethod.js, classDisplayName: , className: , name: myMethod // root.myMethod.myMethod.js
COMPLETED SUCCESS // root.myMethod.myMethod.js
COMPLETED SUCCESS // root.myMethod
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
serviceMessage(TestSuiteFinished(""))
}
}
@Test
fun appendLeafWithSuite() {
skipRoots = true
nameOfLeafTestToAppend = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE MyTest // root.MyTest
STARTED SUITE myMethod // root.MyTest.myMethod
STARTED TEST displayName: myMethod.js, classDisplayName: MyTest, className: MyTest, name: myMethod // root.MyTest.myMethod.myMethod.js
COMPLETED SUCCESS // root.MyTest.myMethod.myMethod.js
COMPLETED SUCCESS // root.MyTest.myMethod
COMPLETED SUCCESS // root.MyTest
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestSuiteStarted("MyTest"))
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
serviceMessage(TestSuiteFinished("MyTest"))
serviceMessage(TestSuiteFinished(""))
}
}
}
@@ -0,0 +1,58 @@
package org.jetbrains.kotlin.gradle.internal.testing.tcsmc
import jetbrains.buildServer.messages.serviceMessages.TestFinished
import jetbrains.buildServer.messages.serviceMessages.TestStarted
import org.junit.Test
class AppendRoot : TCServiceMessagesClientTest() {
@Test
fun testWithFqn() {
nameOfRootSuiteToAppend = "ios"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE ios // root.ios
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: my.company.product.MyTest, name: myMethod // root.ios.my.company.product.MyTest.myMethod
COMPLETED SUCCESS // root.ios.my.company.product.MyTest.myMethod
COMPLETED SUCCESS // root.ios
COMPLETED SUCCESS // root
""") {
serviceMessage(TestStarted("my.company.product.MyTest.myMethod", false, null))
serviceMessage(TestFinished("my.company.product.MyTest.myMethod", 1))
}
}
@Test
fun testWithClassName() {
nameOfRootSuiteToAppend = "ios"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE ios // root.ios
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: MyTest, name: myMethod // root.ios.MyTest.myMethod
COMPLETED SUCCESS // root.ios.MyTest.myMethod
COMPLETED SUCCESS // root.ios
COMPLETED SUCCESS // root
""") {
serviceMessage(TestStarted("MyTest.myMethod", false, null))
serviceMessage(TestFinished("MyTest.myMethod", 1))
}
}
@Test
fun testRoot() {
nameOfRootSuiteToAppend = "ios"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE ios // root.ios
STARTED TEST displayName: myMethod, classDisplayName: ios, className: ios, name: myMethod // root.ios.myMethod
COMPLETED SUCCESS // root.ios.myMethod
COMPLETED SUCCESS // root.ios
COMPLETED SUCCESS // root
""") {
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
}
}
}
@@ -0,0 +1,197 @@
package org.jetbrains.kotlin.gradle.internal.testing.tcsmc
import jetbrains.buildServer.messages.serviceMessages.*
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClient
import org.junit.Test
class Complex: TCServiceMessagesClientTest() {
@Test
fun testComplexJs() {
nameOfRootSuiteToReplace = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE js // root.js
STARTED SUITE MyTest // root.js.MyTest
STARTED TEST displayName: myTest1, classDisplayName: MyTest, className: my.company.product.MyTest, name: myTest1 // root.js.MyTest.my.company.product.MyTest.myTest1
COMPLETED SUCCESS // root.js.MyTest.my.company.product.MyTest.myTest1
STARTED TEST displayName: myTest2, classDisplayName: MyTest, className: my.company.product.MyTest, name: myTest2 // root.js.MyTest.my.company.product.MyTest.myTest2
FAILURE testFailed // root.js.MyTest.my.company.product.MyTest.myTest2
COMPLETED FAILURE // root.js.MyTest.my.company.product.MyTest.myTest2
STARTED TEST displayName: myTest3, classDisplayName: MyTest, className: my.company.product.MyTest, name: myTest3 // root.js.MyTest.my.company.product.MyTest.myTest3
COMPLETED SKIPPED // root.js.MyTest.my.company.product.MyTest.myTest3
STARTED SUITE MyTestNested // root.js.MyTest.MyTestNested
STARTED TEST displayName: myTest4, classDisplayName: MyTestNested, className: my.company.product.MyTest.MyTestNested, name: myTest4 // root.js.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest4
COMPLETED SUCCESS // root.js.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest4
STARTED TEST displayName: myTest5, classDisplayName: MyTestNested, className: my.company.product.MyTest.MyTestNested, name: myTest5 // root.js.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest5
FAILURE testFailed // root.js.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest5
COMPLETED FAILURE // root.js.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest5
STARTED TEST displayName: myTest6, classDisplayName: MyTestNested, className: my.company.product.MyTest.MyTestNested, name: myTest6 // root.js.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest6
COMPLETED SKIPPED // root.js.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest6
STARTED SUITE MyTestNestedNested // root.js.MyTest.MyTestNested.MyTestNestedNested
STARTED TEST displayName: myTest7, classDisplayName: MyTestNestedNested, className: my.company.product.MyTest.MyTestNestedNested, name: myTest7 // root.js.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest7
COMPLETED SUCCESS // root.js.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest7
STARTED TEST displayName: myTest8, classDisplayName: MyTestNestedNested, className: my.company.product.MyTest.MyTestNestedNested, name: myTest8 // root.js.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest8
FAILURE testFailed // root.js.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest8
COMPLETED FAILURE // root.js.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest8
STARTED TEST displayName: myTest9, classDisplayName: MyTestNestedNested, className: my.company.product.MyTest.MyTestNestedNested, name: myTest9 // root.js.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest9
COMPLETED SKIPPED // root.js.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest9
COMPLETED FAILURE // root.js.MyTest.MyTestNested.MyTestNestedNested
COMPLETED FAILURE // root.js.MyTest.MyTestNested
COMPLETED FAILURE // root.js.MyTest
STARTED SUITE MyTest2 // root.js.MyTest2
STARTED TEST displayName: myTest10, classDisplayName: MyTest2, className: my.company.product.MyTest2, name: myTest10 // root.js.MyTest2.my.company.product.MyTest2.myTest10
COMPLETED SUCCESS // root.js.MyTest2.my.company.product.MyTest2.myTest10
STARTED TEST displayName: myTest11, classDisplayName: MyTest2, className: my.company.product.MyTest2, name: myTest11 // root.js.MyTest2.my.company.product.MyTest2.myTest11
FAILURE testFailed // root.js.MyTest2.my.company.product.MyTest2.myTest11
COMPLETED FAILURE // root.js.MyTest2.my.company.product.MyTest2.myTest11
STARTED TEST displayName: myTest12, classDisplayName: MyTest2, className: my.company.product.MyTest2, name: myTest12 // root.js.MyTest2.my.company.product.MyTest2.myTest12
COMPLETED SKIPPED // root.js.MyTest2.my.company.product.MyTest2.myTest12
STARTED SUITE MyTest2Nested // root.js.MyTest2.MyTest2Nested
STARTED TEST displayName: myTest13, classDisplayName: MyTestNested, className: my.company.product.MyTest2.MyTestNested, name: myTest13 // root.js.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest13
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest13
STARTED TEST displayName: myTest14, classDisplayName: MyTestNested, className: my.company.product.MyTest2.MyTestNested, name: myTest14 // root.js.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest14
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest14
STARTED TEST displayName: myTest15, classDisplayName: MyTestNested, className: my.company.product.MyTest2.MyTestNested, name: myTest15 // root.js.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest15
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest15
STARTED SUITE MyTest2NestedNested // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested
STARTED TEST displayName: myTest16, classDisplayName: MyTest2NestedNested, className: my.company.product.MyTest2.MyTest2NestedNested, name: myTest16 // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest16
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest16
STARTED TEST displayName: myTest17, classDisplayName: MyTest2NestedNested, className: my.company.product.MyTest2.MyTest2NestedNested, name: myTest17 // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest17
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest17
STARTED TEST displayName: myTest18, classDisplayName: MyTest2NestedNested, className: my.company.product.MyTest2.MyTest2NestedNested, name: myTest18 // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest18
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest18
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested.MyTest2NestedNested
COMPLETED SKIPPED // root.js.MyTest2.MyTest2Nested
COMPLETED FAILURE // root.js.MyTest2
COMPLETED FAILURE // root.js
COMPLETED FAILURE // root
""") {
serviceMessage(TestSuiteStarted(""))
complexFixture()
serviceMessage(TestSuiteFinished(""))
}
}
@Test
fun testComplexIos() {
nameOfRootSuiteToAppend = "ios"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE ios // root.ios
STARTED SUITE MyTest // root.ios.MyTest
STARTED TEST displayName: myTest1, classDisplayName: MyTest, className: my.company.product.MyTest, name: myTest1 // root.ios.MyTest.my.company.product.MyTest.myTest1
COMPLETED SUCCESS // root.ios.MyTest.my.company.product.MyTest.myTest1
STARTED TEST displayName: myTest2, classDisplayName: MyTest, className: my.company.product.MyTest, name: myTest2 // root.ios.MyTest.my.company.product.MyTest.myTest2
FAILURE testFailed // root.ios.MyTest.my.company.product.MyTest.myTest2
COMPLETED FAILURE // root.ios.MyTest.my.company.product.MyTest.myTest2
STARTED TEST displayName: myTest3, classDisplayName: MyTest, className: my.company.product.MyTest, name: myTest3 // root.ios.MyTest.my.company.product.MyTest.myTest3
COMPLETED SKIPPED // root.ios.MyTest.my.company.product.MyTest.myTest3
STARTED SUITE MyTestNested // root.ios.MyTest.MyTestNested
STARTED TEST displayName: myTest4, classDisplayName: MyTestNested, className: my.company.product.MyTest.MyTestNested, name: myTest4 // root.ios.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest4
COMPLETED SUCCESS // root.ios.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest4
STARTED TEST displayName: myTest5, classDisplayName: MyTestNested, className: my.company.product.MyTest.MyTestNested, name: myTest5 // root.ios.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest5
FAILURE testFailed // root.ios.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest5
COMPLETED FAILURE // root.ios.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest5
STARTED TEST displayName: myTest6, classDisplayName: MyTestNested, className: my.company.product.MyTest.MyTestNested, name: myTest6 // root.ios.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest6
COMPLETED SKIPPED // root.ios.MyTest.MyTestNested.my.company.product.MyTest.MyTestNested.myTest6
STARTED SUITE MyTestNestedNested // root.ios.MyTest.MyTestNested.MyTestNestedNested
STARTED TEST displayName: myTest7, classDisplayName: MyTestNestedNested, className: my.company.product.MyTest.MyTestNestedNested, name: myTest7 // root.ios.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest7
COMPLETED SUCCESS // root.ios.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest7
STARTED TEST displayName: myTest8, classDisplayName: MyTestNestedNested, className: my.company.product.MyTest.MyTestNestedNested, name: myTest8 // root.ios.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest8
FAILURE testFailed // root.ios.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest8
COMPLETED FAILURE // root.ios.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest8
STARTED TEST displayName: myTest9, classDisplayName: MyTestNestedNested, className: my.company.product.MyTest.MyTestNestedNested, name: myTest9 // root.ios.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest9
COMPLETED SKIPPED // root.ios.MyTest.MyTestNested.MyTestNestedNested.my.company.product.MyTest.MyTestNestedNested.myTest9
COMPLETED FAILURE // root.ios.MyTest.MyTestNested.MyTestNestedNested
COMPLETED FAILURE // root.ios.MyTest.MyTestNested
COMPLETED FAILURE // root.ios.MyTest
STARTED SUITE MyTest2 // root.ios.MyTest2
STARTED TEST displayName: myTest10, classDisplayName: MyTest2, className: my.company.product.MyTest2, name: myTest10 // root.ios.MyTest2.my.company.product.MyTest2.myTest10
COMPLETED SUCCESS // root.ios.MyTest2.my.company.product.MyTest2.myTest10
STARTED TEST displayName: myTest11, classDisplayName: MyTest2, className: my.company.product.MyTest2, name: myTest11 // root.ios.MyTest2.my.company.product.MyTest2.myTest11
FAILURE testFailed // root.ios.MyTest2.my.company.product.MyTest2.myTest11
COMPLETED FAILURE // root.ios.MyTest2.my.company.product.MyTest2.myTest11
STARTED TEST displayName: myTest12, classDisplayName: MyTest2, className: my.company.product.MyTest2, name: myTest12 // root.ios.MyTest2.my.company.product.MyTest2.myTest12
COMPLETED SKIPPED // root.ios.MyTest2.my.company.product.MyTest2.myTest12
STARTED SUITE MyTest2Nested // root.ios.MyTest2.MyTest2Nested
STARTED TEST displayName: myTest13, classDisplayName: MyTestNested, className: my.company.product.MyTest2.MyTestNested, name: myTest13 // root.ios.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest13
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest13
STARTED TEST displayName: myTest14, classDisplayName: MyTestNested, className: my.company.product.MyTest2.MyTestNested, name: myTest14 // root.ios.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest14
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest14
STARTED TEST displayName: myTest15, classDisplayName: MyTestNested, className: my.company.product.MyTest2.MyTestNested, name: myTest15 // root.ios.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest15
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested.my.company.product.MyTest2.MyTestNested.myTest15
STARTED SUITE MyTest2NestedNested // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested
STARTED TEST displayName: myTest16, classDisplayName: MyTest2NestedNested, className: my.company.product.MyTest2.MyTest2NestedNested, name: myTest16 // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest16
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest16
STARTED TEST displayName: myTest17, classDisplayName: MyTest2NestedNested, className: my.company.product.MyTest2.MyTest2NestedNested, name: myTest17 // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest17
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest17
STARTED TEST displayName: myTest18, classDisplayName: MyTest2NestedNested, className: my.company.product.MyTest2.MyTest2NestedNested, name: myTest18 // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest18
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested.my.company.product.MyTest2.MyTest2NestedNested.myTest18
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested.MyTest2NestedNested
COMPLETED SKIPPED // root.ios.MyTest2.MyTest2Nested
COMPLETED FAILURE // root.ios.MyTest2
COMPLETED FAILURE // root.ios
COMPLETED FAILURE // root
""") {
complexFixture()
}
}
private fun TCServiceMessagesClient.complexFixture() {
serviceMessage(TestSuiteStarted("MyTest"))
serviceMessage(TestStarted("my.company.product.MyTest.myTest1", false, null))
serviceMessage(TestFinished("my.company.product.MyTest.myTest1", 1))
serviceMessage(TestStarted("my.company.product.MyTest.myTest2", false, null))
serviceMessage(TestFailed("my.company.product.MyTest.myTest2", null))
serviceMessage(TestFinished("my.company.product.MyTest.myTest2", 1))
serviceMessage(TestIgnored("my.company.product.MyTest.myTest3", ""))
serviceMessage(TestSuiteStarted("MyTestNested"))
serviceMessage(TestStarted("my.company.product.MyTest.MyTestNested.myTest4", false, null))
serviceMessage(TestFinished("my.company.product.MyTest.MyTestNested.myTest4", 1))
serviceMessage(TestStarted("my.company.product.MyTest.MyTestNested.myTest5", false, null))
serviceMessage(TestFailed("my.company.product.MyTest.MyTestNested.myTest5", null))
serviceMessage(TestFinished("my.company.product.MyTest.MyTestNested.myTest5", 1))
serviceMessage(TestIgnored("my.company.product.MyTest.MyTestNested.myTest6", ""))
serviceMessage(TestSuiteStarted("MyTestNestedNested"))
serviceMessage(TestStarted("my.company.product.MyTest.MyTestNestedNested.myTest7", false, null))
serviceMessage(TestFinished("my.company.product.MyTest.MyTestNestedNested.myTest7", 1))
serviceMessage(TestStarted("my.company.product.MyTest.MyTestNestedNested.myTest8", false, null))
serviceMessage(TestFailed("my.company.product.MyTest.MyTestNestedNested.myTest8", null))
serviceMessage(TestFinished("my.company.product.MyTest.MyTestNestedNested.myTest8", 1))
serviceMessage(TestIgnored("my.company.product.MyTest.MyTestNestedNested.myTest9", ""))
serviceMessage(TestSuiteFinished("MyTestNestedNested"))
serviceMessage(TestSuiteFinished("MyTestNested"))
serviceMessage(TestSuiteFinished("MyTest"))
serviceMessage(TestSuiteStarted("MyTest2"))
serviceMessage(TestStarted("my.company.product.MyTest2.myTest10", false, null))
serviceMessage(TestFinished("my.company.product.MyTest2.myTest10", 1))
serviceMessage(TestStarted("my.company.product.MyTest2.myTest11", false, null))
serviceMessage(TestFailed("my.company.product.MyTest2.myTest11", null))
serviceMessage(TestFinished("my.company.product.MyTest2.myTest11", 1))
serviceMessage(TestIgnored("my.company.product.MyTest2.myTest12", ""))
serviceMessage(TestSuiteStarted("MyTest2Nested"))
serviceMessage(TestIgnored("my.company.product.MyTest2.MyTestNested.myTest13", ""))
serviceMessage(TestIgnored("my.company.product.MyTest2.MyTestNested.myTest14", ""))
serviceMessage(TestIgnored("my.company.product.MyTest2.MyTestNested.myTest15", ""))
serviceMessage(TestSuiteStarted("MyTest2NestedNested"))
serviceMessage(TestIgnored("my.company.product.MyTest2.MyTest2NestedNested.myTest16", ""))
serviceMessage(TestIgnored("my.company.product.MyTest2.MyTest2NestedNested.myTest17", ""))
serviceMessage(TestIgnored("my.company.product.MyTest2.MyTest2NestedNested.myTest18", ""))
serviceMessage(TestSuiteFinished("MyTest2NestedNested"))
serviceMessage(TestSuiteFinished("MyTest2Nested"))
serviceMessage(TestSuiteFinished("MyTest2"))
}
}
@@ -0,0 +1,66 @@
package org.jetbrains.kotlin.gradle.internal.testing.tcsmc
import jetbrains.buildServer.messages.serviceMessages.TestFinished
import jetbrains.buildServer.messages.serviceMessages.TestStarted
import jetbrains.buildServer.messages.serviceMessages.TestSuiteFinished
import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted
import org.junit.Test
class ReplaceRoot : TCServiceMessagesClientTest() {
@Test
fun replaceRootWithFqn() {
nameOfRootSuiteToReplace = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE js // root.js
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: my.company.product.MyTest, name: myMethod // root.js.my.company.product.MyTest.myMethod
COMPLETED SUCCESS // root.js.my.company.product.MyTest.myMethod
COMPLETED SUCCESS // root.js
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestStarted("my.company.product.MyTest.myMethod", false, null))
serviceMessage(TestFinished("my.company.product.MyTest.myMethod", 1))
serviceMessage(TestSuiteFinished(""))
}
}
@Test
fun replaceRootWithClassName() {
nameOfRootSuiteToReplace = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE js // root.js
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: MyTest, name: myMethod // root.js.MyTest.myMethod
COMPLETED SUCCESS // root.js.MyTest.myMethod
COMPLETED SUCCESS // root.js
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestStarted("MyTest.myMethod", false, null))
serviceMessage(TestFinished("MyTest.myMethod", 1))
serviceMessage(TestSuiteFinished(""))
}
}
@Test
fun replaceRoot() {
nameOfRootSuiteToReplace = "js"
assertEvents("""
STARTED SUITE root // root
STARTED SUITE js // root.js
STARTED TEST displayName: myMethod, classDisplayName: js, className: js, name: myMethod // root.js.myMethod
COMPLETED SUCCESS // root.js.myMethod
COMPLETED SUCCESS // root.js
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
serviceMessage(TestSuiteFinished(""))
}
}
}
@@ -0,0 +1,30 @@
package org.jetbrains.kotlin.gradle.internal.testing.tcsmc
import jetbrains.buildServer.messages.serviceMessages.TestFinished
import jetbrains.buildServer.messages.serviceMessages.TestStarted
import jetbrains.buildServer.messages.serviceMessages.TestSuiteFinished
import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted
import org.junit.Test
class SkipRoot: TCServiceMessagesClientTest() {
@Test
fun testSkipRoot() {
skipRoots = true
assertEvents("""
STARTED SUITE root // root
STARTED SUITE MyTest // root.MyTest
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: MyTest, name: myMethod // root.MyTest.myMethod
COMPLETED SUCCESS // root.MyTest.myMethod
COMPLETED SUCCESS // root.MyTest
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted(""))
serviceMessage(TestSuiteStarted("MyTest"))
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
serviceMessage(TestSuiteFinished("MyTest"))
serviceMessage(TestSuiteFinished(""))
}
}
}
@@ -0,0 +1,43 @@
package org.jetbrains.kotlin.gradle.internal.testing.tcsmc
import org.gradle.internal.operations.OperationIdentifier
import org.jetbrains.kotlin.gradle.internal.testing.RecordingTestResultProcessor
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClient
import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings
import org.slf4j.LoggerFactory
import kotlin.test.assertEquals
open class TCServiceMessagesClientTest {
protected var nameOfRootSuiteToAppend: String? = null
protected var nameOfRootSuiteToReplace: String? = null
protected var nameOfLeafTestToAppend: String? = null
protected var skipRoots: Boolean = false
internal fun assertEvents(assertion: String, produceServiceMessage: TCServiceMessagesClient.() -> Unit) {
val results = RecordingTestResultProcessor()
val client = createClient(results)
client.root(OperationIdentifier(1)) {
client.produceServiceMessage()
}
assertEquals(
assertion.trim(),
results.output.toString().trim()
)
}
internal open fun createClient(results: RecordingTestResultProcessor): TCServiceMessagesClient {
return TCServiceMessagesClient(
results,
TCServiceMessagesClientSettings(
"root",
nameOfRootSuiteToAppend,
nameOfRootSuiteToReplace,
nameOfLeafTestToAppend,
skipRoots
),
LoggerFactory.getLogger("test")
)
}
}
@@ -0,0 +1,65 @@
package org.jetbrains.kotlin.gradle.internal.testing.tcsmc
import jetbrains.buildServer.messages.serviceMessages.TestFinished
import jetbrains.buildServer.messages.serviceMessages.TestStarted
import jetbrains.buildServer.messages.serviceMessages.TestSuiteFinished
import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted
import org.junit.Test
class TestNameParsing: TCServiceMessagesClientTest() {
@Test
fun testFqn() {
assertEvents("""
STARTED SUITE root // root
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: my.company.product.MyTest, name: myMethod // root.my.company.product.MyTest.myMethod
COMPLETED SUCCESS // root.my.company.product.MyTest.myMethod
COMPLETED SUCCESS // root
""") {
serviceMessage(TestStarted("my.company.product.MyTest.myMethod", false, null))
serviceMessage(TestFinished("my.company.product.MyTest.myMethod", 1))
}
}
@Test
fun testSimpleClassName() {
assertEvents("""
STARTED SUITE root // root
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: MyTest, name: myMethod // root.MyTest.myMethod
COMPLETED SUCCESS // root.MyTest.myMethod
COMPLETED SUCCESS // root
""") {
serviceMessage(TestStarted("MyTest.myMethod", false, null))
serviceMessage(TestFinished("MyTest.myMethod", 1))
}
}
@Test
fun testParentSuite() {
assertEvents("""
STARTED SUITE root // root
STARTED SUITE MyTest // root.MyTest
STARTED TEST displayName: myMethod, classDisplayName: MyTest, className: MyTest, name: myMethod // root.MyTest.myMethod
COMPLETED SUCCESS // root.MyTest.myMethod
COMPLETED SUCCESS // root.MyTest
COMPLETED SUCCESS // root
""") {
serviceMessage(TestSuiteStarted("MyTest"))
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
serviceMessage(TestSuiteFinished("MyTest"))
}
}
@Test
fun testAlone() {
assertEvents("""
STARTED SUITE root // root
STARTED TEST displayName: myMethod, classDisplayName: root, className: root, name: myMethod // root.myMethod
COMPLETED SUCCESS // root.myMethod
COMPLETED SUCCESS // root
""") {
serviceMessage(TestStarted("myMethod", false, null))
serviceMessage(TestFinished("myMethod", 1))
}
}
}
@@ -0,0 +1,45 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
appveyor.yml
circle.yml
codeship-services.yml
codeship-steps.yml
wercker.yml
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.travis.yml
# misc
*.md
@@ -0,0 +1,6 @@
Kotlin/JS TeamCity tests results reporter.
### Usage
`yarn add @kotlin/js-tests-teamcity --dev`
`yarn run kotlin-js-tests`
@@ -0,0 +1,49 @@
import com.moowork.gradle.node.yarn.YarnTask
plugins {
id("base")
id("com.moowork.node") version "1.2.0"
}
node {
version = "11.9.0"
download = true
nodeModulesDir = projectDir
}
tasks {
"yarn" {
outputs.upToDateWhen {
projectDir.resolve("node_modules").isDirectory
}
}
create<YarnTask>("yarnBuild") {
group = "build"
dependsOn("yarn")
setWorkingDir(projectDir)
args = listOf("build")
inputs.dir(projectDir.resolve("src"))
outputs.file(projectDir.resolve("lib/kotlin-js-test.js"))
}
create<Delete>("cleanYarn") {
group = "build"
delete = setOf(
projectDir.resolve("node_modules"),
projectDir.resolve("lib"),
projectDir.resolve(".rpt2_cache")
)
}
getByName("clean").dependsOn("cleanYarn")
}
artifacts {
add("archives", projectDir.resolve("lib/kotlin-js-test.js")) {
builtBy("yarnBuild")
}
}
+65
View File
@@ -0,0 +1,65 @@
import {TeamCityMessagesFlow} from "./src/TeamCityMessagesFlow";
import {directRunner, KotlinTestRunner} from "./src/KotlinTestRunner";
import {IgnoredTestSuitesReporting, runWithTeamCityReporter} from "./src/KotlinTestTeamCityReporter";
import {CliArgsParser} from "./src/CliArgsParser";
import {configureFiltering} from "./src/CliFiltertingConfiguration";
import {hrTimer} from "./src/Timer";
const kotlin_test = require('kotlin-test');
const parser = new CliArgsParser({
version: VERSION,
bin: BIN,
description: DESCRIPTION,
usage: "[-t --tests] [-e --exclude] <module_name1>, <module_name2>, ..",
args: {
include: {
keys: ['--tests', '--include'],
help: "Tests to include. Example: MySuite.test1,MySuite.MySubSuite.*,*unix*,!*windows*",
default: "*"
},
exclude: {
keys: ['--exclude'],
help: "Tests to exclude. Example: MySuite.test1,MySuite.MySubSuite.*,*unix*"
},
ignoredTestSuites: {
keys: ['--ignoredTestSuites'],
help: "How to deal with ignored test suites",
single: true,
values: [
IgnoredTestSuitesReporting.skip,
IgnoredTestSuitesReporting.reportAsIgnoredTest,
IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored
],
valuesHelp: [
"don't report ignored test suites",
"useful to speedup large ignored test suites",
"will cause visiting all inner tests",
],
default: IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored
}
},
freeArgsTitle: "module_name"
});
const processArgs = process.argv.slice(2);
const untypedArgs = parser.parse(processArgs);
const args = {
onIgnoredTestSuites: (untypedArgs.ignoredTestSuites
|| IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored) as IgnoredTestSuitesReporting,
include: untypedArgs.include as string[],
exclude: untypedArgs.exclude as string[],
};
const teamCity = new TeamCityMessagesFlow(null, (payload) => console.log(payload));
let runner: KotlinTestRunner = directRunner;
runner = runWithTeamCityReporter(runner, args.onIgnoredTestSuites, teamCity, hrTimer);
runner = configureFiltering(runner, args.include, args.exclude);
kotlin_test.setAdapter(runner);
untypedArgs.free.forEach((arg: string) => {
require(arg);
});
@@ -0,0 +1,4 @@
declare const DEBUG: boolean;
declare const VERSION: string;
declare const BIN: string;
declare const DESCRIPTION: string;
@@ -0,0 +1,30 @@
{
"name": "@kotlin/js-tests-teamcity",
"version": "0.0.1",
"description": "Simple Kotlin/JS tests runner with TeamCity reporter",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"bin": {
"kotlin-js-tests": "lib/kotlin-js-test.js"
},
"files": [
"lib/**/*"
],
"scripts": {
"build": "rimraf lib/* && rollup -c rollup.config.js"
},
"dependencies": {
"@types/node": "^10.12.21"
},
"devDependencies": {
"copyfiles": "^2.1.0",
"rimraf": "^2.6.3",
"rollup": "^1.1.2",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-typescript2": "^0.19.2",
"rollup-plugin-uglify": "^6.0.2",
"typescript": "^3.3.1"
}
}
@@ -0,0 +1,59 @@
import typescript from 'rollup-plugin-typescript2';
import {uglify} from "rollup-plugin-uglify";
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
const pckg = require('./package.json');
export default [
{
input: './cli.ts',
output: {
file: 'lib/kotlin-js-test.js',
format: 'cjs',
banner: '#!/usr/bin/env node'
},
plugins: [
nodeResolve({
jsnext: true,
main: true
}),
commonjs(),
typescript({
tsconfig: "tsconfig.json"
}),
uglify({
sourcemap: true,
compress: {
// hoist_funs: true,
// hoist_vars: true,
toplevel: true,
unsafe: true,
dead_code: true,
global_defs: {
DEBUG: false,
VERSION: pckg.version,
BIN: Object.keys(pckg.bin)[0],
DESCRIPTION: pckg.description
}
},
mangle: {
properties: {
keep_quoted: true,
reserved: [
"argv", "hrtime",
"kotlin_test", "kotlin", "setAdapter", "setAssertHook_4duqou$",
"suite", "test",
"stack"
]
},
toplevel: true,
},
// output: {
// beautify: true
// }
}),
// sourceMaps()
]
}
]
@@ -0,0 +1,115 @@
import {println, startsWith} from "./utils";
export type CliDescription = {
version: string,
bin: string,
description: string,
usage: string,
args: {
[k: string]: CliArgDescription,
},
freeArgsTitle: string
}
export type CliArgValues = {
[k: string]: string[] | string,
free: string[]
}
export type CliArgDescription = {
keys: string[],
help: string,
values?: string[],
valuesHelp?: string[],
default?: string,
single?: true
}
export class CliArgsParser {
constructor(private description: CliDescription) {
}
printUsage() {
const description = this.description;
println(`${description.bin} v${description.version} - ${description.description}`);
println();
println(`Usage: ${description.bin} ${description.usage}`);
println();
for (let key in description.args) {
const data = description.args[key];
println(' ' + data.keys.join(', '));
const indent = ' ';
println(`${indent}${data.help}`);
if (data.values && data.valuesHelp) {
println(`${indent}Possible values:`);
for (let i = 0; i < data.values.length; i++) {
const value = data.values[i];
const help = data.valuesHelp[i];
println(`${indent} - "${value}": ${help}`)
}
}
if (data.default) println(`${indent}By default: ${data.default}`);
println('')
}
}
badArgsExit(message: string) {
println(message);
println();
this.printUsage();
process.exit(1)
}
parse(args: string[]): CliArgValues {
const description = this.description;
const result: CliArgValues = {
free: []
};
for (let key in description.args) {
if (!description.args[key].single) {
result[key] = [];
}
}
// process all arguments from left to right
args: while (args.length != 0) {
const arg = args.shift() as string;
if (startsWith(arg, '--')) {
for (let argName in description.args) {
const argDescription = description.args[argName];
if (argDescription.keys.indexOf(arg) != -1) {
if (args.length == 0) {
this.badArgsExit("Missed value after option " + arg);
}
const value = args.shift() as string;
if (argDescription.values && argDescription.values.indexOf(value) == -1) {
this.badArgsExit("Unsupported value for option " + arg);
}
if (argDescription.single) {
result[argName] = value;
} else {
(result[argName] as string[]).push(value);
}
continue args;
}
}
this.badArgsExit("Unknown option: " + arg);
} else {
result.free.push(arg)
}
}
if (result.free.length == 0) {
this.badArgsExit(`At least one ${description.freeArgsTitle} should be provided`)
}
return result
}
}
@@ -0,0 +1,52 @@
import {KotlinTestRunner} from "./KotlinTestRunner";
import {
allTest,
CompositeTestFilter,
KotlinTestsFilter,
newKotlinTestsFilter,
runWithFilter
} from "./KotlinTestsFilter";
import {flatMap, println, pushIfNotNull} from "./utils";
export function configureFiltering(
runner: KotlinTestRunner,
includeWildcards: string[],
excludeWildcards: string[]
): KotlinTestRunner {
const include: KotlinTestsFilter[] = [];
const exclude: KotlinTestsFilter[] = [];
function collectWildcards(
value: string[],
positive: KotlinTestsFilter[],
negative: KotlinTestsFilter[]
) {
flatMap(value, (t: string) => t.split(','))
.map(t => {
if (t.length && t[0] == '!') {
pushIfNotNull(negative, newKotlinTestsFilter(t.substring(1)))
} else {
pushIfNotNull(positive, newKotlinTestsFilter(t))
}
})
}
collectWildcards(includeWildcards, include, exclude);
collectWildcards(excludeWildcards, exclude, include);
if (include.length == 0 && exclude.length == 0) {
return runner
} else {
if (include.length == 0) {
include.push(allTest)
}
const filter = new CompositeTestFilter(include, exclude);
if (DEBUG) {
println(filter.toString());
}
return runWithFilter(runner, filter)
}
}
@@ -0,0 +1,14 @@
export interface KotlinTestRunner {
suite(name: string, isIgnored: boolean, fn: () => void): void
test(name: string, isIgnored: boolean, fn: () => void): void
}
export const directRunner: KotlinTestRunner = {
suite(name: string, isIgnored: boolean, fn: () => void): void {
if (!isIgnored) fn()
},
test(name: string, isIgnored: boolean, fn: () => void): void {
if (!isIgnored) fn()
}
};
@@ -0,0 +1,104 @@
import {KotlinTestRunner} from "./KotlinTestRunner";
import {TeamCityMessageData, TeamCityMessagesFlow} from "./TeamCityMessagesFlow";
import {Timer} from "./Timer";
const kotlin_test = require('kotlin-test');
// don't use enum as it is not minified by uglify
export type IgnoredTestSuitesReporting
= "skip" | "reportAsIgnoredTest" | "reportAllInnerTestsAsIgnored"
export const IgnoredTestSuitesReporting: { [key: string]: IgnoredTestSuitesReporting } = {
skip: "skip",
reportAsIgnoredTest: "reportAsIgnoredTest",
reportAllInnerTestsAsIgnored: "reportAllInnerTestsAsIgnored"
};
// to reduce minified code size
function withName(name: string, data?: TeamCityMessageData): TeamCityMessageData {
data = data || {};
data["name"] = name;
return data
}
export function runWithTeamCityReporter(
runner: KotlinTestRunner,
ignoredTestSuites: IgnoredTestSuitesReporting,
teamCity: TeamCityMessagesFlow,
timer: Timer<any> | null
): KotlinTestRunner {
let inIgnoredSuite = false;
let currentAssertionResult: { expected: any, actual: any } | null = null;
kotlin_test.kotlin.test.setAssertHook_4duqou$(function (assertionResult: { expected: any, actual: any }) {
currentAssertionResult = assertionResult;
});
return {
suite: function (name: string, isIgnored: boolean, fn: () => void) {
if (isIgnored) {
if (ignoredTestSuites == IgnoredTestSuitesReporting.skip) return;
else if (ignoredTestSuites == IgnoredTestSuitesReporting.reportAsIgnoredTest) {
teamCity.sendMessage('testIgnored', withName(name, {"suite": true}));
return
}
}
teamCity.sendMessage('testSuiteStarted', withName(name));
// noinspection UnnecessaryLocalVariableJS
const alreadyInIgnoredSuite = inIgnoredSuite;
if (!alreadyInIgnoredSuite && isIgnored) {
inIgnoredSuite = true;
}
try {
if (isIgnored && ignoredTestSuites == IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored) {
fn();
} else {
runner.suite(name, isIgnored, fn)
}
} finally {
if (isIgnored && !alreadyInIgnoredSuite) {
inIgnoredSuite = false;
}
const data: TeamCityMessageData = withName(name);
// extension only for Gradle
if (isIgnored) data["ignored"] = true;
teamCity.sendMessage('testSuiteFinished', data);
}
},
test: function (name: string, isIgnored: boolean, fn: () => void) {
if (inIgnoredSuite || isIgnored) {
teamCity.sendMessage('testIgnored', withName(name));
} else {
const startTime = timer ? timer.start() : null;
teamCity.sendMessage('testStarted', withName(name));
try {
runner.test(name, isIgnored, fn);
} catch (e) {
const data: TeamCityMessageData = withName(name, {
"message": e.message,
"details": e.stack
});
if (currentAssertionResult) {
data["type"] = 'comparisonFailure';
data["expected"] = currentAssertionResult.expected;
data["actual"] = currentAssertionResult.actual;
}
teamCity.sendMessage('testFailed', data);
} finally {
currentAssertionResult = null;
const data: TeamCityMessageData = withName(name);
if (startTime) {
data["duration"] = timer!!.end(startTime); // ns to ms
}
teamCity.sendMessage('testFinished', data);
}
}
}
}
}
@@ -0,0 +1,168 @@
import {escapeRegExp, startsWith, trim} from "./utils";
import {KotlinTestRunner} from "./KotlinTestRunner";
export interface KotlinTestsFilter {
mayContainTestsFromSuite(fqn: string): boolean;
containsTest(fqn: string): boolean;
}
export function runWithFilter(
runner: KotlinTestRunner,
filter: KotlinTestsFilter,
): KotlinTestRunner {
let path: string[] = [];
function pathString() {
// skip root
return path.slice(1).join('.')
}
return {
suite: function (name: string, isIgnored: boolean, fn: () => void) {
path.push(name);
try {
if (path.length > 1 && !filter.mayContainTestsFromSuite(pathString())) return;
runner.suite(name, isIgnored, fn);
} finally {
path.pop()
}
},
test: function (name: string, isIgnored: boolean, fn: () => void) {
path.push(name);
try {
if (!filter.containsTest(pathString())) return;
runner.test(name, isIgnored, fn);
} finally {
path.pop()
}
}
};
}
export function newKotlinTestsFilter(wildcard: string | null): KotlinTestsFilter | null {
if (wildcard == null) return null;
wildcard = trim(wildcard);
wildcard = wildcard.replace(/\*+/, '*'); // ** => *
if (wildcard.length == 0) return null;
else if (wildcard == '*') return allTest;
else if (wildcard.indexOf('*') == -1) return new ExactFilter(wildcard);
else if (startsWith(wildcard, '*')) return new RegExpKotlinTestsFilter(wildcard);
else {
// optimize for cases like "Something*", "Something*a*b" and so on.
// by adding explicit prefix matcher to not visit unneeded suites
// (RegExpKotlinTestsFilter doesn't support suites matching)
const [prefix, rest] = wildcard.split('*', 2);
return new StartsWithFilter(prefix, rest ? new RegExpKotlinTestsFilter(wildcard) : null)
}
}
export const allTest = new class implements KotlinTestsFilter {
mayContainTestsFromSuite(fqn: string): boolean {
return true;
}
containsTest(fqn: string): boolean {
return true;
}
};
export class StartsWithFilter implements KotlinTestsFilter {
constructor(
public readonly prefix: string,
public readonly filter: RegExpKotlinTestsFilter | null
) {
}
isPrefixMatched(fqn: string): boolean {
return startsWith(fqn + ".", this.prefix);
}
mayContainTestsFromSuite(fqn: string): boolean {
return this.isPrefixMatched(fqn);
}
containsAllTestsFromSuite(fqn: string): boolean {
return this.filter == null && this.isPrefixMatched(fqn);
}
containsTest(fqn: string): boolean {
return startsWith(fqn, this.prefix)
&& (this.filter == null || this.filter.containsTest(fqn));
}
}
export class ExactFilter implements KotlinTestsFilter {
constructor(public fqn: string) {
}
mayContainTestsFromSuite(fqn: string): boolean {
return startsWith(fqn, this.fqn);
}
containsTest(fqn: string): boolean {
return fqn === this.fqn;
}
}
export class RegExpKotlinTestsFilter implements KotlinTestsFilter {
public readonly regexp: RegExp;
constructor(wildcard: string) {
this.regexp = RegExp(wildcard
.split('*')
.map(it => escapeRegExp(it))
.join('.*'));
}
mayContainTestsFromSuite(fqn: string): boolean {
return true
}
containsTest(fqn: string): boolean {
return this.regexp!.test(fqn)
}
toString(): string {
return this.regexp.toString()
}
}
export class CompositeTestFilter implements KotlinTestsFilter {
private readonly excludePrefix: StartsWithFilter[] = [];
constructor(
public include: KotlinTestsFilter[],
public exclude: KotlinTestsFilter[]
) {
this.exclude.forEach(it => {
if (it instanceof StartsWithFilter && it.filter == null)
this.excludePrefix.push(it)
})
}
mayContainTestsFromSuite(fqn: string): boolean {
for (const excl of this.excludePrefix) {
if (excl.containsAllTestsFromSuite(fqn)) return false
}
for (const incl of this.include) {
if (incl.mayContainTestsFromSuite(fqn)) return true
}
return false;
}
containsTest(fqn: string): boolean {
for (const excl of this.exclude) {
if (excl.containsTest(fqn)) return false
}
for (const incl of this.include) {
if (incl.containsTest(fqn)) return true
}
return false
}
}
@@ -0,0 +1,32 @@
import {
allTest,
CompositeTestFilter,
ExactFilter,
RegExpKotlinTestsFilter,
StartsWithFilter
} from "./KotlinTestsFilter";
declare const DEBUG: boolean;
if (DEBUG) {
allTest.toString = function () {
return "(ALL)"
};
StartsWithFilter.prototype.toString = function (): string {
return "(STARTS WITH " + this.prefix + (this.filter ? (" AND " + this.filter) : "") + ")"
};
RegExpKotlinTestsFilter.prototype.toString = function (): string {
return "(REGEXP " + this.regexp + ")"
};
ExactFilter.prototype.toString = function (): string {
return "(EXACT " + this.fqn + ")"
};
CompositeTestFilter.prototype.toString = function (): string {
return this.include.map(it => it.toString()).join(" OR ")
+ "\n NOT (" + this.exclude.map(it => it.toString()).join(" OR ") + ")"
};
}
@@ -0,0 +1,23 @@
import {dateTimeWithoutTimeZone, newFlowId, tcEscape} from "./utils"
export type TeamCityMessageData = { [key: string]: any }
export class TeamCityMessagesFlow {
public readonly id: number;
constructor(id: number | null, private readonly send: (payload: string) => void) {
this.id = id || newFlowId()
}
sendMessage(type: string, args: TeamCityMessageData) {
args['flowId'] = this.id;
args['timestamp'] = dateTimeWithoutTimeZone();
const serializedArgs = Object
.keys(args)
.map((key) => `${key}='${tcEscape(args[key])}'`)
.join(' ');
this.send(`##teamcity[${type} ${serializedArgs}]`)
}
}
@@ -0,0 +1,16 @@
export interface Timer<T> {
start(): T
end(start: T): number
}
export const hrTimer: Timer<[number, number]> = {
start(): [number, number] {
return process.hrtime();
},
end(start: [number, number]): number {
const elapsedHr = process.hrtime(start);
return elapsedHr[0] + (elapsedHr[1] / 1e6); // ns to ms
}
};
@@ -0,0 +1,96 @@
/**
* from teamcity-service-messages
* Copyright (c) 2013 Aaron Forsander
*
* Escape string for TeamCity output.
* @see https://confluence.jetbrains.com/display/TCD65/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-servMsgsServiceMessages
*/
export function tcEscape(str: string): string {
if (!str) {
return '';
}
return str
.toString()
.replace(/\|/g, '||')
.replace(/\n/g, '|n')
.replace(/\r/g, '|r')
.replace(/\[/g, '|[')
.replace(/\]/g, '|]')
.replace(/\u0085/g, '|x') // next line
.replace(/\u2028/g, '|l') // line separator
.replace(/\u2029/g, '|p') // paragraph separator
.replace(/'/g, '|\'');
}
/**
* From teamcity-service-messages.
* Copyright 2013 Aaron Forsander
*/
export function newFlowId(): number {
return Math.floor(Math.random() * (1e10 - 1e6 + 1)) + 1e6;
}
/**
* From teamcity-service-messages.
* Copyright 2013 Aaron Forsander
*/
export function dateTimeWithoutTimeZone(): string {
// TeamCity not fully support ISO 8601 (see TW-36173) so we need to cut off 'Z' at the end.
return new Date().toISOString().slice(0, -1);
}
/**
* From lodash.
* Copyright JS Foundation and other contributors <https://js.foundation/>
*/
export function startsWith(string: string, target: string) {
return string.slice(0, target.length) == target;
}
/**
* From lodash.
* Copyright JS Foundation and other contributors <https://js.foundation/>
*/
export function trim(str: string): string {
return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
}
/**
* Used to match `RegExp`
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
*/
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
reHasRegExpChar = RegExp(reRegExpChar.source);
/**
* Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
* "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
*
* From lodash.
* Copyright JS Foundation and other contributors <https://js.foundation/>
*/
export function escapeRegExp(string: string) {
return (string && reHasRegExpChar.test(string))
? string.replace(reRegExpChar, '\\$&')
: string;
}
export function pushIfNotNull<T>(list: T[], value: T) {
if (value !== null) list.push(value)
}
export function flatMap<T>(arr: T[], f: (item: T) => T[]): T[] {
const result: T[] = [];
arr.forEach(item => {
f(item).forEach(x => {
result.push(x)
})
});
return result;
}
export function println(message ?: string) {
console.log(message)
}
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"module": "ESNext",
// "declaration": true,
// "outDir": "./lib",
"strict": true,
// "sourceMap": true,
"allowSyntheticDefaultImports": true,
"inlineSourceMap": true
},
"exclude": [
"**/*Debug.ts"
]
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -122,6 +122,7 @@ include ":kotlin-build-common",
":kotlin-test:kotlin-test-testng", ":kotlin-test:kotlin-test-testng",
":kotlin-test:kotlin-test-js", ":kotlin-test:kotlin-test-js",
":kotlin-test:kotlin-test-js:kotlin-test-js-it", ":kotlin-test:kotlin-test-js:kotlin-test-js-it",
":kotlin-test-nodejs-runner",
":kotlinx-metadata", ":kotlinx-metadata",
":kotlinx-metadata-jvm", ":kotlinx-metadata-jvm",
":prepare:build.version", ":prepare:build.version",
@@ -263,6 +264,7 @@ project(':kotlin-test:kotlin-test-junit5').projectDir = "$rootDir/libraries/kotl
project(':kotlin-test:kotlin-test-testng').projectDir = "$rootDir/libraries/kotlin.test/testng" as File project(':kotlin-test:kotlin-test-testng').projectDir = "$rootDir/libraries/kotlin.test/testng" as File
project(':kotlin-test:kotlin-test-js').projectDir = "$rootDir/libraries/kotlin.test/js" as File project(':kotlin-test:kotlin-test-js').projectDir = "$rootDir/libraries/kotlin.test/js" as File
project(':kotlin-test:kotlin-test-js:kotlin-test-js-it').projectDir = "$rootDir/libraries/kotlin.test/js/it" as File project(':kotlin-test:kotlin-test-js:kotlin-test-js-it').projectDir = "$rootDir/libraries/kotlin.test/js/it" as File
project(':kotlin-test-nodejs-runner').projectDir = "$rootDir/libraries/tools/kotlin-test-nodejs-runner" as File
project(':kotlin-reflect').projectDir = "$rootDir/libraries/reflect" as File project(':kotlin-reflect').projectDir = "$rootDir/libraries/reflect" as File
project(':kotlin-reflect-api').projectDir = "$rootDir/libraries/reflect/api" as File project(':kotlin-reflect-api').projectDir = "$rootDir/libraries/reflect/api" as File
project(':kotlinx-metadata').projectDir = "$rootDir/libraries/kotlinx-metadata" as File project(':kotlinx-metadata').projectDir = "$rootDir/libraries/kotlinx-metadata" as File