Skip to content

Commit

Permalink
Infrastructure for source-based code coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
sbogolepov committed Mar 12, 2019
1 parent f043a6b commit b22404c
Show file tree
Hide file tree
Showing 38 changed files with 1,365 additions and 29 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 68 additions & 0 deletions CODE_COVERAGE.md
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 3 additions & 2 deletions backend.native/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<*>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,19 @@ 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
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

/**
Expand Down Expand Up @@ -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>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ internal val bitcodePhase = namedIrModulePhase(
escapeAnalysisPhase then
codegenPhase then
finalizeDebugInfoPhase then
bitcodePassesPhase then
cStubsPhase
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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?) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit b22404c

Please sign in to comment.