blob: 38f978bde359cfed23522979dff58dc42810a2b8 [file]
// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.TestDescriptor
import org.gradle.api.tasks.testing.TestListener
import org.gradle.api.tasks.testing.TestResult
public class TestConfigurationHelper {
public companion object {
private data class ResultSinkInfo(val address: String, val authToken: String)
private val resultSinkInfo: ResultSinkInfo? by lazy {
val luciContextPath = System.getenv("LUCI_CONTEXT")
if (luciContextPath != null) {
try {
val content = File(luciContextPath).readText()
val json = Gson().fromJson(content, JsonObject::class.java)
val resultSink = json.getAsJsonObject("result_sink")
if (resultSink != null) {
val address = resultSink.get("address")?.asString
val authToken = resultSink.get("auth_token")?.asString
if (address != null && authToken != null) {
ResultSinkInfo(address, authToken)
} else {
null
}
} else {
null
}
} catch (e: Exception) {
null
}
} else {
null
}
}
private fun retrace(
rootDir: File,
r8jar: File,
mappingFile: File,
exception: Throwable,
printObfuscatedStacktraces: Boolean,
): String {
val out = StringBuilder()
val header = "RETRACED STACKTRACE: " + System.currentTimeMillis()
out.append("\n--------------------------------------\n")
out.append("${header}\n")
out.append("--------------------------------------\n")
val retracePath = rootDir.resolveAll("tools", "retrace.py")
val command =
mutableListOf(
"python3",
retracePath.toString(),
"--quiet",
"--map",
mappingFile.toString(),
"--r8jar",
r8jar.toString(),
)
val process = ProcessBuilder(command).start()
process.outputStream.use { exception.printStackTrace(PrintStream(it)) }
process.outputStream.close()
val processCompleted = process.waitFor(20L, TimeUnit.SECONDS) && process.exitValue() == 0
out.append(process.inputStream.bufferedReader().use { it.readText() })
if (!processCompleted) {
out.append(command.joinToString(" ") + "\n")
out.append("ERROR DURING RETRACING: " + System.currentTimeMillis() + "\n")
out.append(process.errorStream.bufferedReader().use { it.readText() })
}
if (printObfuscatedStacktraces || !processCompleted) {
out.append("\n\n--------------------------------------\n")
out.append("OBFUSCATED STACKTRACE\n")
out.append("--------------------------------------\n")
var baos = ByteArrayOutputStream()
exception.printStackTrace(PrintStream(baos, true, StandardCharsets.UTF_8))
out.append(baos.toString())
}
return out.toString()
}
@OptIn(ExperimentalEncodingApi::class)
private fun reportToResultSink(
desc: TestDescriptor?,
result: TestResult?,
rootDir: File,
isR8Lib: Boolean,
r8Jar: File?,
r8LibMappingFile: File?,
printObfuscatedStacktraces: Boolean,
) {
val info = resultSinkInfo ?: return
if (desc == null || result == null || desc.className == null) return
val testId = "${desc.className}.${desc.name}"
val status =
when (result.resultType) {
TestResult.ResultType.SUCCESS -> "PASS"
TestResult.ResultType.FAILURE -> "FAIL"
TestResult.ResultType.SKIPPED -> "SKIP"
else -> "FAIL"
}
val expected = result.resultType == TestResult.ResultType.SUCCESS
val durationMs = result.endTime - result.startTime
val duration = "${durationMs / 1000.0}s"
var stackTraceStr: String? = null
if (result.resultType == TestResult.ResultType.FAILURE && result.exception != null) {
val exception = result.exception as Throwable
stackTraceStr =
if (isR8Lib && r8Jar != null && r8LibMappingFile != null) {
retrace(rootDir, r8Jar, r8LibMappingFile, exception, printObfuscatedStacktraces)
} else {
val baos = ByteArrayOutputStream()
exception.printStackTrace(PrintStream(baos, true, StandardCharsets.UTF_8))
baos.toString()
}
}
val payloadObj = JsonObject()
val testResultsArr = com.google.gson.JsonArray()
val testResultObj = JsonObject()
val testIdStructuredObj = JsonObject()
val testIdCaseNameComponents = JsonArray()
testIdCaseNameComponents.add(desc.displayName.substringBeforeLast('['))
val argumentString = desc.displayName.substringAfterLast('[').substringBeforeLast(']')
testIdStructuredObj.addProperty("module", "junit")
testIdStructuredObj.addProperty("coarseName", desc.className?.substringBeforeLast(".") ?: "")
testIdStructuredObj.addProperty("fineName", desc.className?.substringAfterLast(".") ?: "")
testIdStructuredObj.add("caseNameComponents", testIdCaseNameComponents)
testResultObj.add("testIdStructured", testIdStructuredObj)
testResultObj.addProperty("testId", testId)
testResultObj.addProperty("status", status)
testResultObj.addProperty("expected", expected)
testResultObj.addProperty("duration", duration)
val summaryHtml = "<p>Arguments string: $argumentString</p>"
if (stackTraceStr != null) {
val artifact = JsonObject()
val encodedStackTrace = Base64.Default.encode(stackTraceStr.encodeToByteArray())
val jsonObject = JsonObject()
jsonObject.addProperty("contents", encodedStackTrace)
artifact.add("artifact-content-in-request", jsonObject)
testResultObj.add("artifacts", artifact)
testResultObj.addProperty(
"summaryHtml",
summaryHtml +
"<p>Stack trace:</p>" +
"<p><text-artifact artifact-id=\"artifact-content-in-request\"></p>",
)
} else {
testResultObj.addProperty("summaryHtml", summaryHtml)
}
testResultsArr.add(testResultObj)
payloadObj.add("testResults", testResultsArr)
try {
val url = URL("http://${info.address}/prpc/luci.resultsink.v1.Sink/ReportTestResults")
val conn = url.openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.doOutput = true
conn.setRequestProperty("Content-Type", "application/json")
conn.setRequestProperty("Authorization", "ResultSink ${info.authToken}")
conn.outputStream.use {
it.write(Gson().toJson(payloadObj).toByteArray(StandardCharsets.UTF_8))
}
conn.inputStream.use { it.readBytes() }
} catch (e: Exception) {
// Ignore
}
}
public fun setupTestTask(test: Test, isR8Lib: Boolean, r8Jar: File?, r8LibMappingFile: File?) {
// TODO(b/489058560) Enable when we have figured out re-running single test variants.
// test.useJUnitPlatform()
test.useJUnit()
test.systemProperty("junit.jupiter.execution.parallel.enabled", "true")
val project = test.project
if (project.hasProperty("testfilter")) {
val testFilter = project.property("testfilter").toString()
test.filter.setFailOnNoMatchingTests(false)
test.filter.setIncludePatterns(*(testFilter.split("|").toTypedArray()))
}
if (project.hasProperty("kotlin_compiler_dev")) {
test.systemProperty("com.android.tools.r8.kotlincompilerdev", "1")
}
if (project.hasProperty("kotlin_compiler_old")) {
test.systemProperty("com.android.tools.r8.kotlincompilerold", "1")
}
if (project.hasProperty("dex_vm") && project.property("dex_vm") != "default") {
println("NOTE: Running with non default vm: " + project.property("dex_vm"))
test.systemProperty("dex_vm", project.property("dex_vm")!!)
}
// Forward runtime configurations for test parameters.
if (project.hasProperty("runtimes")) {
println("NOTE: Running with runtimes: " + project.property("runtimes"))
test.systemProperty("runtimes", project.property("runtimes")!!)
}
if (project.hasProperty("art_profile_rewriting_completeness_check")) {
test.systemProperty(
"com.android.tools.r8.artprofilerewritingcompletenesscheck",
project.property("art_profile_rewriting_completeness_check")!!,
)
}
if (project.hasProperty("disable_assertions")) {
test.enableAssertions = false
}
// Forward project properties into system properties.
listOf(
"local_development",
"slow_tests",
"desugar_jdk_json_dir",
"desugar_jdk_libs",
"test_dir",
"command_cache_dir",
"command_cache_stats_dir",
)
.forEach {
val propertyName = it
if (project.hasProperty(propertyName)) {
project.property(propertyName)?.let { v -> test.systemProperty(propertyName, v) }
}
}
if (project.hasProperty("no_internal")) {
test.exclude("com/android/tools/r8/internal/**")
}
if (project.hasProperty("only_internal")) {
test.include("com/android/tools/r8/internal/**")
}
// Exclude sanity checks on lib jars when running without R8 lib,
// since we don't build processkeepruleslib.jar with test.py --no-r8lib.
if (!isR8Lib) {
test.exclude("com/android/tools/r8/processkeeprules/sanitychecks/**")
}
if (project.hasProperty("no_arttests")) {
test.exclude("com/android/tools/r8/art/**")
}
if (project.hasProperty("test_xmx")) {
test.maxHeapSize = project.property("test_xmx")!!.toString()
} else {
test.maxHeapSize = "4G"
}
val printTimes = project.hasProperty("print_times")
val oneLinePerTest = project.hasProperty("one_line_per_test")
val hasUpdateTestTimestamp = project.hasProperty("update_test_timestamp")
val rootDir = project.getRoot()
val printObfuscatedStacktraces = project.hasProperty("print_obfuscated_stacktraces")
if (isR8Lib || oneLinePerTest || hasUpdateTestTimestamp) {
val updateTestTimestampPath =
if (hasUpdateTestTimestamp) {
project.property("update_test_timestamp")!!.toString()
} else {
null
}
test.addTestListener(
object : TestListener {
val testTimes = mutableMapOf<TestDescriptor?, Long>()
val maxPrintTimesCount = 200
override fun beforeSuite(desc: TestDescriptor?) {}
override fun afterSuite(desc: TestDescriptor?, result: TestResult?) {
if (printTimes) {
// desc.parent == null when we are all done
if (desc?.parent == null) {
testTimes
.toList()
.sortedByDescending { it.second }
.take(maxPrintTimesCount)
.forEach { println("${it.first} took: ${it.second}") }
}
}
}
override fun beforeTest(desc: TestDescriptor?) {
if (oneLinePerTest) {
println("Start executing ${desc}")
}
if (printTimes) {
testTimes[desc] = Date().getTime()
}
}
override fun afterTest(desc: TestDescriptor?, result: TestResult?) {
if (oneLinePerTest) {
println("Done executing ${desc} with result: ${result?.resultType}")
}
if (printTimes) {
testTimes[desc] = Date().getTime() - testTimes[desc]!!
}
if (updateTestTimestampPath != null) {
File(updateTestTimestampPath).writeText(Date().getTime().toString())
}
if (result?.resultType == TestResult.ResultType.FAILURE && result.exception != null) {
val exception = result.exception as Throwable
if (isR8Lib) {
println(
retrace(
rootDir,
r8Jar!!,
r8LibMappingFile!!,
exception,
printObfuscatedStacktraces,
)
)
} else {
val baos = ByteArrayOutputStream()
exception.printStackTrace(PrintStream(baos, true, StandardCharsets.UTF_8))
println(baos)
}
}
reportToResultSink(
desc,
result,
rootDir,
isR8Lib,
r8Jar,
r8LibMappingFile,
printObfuscatedStacktraces,
)
}
}
)
} else {
test.addTestListener(
object : TestListener {
override fun beforeSuite(desc: TestDescriptor?) {}
override fun afterSuite(desc: TestDescriptor?, result: TestResult?) {}
override fun beforeTest(desc: TestDescriptor?) {}
override fun afterTest(desc: TestDescriptor?, result: TestResult?) {
if (result?.resultType == TestResult.ResultType.FAILURE && result.exception != null) {
val exception = result.exception as Throwable
val baos = ByteArrayOutputStream()
exception.printStackTrace(PrintStream(baos, true, StandardCharsets.UTF_8))
println(baos)
}
reportToResultSink(
desc,
result,
rootDir,
isR8Lib,
r8Jar,
r8LibMappingFile,
printObfuscatedStacktraces,
)
}
}
)
}
val userDefinedCoresPerFork = System.getenv("R8_GRADLE_CORES_PER_FORK")
val processors = Runtime.getRuntime().availableProcessors()
// See https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html.
if (!userDefinedCoresPerFork.isNullOrEmpty()) {
test.maxParallelForks = processors.div(userDefinedCoresPerFork.toInt())
} else {
// On work machines this seems to give the best test execution time (without freezing).
test.maxParallelForks = maxOf(processors.div(8), 1)
// On low cpu count machines (bots) we under subscribe, so increase the count.
if (processors == 32) {
test.maxParallelForks = 15
}
}
}
}
}