[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( val SKIP_INLINE_CHECK_IN by stringDirective(
description = "Skip checking of specific methods in ${BytecodeInliningHandler::class.java}" 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 package org.jetbrains.kotlin.codegen
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.backend.common.output.OutputFile 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.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.test.KotlinBaseTest
import org.jetbrains.kotlin.utils.keysToMap import org.jetbrains.kotlin.test.util.JUnit4Assertions
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
import org.junit.Assert import org.junit.Assert
import java.io.File
import java.io.StringReader import java.io.StringReader
object SMAPTestUtil { 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? { private fun extractSmapFromTestDataFile(file: KotlinBaseTest.TestFile, separateCompilation: Boolean): SMAPAndFile? {
if (!checkExtension(file, separateCompilation)) return null 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)) Assert.assertEquals("Smap data differs for $ktFileName", normalize(source.smap), normalize(data?.smap))
} }
checkNoConflictMappings(compiledSmaps) checkNoConflictMappings(compiledSmaps, JUnit4Assertions)
}
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()
)
}
} }
private fun normalize(text: String?) = private fun normalize(text: String?) =
text?.let { StringUtil.convertLineSeparators(it.trim()) } 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(":")
}
}
}
} }