Infrastructure for source-based code coverage
This commit is contained in:
committed by
Sergey Bogolepov
parent
f043a6bf58
commit
b22404c947
+4
-1
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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<*>
|
||||
|
||||
+23
-9
@@ -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,
|
||||
|
||||
+3
-3
@@ -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>>
|
||||
|
||||
|
||||
+6
-1
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -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 }
|
||||
|
||||
|
||||
+1
@@ -275,6 +275,7 @@ internal val bitcodePhase = namedIrModulePhase(
|
||||
escapeAnalysisPhase then
|
||||
codegenPhase then
|
||||
finalizeDebugInfoPhase then
|
||||
bitcodePassesPhase then
|
||||
cStubsPhase
|
||||
)
|
||||
|
||||
|
||||
+10
@@ -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",
|
||||
|
||||
+18
-2
@@ -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)
|
||||
|
||||
+98
@@ -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>
|
||||
)
|
||||
+127
@@ -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()
|
||||
}
|
||||
}
|
||||
+239
@@ -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)
|
||||
}
|
||||
}
|
||||
+52
@@ -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)!!
|
||||
}
|
||||
}
|
||||
+77
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 \
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 §ion = 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,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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ if (MPPTools.isMacos()) {
|
||||
include ':opengl'
|
||||
include ':symbolication'
|
||||
include ':uikit'
|
||||
include ':coverage'
|
||||
}
|
||||
|
||||
if (MPPTools.isWindows()) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user