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/artifacts/dist_auto_*
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(projectRuntimeJar(":kotlin-compiler-embeddable"))
testCompile(intellijCoreDep()) { includeJars("jdom") }
// testCompileOnly dependency on non-shaded artifacts is needed for IDE support
// testRuntime on shaded artifact is needed for running tests with shaded compiler
testCompileOnly(project(path = ":kotlin-gradle-plugin-test-utils-embeddable", configuration = "compile"))
@@ -1,9 +1,13 @@
package org.jetbrains.kotlin.gradle
import com.intellij.testFramework.TestDataFile
import org.gradle.api.logging.LogLevel
import org.gradle.tooling.GradleConnector
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.ModelFetcherBuildAction
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 {
val taskOutputRegex = ("(?:\\[LIFECYCLE] \\[class org\\.gradle(?:\\.internal\\.buildevents)?\\.TaskExecutionLogger] :$taskName|" +
"\\[org\\.gradle\\.execution\\.plan\\.DefaultPlanExecutor\\] :$taskName.*?started)" +
@@ -575,6 +596,71 @@ abstract class BaseGradleIT {
File(projectDir, it).takeIf(File::exists)
}.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> =
params.toMutableList().apply {
add("--stacktrace")
@@ -16,6 +16,7 @@
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.IMPLEMENT_CONFIG_NAME
import org.jetbrains.kotlin.gradle.plugin.IMPLEMENT_DEPRECATION_WARNING
@@ -25,6 +26,7 @@ import org.junit.Test
import java.io.File
import kotlin.test.assertTrue
@TestDataPath("\$CONTENT_ROOT/resources")
class MultiplatformGradleIT : BaseGradleIT() {
@Test
@@ -293,4 +295,81 @@ class MultiplatformGradleIT : BaseGradleIT() {
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
val jar: Jar by tasks
runtimeJar(rewriteDepsToShadedCompiler(jar))
val jarContents by configurations.creating
sourcesJar()
javadocJar()
repositories {
google()
maven(url = "https://plugins.gradle.org/m2/")
}
pill {
@@ -61,6 +62,9 @@ dependencies {
runtime(projectRuntimeJar(":kotlin-scripting-compiler-embeddable"))
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
compileOnly("com.android.tools.build:gradle: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"))
}
runtimeJar(rewriteDepsToShadedCompiler(jar)) {
dependsOn(jarContents)
from {
jarContents.asFileTree.map {
if (it.endsWith(".jar")) zipTree(it)
else it
}
}
}
tasks {
withType<KotlinCompile> {
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
import org.gradle.api.plugins.JavaBasePlugin
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.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
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.targets.js.nodejs.NodeJsPlugin
import org.jetbrains.kotlin.gradle.tasks.KotlinTasksProvider
class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
KotlinTargetConfigurator<KotlinJsCompilation>(true, true, kotlinPluginVersion) {
KotlinTargetConfigurator<KotlinJsCompilation>(true, true, kotlinPluginVersion) {
override fun buildCompilationProcessor(compilation: KotlinJsCompilation): KotlinSourceSetProcessor<*> {
val tasksProvider = KotlinTasksProvider(compilation.target.targetName)
@@ -28,59 +21,11 @@ class KotlinJsTargetConfigurator(kotlinPluginVersion: String) :
override fun configureTest(target: KotlinOnlyTarget<KotlinJsCompilation>) {
target.compilations.all {
configureTest(it)
}
}
it.compileKotlinTask.kotlinOptions.moduleKind = "umd"
private fun configureTest(compilation: KotlinCompilationToRunnableFiles<*>) {
val target = compilation.target
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)
if (it.name == KotlinCompilation.TEST_COMPILATION_NAME) {
KotlinJsCompilationTestsConfigurator(it).configure()
}
}
}
}
@@ -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-js",
":kotlin-test:kotlin-test-js:kotlin-test-js-it",
":kotlin-test-nodejs-runner",
":kotlinx-metadata",
":kotlinx-metadata-jvm",
":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-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-nodejs-runner').projectDir = "$rootDir/libraries/tools/kotlin-test-nodejs-runner" as File
project(':kotlin-reflect').projectDir = "$rootDir/libraries/reflect" as File
project(':kotlin-reflect-api').projectDir = "$rootDir/libraries/reflect/api" as File
project(':kotlinx-metadata').projectDir = "$rootDir/libraries/kotlinx-metadata" as File