Infrastructure for source-based code coverage

This commit is contained in:
Sergey Bogolepov
2019-02-04 17:53:34 +07:00
committed by Sergey Bogolepov
parent f043a6bf58
commit b22404c947
38 changed files with 1365 additions and 29 deletions
+4 -1
View File
@@ -55,4 +55,7 @@ backend.native/tests/teamcity-test.property
# Sample output
samples/**/*.kt.bc-build
samples/androidNativeActivity/Polyhedron
samples/androidNativeActivity/Polyhedron
# CMake
llvmCoverageMappingC/CMakeLists.txt
+68
View File
@@ -0,0 +1,68 @@
# Code Coverage
Kotlin/Native has a code coverage support that is based on Clang's
[Source-based Code Coverage](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html).
**Please note**:
1. Coverage support is in it's very early days and is in active development. Known issues and restrictions are:
* Coverage information may be inaccurate.
* Line execution counts may be wrong.
* Only macOS and iOS simulator binaries are supported.
2. Most of described functionality will be incorporated into Gradle plugin.
### Usage
#### TL;DR
```bash
kotlinc-native main.kt -Xcoverage
./program.kexe
llvm-profdata merge program.kexe.profraw -o program.profdata
llvm-cov report program.kexe -instr-profile program.profdata
```
#### Compiling with coverage enabled
There are 2 compiler flags that allows to generate coverage information:
* `-Xcoverage`. Generate coverage for immediate sources.
* `-Xlibrary-to-cover=<path>`. Generate coverage for specified `klib`.
Note that library also should be either linked via `-library/-l` compiler option or be a transitive dependency.
#### Running covered executable
After the execution of the compiled binary (ex. `program.kexe`) `program.kexe.profraw` will be generated.
By default it will be generated in the same location where binary was created. The are two ways to override this behavior:
* `-Xcoverage-file=<path>` compiler flag.
* `LLVM_PROFILE_FILE` environment variable. So if you run your program like this:
```
LLVM_PROFILE_FILE=build/program.profraw ./program.kexe
```
Then the coverage information will be stored to the `build` dir as `program.profraw`.
#### Parsing `*.profraw`
Generated file can be parsed with `llvm-profdata` utility. Basic usage:
```
llvm-profdata merge default.profraw -o program.profdata
```
See [command guide](http://llvm.org/docs/CommandGuide/llvm-profdata.html) for more options.
#### Creating reports
The last step is to create a report from the `program.profdata` file.
It can be done with `llvm-cov` utility (refer to [command guide](http://llvm.org/docs/CommandGuide/llvm-cov.html) for detailed usage).
For example, we can see a basic report using:
```
llvm-cov report program.kexe -instr-profile program.profdata
```
Or show a line-by-line coverage information in html:
```
llvm-cov show program.kexe -instr-profile program.profdata -format=html > report.html
```
### Sample
Usually coverage information is collected during running of the tests.
Please refer to `samples/coverage` to see how it can be done.
### Useful links
* [LLVM Code Coverage Mapping Format](https://llvm.org/docs/CoverageMappingFormat.html)
+3 -2
View File
@@ -80,11 +80,12 @@ task renamePackage {
kotlinNativeInterop {
llvm {
dependsOn ":llvmDebugInfoC:debugInfoStaticLibrary"
dependsOn ":llvmCoverageMappingC:coverageMappingStaticLibrary"
defFile 'llvm.def'
if (!project.parent.convention.plugins.platformInfo.isWindows())
compilerOpts "-fPIC"
compilerOpts "-I$llvmDir/include", "-I${project(':llvmDebugInfoC').projectDir}/src/main/include"
linkerOpts "-L$llvmDir/lib", "-L${project(':llvmDebugInfoC').buildDir}/libs/debugInfo/static"
compilerOpts "-I$llvmDir/include", "-I${project(':llvmDebugInfoC').projectDir}/src/main/include", "-I${project(':llvmCoverageMappingC').projectDir}/src/main/include"
linkerOpts "-L$llvmDir/lib", "-L${project(':llvmDebugInfoC').buildDir}/libs/debugInfo/static", "-L${project(':llvmCoverageMappingC').buildDir}/libs/coverageMapping/static"
}
hash { // TODO: copy-pasted from ':common:compileHash'
@@ -195,6 +195,9 @@ class K2Native : CLICompiler<K2NativeCompilerArguments>() {
put(BITCODE_EMBEDDING_MODE, selectBitcodeEmbeddingMode(this, arguments, outputKind))
put(DEBUG_INFO_VERSION, arguments.debugInfoFormatVersion.toInt())
put(COVERAGE, arguments.coverage)
put(LIBRARIES_TO_COVER, arguments.coveredLibraries.toNonNullList())
arguments.coverageFile?.let { put(PROFRAW_PATH, it) }
}
}
}
@@ -162,6 +162,19 @@ class K2NativeCompilerArguments : CommonCompilerArguments() {
@Argument(value = "-Xdebug-info-version", description = "generate debug info of given version (1, 2)")
var debugInfoFormatVersion: String = "1" /* command line parser doesn't accept kotlin.Int type */
@Argument(value = "-Xcoverage", description = "emit coverage")
var coverage: Boolean = false
@Argument(
value = "-Xlibrary-to-cover",
valueDescription = "<path>",
description = "Path to library that should be covered."
)
var coveredLibraries: Array<String>? = null
@Argument(value = "-Xcoverage-file", valueDescription = "<path>", description = "Save coverage information to the given file")
var coverageFile: String? = null
override fun configureAnalysisFlags(collector: MessageCollector): MutableMap<AnalysisFlag<*>, Any> =
super.configureAnalysisFlags(collector).also {
val useExperimental = it[AnalysisFlags.useExperimental] as List<*>
@@ -4,9 +4,7 @@
*/
package org.jetbrains.kotlin.backend.konan
import llvm.LLVMLinkModules2
import llvm.LLVMModuleRef
import llvm.LLVMWriteBitcodeToFile
import llvm.*
import org.jetbrains.kotlin.backend.konan.library.impl.buildLibrary
import org.jetbrains.kotlin.backend.konan.llvm.parseBitcodeFile
import org.jetbrains.kotlin.konan.KonanAbiVersion
@@ -27,6 +25,22 @@ internal fun produceCStubs(context: Context) {
}
}
private fun shouldRunBitcodePasses(context: Context): Boolean =
context.coverage.enabled
internal fun runBitcodePasses(context: Context) {
if (!shouldRunBitcodePasses(context)) {
return
}
val llvmModule = context.llvmModule!!
val passManager = LLVMCreatePassManager()!!
val targetLibraryInfo = LLVMGetTargetLibraryInfo(llvmModule)
LLVMAddTargetLibraryInfo(targetLibraryInfo, passManager)
context.coverage.addLlvmPasses(passManager)
LLVMRunPassManager(passManager, llvmModule)
LLVMDisposePassManager(passManager)
}
internal fun produceOutput(context: Context) {
val config = context.config.configuration
@@ -44,15 +58,15 @@ internal fun produceOutput(context: Context) {
val generatedBitcodeFiles =
if (produce == CompilerOutputKind.DYNAMIC || produce == CompilerOutputKind.STATIC) {
produceCAdapterBitcode(
context.config.clang,
tempFiles.cAdapterCppName,
context.config.clang,
tempFiles.cAdapterCppName,
tempFiles.cAdapterBitcodeName)
listOf(tempFiles.cAdapterBitcodeName)
} else emptyList()
val nativeLibraries =
val nativeLibraries =
context.config.nativeLibraries +
context.config.defaultNativeLibraries +
context.config.defaultNativeLibraries +
generatedBitcodeFiles
for (library in nativeLibraries) {
@@ -75,14 +89,14 @@ internal fun produceOutput(context: Context) {
val library = buildLibrary(
context.config.nativeLibraries,
context.config.nativeLibraries,
context.config.includeBinaries,
neededLibraries,
context.serializedLinkData!!,
versions,
target,
output,
libraryName,
libraryName,
null,
nopack,
manifestProperties,
@@ -42,7 +42,6 @@ import org.jetbrains.kotlin.builtins.konan.KonanBuiltIns
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.ir.symbols.impl.IrSimpleFunctionSymbolImpl
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.metadata.konan.KonanProtoBuf
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi2ir.generators.GeneratorContext
@@ -50,13 +49,12 @@ import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.module
import org.jetbrains.kotlin.resolve.scopes.MemberScope
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver
import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor
import org.jetbrains.kotlin.serialization.deserialization.getName
import java.lang.System.out
import kotlin.LazyThreadSafetyMode.PUBLICATION
import kotlin.reflect.KProperty
import org.jetbrains.kotlin.backend.common.ir.copyTo
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExport
import org.jetbrains.kotlin.backend.konan.llvm.coverage.CoverageManager
import org.jetbrains.kotlin.ir.symbols.impl.IrTypeParameterSymbolImpl
/**
@@ -324,6 +322,8 @@ internal class Context(config: KonanConfig) : KonanBackendContext(config) {
val cStubsManager = CStubsManager()
val coverage = CoverageManager(this)
lateinit var privateFunctions: List<Pair<IrFunction, DataFlowIR.FunctionSymbol.Declared>>
lateinit var privateClasses: List<Pair<IrClass, DataFlowIR.Type.Declared>>
@@ -106,7 +106,12 @@ class KonanConfigKeys {
= CompilerConfigurationKey.create("verbose backend phases")
val DEBUG_INFO_VERSION: CompilerConfigurationKey<Int>
= CompilerConfigurationKey.create("debug info format version")
val COVERAGE: CompilerConfigurationKey<Boolean>
= CompilerConfigurationKey.create("emit coverage info for sources")
val LIBRARIES_TO_COVER: CompilerConfigurationKey<List<String>>
= CompilerConfigurationKey.create<List<String>>("libraries that should be covered")
val PROFRAW_PATH: CompilerConfigurationKey<String?>
= CompilerConfigurationKey.create("path to *.profraw coverage output")
}
}
@@ -45,6 +45,15 @@ internal class LinkStage(val context: Context) {
addAll(elements.filter { !it.isEmpty() })
}
private val exportedSymbols = context.coverage.addExportedSymbols()
private fun mangleSymbol(symbol: String) =
if (target.family == Family.IOS || target.family == Family.OSX) {
"_$symbol"
} else {
symbol
}
private fun runTool(command: List<String>) = runTool(*command.toTypedArray())
private fun runTool(vararg command: String) =
Command(*command)
@@ -65,6 +74,8 @@ internal class LinkStage(val context: Context) {
}
command.addNonEmpty(platform.llvmLtoDynamicFlags)
command.addNonEmpty(files)
// Prevent symbols from being deleted by DCE.
command.addNonEmpty(exportedSymbols.map { "-exported-symbol=${mangleSymbol(it)}"} )
runTool(command)
return combined
@@ -208,6 +219,7 @@ internal class LinkStage(val context: Context) {
val objectFiles = mutableListOf<String>()
fun makeObjectFiles() {
val bitcodeFiles = listOf(emitted) +
libraries.map { it.bitcodePaths }.flatten().filter { it.isBitcode }
@@ -275,6 +275,7 @@ internal val bitcodePhase = namedIrModulePhase(
escapeAnalysisPhase then
codegenPhase then
finalizeDebugInfoPhase then
bitcodePassesPhase then
cStubsPhase
)
@@ -151,6 +151,16 @@ internal val cStubsPhase = makeKonanModuleOpPhase(
op = { context, _ -> produceCStubs(context) }
)
/**
* Runs specific passes over context.llvmModule. The main compilation pipeline
* is performed by [linkPhase].
*/
internal val bitcodePassesPhase = makeKonanModuleOpPhase(
name = "BitcodePasses",
description = "Run custom LLVM passes over bitcode",
op = { context, _ -> runBitcodePasses(context) }
)
internal val produceOutputPhase = makeKonanModuleOpPhase(
name = "ProduceOutput",
description = "Produce output",
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.backend.common.ir.ir2string
import org.jetbrains.kotlin.backend.konan.*
import org.jetbrains.kotlin.backend.konan.descriptors.*
import org.jetbrains.kotlin.backend.konan.ir.*
import org.jetbrains.kotlin.backend.konan.llvm.coverage.*
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExport
import org.jetbrains.kotlin.backend.konan.optimizations.*
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
@@ -308,6 +309,8 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
override fun visitModuleFragment(declaration: IrModuleFragment) {
context.log{"visitModule : ${ir2string(declaration)}"}
context.coverage.collectRegions(declaration)
initializeCachedBoxes(context)
declaration.acceptChildrenVoid(this)
@@ -317,7 +320,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
codegen.objCDataGenerator?.finishModule()
BitcodeEmbedding.processModule(context.llvm)
context.coverage.writeRegionInfo()
appendDebugSelector()
appendLlvmUsed("llvm.used", context.llvm.usedFunctions + context.llvm.usedGlobals)
appendLlvmUsed("llvm.compiler.used", context.llvm.compilerUsedGlobals)
@@ -582,7 +585,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
/**
* The [CodeContext] enclosing the entire function body.
*/
private inner class FunctionScope (val declaration: IrFunction?, val functionGenerationContext: FunctionGenerationContext) : InnerScopeImpl() {
private inner class FunctionScope(val declaration: IrFunction?, val functionGenerationContext: FunctionGenerationContext) : InnerScopeImpl() {
constructor(llvmFunction:LLVMValueRef, name:String, functionGenerationContext: FunctionGenerationContext):
@@ -595,6 +598,10 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
codegen.llvmFunction(it)
}
val coverageInstrumentation: LLVMCoverageInstrumentation? =
context.coverage.tryGetInstrumentation(declaration) { function, args -> functionGenerationContext.call(function, args) }
private var name:String? = declaration?.name?.asString()
override fun genReturn(target: IrSymbolOwner, value: LLVMValueRef?) {
@@ -666,6 +673,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
val parameterScope = ParameterScope(declaration, functionGenerationContext)
using(parameterScope) {
using(VariableScope()) {
recordCoverage(body)
when (body) {
is IrBlockBody -> body.statements.forEach { generateStatement(it) }
is IrExpressionBody -> generateStatement(body.expression)
@@ -746,10 +754,18 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
}
}
private fun recordCoverage(irElement: IrElement) {
val scope = currentCodeContext.functionScope()
if (scope is FunctionScope) {
scope.coverageInstrumentation?.instrumentIrElement(irElement)
}
}
//-------------------------------------------------------------------------//
private fun evaluateExpression(value: IrExpression): LLVMValueRef {
updateBuilderDebugLocation(value)
recordCoverage(value)
when (value) {
is IrTypeOperatorCall -> return evaluateTypeOperator (value)
is IrCall -> return evaluateCall (value)
@@ -0,0 +1,98 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.kotlin.backend.konan.llvm.coverage
import org.jetbrains.kotlin.backend.common.ir.ir2string
import org.jetbrains.kotlin.backend.konan.llvm.column
import org.jetbrains.kotlin.backend.konan.llvm.line
import org.jetbrains.kotlin.backend.konan.llvm.symbolName
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.name
/**
* The most important class in the coverage package.
* It describes textual region of the code that is associated with IrElement.
* Besides the obvious [file] and line/column borders, it has [RegionKind] which is described later.
*/
class Region(
val file: IrFile,
val startLine: Int,
val startColumn: Int,
val endLine: Int,
val endColumn: Int,
val kind: RegionKind
) {
companion object {
fun fromIr(irElement: IrElement, irFile: IrFile, kind: RegionKind) =
fromOffset(irElement.startOffset, irElement.endOffset, irFile, kind)
fun fromOffset(startOffset: Int, endOffset: Int, irFile: IrFile, kind: RegionKind) =
Region(
irFile,
irFile.fileEntry.line(startOffset),
irFile.fileEntry.column(startOffset),
irFile.fileEntry.line(endOffset),
irFile.fileEntry.column(endOffset),
kind
)
}
override fun toString(): String {
val expansion = (kind as? RegionKind.Expansion)?.let { " expand to " + it.expandedFile.name } ?: ""
return "${file.name}$expansion: ${kind::class.simpleName} $startLine, $startColumn -> $endLine, $endColumn"
}
}
/**
* Describes what is the given code region.
* Based on llvm::coverage::CounterMappingRegion.
* Currently only [RegionKind.Code] is used.
*/
sealed class RegionKind {
/**
* Regular peace of code.
*/
object Code : RegionKind()
/**
* Empty line.
*/
object Gap : RegionKind()
/**
* Region of code that is an expansion of another source file.
* Used for inline function.
*/
class Expansion(val expandedFile: IrFile) : RegionKind()
}
/**
* "Regional" description of the [function].
*/
class FunctionRegions(
val function: IrFunction,
val regions: Map<IrElement, Region>
) {
// Enumeration is required for serialization and instrumentation calls.
val regionEnumeration = regions.values.mapIndexed { index, region -> region to index }.toMap()
// Actually, it should be computed.
// But since we don't support PGO structural hash doesn't really matter for now.
val structuralHash: Long = 0
override fun toString(): String = buildString {
appendln("${function.symbolName} regions:")
regions.forEach { (irElem, region) -> appendln("${ir2string(irElem)} -> ($region)") }
}
}
/**
* Since file is the biggest unit in terms of the code coverage
* we aggregate [FunctionRegions] per [file].
*/
class FileRegionInfo(
val file: IrFile,
val functions: List<FunctionRegions>
)
@@ -0,0 +1,127 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.kotlin.backend.konan.llvm.coverage
import llvm.LLVMAddInstrProfPass
import llvm.LLVMPassManagerRef
import llvm.LLVMValueRef
import org.jetbrains.kotlin.backend.konan.Context
import org.jetbrains.kotlin.backend.konan.KonanConfigKeys
import org.jetbrains.kotlin.backend.konan.isNativeBinary
import org.jetbrains.kotlin.backend.konan.reportCompilationError
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.util.removeSuffixIfPresent
import org.jetbrains.kotlin.resolve.descriptorUtil.module
/**
* "Umbrella" class of all the of the code coverage related logic.
*/
internal class CoverageManager(val context: Context) {
private val shouldCoverProgram: Boolean =
context.config.configuration.getBoolean(KonanConfigKeys.COVERAGE)
private val librariesToCover: Set<String> =
context.config.configuration.getList(KonanConfigKeys.LIBRARIES_TO_COVER)
.map { File(it).absolutePath.removeSuffixIfPresent(".klib") }
.toSet()
private val llvmProfileFilenameGlobal = "__llvm_profile_filename"
private val defaultOutputFilePath: String by lazy {
"${context.config.outputFile}.profraw"
}
private val outputFileName: String =
context.config.configuration.get(KonanConfigKeys.PROFRAW_PATH)
?.let { File(it).absolutePath }
?: defaultOutputFilePath
val enabled: Boolean =
shouldCoverProgram || librariesToCover.isNotEmpty()
init {
if (enabled && !checkRestrictions()) {
context.reportCompilationError("Coverage is only supported for macOS and iOS simulator binaries for now.")
}
}
private fun checkRestrictions(): Boolean {
val isKindAllowed = with(context.config.produce) { isNativeBinary || this == CompilerOutputKind.BITCODE }
val target = context.config.target
val isTargetAllowed = target == KonanTarget.MACOS_X64 || target == KonanTarget.IOS_X64
return isKindAllowed && isTargetAllowed
}
private val filesRegionsInfo = mutableListOf<FileRegionInfo>()
private fun getFunctionRegions(irFunction: IrFunction) =
filesRegionsInfo.flatMap { it.functions }.firstOrNull { it.function == irFunction }
private val coveredModules: Set<ModuleDescriptor> by lazy {
val coveredUserCode = if (shouldCoverProgram) setOf(context.moduleDescriptor) else emptySet()
val coveredLibs = context.irModules.filter { it.key in librariesToCover }.values
.map { it.descriptor }.toSet()
coveredLibs + coveredUserCode
}
private fun fileCoverageFilter(file: IrFile) =
file.packageFragmentDescriptor.module in coveredModules
/**
* Walk [irModuleFragment] subtree and collect [FileRegionInfo] for files that are part of [coveredModules].
*/
fun collectRegions(irModuleFragment: IrModuleFragment) {
if (enabled) {
val regions = CoverageRegionCollector(this::fileCoverageFilter).collectFunctionRegions(irModuleFragment)
filesRegionsInfo += regions
}
}
/**
* @return [LLVMCoverageInstrumentation] instance if [irFunction] should be covered.
*/
fun tryGetInstrumentation(irFunction: IrFunction?, callSitePlacer: (function: LLVMValueRef, args: List<LLVMValueRef>) -> Unit) =
if (enabled && irFunction != null) {
getFunctionRegions(irFunction)?.let { LLVMCoverageInstrumentation(context, it, callSitePlacer) }
} else {
null
}
/**
* Add __llvm_coverage_mapping to the LLVM module.
*/
fun writeRegionInfo() {
if (enabled) {
LLVMCoverageWriter(context, filesRegionsInfo).write()
}
}
/**
* Add InstrProfilingLegacyPass to the list of llvm passes
*/
fun addLlvmPasses(passManager: LLVMPassManagerRef) {
if (enabled) {
LLVMAddInstrProfPass(passManager, outputFileName)
}
}
/**
* Since we performing instruction profiling before internalization and global dce
* __llvm_profile_filename need to be added to exported symbols.
*/
fun addExportedSymbols(): List<String> =
if (enabled) {
listOf(llvmProfileFilenameGlobal)
} else {
emptyList()
}
}
@@ -0,0 +1,239 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.kotlin.backend.konan.llvm.coverage
import org.jetbrains.kotlin.backend.common.pop
import org.jetbrains.kotlin.backend.common.push
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
/**
* Collect all regions in the module.
* @param fileFilter filters files that should be processed.
*/
internal class CoverageRegionCollector(private val fileFilter: (IrFile) -> Boolean) {
fun collectFunctionRegions(irModuleFragment: IrModuleFragment): List<FileRegionInfo> =
irModuleFragment.files
.filter(fileFilter)
.map { file ->
val collector = FunctionsCollector(file)
collector.visitFile(file)
FileRegionInfo(file, collector.functionRegions)
}
private inner class FunctionsCollector(val file: IrFile) : IrElementVisitorVoid {
val functionRegions = mutableListOf<FunctionRegions>()
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitFunction(declaration: IrFunction) {
if (!declaration.isInline && !declaration.isExternal) {
declaration.body?.let {
val regionsCollector = IrFunctionRegionsCollector(fileFilter, file)
regionsCollector.visitBody(it)
if (regionsCollector.regions.isNotEmpty()) {
functionRegions += FunctionRegions(declaration, regionsCollector.regions)
}
}
}
// TODO: Decide how to work with local functions. Should they be process separately?
declaration.acceptChildrenVoid(this)
}
}
}
/**
* Very similar to [org.jetbrains.kotlin.backend.konan.llvm.CodeGeneratorVisitor] but instead of bitcode generation we collect regions.
* [fileFilter]: specify which files should be processed by code coverage. Here it is required
* for checking calls to inline functions from other files.
* TODO: for now it is very inaccurate.
*/
private class IrFunctionRegionsCollector(
val fileFilter: (IrFile) -> Boolean,
val irFile: IrFile
) : IrElementVisitorVoid {
private val irFileStack = mutableListOf(irFile)
private val currentFile: IrFile
get() = irFileStack.last()
val regions = mutableMapOf<IrElement, Region>()
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitFunction(declaration: IrFunction) {
}
override fun visitExpression(expression: IrExpression) {
collectRegions(expression)
}
override fun visitVariable(declaration: IrVariable) {
declaration.initializer?.let { collectRegions(it) }
}
override fun visitBody(body: IrBody) = when (body) {
is IrExpressionBody -> body.acceptChildrenVoid(this)
is IrBlockBody -> body.acceptChildrenVoid(this)
else -> error("Unexpected function body type: $body")
}
fun collectRegions(value: IrExpression): Unit = when (value) {
is IrTypeOperatorCall -> collectTypeOperator(value)
is IrCall -> collectCall(value)
is IrDelegatingConstructorCall -> collectCall(value)
is IrInstanceInitializerCall -> collectInstanceInitializerCall(value)
is IrGetValue -> collectGetValue(value)
is IrSetVariable -> collectSetVariable(value)
is IrGetField -> collectGetField(value)
is IrSetField -> collectSetField(value)
is IrConst<*> -> collectConst(value)
is IrReturn -> collectReturn(value)
is IrWhen -> collectWhen(value)
is IrThrow -> collectThrow(value)
is IrTry -> collectTry(value)
is IrReturnableBlock -> collectReturnableBlock(value)
is IrContainerExpression -> collectContainerExpression(value)
is IrWhileLoop -> collectWhileLoop(value)
is IrDoWhileLoop -> collectDoWhileLoop(value)
is IrVararg -> collectVararg(value)
is IrBreak -> collectBreak(value)
is IrContinue -> collectContinue(value)
is IrGetObjectValue -> collectGetObjectValue(value)
is IrFunctionReference -> collectFunctionReference(value)
is IrSuspendableExpression -> collectSuspendableExpression(value)
is IrSuspensionPoint -> collectSuspensionPoint(value)
else -> {
}
}
private fun collectInstanceInitializerCall(instanceInitializerCall: IrInstanceInitializerCall) {
}
private fun collectGetValue(getValue: IrGetValue) {
recordRegion(getValue)
}
private fun collectSetVariable(setVariable: IrSetVariable) {
setVariable.value.acceptVoid(this)
}
private fun collectGetField(getField: IrGetField) {
getField.receiver?.let { collectRegions(it) }
}
private fun collectSetField(setField: IrSetField) {
collectRegions(setField.value)
setField.receiver?.let { collectRegions(it) }
}
private fun collectConst(const: IrConst<*>) {
recordRegion(const)
}
private fun collectReturn(irReturn: IrReturn) {
collectRegions(irReturn.value)
}
private fun collectWhen(irWhen: IrWhen) {
irWhen.branches.forEach { branch ->
// Do not record location for else branch since it doesn't look correct.
if (branch.condition !is IrConst<*>) {
collectRegions(branch.condition)
}
collectRegions(branch.result)
}
}
private fun collectThrow(irThrow: IrThrow) {
collectRegions(irThrow.value)
recordRegion(irThrow)
}
private fun collectTry(irTry: IrTry) {
}
private fun collectReturnableBlock(returnableBlock: IrReturnableBlock) {
val file = (returnableBlock.sourceFileSymbol?.owner)
if (file != null && file != currentFile && fileFilter(file)) {
recordRegion(returnableBlock)
irFileStack.push(file)
returnableBlock.acceptChildrenVoid(this)
irFileStack.pop()
}
}
private fun collectContainerExpression(containerExpression: IrContainerExpression) {
containerExpression.acceptChildrenVoid(this)
}
private fun collectWhileLoop(whileLoop: IrWhileLoop) {
collectRegions(whileLoop.condition)
whileLoop.body?.let { collectRegions(it) }
}
private fun collectDoWhileLoop(doWhileLoop: IrDoWhileLoop) {
collectRegions(doWhileLoop.condition)
doWhileLoop.body?.let { collectRegions(it) }
}
private fun collectVararg(vararg: IrVararg) {
vararg.elements.forEach { it.acceptVoid(this) }
}
private fun collectBreak(irBreak: IrBreak) {
recordRegion(irBreak)
}
private fun collectContinue(irContinue: IrContinue) {
recordRegion(irContinue)
}
private fun collectGetObjectValue(getObjectValue: IrGetObjectValue) {
}
private fun collectFunctionReference(functionReference: IrFunctionReference) {
}
private fun collectSuspendableExpression(suspendableExpression: IrSuspendableExpression) {
}
private fun collectSuspensionPoint(suspensionPoint: IrSuspensionPoint) {
}
private fun collectTypeOperator(typeOperatorCall: IrTypeOperatorCall) {
}
private fun collectCall(call: IrFunctionAccessExpression) {
recordRegion(call, RegionKind.Code)
call.acceptChildrenVoid(this)
}
private fun recordRegion(irElement: IrElement, kind: RegionKind = RegionKind.Code) {
if (irElement.startOffset == UNDEFINED_OFFSET || irElement.endOffset == UNDEFINED_OFFSET) {
return
}
regions[irElement] = Region.fromIr(irElement, currentFile, kind)
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.kotlin.backend.konan.llvm.coverage
import llvm.LLVMConstBitCast
import llvm.LLVMCreatePGOFunctionNameVar
import llvm.LLVMInstrProfIncrement
import llvm.LLVMValueRef
import org.jetbrains.kotlin.backend.konan.Context
import org.jetbrains.kotlin.backend.konan.llvm.*
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrFunction
/**
* Places calls to `llvm.instrprof.increment` in the beginning of the each
* region in [functionRegions].
*/
internal class LLVMCoverageInstrumentation(
override val context: Context,
private val functionRegions: FunctionRegions,
private val callSitePlacer: (function: LLVMValueRef, args: List<LLVMValueRef>) -> Unit
) : ContextUtils {
private val functionNameGlobal = createFunctionNameGlobal(functionRegions.function)
private val functionHash = Int64(functionRegions.structuralHash).llvm
fun instrumentIrElement(element: IrElement) {
functionRegions.regions[element]?.let {
placeRegionIncrement(it)
}
}
/**
* See https://llvm.org/docs/LangRef.html#llvm-instrprof-increment-intrinsic
*/
private fun placeRegionIncrement(region: Region) {
val numberOfRegions = Int32(functionRegions.regions.size).llvm
val regionNumber = Int32(functionRegions.regionEnumeration.getValue(region)).llvm
val args = listOf(functionNameGlobal, functionHash, numberOfRegions, regionNumber)
callSitePlacer(LLVMInstrProfIncrement(context.llvmModule)!!, args)
}
// Each profiled function should have a global with its name in a specific format.
private fun createFunctionNameGlobal(function: IrFunction): LLVMValueRef {
val name = context.llvmDeclarations.forFunction(function).llvmFunction.name
val pgoFunctionName = LLVMCreatePGOFunctionNameVar(function.llvmFunction, name)!!
return LLVMConstBitCast(pgoFunctionName, int8TypePtr)!!
}
}
@@ -0,0 +1,77 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.kotlin.backend.konan.llvm.coverage
import kotlinx.cinterop.*
import llvm.*
import org.jetbrains.kotlin.backend.konan.Context
import org.jetbrains.kotlin.backend.konan.llvm.name
import org.jetbrains.kotlin.backend.konan.llvm.symbolName
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.name
import org.jetbrains.kotlin.ir.declarations.path
import org.jetbrains.kotlin.konan.file.File
private fun RegionKind.toLLVMCoverageRegionKind(): LLVMCoverageRegionKind = when (this) {
RegionKind.Code -> LLVMCoverageRegionKind.CODE
RegionKind.Gap -> LLVMCoverageRegionKind.GAP
is RegionKind.Expansion -> LLVMCoverageRegionKind.EXPANSION
}
private fun LLVMCoverageRegion.populateFrom(region: Region, regionId: Int, filesIndex: Map<IrFile, Int>) = apply {
fileId = filesIndex.getValue(region.file)
lineStart = region.startLine
columnStart = region.startColumn
lineEnd = region.endLine
columnEnd = region.endColumn
counterId = regionId
kind = region.kind.toLLVMCoverageRegionKind()
expandedFileId = if (region.kind is RegionKind.Expansion) filesIndex.getValue(region.kind.expandedFile) else 0
}
/**
* Writes all of the coverage information to the [org.jetbrains.kotlin.backend.konan.Context.llvmModule].
* See http://llvm.org/docs/CoverageMappingFormat.html for the format description.
*/
internal class LLVMCoverageWriter(
private val context: Context,
private val filesRegionsInfo: List<FileRegionInfo>
) {
fun write() {
if (filesRegionsInfo.isEmpty()) return
val module = context.llvmModule
?: error("LLVM module should be initialized.")
val filesIndex = filesRegionsInfo.mapIndexed { index, fileRegionInfo -> fileRegionInfo.file to index }.toMap()
val coverageGlobal = memScoped {
val (functionMappingRecords, functionCoverages) = filesRegionsInfo.flatMap { it.functions }.map { functionRegions ->
val regions = (functionRegions.regions.values).map { region ->
alloc<LLVMCoverageRegion>().populateFrom(region, functionRegions.regionEnumeration.getValue(region), filesIndex).ptr
}
val fileIds = functionRegions.regions.map { filesIndex.getValue(it.value.file) }.toSet().toIntArray()
val functionCoverage = LLVMWriteCoverageRegionMapping(
fileIds.toCValues(), fileIds.size.signExtend(),
regions.toCValues(), regions.size.signExtend())
val functionName = context.llvmDeclarations.forFunction(functionRegions.function).llvmFunction.name
val functionMappingRecord = LLVMAddFunctionMappingRecord(LLVMGetModuleContext(context.llvmModule),
functionName, functionRegions.structuralHash, functionCoverage)!!
Pair(functionMappingRecord, functionCoverage)
}.unzip()
val (filenames, fileIds) = filesIndex.entries.toList().map { File(it.key.path).absolutePath to it.value }.unzip()
val retval = LLVMCoverageEmit(module, functionMappingRecords.toCValues(), functionMappingRecords.size.signExtend(),
filenames.toCStringArray(this), fileIds.toIntArray().toCValues(), fileIds.size.signExtend(),
functionCoverages.map { it }.toCValues(), functionCoverages.size.signExtend())!!
// TODO: Is there a better way to cleanup fields of T* type in `memScoped`?
functionCoverages.forEach { LLVMFunctionCoverageDispose(it) }
retval
}
context.llvm.usedGlobals.add(coverageGlobal)
}
}
+5 -4
View File
@@ -1,7 +1,7 @@
headers = llvm-c/Core.h llvm-c/Target.h llvm-c/Analysis.h llvm-c/BitWriter.h \
llvm-c/BitReader.h llvm-c/Linker.h DebugInfoC.h
llvm-c/BitReader.h llvm-c/Linker.h DebugInfoC.h CoverageMappingC.h
headerFilter = llvm-c/* DebugInfoC.h
headerFilter = llvm-c/* DebugInfoC.h CoverageMappingC.h
compilerOpts = -std=c99 \
-Wall -W -Wno-unused-parameter -Wwrite-strings -Wmissing-field-initializers \
@@ -16,11 +16,12 @@ linkerOpts = -fvisibility-inlines-hidden \
-pedantic -Wno-long-long -Wcovered-switch-default -Wnon-virtual-dtor -Wdelete-non-virtual-dtor \
-std=c++11 \
-DNDEBUG -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS \
-ldebugInfo -lLLVMTarget -lLLVMMC -lLLVMLinker -lLLVMTransformUtils -lLLVMBitWriter \
-ldebugInfo -lcoverageMapping -lLLVMCoverage -lLLVMTarget -lLLVMInstrumentation \
-lLLVMMC -lLLVMLinker -lLLVMTransformUtils -lLLVMBitWriter \
-lLLVMBitReader -lLLVMAnalysis -lLLVMProfileData -lLLVMObject -lLLVMMCParser -lLLVMMC \
-lLLVMCore -lLLVMBinaryFormat -lLLVMSupport -lLLVMDemangle
# ./llvm-config --libs analysis bitreader bitwriter core linker target
# ./llvm-config --libs analysis bitreader bitwriter core linker target coverage
linkerOpts.osx = -fPIC \
+65
View File
@@ -5,6 +5,7 @@
import groovy.json.JsonOutput
import org.jetbrains.kotlin.*
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.nio.file.Paths
@@ -3211,6 +3212,70 @@ task runKonanRegexTests(type: RunKonanTest) {
runnerLogger = RunKonanTest.Logger.GTEST
}
task coverage_basic_program(type: RunStandaloneKonanTest) {
disabled = project.testTarget != "ios_x64" && project.testTarget != "macos_x64"
description = "Test that `-Xcoverage` generates correct __llvm_coverage information"
def dir = buildDir.absolutePath
createOutputDirectory()
flags = ["-Xcoverage-file=$dir/program.profraw", "-Xcoverage", "-entry", "coverage.basic.program.main"]
source = "$projectDir/coverage/basic/program/main.kt"
doLast {
execLlvm("llvm-profdata") {
args "merge"
args "$dir/program.profraw"
args "-o", "$dir/program.profdata"
}
// Should fail in case of corrupted __llvm_coverage.
execLlvm("llvm-cov") {
def program = buildExePath()
def suffix = target.family.exeSuffix
args "report"
args "$program.$suffix"
args "-instr-profile", "$dir/program.profdata"
}
}
}
task coverage_basic_library(type: RunStandaloneKonanTest) {
disabled = project.testTarget != "ios_x64" && project.testTarget != "macos_x64"
description = "Test that `-Xlibrary-to-cover` generates correct __llvm_coverage information"
def dir = buildDir.absolutePath
createOutputDirectory()
doFirst {
def konancScript = isWindows() ? "konanc.bat" : "konanc"
def konanc = "$dist/bin/$konancScript"
"$konanc $projectDir/coverage/basic/library/library.kt -p library -o $dir/lib_to_cover".execute().waitFor()
}
flags = ["-Xcoverage-file=$dir/program.profraw", "-l", "$dir/lib_to_cover", "-Xlibrary-to-cover=$dir/lib_to_cover.klib", "-entry", "coverage.basic.library.main"]
source = "$projectDir/coverage/basic/library/main.kt"
doLast {
execLlvm("llvm-profdata") {
args "merge"
args "$dir/program.profraw"
args "-o", "$dir/program.profdata"
}
// Should fail in case of corrupted __llvm_coverage.
execLlvm("llvm-cov") {
def program = buildExePath()
def suffix = target.family.exeSuffix
args "report"
args "$program.$suffix"
args "-instr-profile", "$dir/program.profdata"
}
}
}
task buildKonanStdlibTests(type: BuildKonanTest) {
outputSourceSetName = "testOutputStdlib"
flags = [ "-Xmulti-platform", "-friend-modules", project.rootProject.file("${project.properties['konan.home']}/klib/common/stdlib") ]
@@ -0,0 +1,3 @@
package coverage_library
fun foo() = "foo"
@@ -0,0 +1,5 @@
package coverage.basic.library
fun main() {
println(coverage_library.foo())
}
@@ -0,0 +1,5 @@
package coverage.basic.program
fun main() {
println("OK")
}
+5
View File
@@ -92,6 +92,7 @@ void setupClang(Project project) {
project.convention.plugins.platformManager = project.rootProject.ext.platformManager
project.convention.plugins.execClang = new org.jetbrains.kotlin.ExecClang(project)
project.convention.plugins.execLlvm = new org.jetbrains.kotlin.ExecLlvm(project)
project.plugins.withType(NativeComponentPlugin) {
project.model {
@@ -256,6 +257,10 @@ task distCompiler(type: Copy) {
into('konan/nativelib')
}
from(project(':llvmCoverageMappingC').file('build/libs/coverageMapping/shared')) {
into('konan/nativelib')
}
from(project(':llvmDebugInfoC').file('build/libs/debugInfo/shared')) {
into('konan/nativelib')
}
@@ -0,0 +1,28 @@
package org.jetbrains.kotlin
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec
import org.gradle.util.ConfigureUtil
internal class ExecLlvm(private val project: Project) {
fun execLlvm(tool: String, closure: Closure<in ExecSpec>): ExecResult =
this.execLlvm(tool, ConfigureUtil.configureUsing(closure))
fun execLlvm(tool: String, action: Action<in ExecSpec>): ExecResult {
val extendedAction = Action<ExecSpec> { execSpec ->
action.execute(execSpec)
execSpec.apply {
if (executable == null) {
val llvmDir = project.findProperty("llvmDir")
executable = "$llvmDir/bin/$tool"
}
}
}
return project.exec(extendedAction)
}
}
+56
View File
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply plugin: "cpp"
apply plugin: "c"
buildscript {
ext.rootBuildDirectory = file('..')
apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle"
dependencies {
classpath "org.jetbrains.kotlin:kotlin-stdlib:$buildKotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-native-shared:$konanVersion"
}
}
import org.jetbrains.kotlin.konan.target.ClangArgs
model {
components {
coverageMapping(NativeLibrarySpec) {
sources.cpp.source.srcDirs "src/main/cpp"
binaries.withType(StaticLibraryBinarySpec) { binary ->
if (!project.parent.convention.plugins.platformInfo.isWindows())
cppCompiler.args "-fPIC"
cppCompiler.args "--std=c++11", "-I${llvmDir}/include", "-I${projectDir}/src/main/include"
linker.args "-L${llvmDir}/lib", "-lLLVMCore", "-lLLVMSupport"
}
binaries.withType(SharedLibraryBinarySpec) { binary ->
buildable = false
}
}
}
toolChains {
clang(Clang) {
eachPlatform {
cppCompiler.withArguments(ClangArgs.&filterGradleNativeSoftwareFlags)
}
}
}
}
@@ -0,0 +1,230 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
#include "CoverageMappingC.h"
#include <llvm/ProfileData/Coverage/CoverageMapping.h>
#include <llvm/ProfileData/Coverage/CoverageMappingWriter.h>
#include <llvm/IR/GlobalVariable.h>
#include <llvm/Support/raw_ostream.h>
#include <llvm/ADT/Triple.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
#include <llvm/IR/Type.h>
#include <llvm/IR/DerivedTypes.h>
#include <llvm/IR/Constants.h>
#include <llvm/IR/Intrinsics.h>
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/Analysis/TargetLibraryInfo.h>
#include <llvm/Transforms/Instrumentation.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <utility>
#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
using namespace llvm;
using namespace llvm::coverage;
namespace llvm {
DEFINE_SIMPLE_CONVERSION_FUNCTIONS(TargetLibraryInfoImpl, LLVMTargetLibraryInfoRef)
}
struct LLVMFunctionCoverage {
explicit LLVMFunctionCoverage(std::string coverageData) : coverageData(std::move(coverageData)) {}
std::string coverageData;
};
static coverage::CounterMappingRegion::RegionKind determineRegionKind(const struct LLVMCoverageRegion& region) {
switch (region.kind) {
case LLVMCoverageRegionKind::CODE:
return coverage::CounterMappingRegion::RegionKind::CodeRegion;
case LLVMCoverageRegionKind::GAP:
return coverage::CounterMappingRegion::RegionKind::GapRegion;
case LLVMCoverageRegionKind::EXPANSION:
return coverage::CounterMappingRegion::RegionKind::ExpansionRegion;
}
}
static coverage::CounterMappingRegion createCounterMappingRegion(struct LLVMCoverageRegion& region) {
auto regionKind = determineRegionKind(region);
int expandedFileId = 0;
if (regionKind == coverage::CounterMappingRegion::RegionKind::ExpansionRegion) {
expandedFileId = region.expandedFileId;
}
const Counter &counter = coverage::Counter::getCounter(region.counterId);
return coverage::CounterMappingRegion(counter, region.fileId, expandedFileId, region.lineStart,
region.columnStart, region.lineEnd, region.columnEnd, regionKind);
}
LLVMFunctionCoverage* LLVMWriteCoverageRegionMapping(unsigned int *fileIdMapping, size_t fileIdMappingSize,
struct LLVMCoverageRegion **mappingRegions, size_t mappingRegionsSize) {
std::vector<coverage::CounterMappingRegion> counterMappingRegions;
for (size_t i = 0; i < mappingRegionsSize; ++i) {
struct LLVMCoverageRegion region = *mappingRegions[i];
counterMappingRegions.emplace_back(createCounterMappingRegion(region));
}
CoverageMappingWriter writer(ArrayRef<unsigned int>(fileIdMapping, fileIdMappingSize), None, counterMappingRegions);
std::string CoverageMapping;
raw_string_ostream OS(CoverageMapping);
writer.write(OS);
OS.flush();
// Should be disposed with `LLVMFunctionCoverageDispose`.
return new LLVMFunctionCoverage(CoverageMapping);
}
void LLVMFunctionCoverageDispose(struct LLVMFunctionCoverage* functionCoverage) {
delete functionCoverage;
}
static StructType *getFunctionRecordTy(LLVMContext &Ctx) {
#define COVMAP_FUNC_RECORD(Type, LLVMType, Name, Init) LLVMType,
Type *FunctionRecordTypes[] = {
#include "llvm/ProfileData/InstrProfData.inc"
};
StructType *FunctionRecordTy = StructType::get(Ctx, makeArrayRef(FunctionRecordTypes), true);
return FunctionRecordTy;
}
static llvm::Constant *addFunctionMappingRecord(llvm::LLVMContext &Ctx, StringRef NameValue, uint64_t FuncHash,
const std::string &CoverageMapping) {
llvm::StructType *FunctionRecordTy = getFunctionRecordTy(Ctx);
#define COVMAP_FUNC_RECORD(Type, LLVMType, Name, Init) Init,
llvm::Constant *FunctionRecordVals[] = {
#include "llvm/ProfileData/InstrProfData.inc"
};
return llvm::ConstantStruct::get(FunctionRecordTy, makeArrayRef(FunctionRecordVals));
}
// See https://github.com/llvm/llvm-project/blob/fa8fa044ec46b94e64971efa8852df0d58114062/clang/lib/CodeGen/CoverageMappingGen.cpp#L1284.
LLVMValueRef LLVMAddFunctionMappingRecord(LLVMContextRef context, const char *name, uint64_t hash,
struct LLVMFunctionCoverage *coverageMapping) {
return llvm::wrap(addFunctionMappingRecord(*llvm::unwrap(context), name, hash, coverageMapping->coverageData));
}
// See https://github.com/llvm/llvm-project/blob/fa8fa044ec46b94e64971efa8852df0d58114062/clang/lib/CodeGen/CoverageMappingGen.cpp#L1335.
// Please note that llvm/ProfileData/InstrProfData.inc refers to variable names of the function that includes it. So be careful with renaming.
static llvm::GlobalVariable* emitCoverageGlobal(
llvm::LLVMContext &Ctx,
llvm::Module &module,
std::vector<llvm::Constant *> &FunctionRecords,
const llvm::SmallVector<StringRef, 16> &FilenameRefs,
const std::string &RawCoverageMappings) {
auto *Int32Ty = llvm::Type::getInt32Ty(Ctx);
std::string FilenamesAndCoverageMappings;
llvm::raw_string_ostream outputStream(FilenamesAndCoverageMappings);
CoverageFilenamesSectionWriter(FilenameRefs).write(outputStream);
outputStream << RawCoverageMappings;
size_t CoverageMappingSize = RawCoverageMappings.size();
size_t FilenamesSize = outputStream.str().size() - CoverageMappingSize;
// See https://llvm.org/docs/CoverageMappingFormat.html#llvm-ir-representation
//
// > Coverage mapping data which is an array of bytes. Zero paddings are added at the end to force 8 byte alignment.
//
if (size_t rem = outputStream.str().size() % 8) {
CoverageMappingSize += 8 - rem;
for (size_t i = 0; i < 8 - rem; ++i) {
outputStream << '\0';
}
}
StructType *functionRecordTy = getFunctionRecordTy(Ctx);
// Create the deferred function records array
auto functionRecordsTy = llvm::ArrayType::get(functionRecordTy, FunctionRecords.size());
auto functionRecordsVal = llvm::ConstantArray::get(functionRecordsTy, FunctionRecords);
llvm::Type *CovDataHeaderTypes[] = {
#define COVMAP_HEADER(Type, LLVMType, Name, Init) LLVMType,
#include "llvm/ProfileData/InstrProfData.inc"
};
auto CovDataHeaderTy = llvm::StructType::get(Ctx, makeArrayRef(CovDataHeaderTypes));
llvm::Constant *CovDataHeaderVals[] = {
#define COVMAP_HEADER(Type, LLVMType, Name, Init) Init,
#include "llvm/ProfileData/InstrProfData.inc"
};
auto covDataHeaderVal = llvm::ConstantStruct::get(CovDataHeaderTy, makeArrayRef(CovDataHeaderVals));
auto *filenamesAndMappingsVal = llvm::ConstantDataArray::getString(Ctx, outputStream.str(), false);
// Create the coverage data record
llvm::Type *covDataTypes[] = {CovDataHeaderTy, functionRecordsTy, filenamesAndMappingsVal->getType()};
auto covDataTy = llvm::StructType::get(Ctx, makeArrayRef(covDataTypes));
llvm::Constant *TUDataVals[] = {covDataHeaderVal, functionRecordsVal, filenamesAndMappingsVal};
auto covDataVal = llvm::ConstantStruct::get(covDataTy, makeArrayRef(TUDataVals));
// Will be deleted when module is disposed.
return new llvm::GlobalVariable(module, covDataTy, true, llvm::GlobalValue::InternalLinkage,
covDataVal, llvm::getCoverageMappingVarName());
}
static std::string createRawCoverageMapping(struct LLVMFunctionCoverage **functionCoverages, size_t functionCoveragesSize) {
std::vector<std::string> coverageMappings;
for (size_t i = 0; i < functionCoveragesSize; ++i) {
coverageMappings.emplace_back(functionCoverages[i]->coverageData);
}
return llvm::join(coverageMappings.begin(), coverageMappings.end(), "");
}
LLVMValueRef LLVMCoverageEmit(LLVMModuleRef moduleRef,
LLVMValueRef *records, size_t recordsSize,
const char **filenames, int *filenamesIndices, size_t filenamesSize,
struct LLVMFunctionCoverage **functionCoverages, size_t functionCoveragesSize) {
LLVMContext &ctx = *unwrap(LLVMGetModuleContext(moduleRef));
Module &module = *unwrap(moduleRef);
std::vector<Constant *> functionRecords;
for (size_t i = 0; i < recordsSize; ++i) {
functionRecords.push_back(dyn_cast<Constant>(unwrap(records[i])));
}
llvm::SmallVector<StringRef, 16> filenameRefs;
filenameRefs.resize(filenamesSize);
for (size_t i = 0; i < filenamesSize; ++i) {
if (sys::path::is_absolute(filenames[i])) {
filenameRefs[filenamesIndices[i]] = filenames[i];
} else {
SmallString<256> path(filenames[i]);
sys::fs::make_absolute(path);
sys::path::remove_dots(path, true);
filenameRefs[filenamesIndices[i]] = path;
}
}
const std::string &rawCoverageMappings = createRawCoverageMapping(functionCoverages, functionCoveragesSize);
GlobalVariable *coverageGlobal = emitCoverageGlobal(ctx, module, functionRecords, filenameRefs, rawCoverageMappings);
const std::string &section = getInstrProfSectionName(IPSK_covmap, Triple(module.getTargetTriple()).getObjectFormat());
coverageGlobal->setSection(section);
coverageGlobal->setAlignment(8);
return wrap(coverageGlobal);
}
LLVMValueRef LLVMInstrProfIncrement(LLVMModuleRef moduleRef) {
Module &module = *unwrap(moduleRef);
return wrap(Intrinsic::getDeclaration(&module, Intrinsic::instrprof_increment, None));
}
LLVMValueRef LLVMCreatePGOFunctionNameVar(LLVMValueRef llvmFunction, const char *pgoFunctionName) {
auto *fnPtr = cast<llvm::Function>(unwrap(llvmFunction));
return wrap(createPGOFuncNameVar(*fnPtr, pgoFunctionName));
}
void LLVMAddInstrProfPass(LLVMPassManagerRef passManagerRef, const char* outputFileName) {
legacy::PassManagerBase *passManager = unwrap(passManagerRef);
InstrProfOptions options;
options.InstrProfileOutput = outputFileName;
passManager->add(createInstrProfilingLegacyPass(options));
}
LLVMTargetLibraryInfoRef LLVMGetTargetLibraryInfo(LLVMModuleRef moduleRef) {
auto* libraryInfo = new TargetLibraryInfoImpl(Triple(unwrap(moduleRef)->getTargetTriple()));
return llvm::wrap(libraryInfo);
}
@@ -0,0 +1,81 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
#ifndef __COVERAGE_MAPPING_C_H__
# define __COVERAGE_MAPPING_C_H__
#include <llvm-c/Core.h>
#include <llvm-c/Target.h>
# ifdef __cplusplus
extern "C" {
# endif
/**
* See org.jetbrains.kotlin.backend.konan.llvm.coverage.RegionKind.
*/
enum LLVMCoverageRegionKind {
CODE,
GAP,
EXPANSION
};
/**
* See org.jetbrains.kotlin.backend.konan.llvm.coverage.Region.
*/
struct LLVMCoverageRegion {
int fileId;
int lineStart;
int columnStart;
int lineEnd;
int columnEnd;
int counterId;
int expandedFileId;
enum LLVMCoverageRegionKind kind;
};
struct LLVMFunctionCoverage;
/**
* Add record in the following format: https://llvm.org/docs/CoverageMappingFormat.html#function-record.
*/
LLVMValueRef
LLVMAddFunctionMappingRecord(LLVMContextRef context, const char *name, uint64_t hash, struct LLVMFunctionCoverage* coverageMapping);
/**
* Wraps creation of coverage::CoverageMappingWriter and call to coverage::CoverageMappingWriter::write.
*/
struct LLVMFunctionCoverage* LLVMWriteCoverageRegionMapping(unsigned int *fileIdMapping, size_t fileIdMappingSize,
struct LLVMCoverageRegion **mappingRegions, size_t mappingRegionsSize);
void LLVMFunctionCoverageDispose(struct LLVMFunctionCoverage* functionCoverage);
/**
* Create __llvm_coverage_mapping global.
*/
LLVMValueRef LLVMCoverageEmit(LLVMModuleRef moduleRef, LLVMValueRef *records, size_t recordsSize,
const char **filenames, int *filenamesIndices, size_t filenamesSize,
struct LLVMFunctionCoverage** functionCoverages, size_t functionCoveragesSize);
/**
* Wrapper for `llvm.instrprof.increment` declaration.
*/
LLVMValueRef LLVMInstrProfIncrement(LLVMModuleRef moduleRef);
/**
* Wrapper for llvm::createPGOFuncNameVar.
*/
LLVMValueRef LLVMCreatePGOFunctionNameVar(LLVMValueRef llvmFunction, const char *pgoFunctionName);
void LLVMAddInstrProfPass(LLVMPassManagerRef passManagerRef, const char* outputFileName);
LLVMTargetLibraryInfoRef LLVMGetTargetLibraryInfo(LLVMModuleRef moduleRef);
# ifdef __cplusplus
}
# endif
#endif
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
* Copyright 2010-2019 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
+12
View File
@@ -0,0 +1,12 @@
# Code Coverage usage sample
This example shows how to collect coverage information during execution of the test suite.
Please note that this functionality will be incorporated into Gradle plugin so you won't need to do it by hand in the nearest future.
### Prerequisites
`createCoverageReport` task requires `llvm-profdata` and `llvm-cov` to be added to the `$PATH`.
They can be found in the Kotlin/Native dependencies dir. By default it should look like
`$HOME/.konan/dependencies/clang-llvm-6.0.1-darwin-macos/bin`
### Usage
Just run `createCoverageReport` task.
+50
View File
@@ -0,0 +1,50 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("org.jetbrains.kotlin.multiplatform")
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib-common"))
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
}
macosX64("macos") {
binaries {
executable(listOf(DEBUG)) {
entryPoint = "coverage.main"
}
}
binaries.getExecutable("test", DEBUG).apply {
freeCompilerArgs = mutableListOf(
"-Xlibrary-to-cover=${compilations["main"].output.classesDirs.singleFile.absolutePath}"
)
}
}
}
tasks.create("createCoverageReport") {
dependsOn("macosTest")
description = "Create coverage report"
doLast {
val testDebugBinary = kotlin.targets["macos"].let { it as KotlinNativeTarget }.binaries.getExecutable("test", "debug").outputFile
exec {
commandLine("llvm-profdata", "merge", "$testDebugBinary.profraw", "-o", "$testDebugBinary.profdata")
}
exec {
commandLine("llvm-cov", "show", "$testDebugBinary", "-instr-profile", "$testDebugBinary.profdata")
}
}
}
+1
View File
@@ -0,0 +1 @@
kotlin.code.style=official
@@ -0,0 +1,17 @@
fun baz(args: Array<String>) {
if (args.size > 4) {
println("Too many")
} else {
println("Fine")
}
}
class A {
init {
println("A::init")
}
fun f() {
println("A::f")
}
}
@@ -0,0 +1,3 @@
fun main() {
baz(arrayOf("abc"))
}
@@ -0,0 +1,15 @@
import kotlin.test.Test
import kotlin.test.assertTrue
class CoverageTests {
@Test
fun testHello() {
main()
}
@Test
fun testA() {
val a = A()
a.f()
}
}
+1
View File
@@ -44,6 +44,7 @@ if (MPPTools.isMacos()) {
include ':opengl'
include ':symbolication'
include ':uikit'
include ':coverage'
}
if (MPPTools.isWindows()) {
+1
View File
@@ -19,6 +19,7 @@ include ':Interop:Indexer'
include ':Interop:JsRuntime'
include ':Interop:StubGenerator'
include ':Interop:Runtime'
include ':llvmCoverageMappingC'
include ':llvmDebugInfoC'
include ':libclangext'
include ':klib'
@@ -106,6 +106,6 @@ open class Command(initialCommand: List<String>) {
}
private fun log() {
if (logger != null) logger!! { command.toList<String>().joinToString(" ") }
if (logger != null) logger!! { command.joinToString(" ") }
}
}
@@ -133,15 +133,32 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables)
private val libtool = "$absoluteTargetToolchain/usr/bin/libtool"
private val linker = "$absoluteTargetToolchain/usr/bin/ld"
private val dsymutil = "$absoluteLlvmHome/bin/llvm-dsymutil"
private val compilerRtLibrary: String? by lazy {
val suffix = when (configurables.target.family) {
private fun provideCompilerRtLibrary(libraryName: String): String? {
val suffix = if (libraryName.isNotEmpty() && target == KonanTarget.IOS_X64) {
"iossim"
} else {
when (val family = configurables.target.family) {
Family.OSX -> "osx"
Family.IOS -> "ios"
else -> TODO()
else -> error("Family $family is unsupported")
}
val dir = File("$absoluteTargetToolchain/usr/lib/clang/").listFiles.firstOrNull()?.absolutePath
if (dir != null) "$dir/lib/darwin/libclang_rt.$suffix.a" else null
}
val dir = File("$absoluteTargetToolchain/usr/lib/clang/").listFiles.firstOrNull()?.absolutePath
val mangledName = if (libraryName.isEmpty()) "" else "${libraryName}_"
return if (dir != null) "$dir/lib/darwin/libclang_rt.$mangledName$suffix.a" else null
}
private val compilerRtLibrary: String? by lazy {
provideCompilerRtLibrary("")
}
// Code coverage requires this library.
private val profileLibrary: String? by lazy {
provideCompilerRtLibrary("profile")
}
private val osVersionMinFlags: List<String> by lazy {
listOf(osVersionMinFlagLd, osVersionMin + ".0")
}
@@ -171,6 +188,7 @@ open class MacOSBasedLinker(targetProperties: AppleConfigurables)
if (dynamic) +linkerDynamicFlags
+linkerKonanFlags
if (compilerRtLibrary != null) +compilerRtLibrary!!
if (profileLibrary != null) +profileLibrary!!
+libraries
+linkerArgs
+rpath(dynamic)