[K/N] Small clean-up in LLDB tests

This commit is contained in:
Dmitriy Dolovov
2022-12-12 17:44:28 +01:00
committed by Space Team
parent 842a66c3de
commit f07f7885c5
11 changed files with 173 additions and 151 deletions
@@ -98,7 +98,12 @@ fun main() {
private inline fun <reified T : Annotation> provider() = annotation(T::class.java)
private fun debugOnly() = annotation(EnforcedProperty::class.java, Pair("property", ClassLevelProperty.OPTIMIZATION_MODE), Pair("propertyValue", "DEBUG"))
private fun debugOnly() = annotation(
EnforcedProperty::class.java,
"property" to ClassLevelProperty.OPTIMIZATION_MODE,
"propertyValue" to "DEBUG"
)
private fun codegen() = annotation(Tag::class.java, "codegen")
private fun debugger() = annotation(Tag::class.java, "debugger")
private fun infrastructure() = annotation(Tag::class.java, "infrastructure")
@@ -80,7 +80,7 @@ private object NativeTestSupport {
nativeHome,
computeNativeClassLoader(),
computeBaseDirs(),
DebugUtils()
LLDB(nativeHome)
)
} as TestProcessSettings
@@ -16,13 +16,14 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.TestDirectives.LLDB_TRACE
import org.jetbrains.kotlin.konan.blackboxtest.support.TestDirectives.TEST_RUNNER
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.TestRunCheck
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.TestRunCheck.OutputDataFile
import org.jetbrains.kotlin.konan.blackboxtest.support.util.LldbSessionSpecification
import org.jetbrains.kotlin.konan.blackboxtest.support.util.LLDBSessionSpec
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.directives.model.StringDirective
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.test.services.impl.RegisteredDirectivesParser
import org.junit.jupiter.api.Assertions
import java.io.File
internal object TestDirectives : SimpleDirectivesContainer() {
@@ -214,10 +215,14 @@ internal fun parseEntryPoint(registeredDirectives: RegisteredDirectives, locatio
return entryPoint
}
internal fun parseLLDBSpec(baseDir:File, registeredDirectives: RegisteredDirectives, location: Location): LldbSessionSpecification {
internal fun parseLLDBSpec(baseDir: File, registeredDirectives: RegisteredDirectives, location: Location): LLDBSessionSpec {
val specFile = parseFileBasedDirective(baseDir, LLDB_TRACE, registeredDirectives, location)
?: fail { "$location: A lldb session specification must be provided" }
return LldbSessionSpecification.parse(specFile.readText())
?: fail { "$location: An LLDB session specification must be provided" }
return try {
LLDBSessionSpec.parse(specFile.readText())
} catch (e: Exception) {
Assertions.fail<Nothing>("$location: Cannot parse LLDB session specification: " + e.message, e)
}
}
internal fun parseModule(parsedDirective: RegisteredDirectivesParser.ParsedDirective, location: Location): TestModule.Exclusive {
@@ -48,9 +48,9 @@ internal class StandardTestCaseGroupProvider : TestCaseGroupProvider {
val testCases = includedTestDataFiles.map { testDataFile -> createTestCase(testDataFile, settings) }
if (!settings.get<DebugUtils>().lldbIsAvailable) {
val lldbTests = testCases.filter { it.kind == TestKind.STANDALONE_LLDB }
lldbTests.mapTo(disabledTestCaseIds) { it.id }
val lldbTestCases = testCases.filter { it.kind == TestKind.STANDALONE_LLDB }
if (lldbTestCases.isNotEmpty() && !settings.get<LLDB>().isAvailable) {
lldbTestCases.mapTo(disabledTestCaseIds) { it.id }
}
TestCaseGroup.Default(disabledTestCaseIds, testCases)
@@ -205,11 +205,13 @@ internal class StandardTestCaseGroupProvider : TestCaseGroupProvider {
fixPackageNames(testModules.values, nominalPackageName, testDataFile)
}
val lldbSpec = if (testKind == TestKind.STANDALONE_LLDB) parseLLDBSpec(
baseDir = testDataFile.parentFile,
registeredDirectives,
location
) else null
val lldbSpec = if (testKind == TestKind.STANDALONE_LLDB)
parseLLDBSpec(
baseDir = testDataFile.parentFile,
registeredDirectives,
location
)
else null
val testCase = TestCase(
id = TestCaseId.TestDataFile(testDataFile),
@@ -221,7 +223,7 @@ internal class StandardTestCaseGroupProvider : TestCaseGroupProvider {
computeExecutionTimeoutCheck(settings, expectedTimeoutFailure),
computeExitCodeCheck(testKind, registeredDirectives, location),
computeOutputDataFileCheck(testDataFile, registeredDirectives, location),
lldbSpec?.let { OutputMatcher(it::matchOutput) }
lldbSpec?.let { OutputMatcher { output -> lldbSpec.checkLLDBOutput(output, settings.get()) } }
),
extras = when (testKind) {
TestKind.STANDALONE_NO_TR -> {
@@ -236,7 +238,7 @@ internal class StandardTestCaseGroupProvider : TestCaseGroupProvider {
TestKind.STANDALONE_LLDB -> {
NoTestRunnerExtras(
entryPoint = parseEntryPoint(registeredDirectives, location),
arguments = lldbSpec!!.generateCLIArguments(settings.get<KotlinNativeHome>().lldbPrettyPrinters)
arguments = lldbSpec!!.generateCLIArguments(settings.get<LLDB>().prettyPrinters)
)
}
}
@@ -114,7 +114,7 @@ internal abstract class AbstractLocalProcessRunner<R>(private val checks: TestRu
} catch (t: Throwable) {
if (t is Exception || t is AssertionError) {
fail<Nothing>(
getLoggedRun().withErrorMessage("Tested process output has not passed validation:\n\n" + t.message),
getLoggedRun().withErrorMessage("Tested process output has not passed validation: " + t.message),
t
)
} else {
@@ -34,7 +34,6 @@ internal open class BaseTestRunProvider {
// Note: TestRunParameter.WithLLDB adds program arguments and would therefore conflict
// with other TestRunParameters that do the same (such as WithTCTestLogger).
add(TestRunParameter.WithLLDB(testCase.extras<NoTestRunnerExtras>().arguments))
addIfNotNull(testCase.extras<NoTestRunnerExtras>().inputDataFile?.let(TestRunParameter::WithInputData))
}
TestKind.STANDALONE_NO_TR -> {
assertTrue(testName == null)
@@ -8,10 +8,7 @@ package org.jetbrains.kotlin.konan.blackboxtest.support.runner
import org.jetbrains.kotlin.konan.blackboxtest.support.*
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact.Executable
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult.Success
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.KotlinNativeHome
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.util.DumpedTestListing
import org.jetbrains.kotlin.konan.blackboxtest.support.util.LldbSessionSpecification
import org.jetbrains.kotlin.konan.blackboxtest.support.util.startsWith
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertFalse
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
@@ -15,7 +15,6 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilati
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationFactory
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult.Companion.assertSuccess
import org.jetbrains.kotlin.konan.blackboxtest.support.group.TestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.DebugUtils
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeCache
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TreeNode
@@ -104,13 +103,7 @@ internal class TestRunProvider(
fun createTestRun(testRunName: String, testName: TestName?) = createTestRun(testCase, executable, testRunName, testName)
when (testCase.kind) {
TestKind.STANDALONE_LLDB -> {
if (!settings.get<DebugUtils>().lldbIsAvailable) TreeNode.oneLevel<TestRun>()
val testRunName = testCase.extras<NoTestRunnerExtras>().entryPoint.substringAfterLast('.')
val testRun = createTestRun(testRunName, testName = null)
TreeNode.oneLevel(testRun)
}
TestKind.STANDALONE_NO_TR -> {
TestKind.STANDALONE_NO_TR, TestKind.STANDALONE_LLDB -> {
val testRunName = testCase.extras<NoTestRunnerExtras>().entryPoint.substringAfterLast('.')
val testRun = createTestRun(testRunName, testName = null)
TreeNode.oneLevel(testRun)
@@ -30,14 +30,15 @@ internal class KotlinNativeTargets(val testTarget: KonanTarget, val hostTarget:
internal class KotlinNativeHome(val dir: File) {
val librariesDir: File = dir.resolve("klib")
val stdlibFile: File = librariesDir.resolve("common/stdlib")
val lldbPrettyPrinters: File = dir.resolve("tools/konan_lldb.py")
val properties: Properties by lazy {
dir.resolve("konan/konan.properties").inputStream().use { Properties().apply { load(it) } }
}
}
internal class DebugUtils {
val lldbIsAvailable: Boolean by lazy {
internal class LLDB(nativeHome: KotlinNativeHome) {
val prettyPrinters: File = nativeHome.dir.resolve("tools/konan_lldb.py")
val isAvailable: Boolean by lazy {
try {
val exitCode = ProcessBuilder("lldb", "-version").start().waitFor()
exitCode == 0
@@ -0,0 +1,139 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.KotlinNativeTargets
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.File
private class Step(val command: String, val body: List<String>) {
companion object {
fun parse(block: String, expectedPrefix: String): Step {
val lines = block.lines()
val commandWithPrefix = lines.first()
assertTrue(commandWithPrefix.startsWith(expectedPrefix)) {
"The command should start with $expectedPrefix. Got: $commandWithPrefix"
}
val command = commandWithPrefix.removePrefix(expectedPrefix).trimStart()
val body = lines.drop(1).filterNot(String::isBlank)
return Step(command, body)
}
}
}
internal class LLDBSessionSpec private constructor(private val expectedSteps: List<Step>) {
fun generateCLIArguments(prettyPrinters: File): List<String> = buildList {
this += "-b"
this += "-o"
this += "command script import ${prettyPrinters.absolutePath}"
expectedSteps.forEach { step ->
this += "-o"
this += step.command
}
}
fun checkLLDBOutput(output: String, nativeTargets: KotlinNativeTargets): Boolean {
val blocks = output.split(LLDB_OUTPUT_SEPARATOR).filterNot(String::isBlank)
val meaningfulBlocks = if (nativeTargets.testTarget == nativeTargets.hostTarget) {
// TODO: why are these two leading blocks only checked for the host target?
val createTargetBlock = blocks.getOrElse(0) { "" }
assertTrue(createTargetBlock.startsWith("(lldb) target create")) {
"Missing block \"target create\". Got: $createTargetBlock"
}
val commandScriptBlock = blocks.getOrElse(1) { "" }
assertTrue(commandScriptBlock.startsWith("(lldb) command script import")) {
"Missing block \"command script import\". Got: $commandScriptBlock"
}
blocks.drop(2)
} else {
blocks.drop(2).dropLast(1)
}
val recordedSteps = meaningfulBlocks.map { block -> Step.parse(block, LLDB_COMMAND_PREFIX) }
assertTrue(expectedSteps.size == recordedSteps.size) {
"""
The number of responses do not match the number of commands.
- Commands (${expectedSteps.size}): ${expectedSteps.map { it.command }}
- Responses (${recordedSteps.size}): ${recordedSteps.map { it.command }}
""".trimIndent()
}
for ((expectedStep, recordedStep) in expectedSteps.zip(recordedSteps)) {
assertTrue(expectedStep.command == recordedStep.command) {
"""
Wrong command in response.
- Expected: ${expectedStep.command}
- Actual: ${recordedStep.command}
""".trimIndent()
}
val mismatch = findMismatch(expectedStep.body, recordedStep.body)
if (mismatch != null) {
fail {
buildString {
appendLine("Wrong LLDB output.")
append("- Command: ").appendLine(expectedStep.command)
append("- Expected (pattern): ").appendLine(mismatch)
appendLine("- Actual:")
recordedStep.body.joinTo(this, separator = "\n")
}
}
}
}
return true
}
private fun findMismatch(patterns: List<String>, actualLines: List<String>): String? {
val indices = mutableListOf<Int>()
for (pattern in patterns) {
val idx = actualLines.indexOfFirst { match(pattern, it) }
if (idx == -1) {
return pattern
}
indices += idx
}
assertTrue(indices == indices.sorted())
return null
}
private fun match(pattern: String, line: String): Boolean {
val chunks = pattern.split(LINE_WILDCARD)
.filter { it.isNotBlank() }
.map { it.trim() }
assertTrue(chunks.isNotEmpty())
val trimmedLine = line.trim()
val indices = chunks.map { trimmedLine.indexOf(it) }
if (indices.any { it == -1 } || indices != indices.sorted()) return false
if (!(trimmedLine.startsWith(chunks.first()) || pattern.startsWith("[..]"))) return false
if (!(trimmedLine.endsWith(chunks.last()) || pattern.endsWith("[..]"))) return false
return true
}
companion object {
private const val LLDB_COMMAND_PREFIX = "(lldb)"
private const val SPEC_COMMAND_PREFIX = ">"
private val LLDB_OUTPUT_SEPARATOR = """(?=\(lldb\))""".toRegex()
private val SPEC_BLOCK_SEPARATOR = "(?=^>)".toRegex(RegexOption.MULTILINE)
private val LINE_WILDCARD = """\s*\[\.\.]\s*""".toRegex()
fun parse(lldbSpec: String): LLDBSessionSpec = LLDBSessionSpec(
lldbSpec.split(SPEC_BLOCK_SEPARATOR)
.filterNot(String::isBlank)
.map { block -> Step.parse(block, SPEC_COMMAND_PREFIX) }
)
}
}
@@ -1,119 +0,0 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.File
private class Step(val command: String, val body: List<String>)
class LldbSessionSpecification private constructor(
private val steps: List<Step>,
) {
fun generateCLIArguments(pp: File): List<String> {
val args = mutableListOf("-b", "-o", "command script import ${pp.absolutePath}")
args.addAll(steps.flatMap { listOf("-o", it.command) })
return args
}
// TODO: Re-introduce this when we add support for iOS simulator based testing.
fun targetIsHost() = true
fun matchOutput(output: String): Boolean {
val blocks = output.split(lldbOutputCommand).filterNot(String::isEmpty)
if (targetIsHost()) {
check(blocks[0].startsWith("(lldb) target create")) { "Missing block \"target create\". Got: ${blocks[0]}" }
check(blocks[1].startsWith("(lldb) command script import")) {
"Missing block \"command script import\". Got: ${blocks[1]}"
}
}
val responses = if (targetIsHost())
blocks.drop(2)
else
blocks.drop(2).dropLast(1)
val recordedSteps = responses.map { Step(it.lines().first(), it.lines().drop(1)) }
if (recordedSteps.size != steps.size) {
val message = """
|Responses do not match commands.
|
|COMMANDS: |${steps.map { it.command }} (${steps.size})
|RESPONSES: |${recordedSteps.map { it.command }} (${recordedSteps.size})
""".trimMargin()
fail { message }
}
steps.zip(recordedSteps).all { (step, recordedStep) -> recordedStep.command == "(lldb) ${step.command}" }
for ((step, recordedStep) in steps.zip(recordedSteps)) {
check(recordedStep.command == "(lldb) ${step.command}") {
"Wrong command in response. Expected: (lldb) ${step.command}. Got: ${recordedStep.command}"
}
val mismatch = findMismatch(step.body, recordedStep.body)
if (mismatch != null) {
val message = """
|Wrong LLDB output.
|
|COMMAND: ${step.command}
|PATTERN: $mismatch
|OUTPUT:
|${recordedStep.body.joinToString("\n")}
""".trimMargin()
fail { message }
}
}
return true
}
private fun findMismatch(patterns: List<String>, actualLines: List<String>): String? {
val indices = mutableListOf<Int>()
for (pattern in patterns) {
val idx = actualLines.indexOfFirst { match(pattern, it) }
if (idx == -1) {
return pattern
}
indices += idx
}
check(indices == indices.sorted())
return null
}
private fun match(pattern: String, line: String): Boolean {
val chunks = pattern.split(wildcard)
.filter { it.isNotBlank() }
.map { it.trim() }
check(chunks.isNotEmpty())
val trimmedLine = line.trim()
val indices = chunks.map { trimmedLine.indexOf(it) }
if (indices.any { it == -1 } || indices != indices.sorted()) return false
if (!(trimmedLine.startsWith(chunks.first()) || pattern.startsWith("[..]"))) return false
if (!(trimmedLine.endsWith(chunks.last()) || pattern.endsWith("[..]"))) return false
return true
}
companion object {
val lldbOutputCommand = """(?=\(lldb\))""".toRegex()
val wildcard = """\s*\[\.\.]\s*""".toRegex()
val separator = "(?=^>)".toRegex(RegexOption.MULTILINE)
fun parse(spec: String): LldbSessionSpecification {
val blocks = spec.trimIndent()
.split(separator)
.filterNot(String::isEmpty)
for (cmd in blocks) {
check(cmd.startsWith(">")) { "Invalid lldb session specification: $cmd" }
}
val steps = blocks.map {
Step(it.lines().first().substring(1).trim(), it.lines().drop(1).filter { it.isNotBlank() })
}
return LldbSessionSpecification(steps)
}
}
}