From b0f3ee945908bbfbcc0198b558fcaf1c9a2d7151 Mon Sep 17 00:00:00 2001 From: Vasily Levchenko Date: Thu, 7 Feb 2019 17:08:56 +0300 Subject: [PATCH] [build][test][environment] improvemnt of test reporting infrostructure 1. reduced number of json file generation 2. Groovy's JsonSlurp replaced with Kotlin Json serialization/deserialization 3. improved naive groovy -> kotlin convertion (46d32c606) 4. some refactoring. --- backend.native/tests/build.gradle | 34 ++-- buildSrc/build.gradle | 2 + buildSrc/plugins/build.gradle | 16 +- .../org/jetbrains/kotlin/KonanTest.groovy | 161 ++++++++--------- .../jetbrains/kotlin/ExternalReportUtils.kt | 26 +++ .../org/jetbrains/kotlin/KonanTestSuite.kt | 139 --------------- .../jetbrains/kotlin/KonanTestSuiteReport.kt | 168 ++++++++++++++++++ .../kotlin/org/jetbrains/kotlin/Reporter.kt | 131 +++++++++----- 8 files changed, 382 insertions(+), 295 deletions(-) create mode 100644 buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/ExternalReportUtils.kt delete mode 100644 buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuite.kt create mode 100644 buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuiteReport.kt diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index b2b131ee885..4fd6c4b206b 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -177,31 +177,29 @@ task sanity { dependsOn(tasksOf { it instanceof RunStdlibTest }) } -// Collect results in one json. +// Collect reports in one json. task resultsTask() { doLast { - Map> results = [:] + List reports = [] def statistics = new Statistics() tasks.withType(RunExternalTestGroup).matching { it.state.executed }.each { - results.put(it.name, it.results) - statistics.add(it.statistics) + reports += new KonanTestGroupReport(it.name, it.testGroupReporter.suiteReports) + statistics.add(it.testGroupReporter.statistics) } tasks.withType(RunStdlibTest).matching {it.state.executed }.each { statistics.add(it.statistics) } - def output = ["statistics": statistics, "tests": results] - def json = JsonOutput.toJson(output) - def reportFile = new File(testOutputExternal, "results.json") - testOutputExternal.mkdirs() - reportFile.write(JsonOutput.prettyPrint(json)) - println("DONE.\n\n" + - "TOTAL: $statistics.total\n" + - "PASSED: $statistics.passed\n" + - "FAILED: $statistics.failed\n" + - "ERROR: $statistics.error\n" + - "SKIPPED: $statistics.skipped") + ExternalReportUtilsKt.saveReport("$testOutputExternal/reports.json", statistics, reports) + use(KonanTestSuiteReportKt) { + project.logger.quiet("DONE.\n\n" + + "TOTAL: $statistics.total\n" + + "PASSED: $statistics.passed\n" + + "FAILED: $statistics.failed\n" + + "ERROR: $statistics.error\n" + + "SKIPPED: $statistics.skipped") + } } } @@ -272,6 +270,12 @@ task slackReport(type:Reporter) { reportHome = rootProject.file("test.output").absoluteFile } +task slackReportNightly(type:NightlyReporter) { + externalMacosReport = "external_macos_results.json" + externalLinuxReport = "external_linux_results.json" + externalWindowsReport = "external_windows_results.json" +} + task sum (type:RunKonanTest) { source = "codegen/function/sum.kt" } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 9876b352d60..66fc9f5e01c 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -28,5 +28,7 @@ allprojects { maven { url 'https://cache-redirector.jetbrains.com/maven-central' } + + maven { url "https://kotlin.bintray.com/kotlinx" } } } diff --git a/buildSrc/plugins/build.gradle b/buildSrc/plugins/build.gradle index e039c9142aa..9cb5f02ad74 100644 --- a/buildSrc/plugins/build.gradle +++ b/buildSrc/plugins/build.gradle @@ -20,27 +20,27 @@ buildscript { ext.rootBuildDirectory = "$rootDir/.." apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle" apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle" + dependencies{ + classpath "org.jetbrains.kotlin:kotlin-serialization:$buildKotlinVersion" + } + } apply plugin: 'kotlin' +apply plugin: 'kotlinx-serialization' -repositories { - maven { - url buildKotlinCompilerRepo - } - maven { - url 'https://cache-redirector.jetbrains.com/maven-central' - } -} +/* don't use repositories: gradle will ignore it anyway, but may confuse gradle build engineer, see outer build.gradle */ dependencies { compile gradleApi() compile localGroovy() compile "org.jetbrains.kotlin:kotlin-stdlib:$buildKotlinVersion" + compile "org.jetbrains.kotlin:kotlin-reflect:$buildKotlinVersion" compile group: 'com.ullink.slack', name: 'simpleslackapi', version: '0.6.0' // An artifact from the included build 'shared' cannot be used here due to https://github.com/gradle/gradle/issues/3768 // TODO: Remove this hack when the bug is fixed compile project(':shared') + compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.10.0" } rootProject.dependencies { diff --git a/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy b/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy index 56b9fa4f3d9..245773c32da 100644 --- a/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy +++ b/buildSrc/plugins/src/main/groovy/org/jetbrains/kotlin/KonanTest.groovy @@ -16,7 +16,6 @@ package org.jetbrains.kotlin -import groovy.json.JsonOutput import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.TaskAction import org.gradle.process.ExecResult @@ -330,12 +329,6 @@ abstract class KonanTest extends JavaExec { } } -class TestFailedException extends RuntimeException { - TestFailedException(String s) { - super(s) - } -} - abstract class ExtKonanTest extends KonanTest { ExtKonanTest() { @@ -424,6 +417,9 @@ class BuildKonanTest extends ExtKonanTest { * Runs test built with Konan's TestRunner */ class RunKonanTest extends ExtKonanTest { + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ public def inDevelopersRun = true public def buildTaskName = 'buildKonanTests' @@ -473,6 +469,9 @@ class RunKonanTest extends ExtKonanTest { } class RunStdlibTest extends RunKonanTest { + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ public def inDevelopersRun = false public def statistics = new Statistics() @@ -504,8 +503,10 @@ class RunStdlibTest extends RunKonanTest { def n = matcher[0][1] as Integer statistics.fail(n) } - if (statistics.total == 0) { - statistics.error(testsTotal != 0 ? testsTotal : 1) + use(KonanTestSuiteReportKt) { + if (statistics.total == 0) { + statistics.error(testsTotal != 0 ? testsTotal : 1) + } } } } @@ -515,6 +516,9 @@ class RunStdlibTest extends RunKonanTest { * Compiles and executes test as a standalone binary */ class RunStandaloneKonanTest extends KonanTest { + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ public def inDevelopersRun = true void compileTest(List filesToCompile, String exe) { @@ -527,6 +531,9 @@ class RunStandaloneKonanTest extends KonanTest { // project.exec + a shell script isolate the jvm // from IDEA. Use the RunKonanTest instead. class RunDriverKonanTest extends KonanTest { + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ public def inDevelopersRun = true RunDriverKonanTest() { @@ -565,6 +572,9 @@ class RunDriverKonanTest extends KonanTest { } class RunInteropKonanTest extends KonanTest { + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ public def inDevelopersRun = true private String interop @@ -593,8 +603,10 @@ class RunInteropKonanTest extends KonanTest { } class LinkKonanTest extends KonanTest { + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ public def inDevelopersRun = true - protected String lib void compileTest(List filesToCompile, String exe) { @@ -607,10 +619,13 @@ class LinkKonanTest extends KonanTest { } class DynamicKonanTest extends KonanTest { - protected String cSource - + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ public def inDevelopersRun = true + protected String cSource + void compileTest(List filesToCompile, String exe) { def libname = "testlib" def dylib = "$outputDirectory/$libname" @@ -640,13 +655,15 @@ class DynamicKonanTest extends KonanTest { } } class RunExternalTestGroup extends RunStandaloneKonanTest { - def inDevelopersRun = false + /** + * overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity] + */ + public def inDevelopersRun = false def groupDirectory = "." def outputSourceSetName = "testOutputExternal" String filter = project.findProperty("filter") - Map results = [:] - Statistics statistics = new Statistics() + def testGroupReporter = new KonanTestGroupReportEnvironment(project) RunExternalTestGroup() { } @@ -933,79 +950,53 @@ fun runTest() { } } - statistics = new Statistics() - def testSuite = createTestSuite(name, statistics) - testSuite.start() - // Build tests in the group - flags = (flags ?: []) + "-tr" - def compileList = [] - ktFiles.each { - source = project.relativePath(it) - if (isEnabledForNativeBackend(source)) { - // Create separate output directory for each test in the group. - outputDirectory = outputRootDirectory + "/${it.name}" - project.file(outputDirectory).mkdirs() - parseLanguageFlags() - compileList.addAll(createTestFiles()) - } - } - compileList.add(project.file("testUtils.kt").absolutePath) - compileList.add(project.file("helpers.kt").absolutePath) - try { - runCompiler(compileList, buildExePath(), flags) - } catch (Exception ex) { - println("ERROR: Compilation failed for test suite: ${testSuite.name} with exception: ${ex}") - println("The following files were unable to compile:") - ktFiles.each { println it.name } - statistics.error(ktFiles.size()) - testSuite.finish() - throw new RuntimeException("Compilation failed", ex) - } - - // Run the tests. - def currentResult = null - outputDirectory = outputRootDirectory - arguments = (arguments ?: []) + "--ktest_logger=SILENT" - ktFiles.each { - source = project.relativePath(it) - def savedArgs = arguments - arguments += "--ktest_filter=_${normalize(it.name)}.*" - println("TEST: $it.name (done: $statistics.total/${ktFiles.size()}, passed: $statistics.passed, skipped: $statistics.skipped)") - def testCase = testSuite.createTestCase(it.name) - testCase.start() - if (isEnabledForNativeBackend(source)) { - try { - println(source) - super.executeTest() - currentResult = testCase.pass() - } catch (TestFailedException e) { - currentResult = testCase.fail(e) - } catch (Exception ex) { - ex.printStackTrace() - currentResult = testCase.error(ex) + testGroupReporter.suite(name) { suite -> + // Build tests in the group + flags = (flags ?: []) + "-tr" + def compileList = [] + ktFiles.each { + source = project.relativePath(it) + if (isEnabledForNativeBackend(source)) { + // Create separate output directory for each test in the group. + outputDirectory = outputRootDirectory + "/${it.name}" + project.file(outputDirectory).mkdirs() + parseLanguageFlags() + compileList.addAll(createTestFiles()) } - } else { - currentResult = testCase.skip() } - arguments = savedArgs - println("TEST $currentResult.status\n") - if (currentResult.status == TestStatus.ERROR || currentResult.status == TestStatus.FAILED) { - println("Command to reproduce: ./gradlew $name -Pfilter=${it.name}\n") + compileList.add(project.file("testUtils.kt").absolutePath) + compileList.add(project.file("helpers.kt").absolutePath) + try { + runCompiler(compileList, buildExePath(), flags) + } catch (Exception ex) { + project.logger.quiet("ERROR: Compilation failed for test suite: $name with exception", ex) + project.logger.quiet("The following files were unable to compile:") + ktFiles.each { project.logger.quiet(it.name) } + suite.abort(ex, ktFiles.size()) + throw new RuntimeException("Compilation failed", ex) + } + + // Run the tests. + def currentResult = null + outputDirectory = outputRootDirectory + arguments = (arguments ?: []) + "--ktest_logger=SILENT" + ktFiles.each { file -> + source = project.relativePath(file) + def savedArgs = arguments + arguments += "--ktest_filter=_${normalize(file.name)}.*" + use(KonanTestSuiteReportKt) { + project.logger.quiet("TEST: $file.name (done: $testGroupReporter.statistics.total/${ktFiles.size()}, passed: $testGroupReporter.statistics.passed, skipped: $testGroupReporter.statistics.skipped)") + } + if (isEnabledForNativeBackend(source)) { + suite.executeTest(file.name) { + project.logger.quiet(source) + super.executeTest() + } + } else { + suite.skipTest(file.name) + } + arguments = savedArgs } - results.put(it.name, currentResult) } - testSuite.finish() - - // Save the report. - def reportFile = project.file("${outputRootDirectory}/results.json") - def json = JsonOutput.toJson(["statistics" : statistics, "tests" : results]) - reportFile.write(JsonOutput.prettyPrint(json)) - println("TOTAL PASSED: $statistics.passed/$statistics.total (SKIPPED: $statistics.skipped)") - } - - KonanTestSuite createTestSuite(String name, Statistics statistics) { - if (System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") != null) - return new TeamcityKonanTestSuite(name, statistics) - return new KonanTestSuite(name, statistics) } } diff --git a/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/ExternalReportUtils.kt b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/ExternalReportUtils.kt new file mode 100644 index 00000000000..58b30ad8230 --- /dev/null +++ b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/ExternalReportUtils.kt @@ -0,0 +1,26 @@ +package org.jetbrains.kotlin + +import kotlinx.io.PrintWriter +import kotlinx.serialization.* +import kotlinx.serialization.json.Json +import java.io.File +import java.io.FileReader + + +@Serializable +data class ExternalTestReport(val statistics: Statistics, val groups: List) + +@ImplicitReflectionSerializer +fun saveReport(reportFileName: String, statistics: Statistics, groups:List){ + File(reportFileName).apply { + parentFile.mkdirs() + PrintWriter(this).use { + it.append(Json.stringify(ExternalTestReport(statistics, groups))) + } + } +} + +@ImplicitReflectionSerializer +fun loadReport(reportFileName: String) : ExternalTestReport = FileReader(reportFileName).use { + return@use Json.parse(it.readText()) + } diff --git a/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuite.kt b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuite.kt deleted file mode 100644 index 99228f7bb82..00000000000 --- a/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuite.kt +++ /dev/null @@ -1,139 +0,0 @@ -package org.jetbrains.kotlin - -import java.io.PrintWriter -import java.io.StringWriter - -enum class TestStatus { - PASSED, - FAILED, - ERROR, - SKIPPED -} - -data class TestResult(val status: TestStatus, val comment: String = "") - -data class Statistics( - var passed: Int = 0, - var failed: Int = 0, - var error: Int = 0, - var skipped: Int = 0) { - - val total: Int - get() = passed + failed + error + skipped - - fun pass(count: Int = 1) { passed += count } - - fun skip(count: Int = 1) { skipped += count } - - fun fail(count: Int = 1) { failed += count } - - fun error(count: Int = 1) { error += count } - - fun add(other: Statistics) { - passed += other.passed - failed += other.failed - error += other.error - skipped += other.skipped - } -} - -open class KonanTestSuite(val name: String, val statistics: Statistics) { - - open inner class KonanTestCase(val name: String) { - open fun start() {} - - open fun pass(): TestResult { - statistics.pass() - return TestResult(TestStatus.PASSED) - } - - open fun fail(e: RuntimeException): TestResult { - statistics.fail() - println(e.message) - return TestResult(TestStatus.FAILED, "Exception: ${e.message}. Cause: ${e.cause?.message}") - } - - open fun error(e: Exception): TestResult { - statistics.error() - return TestResult(TestStatus.ERROR, "Exception: ${e.message}. Cause: ${e.cause?.message}") - } - - open fun skip(): TestResult { - statistics.skip() - return TestResult(TestStatus.SKIPPED) - } - } - - open fun createTestCase(name: String) = KonanTestCase(name) - - open fun start() {} - open fun finish() {} -} - -class TeamcityKonanTestSuite(name: String, statistics: Statistics) : KonanTestSuite(name, statistics) { - - inner class TeamcityKonanTestCase(name: String) : KonanTestCase(name) { - private fun teamcityFinish() { - teamcityReport("testFinished name='$name'") - } - - override fun start() { - teamcityReport("testStarted name='$name'") - } - - override fun pass(): TestResult { - teamcityFinish() - return super.pass() - } - - override fun fail(e: RuntimeException): TestResult { - teamcityReport("testFailed type='comparisonFailure' name='$name' message='${toTeamCityFormat(e.message)}'") - teamcityFinish() - return super.fail(e) - } - - override fun error(e: Exception): TestResult { - val writer = StringWriter() - e.printStackTrace(PrintWriter(writer)) - val rawString = writer.toString() - - teamcityReport("testFailed name='$name' message='${toTeamCityFormat(e.message)}' " + - "details='${toTeamCityFormat(rawString)}'") - teamcityFinish() - return super.error(e) - } - - override fun skip(): TestResult { - teamcityReport("testIgnored name='$name'") - teamcityFinish() - return super.skip() - } - } - - override fun createTestCase(name: String) = TeamcityKonanTestCase(name) - - private fun teamcityReport(msg: String) { - println("##teamcity[$msg]") - } - - /** - * Teamcity require escaping some symbols in pipe manner. - * https://github.com/GitTools/GitVersion/issues/94 - */ - fun toTeamCityFormat(inStr: String?): String = inStr?.let { - it.replace("\\|", "||") - .replace("\r", "|r") - .replace("\n", "|n") - .replace("'", "|'") - .replace("\\[", "|[") - .replace("]", "|]") - } ?: "null" - - override fun start() { - teamcityReport("testSuiteStarted name='$name'") - } - - override fun finish() { - teamcityReport("testSuiteFinished name='$name'") - } -} \ No newline at end of file diff --git a/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuiteReport.kt b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuiteReport.kt new file mode 100644 index 00000000000..5bae6c8527a --- /dev/null +++ b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/KonanTestSuiteReport.kt @@ -0,0 +1,168 @@ +package org.jetbrains.kotlin + +import kotlinx.io.PrintWriter +import kotlinx.io.StringWriter +import kotlinx.serialization.Optional +import kotlinx.serialization.Serializable +import org.gradle.api.Project + +enum class TestStatus { + PASSED, + FAILED, + ERROR, + SKIPPED +} + +@Serializable +data class Statistics( + var passed: Int = 0, + var failed: Int = 0, + var error: Int = 0, + var skipped: Int = 0) { + + + fun pass(count: Int = 1) { passed += count } + + fun skip(count: Int = 1) { skipped += count } + + fun fail(count: Int = 1) { failed += count } + + fun error(count: Int = 1) { error += count } + + fun add(other: Statistics) { + passed += other.passed + failed += other.failed + error += other.error + skipped += other.skipped + } +} + +val Statistics.total: Int + get() = passed + failed + error + skipped + +class TestFailedException(msg:String) : RuntimeException(msg) + +@Serializable +data class KonanTestGroupReport(val name:String, val suites:List) + +@Serializable +data class KonanTestSuiteReport(val name: String, val tests: List) + +@Serializable +data class KonanTestCaseReport(val name:String, val status: TestStatus, @Optional val comment:String? = null) + +class KonanTestSuiteReportEnvironment(val name:String, val project: Project, val statistics: Statistics) { + private val tc = if (Tc.enabled) TeamCityTestPrinter(project) else null + val tests = mutableListOf() + fun executeTest(testName: String, action:() -> Unit) { + var test:KonanTestCaseReport? = null + try { + tc?.startTest(testName) + action() + tc?.passTest(testName) + statistics.pass() + test = KonanTestCaseReport(testName, TestStatus.PASSED) + } catch (e:TestFailedException) { + tc?.failedTest(testName, e) + statistics.fail() + test = KonanTestCaseReport(testName, TestStatus.FAILED, "Exception: ${e.message}. Cause: ${e.cause?.message}") + project.logger.quiet("test: $testName failed") + } catch (e:Exception) { + tc?.errorTest(testName, e) + statistics.error() + test = KonanTestCaseReport(testName, TestStatus.ERROR, "Exception: ${e.message}. Cause: ${e.cause?.message}") + project.logger.quiet("error on test: $testName", e) + } + test!!.apply { + tests += this + if (status == TestStatus.ERROR || status == TestStatus.FAILED) { + project.logger.quiet("Command to reproduce: ./gradlew $name -Pfilter=${test.name}\n") + } + } + } + + fun skipTest(name: String) { + tc?.skipTest(name) + statistics.skip() + tests += KonanTestCaseReport(name, TestStatus.SKIPPED) + } + + fun abort(throwable: Throwable, count:Int) { + statistics.error(count) + project.logger.quiet("suite `$name` aborted with exception", throwable) + } + + internal operator fun invoke(action: (KonanTestSuiteReportEnvironment) -> Unit) { + tc?.suiteStart(name) + action(this) + tc?.suiteFinish(name) + } +} + +class KonanTestGroupReportEnvironment(val project:Project) { + val statistics = Statistics() + val suiteReports = mutableListOf() + fun suite(suiteName:String, action:(KonanTestSuiteReportEnvironment)->Unit) { + val konanTestSuiteEnvironment = KonanTestSuiteReportEnvironment(suiteName, project, statistics) + konanTestSuiteEnvironment { + action(it) + } + suiteReports += KonanTestSuiteReport(suiteName, konanTestSuiteEnvironment.tests) + } +} + +private class TeamCityTestPrinter(val project:Project) { + fun suiteStart(name: String) { + teamcityReport("testSuiteStarted name='$name'") + } + + fun suiteFinish(name: String) { + teamcityReport("testSuiteFinished name='$name'") + } + + fun startTest(name: String) { + teamcityReport("testStarted name='$name'") + } + + fun passTest(testName: String) = teamcityFinish(testName) + + + fun failedTest(testName: String, testFailedException: TestFailedException) { + teamcityReport("testFailed type='comparisonFailure' name='$testName' message='${testFailedException.message.toTeamCityFormat()}'") + teamcityFinish(testName) + } + + fun errorTest(testName: String, exception: Exception) { + val writer = StringWriter() + exception.printStackTrace(PrintWriter(writer)) + val rawString = writer.toString() + + teamcityReport("testFailed name='$testName' message='${exception.message.toTeamCityFormat()}' " + + "details='${rawString.toTeamCityFormat()}'") + teamcityFinish(testName) + } + + fun skipTest(testName: String) { + teamcityReport("testIgnored name='$testName'") + teamcityFinish(testName) + } + + private fun teamcityFinish(testName:String) { + teamcityReport("testFinished name='$testName'") + } + + /** + * Teamcity require escaping some symbols in pipe manner. + * https://github.com/GitTools/GitVersion/issues/94 + */ + private fun String?.toTeamCityFormat(): String = this?.also { + it.replace("\\|", "||") + .replace("\r", "|r") + .replace("\n", "|n") + .replace("'", "|'") + .replace("\\[", "|[") + .replace("]", "|]")} ?: "null" + private fun teamcityReport(msg: String) { + project.logger.quiet("##teamcity[$msg]") + } +} \ No newline at end of file diff --git a/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/Reporter.kt b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/Reporter.kt index e32e910e548..ef5abce47e7 100644 --- a/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/Reporter.kt +++ b/buildSrc/plugins/src/main/kotlin/org/jetbrains/kotlin/Reporter.kt @@ -6,69 +6,104 @@ package org.jetbrains.kotlin import com.ullink.slack.simpleslackapi.impl.SlackSessionFactory -import groovy.json.JsonSlurper +import kotlinx.serialization.ImplicitReflectionSerializer import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction -import java.io.File import java.io.FileInputStream import java.util.* + +internal object Tc { + private val teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") + val enabled:Boolean = (teamcityConfig != null) + private val buildConfig by lazy { + teamcityConfig ?: return@lazy null + val properties = Properties() + properties.load(FileInputStream(teamcityConfig)) + properties + } + val buildId = buildConfig?.getProperty("teamcity.build.id") + val buildTypeId = buildConfig?.getProperty("teamcity.buildType.id") + val konanReporterToken = buildConfig?.getProperty("konan-reporter-token") + val konanChannelName = buildConfig?.getProperty("konan-channel-name") + +} + +private fun buildLogUrlTab(buildId: String?, buildTypeId: String?): String = tabUrl(buildId, buildTypeId, "buildLog") + +private fun tabUrl(buildId: String?, buildTypeId: String?, tab: String?): String = + "http://buildserver.labs.intellij.net/viewLog.html?buildId=$buildId&buildTypeId=$buildTypeId&tab=$tab" + +private fun testReportUrl(buildId: String?, buildTypeId: String?): String = tabUrl(buildId, buildTypeId, "testsInfo") + +private fun sendTextToSlack(report: String) { + with(SlackSessionFactory.createWebSocketSlackSession(Tc.konanReporterToken)) { + connect() + sendMessage(findChannelByName(Tc.konanChannelName), + "Hello, аборигены Котлина!\n текущий статус:\n$report") + disconnect() + } +} + +private fun reportEpilogue(): String { + val logUrl = buildLogUrlTab(Tc.buildId, Tc.buildTypeId) + val testReportUrl = testReportUrl(Tc.buildId, Tc.buildTypeId) + return "\nlog url: $logUrl\ntest report url: $testReportUrl" +} + +private val Statistics.report + get() = "total: $total\npassed: $passed\nfailed: $failed\nerror: $error\nskipped: $skipped" + +private val Statistics.oneLineReport + get() = "(total: $total, passed: $passed, failed: $failed, error: $error, skipped: $skipped)" + + open class Reporter : DefaultTask() { - private fun buildLogUrlTab(buildId: String, buildTypeId: String): String = tabUrl(buildId, buildTypeId, "buildLog") - - private fun tabUrl(buildId: String, buildTypeId: String, tab: String): String = - "http://buildserver.labs.intellij.net/viewLog.html?buildId=$buildId&buildTypeId=$buildTypeId&tab=$tab" - - private fun testReportUrl(buildId: String, buildTypeId: String): String = tabUrl(buildId, buildTypeId, "testsInfo") @Input lateinit var reportHome: String + @ImplicitReflectionSerializer @TaskAction fun report() { - val teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE") - ?: throw RuntimeException("Can't load teamcity config") - val buildProperties = Properties() - buildProperties.load(FileInputStream(teamcityConfig)) - val buildId = buildProperties.getProperty("teamcity.build.id") - val buildTypeId = buildProperties.getProperty("teamcity.buildType.id") - val logUrl = buildLogUrlTab(buildId, buildTypeId) - val testReportUrl = testReportUrl(buildId, buildTypeId) - var epilogue = "\nlog url: $logUrl\ntest report url: $testReportUrl" + val reportJson = loadReport("$reportHome/external/report.json") - val resultsFile = File(reportHome, "external/results.json") - val report: String = if (resultsFile.exists()) { - @Suppress("UNCHECKED_CAST") - val obj = JsonSlurper().parse(resultsFile) as? AbstractMap - ?: throw IllegalStateException("Got incorrect JSON object: ${resultsFile.absolutePath}") - @Suppress("UNCHECKED_CAST") - val statsJSON = obj["statistics"] as? AbstractMap - ?: throw IllegalStateException("Got incorrect statistics object in JSON:" + - resultsFile.absolutePath) - val stats = Statistics( - passed = statsJSON["passed"]!!, - failed = statsJSON["failed"]!!, - error = statsJSON["error"]!!, - skipped = statsJSON["skipped"]!!) - val total = statsJSON["total"]!! - if (total != stats.total) { - val message = "WARNING: total amount of tests doesn't match with the sum of failed, passed, error & skipped" - println(message) - epilogue += message - } - "total: ${stats.total}\npassed: ${stats.passed}\nfailed: ${stats.failed}\n" + - "error:${stats.error}\nskipped:${stats.skipped} $epilogue" - } else { - "Unable to get results\nBuild has probably failed$epilogue" - } - println(report) - with(SlackSessionFactory.createWebSocketSlackSession(buildProperties.getProperty("konan-reporter-token"))) { - connect() - sendMessage(findChannelByName(buildProperties.getProperty("konan-channel-name")), - "Hello, аборигены Котлина!\n текущий статус:\n$report") - disconnect() + val report: String = + "${reportJson.statistics.report}\n ${reportEpilogue()}" + + project.logger.info(report) + sendTextToSlack(report) + } + + +} + +open class NightlyReporter: DefaultTask() { + @Input + lateinit var externalMacosReport:String + @Input + lateinit var externalLinuxReport:String + @Input + lateinit var externalWindowsReport:String + + @ImplicitReflectionSerializer + @TaskAction + fun report() { + val externalMacosJsonReport = loadReport("${project.rootDir.absolutePath}/$externalMacosReport") + val externalLinuxJsonReport = loadReport("${project.rootDir.absolutePath}/$externalLinuxReport") + val externalWindowsJsonReport = loadReport("${project.rootDir.absolutePath}/$externalWindowsReport") + val report = buildString { + append("Mac OS ") + appendln(externalMacosJsonReport.statistics.oneLineReport) + append("Linux ") + appendln(externalLinuxJsonReport.statistics.oneLineReport) + append("Windows ") + appendln(externalWindowsJsonReport.statistics.oneLineReport) + appendln(reportEpilogue()) } + project.logger.info(report) + sendTextToSlack(report) } } \ No newline at end of file