[Test] Implement SMAP dump handler

This commit is contained in:
Dmitriy Novozhilov
2021-01-26 09:47:19 +03:00
parent e3ab3d6be3
commit 92e21e76ba
4 changed files with 182 additions and 57 deletions
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2021 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.codegen
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.backend.common.output.OutputFile
import org.jetbrains.kotlin.codegen.inline.RangeMapping
import org.jetbrains.kotlin.codegen.inline.SMAPParser
import org.jetbrains.kotlin.codegen.inline.toRange
import org.jetbrains.kotlin.test.Assertions
import org.jetbrains.kotlin.utils.keysToMap
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
import java.io.File
object CommonSMAPTestUtil {
fun extractSMAPFromClasses(outputFiles: Iterable<OutputFile>): List<SMAPAndFile> {
return outputFiles.map { outputFile ->
var debugInfo: String? = null
ClassReader(outputFile.asByteArray()).accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visitSource(source: String?, debug: String?) {
debugInfo = debug
}
}, 0)
SMAPAndFile(debugInfo, outputFile.sourceFiles.single(), outputFile.relativePath)
}
}
fun checkNoConflictMappings(compiledSmap: List<SMAPAndFile>?, assertions: Assertions) {
if (compiledSmap == null) return
compiledSmap.mapNotNull(SMAPAndFile::smap).forEach { smapString ->
val smap = SMAPParser.parseOrNull(smapString) ?: throw AssertionError("bad SMAP: $smapString")
val conflictingLines = smap.fileMappings.flatMap { fileMapping ->
fileMapping.lineMappings.flatMap { lineMapping: RangeMapping ->
lineMapping.toRange.keysToMap { lineMapping }.entries
}
}.groupBy { it.key }.entries.filter { it.value.size != 1 }
assertions.assertTrue(conflictingLines.isEmpty()) {
conflictingLines.joinToString(separator = "\n") {
"Conflicting mapping for line ${it.key} in ${it.value.joinToString(transform = Any::toString)}"
}
}
}
}
class SMAPAndFile(val smap: String?, val sourceFile: String, val outputFile: String) {
constructor(smap: String?, sourceFile: File, outputFile: String) : this(smap, getPath(sourceFile), outputFile)
companion object {
fun getPath(file: File): String =
getPath(file.canonicalPath)
fun getPath(canonicalPath: String): String {
//There are some problems with disk name on windows cause LightVirtualFile return it without disk name
return FileUtil.toSystemIndependentName(canonicalPath).substringAfter(":")
}
}
}
}
@@ -0,0 +1,98 @@
/*
* Copyright 2010-2021 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.test.backend.handlers
import org.jetbrains.kotlin.codegen.CommonSMAPTestUtil
import org.jetbrains.kotlin.codegen.inline.GENERATE_SMAP
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.DUMP_SMAP
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.SEPARATE_SMAP_DUMPS
import org.jetbrains.kotlin.test.directives.model.DirectivesContainer
import org.jetbrains.kotlin.test.model.BinaryArtifacts
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.moduleStructure
import org.jetbrains.kotlin.test.utils.MultiModuleInfoDumperImpl
import org.jetbrains.kotlin.test.utils.withExtension
class SMAPDumpHandler(testServices: TestServices) : JvmBinaryArtifactHandler(testServices) {
companion object {
const val SMAP_EXT = "smap"
const val SMAP_SEP_EXT = "smap-separate-compilation"
const val SMAP_NON_SEP_EXT = "smap-nonseparate-compilation"
}
override val directivesContainers: List<DirectivesContainer>
get() = listOf(CodegenTestDirectives)
private val dumper = MultiModuleInfoDumperImpl()
override fun processModule(module: TestModule, info: BinaryArtifacts.Jvm) {
if (!GENERATE_SMAP) return
if (DUMP_SMAP !in module.directives) return
val compiledSmaps = CommonSMAPTestUtil.extractSMAPFromClasses(info.classFileFactory.currentOutput)
CommonSMAPTestUtil.checkNoConflictMappings(compiledSmaps, assertions)
val compiledData = compiledSmaps.groupBy {
it.sourceFile
}.map {
val smap = it.value.sortedByDescending(CommonSMAPTestUtil.SMAPAndFile::outputFile).mapNotNull(CommonSMAPTestUtil.SMAPAndFile::smap).joinToString("\n")
CommonSMAPTestUtil.SMAPAndFile(if (smap.isNotEmpty()) smap else null, it.key, "NOT_SORTED")
}.associateBy { it.sourceFile }
dumper.builderForModule(module).apply {
for (source in compiledData.values) {
appendLine("// FILE: ${source.sourceFile}")
appendLine(source.smap ?: "")
}
}
}
override fun processAfterAllModules(someAssertionWasFailed: Boolean) {
val separateDumpEnabled = separateDumpsEnabled()
val isSeparateCompilation = isSeparateCompilation()
val extension = when {
!separateDumpEnabled -> SMAP_EXT
isSeparateCompilation -> SMAP_SEP_EXT
else -> SMAP_NON_SEP_EXT
}
val testDataFile = testServices.moduleStructure.originalTestDataFiles.first()
val expectedFile = testDataFile.withExtension(extension)
assertions.assertEqualsToFile(expectedFile, dumper.generateResultingDump())
if (separateDumpEnabled && isSeparateCompilation) {
val otherExtension = if (isSeparateCompilation) SMAP_NON_SEP_EXT else SMAP_SEP_EXT
val otherFile = expectedFile.withExtension(otherExtension)
if (!otherFile.exists()) return
val expectedText = expectedFile.readText()
if (expectedText == otherFile.readText()) {
val smapFile = expectedFile.withExtension(SMAP_EXT)
smapFile.writeText(expectedText)
expectedFile.delete()
otherFile.delete()
assertions.fail {
"""
Contents of ${expectedFile.name} and ${otherFile.name} are equals, so they are deleted
and joined to ${smapFile.name}. Please remove $SEPARATE_SMAP_DUMPS directive from
${testDataFile.name} and rerun test
""".trimIndent()
}
}
}
}
private fun isSeparateCompilation(): Boolean {
return testServices.moduleStructure.modules.size > 1
}
private fun separateDumpsEnabled(): Boolean {
return SEPARATE_SMAP_DUMPS in testServices.moduleStructure.allDirectives
}
}
@@ -95,4 +95,17 @@ object CodegenTestDirectives : SimpleDirectivesContainer() {
val SKIP_INLINE_CHECK_IN by stringDirective(
description = "Skip checking of specific methods in ${BytecodeInliningHandler::class.java}"
)
val DUMP_SMAP by directive(
description = """Enables ${SMAPDumpHandler::class}"""
)
val SEPARATE_SMAP_DUMPS by directive(
description = """
If enabled then ${SMAPDumpHandler::class} will dump smap dumps
into ${SMAPDumpHandler.SMAP_SEP_EXT} and ${SMAPDumpHandler.SMAP_EXT}
files instead of ${SMAPDumpHandler.SMAP_EXT} depending of module
structure of test
""".trimIndent()
)
}
@@ -16,36 +16,18 @@
package org.jetbrains.kotlin.codegen
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.backend.common.output.OutputFile
import org.jetbrains.kotlin.codegen.CommonSMAPTestUtil.SMAPAndFile
import org.jetbrains.kotlin.codegen.CommonSMAPTestUtil.checkNoConflictMappings
import org.jetbrains.kotlin.codegen.CommonSMAPTestUtil.extractSMAPFromClasses
import org.jetbrains.kotlin.codegen.inline.GENERATE_SMAP
import org.jetbrains.kotlin.codegen.inline.RangeMapping
import org.jetbrains.kotlin.codegen.inline.SMAPParser
import org.jetbrains.kotlin.codegen.inline.toRange
import org.jetbrains.kotlin.test.KotlinBaseTest
import org.jetbrains.kotlin.utils.keysToMap
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.kotlin.test.util.JUnit4Assertions
import org.junit.Assert
import java.io.File
import java.io.StringReader
object SMAPTestUtil {
private fun extractSMAPFromClasses(outputFiles: Iterable<OutputFile>): List<SMAPAndFile> {
return outputFiles.mapNotNull { outputFile ->
var debugInfo: String? = null
ClassReader(outputFile.asByteArray()).accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visitSource(source: String?, debug: String?) {
debugInfo = debug
}
}, 0)
SMAPAndFile(debugInfo, outputFile.sourceFiles.single(), outputFile.relativePath)
}
}
private fun extractSmapFromTestDataFile(file: KotlinBaseTest.TestFile, separateCompilation: Boolean): SMAPAndFile? {
if (!checkExtension(file, separateCompilation)) return null
@@ -88,43 +70,9 @@ object SMAPTestUtil {
Assert.assertEquals("Smap data differs for $ktFileName", normalize(source.smap), normalize(data?.smap))
}
checkNoConflictMappings(compiledSmaps)
}
private fun checkNoConflictMappings(compiledSmap: List<SMAPAndFile>?) {
if (compiledSmap == null) return
compiledSmap.mapNotNull(SMAPAndFile::smap).forEach { smapString ->
val smap = SMAPParser.parseOrNull(smapString) ?: throw AssertionError("bad SMAP: $smapString")
val conflictingLines = smap.fileMappings.flatMap { fileMapping ->
fileMapping.lineMappings.flatMap { lineMapping: RangeMapping ->
lineMapping.toRange.keysToMap { lineMapping }.entries
}
}.groupBy { it.key }.entries.filter { it.value.size != 1 }
Assert.assertTrue(
conflictingLines.joinToString(separator = "\n") {
"Conflicting mapping for line ${it.key} in ${it.value.joinToString(transform = Any::toString)}"
},
conflictingLines.isEmpty()
)
}
checkNoConflictMappings(compiledSmaps, JUnit4Assertions)
}
private fun normalize(text: String?) =
text?.let { StringUtil.convertLineSeparators(it.trim()) }
private class SMAPAndFile(val smap: String?, val sourceFile: String, val outputFile: String) {
constructor(smap: String?, sourceFile: File, outputFile: String) : this(smap, getPath(sourceFile), outputFile)
companion object {
fun getPath(file: File): String =
getPath(file.canonicalPath)
fun getPath(canonicalPath: String): String {
//There are some problems with disk name on windows cause LightVirtualFile return it without disk name
return FileUtil.toSystemIndependentName(canonicalPath).substringAfter(":")
}
}
}
}