[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 })
|
dependsOn(tasksOf { it instanceof RunStdlibTest })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect results in one json.
|
// Collect reports in one json.
|
||||||
task resultsTask() {
|
task resultsTask() {
|
||||||
doLast {
|
doLast {
|
||||||
Map<String, Map<String, TestResult>> results = [:]
|
List<KonanTestGroupReport> reports = []
|
||||||
def statistics = new Statistics()
|
def statistics = new Statistics()
|
||||||
tasks.withType(RunExternalTestGroup).matching { it.state.executed }.each {
|
tasks.withType(RunExternalTestGroup).matching { it.state.executed }.each {
|
||||||
results.put(it.name, it.results)
|
reports += new KonanTestGroupReport(it.name, it.testGroupReporter.suiteReports)
|
||||||
statistics.add(it.statistics)
|
statistics.add(it.testGroupReporter.statistics)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(RunStdlibTest).matching {it.state.executed }.each {
|
tasks.withType(RunStdlibTest).matching {it.state.executed }.each {
|
||||||
statistics.add(it.statistics)
|
statistics.add(it.statistics)
|
||||||
}
|
}
|
||||||
|
|
||||||
def output = ["statistics": statistics, "tests": results]
|
ExternalReportUtilsKt.saveReport("$testOutputExternal/reports.json", statistics, reports)
|
||||||
def json = JsonOutput.toJson(output)
|
use(KonanTestSuiteReportKt) {
|
||||||
def reportFile = new File(testOutputExternal, "results.json")
|
project.logger.quiet("DONE.\n\n" +
|
||||||
testOutputExternal.mkdirs()
|
"TOTAL: $statistics.total\n" +
|
||||||
reportFile.write(JsonOutput.prettyPrint(json))
|
"PASSED: $statistics.passed\n" +
|
||||||
println("DONE.\n\n" +
|
"FAILED: $statistics.failed\n" +
|
||||||
"TOTAL: $statistics.total\n" +
|
"ERROR: $statistics.error\n" +
|
||||||
"PASSED: $statistics.passed\n" +
|
"SKIPPED: $statistics.skipped")
|
||||||
"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
|
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) {
|
task sum (type:RunKonanTest) {
|
||||||
source = "codegen/function/sum.kt"
|
source = "codegen/function/sum.kt"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,7 @@ allprojects {
|
|||||||
maven {
|
maven {
|
||||||
url 'https://cache-redirector.jetbrains.com/maven-central'
|
url 'https://cache-redirector.jetbrains.com/maven-central'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,27 +20,27 @@ buildscript {
|
|||||||
ext.rootBuildDirectory = "$rootDir/.."
|
ext.rootBuildDirectory = "$rootDir/.."
|
||||||
apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle"
|
apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle"
|
||||||
apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle"
|
apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle"
|
||||||
|
dependencies{
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$buildKotlinVersion"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
repositories {
|
/* don't use repositories: gradle will ignore it anyway, but may confuse gradle build engineer, see outer build.gradle */
|
||||||
maven {
|
|
||||||
url buildKotlinCompilerRepo
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url 'https://cache-redirector.jetbrains.com/maven-central'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile gradleApi()
|
compile gradleApi()
|
||||||
compile localGroovy()
|
compile localGroovy()
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$buildKotlinVersion"
|
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'
|
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
|
// 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
|
// TODO: Remove this hack when the bug is fixed
|
||||||
compile project(':shared')
|
compile project(':shared')
|
||||||
|
compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.10.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.dependencies {
|
rootProject.dependencies {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin
|
package org.jetbrains.kotlin
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
|
||||||
import org.gradle.api.tasks.JavaExec
|
import org.gradle.api.tasks.JavaExec
|
||||||
import org.gradle.api.tasks.TaskAction
|
import org.gradle.api.tasks.TaskAction
|
||||||
import org.gradle.process.ExecResult
|
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 {
|
abstract class ExtKonanTest extends KonanTest {
|
||||||
|
|
||||||
ExtKonanTest() {
|
ExtKonanTest() {
|
||||||
@@ -424,6 +417,9 @@ class BuildKonanTest extends ExtKonanTest {
|
|||||||
* Runs test built with Konan's TestRunner
|
* Runs test built with Konan's TestRunner
|
||||||
*/
|
*/
|
||||||
class RunKonanTest extends ExtKonanTest {
|
class RunKonanTest extends ExtKonanTest {
|
||||||
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
public def inDevelopersRun = true
|
public def inDevelopersRun = true
|
||||||
|
|
||||||
public def buildTaskName = 'buildKonanTests'
|
public def buildTaskName = 'buildKonanTests'
|
||||||
@@ -473,6 +469,9 @@ class RunKonanTest extends ExtKonanTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RunStdlibTest extends RunKonanTest {
|
class RunStdlibTest extends RunKonanTest {
|
||||||
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
public def inDevelopersRun = false
|
public def inDevelopersRun = false
|
||||||
public def statistics = new Statistics()
|
public def statistics = new Statistics()
|
||||||
|
|
||||||
@@ -504,8 +503,10 @@ class RunStdlibTest extends RunKonanTest {
|
|||||||
def n = matcher[0][1] as Integer
|
def n = matcher[0][1] as Integer
|
||||||
statistics.fail(n)
|
statistics.fail(n)
|
||||||
}
|
}
|
||||||
if (statistics.total == 0) {
|
use(KonanTestSuiteReportKt) {
|
||||||
statistics.error(testsTotal != 0 ? testsTotal : 1)
|
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
|
* Compiles and executes test as a standalone binary
|
||||||
*/
|
*/
|
||||||
class RunStandaloneKonanTest extends KonanTest {
|
class RunStandaloneKonanTest extends KonanTest {
|
||||||
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
public def inDevelopersRun = true
|
public def inDevelopersRun = true
|
||||||
|
|
||||||
void compileTest(List<String> filesToCompile, String exe) {
|
void compileTest(List<String> filesToCompile, String exe) {
|
||||||
@@ -527,6 +531,9 @@ class RunStandaloneKonanTest extends KonanTest {
|
|||||||
// project.exec + a shell script isolate the jvm
|
// project.exec + a shell script isolate the jvm
|
||||||
// from IDEA. Use the RunKonanTest instead.
|
// from IDEA. Use the RunKonanTest instead.
|
||||||
class RunDriverKonanTest extends KonanTest {
|
class RunDriverKonanTest extends KonanTest {
|
||||||
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
public def inDevelopersRun = true
|
public def inDevelopersRun = true
|
||||||
|
|
||||||
RunDriverKonanTest() {
|
RunDriverKonanTest() {
|
||||||
@@ -565,6 +572,9 @@ class RunDriverKonanTest extends KonanTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RunInteropKonanTest extends KonanTest {
|
class RunInteropKonanTest extends KonanTest {
|
||||||
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
public def inDevelopersRun = true
|
public def inDevelopersRun = true
|
||||||
|
|
||||||
private String interop
|
private String interop
|
||||||
@@ -593,8 +603,10 @@ class RunInteropKonanTest extends KonanTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LinkKonanTest extends KonanTest {
|
class LinkKonanTest extends KonanTest {
|
||||||
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
public def inDevelopersRun = true
|
public def inDevelopersRun = true
|
||||||
|
|
||||||
protected String lib
|
protected String lib
|
||||||
|
|
||||||
void compileTest(List<String> filesToCompile, String exe) {
|
void compileTest(List<String> filesToCompile, String exe) {
|
||||||
@@ -607,10 +619,13 @@ class LinkKonanTest extends KonanTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DynamicKonanTest extends KonanTest {
|
class DynamicKonanTest extends KonanTest {
|
||||||
protected String cSource
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
public def inDevelopersRun = true
|
public def inDevelopersRun = true
|
||||||
|
|
||||||
|
protected String cSource
|
||||||
|
|
||||||
void compileTest(List<String> filesToCompile, String exe) {
|
void compileTest(List<String> filesToCompile, String exe) {
|
||||||
def libname = "testlib"
|
def libname = "testlib"
|
||||||
def dylib = "$outputDirectory/$libname"
|
def dylib = "$outputDirectory/$libname"
|
||||||
@@ -640,13 +655,15 @@ class DynamicKonanTest extends KonanTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
class RunExternalTestGroup extends RunStandaloneKonanTest {
|
class RunExternalTestGroup extends RunStandaloneKonanTest {
|
||||||
def inDevelopersRun = false
|
/**
|
||||||
|
* overrides [KonanTest::inDevelopersRun] used in [:backend.native:tests:sanity]
|
||||||
|
*/
|
||||||
|
public def inDevelopersRun = false
|
||||||
|
|
||||||
def groupDirectory = "."
|
def groupDirectory = "."
|
||||||
def outputSourceSetName = "testOutputExternal"
|
def outputSourceSetName = "testOutputExternal"
|
||||||
String filter = project.findProperty("filter")
|
String filter = project.findProperty("filter")
|
||||||
Map<String, TestResult> results = [:]
|
def testGroupReporter = new KonanTestGroupReportEnvironment(project)
|
||||||
Statistics statistics = new Statistics()
|
|
||||||
|
|
||||||
RunExternalTestGroup() {
|
RunExternalTestGroup() {
|
||||||
}
|
}
|
||||||
@@ -933,79 +950,53 @@ fun runTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics = new Statistics()
|
testGroupReporter.suite(name) { suite ->
|
||||||
def testSuite = createTestSuite(name, statistics)
|
// Build tests in the group
|
||||||
testSuite.start()
|
flags = (flags ?: []) + "-tr"
|
||||||
// Build tests in the group
|
def compileList = []
|
||||||
flags = (flags ?: []) + "-tr"
|
ktFiles.each {
|
||||||
def compileList = []
|
source = project.relativePath(it)
|
||||||
ktFiles.each {
|
if (isEnabledForNativeBackend(source)) {
|
||||||
source = project.relativePath(it)
|
// Create separate output directory for each test in the group.
|
||||||
if (isEnabledForNativeBackend(source)) {
|
outputDirectory = outputRootDirectory + "/${it.name}"
|
||||||
// Create separate output directory for each test in the group.
|
project.file(outputDirectory).mkdirs()
|
||||||
outputDirectory = outputRootDirectory + "/${it.name}"
|
parseLanguageFlags()
|
||||||
project.file(outputDirectory).mkdirs()
|
compileList.addAll(createTestFiles())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
currentResult = testCase.skip()
|
|
||||||
}
|
}
|
||||||
arguments = savedArgs
|
compileList.add(project.file("testUtils.kt").absolutePath)
|
||||||
println("TEST $currentResult.status\n")
|
compileList.add(project.file("helpers.kt").absolutePath)
|
||||||
if (currentResult.status == TestStatus.ERROR || currentResult.status == TestStatus.FAILED) {
|
try {
|
||||||
println("Command to reproduce: ./gradlew $name -Pfilter=${it.name}\n")
|
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
|
package org.jetbrains.kotlin
|
||||||
|
|
||||||
import com.ullink.slack.simpleslackapi.impl.SlackSessionFactory
|
import com.ullink.slack.simpleslackapi.impl.SlackSessionFactory
|
||||||
import groovy.json.JsonSlurper
|
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||||
import org.gradle.api.DefaultTask
|
import org.gradle.api.DefaultTask
|
||||||
import org.gradle.api.tasks.Input
|
import org.gradle.api.tasks.Input
|
||||||
import org.gradle.api.tasks.TaskAction
|
import org.gradle.api.tasks.TaskAction
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.*
|
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() {
|
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
|
@Input
|
||||||
lateinit var reportHome: String
|
lateinit var reportHome: String
|
||||||
|
|
||||||
|
@ImplicitReflectionSerializer
|
||||||
@TaskAction
|
@TaskAction
|
||||||
fun report() {
|
fun report() {
|
||||||
val teamcityConfig = System.getenv("TEAMCITY_BUILD_PROPERTIES_FILE")
|
|
||||||
?: throw RuntimeException("Can't load teamcity config")
|
|
||||||
|
|
||||||
val buildProperties = Properties()
|
val reportJson = loadReport("$reportHome/external/report.json")
|
||||||
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 resultsFile = File(reportHome, "external/results.json")
|
val report: String =
|
||||||
val report: String = if (resultsFile.exists()) {
|
"${reportJson.statistics.report}\n ${reportEpilogue()}"
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val obj = JsonSlurper().parse(resultsFile) as? AbstractMap<String, Any>
|
project.logger.info(report)
|
||||||
?: throw IllegalStateException("Got incorrect JSON object: ${resultsFile.absolutePath}")
|
sendTextToSlack(report)
|
||||||
@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"]!!,
|
open class NightlyReporter: DefaultTask() {
|
||||||
failed = statsJSON["failed"]!!,
|
@Input
|
||||||
error = statsJSON["error"]!!,
|
lateinit var externalMacosReport:String
|
||||||
skipped = statsJSON["skipped"]!!)
|
@Input
|
||||||
val total = statsJSON["total"]!!
|
lateinit var externalLinuxReport:String
|
||||||
if (total != stats.total) {
|
@Input
|
||||||
val message = "WARNING: total amount of tests doesn't match with the sum of failed, passed, error & skipped"
|
lateinit var externalWindowsReport:String
|
||||||
println(message)
|
|
||||||
epilogue += message
|
@ImplicitReflectionSerializer
|
||||||
}
|
@TaskAction
|
||||||
"total: ${stats.total}\npassed: ${stats.passed}\nfailed: ${stats.failed}\n" +
|
fun report() {
|
||||||
"error:${stats.error}\nskipped:${stats.skipped} $epilogue"
|
val externalMacosJsonReport = loadReport("${project.rootDir.absolutePath}/$externalMacosReport")
|
||||||
} else {
|
val externalLinuxJsonReport = loadReport("${project.rootDir.absolutePath}/$externalLinuxReport")
|
||||||
"Unable to get results\nBuild has probably failed$epilogue"
|
val externalWindowsJsonReport = loadReport("${project.rootDir.absolutePath}/$externalWindowsReport")
|
||||||
}
|
val report = buildString {
|
||||||
println(report)
|
append("Mac OS ")
|
||||||
with(SlackSessionFactory.createWebSocketSlackSession(buildProperties.getProperty("konan-reporter-token"))) {
|
appendln(externalMacosJsonReport.statistics.oneLineReport)
|
||||||
connect()
|
append("Linux ")
|
||||||
sendMessage(findChannelByName(buildProperties.getProperty("konan-channel-name")),
|
appendln(externalLinuxJsonReport.statistics.oneLineReport)
|
||||||
"Hello, аборигены Котлина!\n текущий статус:\n$report")
|
append("Windows ")
|
||||||
disconnect()
|
appendln(externalWindowsJsonReport.statistics.oneLineReport)
|
||||||
|
appendln(reportEpilogue())
|
||||||
}
|
}
|
||||||
|
project.logger.info(report)
|
||||||
|
sendTextToSlack(report)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user