[debug] make test module name more specific

This commit is contained in:
Aleksey Kladov
2017-10-24 11:31:39 +03:00
committed by ilmat192
parent 084b49d04b
commit 9f9f34d2a3
6 changed files with 1 additions and 1 deletions
@@ -0,0 +1,20 @@
apply plugin: 'java'
apply plugin: 'kotlin'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile project(path: ':backend.native', configuration: 'cli_bc')
compile project(':shared')
compile 'junit:junit:4.12'
}
test {
testLogging { exceptionFormat = 'full' }
def dist = project.rootProject.file("dist").canonicalPath
systemProperties = [
'konan.home': dist,
'java.library.path': "$dist/konan/nativelib"
]
}
test.dependsOn ':dist'
@@ -0,0 +1,17 @@
package org.jetbrains.kotlin.compiletest
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.TargetManager
import java.nio.file.Path
import java.nio.file.Paths
object DistProperties {
private val dist: Path = Paths.get(requireProp("konan.home"))
private val konancDriver = if (TargetManager.host.family == Family.WINDOWS) "konanc.bat" else "konanc"
val konanc: Path = dist.resolve("bin/$konancDriver")
val lldb: Path = Paths.get("lldb")
private fun requireProp(name: String): String
= System.getProperty(name) ?: error("Property `$name` is not defined")
}
@@ -0,0 +1,71 @@
package org.jetbrains.kotlin.compiletest
import org.jetbrains.kotlin.cli.bc.K2Native
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.TimeUnit
class ToolDriver(
private val konancDriver: Path,
private val lldb: Path,
private val useInProcessCompiler: Boolean = false
) {
fun compile(source: Path, output: Path, vararg args: String) {
check(!Files.exists(output))
val allArgs = listOf("-output", output.toString(), source.toString(), *args).toTypedArray()
if (useInProcessCompiler) {
K2Native.main(allArgs)
} else {
subprocess(konancDriver, *allArgs).thrownIfFailed()
}
check(Files.exists(output)) {
"Compiler has not produced an output at $output"
}
}
fun runLldb(program: Path, commands: List<String>): String {
val args = commands.flatMap { listOf("-o", it) }
return subprocess(lldb, program.toString(), "-b", *args.toTypedArray())
.thrownIfFailed()
.stdout
}
}
data class ProcessOutput(
val program: Path,
val process: Process,
val stdout: String,
val stderr: String,
val durationMs: Long
) {
fun thrownIfFailed(): ProcessOutput {
fun renderStdStream(name: String, text: String): String =
if (text.isBlank()) "$name is empty" else "$name:\n$text"
check(process.exitValue() == 0) {
"""$program exited with non-zero value: ${process.exitValue()}
|${renderStdStream("stdout", stdout)}
|${renderStdStream("stderr", stderr)}
""".trimMargin()
}
return this
}
}
fun subprocess(program: Path, vararg args: String): ProcessOutput {
val start = System.currentTimeMillis()
val process = ProcessBuilder(program.toString(), *args).start()
val outReader = process.inputStream.bufferedReader()
val errReader = process.errorStream.bufferedReader()
val timeout = 5L
if (!process.waitFor(timeout, TimeUnit.MINUTES)) {
process.destroy()
error("$program is running for more then $timeout minutes")
}
val stdout = outReader.readText()
val stderr = errReader.readText()
return ProcessOutput(program, process, stdout, stderr, System.currentTimeMillis() - start)
}
@@ -0,0 +1,170 @@
package org.jetbrains.kotlin.compiletest
import org.intellij.lang.annotations.Language
import org.junit.Assert.fail
import java.io.IOException
import java.nio.file.Files
/**
* An integration test for debug info.
*
* It works by compiling a given [programText] with debug info,
* then launching lldb, feeding it commands from [lldbSession]
* and matching the output.
*
* [lldbSession] specifies both lldb commands and expected output
* using a DLS, which looks like this:
*
* > b main.kt:5
* Breakpoint 1: [..]
* > r
* Process [..] stopped
* [..] at main.kt:5, [..] stop reason = breakpoint [..]
* > fr var
* (int) a = 92
* (int) b = 2
*
* It consists of blocks of the form
*
* > lldb command
* response line pattern
* another pattern
*
* Command after `>` is passed to lldb exactly. The output of the command
* is then matched against a set of patterns. Matching is done line by line:
* for every pattern, there must be a matching line in the output, but you don't
* have to specify a pattern for every line. In particular, it's possible not to
* specify any patterns at all:
*
* > b main.kt:2
* > r
* > n
* [..] at main.kt:3, [..] stop reason = step over
*
* The patterns themselves are simple. The only special symbol is `[..]` which
* means arbitrary substring, which can help match random data, timings, and OS-dependent output.
* For example, to match
*
* Current executable set to '/tmp/debugger_test7458723719928260513/program.kexe' (x86_64).
*
* one writes
*
* Current executable set to [..]program.kexe[..]
*/
fun lldbTest(@Language("kotlin") programText: String, lldbSession: String) {
if (!haveLldb) {
println("Skipping test: no LLDB")
return
}
val lldbSessionSpec = LldbSessionSpecification.parse(lldbSession)
val tmpdir = Files.createTempDirectory("debugger_test")
tmpdir.toFile().deleteOnExit()
val source = tmpdir.resolve("main.kt")
val output = tmpdir.resolve("program.kexe")
val driver = ToolDriver(DistProperties.konanc, DistProperties.lldb)
Files.write(source, programText.trimIndent().toByteArray())
driver.compile(source, output, "-g")
val result = driver.runLldb(output, lldbSessionSpec.commands)
lldbSessionSpec.match(result)
}
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("""\s*\[\.\.]\s*""".toRegex())
.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
}
private val haveLldb: Boolean by lazy {
val lldbVersion = try {
subprocess(DistProperties.lldb, "-version")
.takeIf { it.process.exitValue() == 0 }
?.stdout
} catch (e: IOException) {
null
}
if (lldbVersion == null) {
println("No LLDB found")
} else {
println("Using $lldbVersion")
}
lldbVersion != null
}
private class LldbSessionSpecification private constructor(
val commands: List<String>,
val patterns: List<List<String>>
) {
fun match(output: String) {
val blocks = output.split("""(?=\(lldb\))""".toRegex())
check(blocks.first().startsWith("(lldb) target create"))
val responses = blocks.drop(1)
val executedCommands = responses.map { it.lines().first() }
val bodies = responses.map { it.lines().drop(1) }
val responsesMatch = executedCommands.size == commands.size
&& commands.zip(executedCommands).all { (cmd, h) -> h == "(lldb) $cmd" }
if (!responsesMatch) {
fail("Responses do not match commands.\nResponses: $executedCommands\nCommands: $commands")
}
for ((patternBody, command) in patterns.zip(bodies).zip(executedCommands)) {
val (pattern, body) = patternBody
val mismatch = findMismatch(pattern, body)
if (mismatch != null) {
val message = """
Wrong LLDB output.
COMMAND: $command
PATTERN: $mismatch
OUTPUT:
${body.joinToString("\n")}
FULL SESSION:
$output
""".trimStart()
fail(message)
}
}
}
companion object {
fun parse(spec: String): LldbSessionSpecification {
val blocks = spec.trimIndent().split("(?=^>)".toRegex(RegexOption.MULTILINE))
for (cmd in blocks) {
check(cmd.startsWith(">")) { "Invalid lldb session specification" }
}
val commands = blocks.map { it.lines().first().substring(1).trim() }
val patterns = blocks.map { it.lines().drop(1).filter { it.isNotBlank() } }
return LldbSessionSpecification(commands, patterns)
}
}
}
@@ -0,0 +1,56 @@
import org.jetbrains.kotlin.compiletest.lldbTest
import org.junit.Test
class LldbTests {
//FIXME: the last one should be main.kt.5
@Test
fun `can step through code`() = lldbTest("""
fun main(args: Array<String>) {
var x = 1
var y = 2
var z = x + y
println(z)
}
""", """
> b main.kt:2
Breakpoint 1: [..]
> r
Process [..] stopped
[..] at main.kt:2, [..] stop reason = breakpoint 1.1
> n
Process [..] stopped
[..] at main.kt:3, [..] stop reason = step over
> n
Process [..] stopped
[..] at main.kt:4, [..] stop reason = step over
> n
Process [..] stopped
[..] at main.kt:3, [..] stop reason = step over
""")
//FIXME: Boolean and Int are wrong
@Test
fun `can inspect values of primitive types`() = lldbTest("""
fun main(args: Array<String>) {
var a: Byte = 1
var b: Int = 2
var c: Long = -3
var d: Char = 'c'
var e: Boolean = true
return
}
""", """
> b main.kt:7
> r
> fr var
(char) a = '\x01'
(int) b = 2
(long) c = -3
(unsigned char) d = 'c'
(void) e = <Unable to determine byte size.>
""")
}