[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.
This commit is contained in:
@@ -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<String, Map<String, TestResult>> results = [:]
|
||||
List<KonanTestGroupReport> 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"
|
||||
}
|
||||
|
||||
@@ -28,5 +28,7 @@ allprojects {
|
||||
maven {
|
||||
url 'https://cache-redirector.jetbrains.com/maven-central'
|
||||
}
|
||||
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String> 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<String> 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<String> 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<String, TestResult> 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<KonanTestGroupReport>)
|
||||
|
||||
@ImplicitReflectionSerializer
|
||||
fun saveReport(reportFileName: String, statistics: Statistics, groups:List<KonanTestGroupReport>){
|
||||
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<ExternalTestReport>(it.readText())
|
||||
}
|
||||
@@ -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'")
|
||||
}
|
||||
}
|
||||
@@ -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<KonanTestSuiteReport>)
|
||||
|
||||
@Serializable
|
||||
data class KonanTestSuiteReport(val name: String, val tests: List<KonanTestCaseReport>)
|
||||
|
||||
@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<KonanTestCaseReport>()
|
||||
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<KonanTestSuiteReport>()
|
||||
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]")
|
||||
}
|
||||
}
|
||||
@@ -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<String, Any>
|
||||
?: throw IllegalStateException("Got incorrect JSON object: ${resultsFile.absolutePath}")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val statsJSON = obj["statistics"] as? AbstractMap<String, Int>
|
||||
?: 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user