Skip to content

Commit

Permalink
276: further implementation of sensors for process test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
rohwerj committed Sep 22, 2023
1 parent a2b850b commit 996417a
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,14 @@ object CoverageStateJsonExporter {
fun readCoverageStateResult(json: String): CoverageStateResult =
Gson().fromJson(json, CoverageStateResult::class.java)

@JvmStatic
fun combineCoverageStateResults(json1: String, json2: String): String {
val result1 = readCoverageStateResult(json1)
val result2 = readCoverageStateResult(json2)
return createCoverageStateResult(
result1.suites + result2.suites,
result1.models.plus(result2.models.filter { new -> !result1.models.map { model -> model.key }.contains(new.key) })
)
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package org.camunda.community.process_test_coverage.core.export

import org.camunda.community.process_test_coverage.core.model.Coverage
import org.camunda.community.process_test_coverage.core.model.Event
import org.camunda.community.process_test_coverage.core.model.Model
import org.camunda.community.process_test_coverage.core.model.Suite

data class CoverageStateResult(
class CoverageStateResult(
val suites: Collection<Suite>,
val models: Collection<Model>
)
) : Coverage {
override fun getEvents() = suites.map { it.getEvents() }.flatten()

override fun getEvents(modelKey: String) = suites.map { it.getEvents(modelKey) }.flatten()

}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
kotlinVersion=1.7.21
kotlinVersion=1.9.10
projectName=report-aggregator-gradle-plugin
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.camunda.community.process_test_coverage.report.aggregator

import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.combineCoverageStateResults
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.createCoverageStateResult
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.readCoverageStateResult
import org.camunda.community.process_test_coverage.core.export.CoverageStateResult
import org.camunda.community.process_test_coverage.report.CoverageReportUtil
import org.gradle.api.Plugin
Expand Down Expand Up @@ -32,20 +34,15 @@ class ReportAggregatorPlugin : Plugin<Project> {
project.logger.debug("Reading file ${it.path}")
it.readText(Charsets.UTF_8)
}
.map { CoverageStateJsonExporter.readCoverageStateResult(it) }
.reduceOrNull { result1, result2 -> CoverageStateResult(
result1.suites + result2.suites,
result1.models.plus(result2.models.filter { new -> !result1.models.map { model -> model.key }.contains(new.key) })
)
}
.reduceOrNull { result1, result2 -> combineCoverageStateResults(result1, result2) }
?.let {
println(outputDirectory)
val report = readCoverageStateResult(it)
CoverageReportUtil.writeReport(
CoverageStateJsonExporter.createCoverageStateResult(it.suites, it.models), false,
createCoverageStateResult(report.suites, report.models), false,
outputDirectory, "report.json"
) { result -> result }
CoverageReportUtil.writeReport(
CoverageStateJsonExporter.createCoverageStateResult(it.suites, it.models), true,
createCoverageStateResult(report.suites, report.models), true,
outputDirectory, "report.html", CoverageReportUtil::generateHtml)
} ?: project.logger.warn("No coverage results found, skipping execution")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package org.camunda.community.process_test_coverage.report.aggregator

import org.apache.maven.MavenExecutionException
import org.apache.maven.doxia.sink.Sink
import org.apache.maven.plugin.AbstractMojo
import org.apache.maven.plugins.annotations.LifecyclePhase
import org.apache.maven.plugins.annotations.Mojo
import org.apache.maven.plugins.annotations.Parameter
import org.apache.maven.project.MavenProject
import org.apache.maven.reporting.MavenReport
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.combineCoverageStateResults
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.createCoverageStateResult
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.readCoverageStateResult
import org.camunda.community.process_test_coverage.core.export.CoverageStateResult
import org.camunda.community.process_test_coverage.report.CoverageReportUtil
import org.codehaus.plexus.util.FileUtils
import java.io.File
Expand Down Expand Up @@ -95,16 +94,13 @@ class ReportAggregatorMojo : AbstractMojo(), MavenReport {
log.debug("Reading file ${it.path}")
FileUtils.fileRead(it)
}
.map { readCoverageStateResult(it) }
.reduceOrNull { result1, result2 -> CoverageStateResult(
result1.suites + result2.suites,
result1.models.plus(result2.models.filter { new -> !result1.models.map { model -> model.key }.contains(new.key) })
)}
.reduceOrNull { result1, result2 -> combineCoverageStateResults(result1, result2) }
?.let {
CoverageReportUtil.writeReport(createCoverageStateResult(it.suites, it.models), false,
val report = readCoverageStateResult(it)
CoverageReportUtil.writeReport(createCoverageStateResult(report.suites, report.models), false,
outputDirectory.path, "report.json"
) { result -> result }
CoverageReportUtil.writeReport(createCoverageStateResult(it.suites, it.models), true,
CoverageReportUtil.writeReport(createCoverageStateResult(report.suites, report.models), true,
outputDirectory.path, "report.html", CoverageReportUtil::generateHtml)
} ?: log.warn("No coverage results found, skipping execution")
}
Expand Down
6 changes: 5 additions & 1 deletion extension/sonar-process-test-coverage-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<artifactId>camunda-process-test-coverage-parent</artifactId>
<groupId>org.camunda.community.process_test_coverage</groupId>
<version>2.0.1-SNAPSHOT</version>
<version>2.1.1-SNAPSHOT</version>
</parent>
<name>Camunda Process Test Coverage Sonar Plugin</name>
<artifactId>sonar-camunda-process-test-coverage-plugin</artifactId>
Expand All @@ -23,6 +23,10 @@
<groupId>org.camunda.community.process_test_coverage</groupId>
<artifactId>camunda-process-test-coverage-core</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm.model</groupId>
<artifactId>camunda-bpmn-model</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.camunda.community.process_test_coverage.sonar

import org.sonar.api.resources.AbstractLanguage

class BpmnLanguage : AbstractLanguage(KEY, NAME) {

companion object {
const val NAME = "BPMN"
const val KEY = "bpmn"
}

override fun getFileSuffixes() = arrayOf(".bpmn")

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.camunda.community.process_test_coverage.sonar

import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition


class BpmnQualityProfile : BuiltInQualityProfilesDefinition {
override fun define(context: BuiltInQualityProfilesDefinition.Context) {
val profile = context.createBuiltInQualityProfile("BPMN Rules", BpmnLanguage.KEY)
profile.setDefault(true)
profile.done()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import org.sonar.api.resources.Qualifiers
class ProcessTestCoveragePlugin : Plugin {

override fun define(context: Plugin.Context) {
context.addExtensions(BpmnLanguage::class.java, BpmnQualityProfile::class.java)
context.addExtension(ProcessTestCoverageMetrics::class.java)
context.addExtension(ProcessTestCoverageSensor::class.java)
context.addExtensions(ProcessTestCoverageSensor::class.java, ProcessTestCoverageProjectSensor::class.java)
context.addExtension(
PropertyDefinition.builder(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY)
.onQualifiers(Qualifiers.PROJECT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.camunda.community.process_test_coverage.sonar

import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.combineCoverageStateResults
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.readCoverageStateResult
import org.sonar.api.batch.sensor.SensorContext
import org.sonar.api.batch.sensor.SensorDescriptor
import org.sonar.api.scanner.sensor.ProjectSensor
import org.sonar.api.utils.log.Loggers
import java.nio.file.Files


class ProcessTestCoverageProjectSensor : ProjectSensor {

companion object {
private val LOG = Loggers.get(ProcessTestCoverageProjectSensor::class.java)
}

override fun describe(descriptor: SensorDescriptor) {
descriptor.name("Process Test Coverage Report Importer")
}

override fun execute(context: SensorContext) {
val reportPathsProvider = ReportPathsProvider(context)
val importer = ReportImporter(context)
importReports(reportPathsProvider, importer)
}

private fun importReports(reportPathsProvider: ReportPathsProvider, importer: ReportImporter) {
val reportPaths = reportPathsProvider.getPaths()
if (reportPaths.isEmpty()) {
LOG.info("No report imported, no coverage information will be imported by Process Test Coverage Report Importer")
return
}
LOG.info(
"Importing {} report(s). Turn your logs in debug mode in order to see the exhaustive list.",
reportPaths.size
)
try {
reportPaths
.map {
LOG.debug("Reading report '{}'", it)
Files.readAllBytes(it).decodeToString()
}
.reduceOrNull { result1, result2 -> combineCoverageStateResults(result1, result2) }
?.let { importer.importProjectCoverage(readCoverageStateResult(it)) }
?: LOG.warn("No coverage results found, skipping analysis")
} catch (e: Exception) {
LOG.error("Coverage reports could not be read/imported. Error: {}", e)
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.camunda.community.process_test_coverage.sonar

import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.combineCoverageStateResults
import org.camunda.community.process_test_coverage.core.export.CoverageStateJsonExporter.readCoverageStateResult
import org.sonar.api.batch.sensor.Sensor
import org.sonar.api.batch.sensor.SensorContext
import org.sonar.api.batch.sensor.SensorDescriptor
Expand Down Expand Up @@ -34,14 +35,17 @@ class ProcessTestCoverageSensor : Sensor {
"Importing {} report(s). Turn your logs in debug mode in order to see the exhaustive list.",
reportPaths.size
)
for (reportPath in reportPaths) {
LOG.debug("Reading report '{}'", reportPath)
try {
val result = CoverageStateJsonExporter.readCoverageStateResult(Files.readAllBytes(reportPath).decodeToString())
importer.importCoverage(result)
} catch (e: Exception) {
LOG.error("Coverage report '{}' could not be read/imported. Error: {}", reportPath, e)
}
try {
reportPaths
.map {
LOG.debug("Reading report '{}'", it)
Files.readAllBytes(it).decodeToString()
}
.reduceOrNull { result1, result2 -> combineCoverageStateResults(result1, result2) }
?.let { importer.importCoverage(readCoverageStateResult(it)) }
?: LOG.warn("No coverage results found, skipping analysis")
} catch (e: Exception) {
LOG.error("Coverage reports could not be read/imported. Error: {}", e)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package org.camunda.community.process_test_coverage.sonar

import org.camunda.bpm.model.bpmn.Bpmn
import org.camunda.bpm.model.bpmn.BpmnModelInstance
import org.camunda.bpm.model.bpmn.instance.*
import org.camunda.bpm.model.xml.instance.ModelElementInstance
import org.camunda.community.process_test_coverage.core.export.CoverageStateResult
import org.camunda.community.process_test_coverage.core.model.Model
import org.sonar.api.batch.fs.InputFile
import org.sonar.api.batch.sensor.SensorContext
import org.sonar.api.utils.log.Loggers
import java.io.ByteArrayInputStream
import java.util.stream.Collectors


class ReportImporter(private val ctx: SensorContext) {
Expand All @@ -12,28 +20,80 @@ class ReportImporter(private val ctx: SensorContext) {
}

fun importCoverage(result: CoverageStateResult) {
if (result.suites.size == 1) {
result.suites.first().let {

val lastDot = it.name.lastIndexOf('.')
val className = it.name.substring(lastDot + 1)
val packageName = it.name.substring(0, lastDot)
val path = "**/${packageName.replace('.', '/')}/$className.*"
val inputFile = ctx.fileSystem().inputFile(ctx.fileSystem().predicates().matchesPathPattern(path))

inputFile?.let { file ->
val resultsMap = result.models.associateBy {
Bpmn.readModelFromStream(it.xml.byteInputStream()).processDefinitionKey()
}
ctx.fileSystem().inputFiles(ctx.fileSystem().predicates().hasLanguage(BpmnLanguage.KEY))
.associateBy {
Bpmn.readModelFromStream(it.inputStream()).processDefinitionKey()
}
.forEach {
LOG.info("Calculating coverage for process {} in file {}", it.key, it.value.filename())
val coverage = resultsMap[it.key]?.let { model -> result.calculateCoverage(model) } ?: 0.0
LOG.info("Coverage for process {} is {}", it.key, coverage)
ctx.newMeasure<Double>()
.on(file)
.forMetric(ProcessTestCoverageMetrics.PROCESS_TEST_COVERAGE)
.withValue(it.calculateCoverage(result.models).asPercent())
.save()
.on(it.value)
.forMetric(ProcessTestCoverageMetrics.PROCESS_TEST_COVERAGE)
.withValue(coverage.asPercent())
.save()
}
}
}

fun importProjectCoverage(result: CoverageStateResult) {
val models = ctx.fileSystem().inputFiles(ctx.fileSystem().predicates().hasLanguage(BpmnLanguage.KEY))
.map { readModel(it) }
val totalElementCount = models.sumOf { it.totalElementCount }
val coveredElementCount = models.sumOf { result.getEventsDistinct(modelKey = it.key).size }
ctx.newMeasure<Double>()
.on(ctx.project())
.forMetric(ProcessTestCoverageMetrics.PROCESS_TEST_COVERAGE)
.withValue((coveredElementCount.toDouble() / totalElementCount.toDouble()).asPercent())
.save()
}

private fun readModel(file: InputFile): Model {
val modelInstance = Bpmn.readModelFromStream(file.inputStream())
val key = modelInstance.processDefinitionKey()
val definitionFlowNodes = getExecutableFlowNodes(modelInstance.getModelElementsByType(FlowNode::class.java), key)
val definitionSequenceFlows = getExecutableSequenceNodes(modelInstance.getModelElementsByType(SequenceFlow::class.java), definitionFlowNodes)

return Model(
key,
definitionFlowNodes.size + definitionSequenceFlows.size,
"unknown",
Bpmn.convertToString(modelInstance)
)
}

private fun getExecutableFlowNodes(flowNodes: Collection<FlowNode>, processId: String): Set<FlowNode> {
return flowNodes.stream()
.filter { node: FlowNode? -> isExecutable(node, processId) }
.collect(Collectors.toSet())
}

private fun getExecutableSequenceNodes(sequenceFlows: Collection<SequenceFlow>, definitionFlowNodes: Set<FlowNode>): Set<SequenceFlow> {
return sequenceFlows.stream()
.filter { s: SequenceFlow -> definitionFlowNodes.contains(s.source) }
.collect(Collectors.toSet())
}

private fun isExecutable(node: ModelElementInstance?, processId: String): Boolean {
if (node == null) {
return false
}
return if (node is Process) {
node.isExecutable && node.id == processId
} else if (node is IntermediateThrowEvent) {
node.eventDefinitions.none { it is LinkEventDefinition }
} else {
LOG.warn("Cannot import coverage results for more than one suite")
isExecutable(node.parentElement, processId)
}
}

private fun BpmnModelInstance.processDefinitionKey() =
getModelElementsByType(Process::class.java).firstOrNull { process -> process.isExecutable }?.id
?: throw IllegalArgumentException("No executable process found")

private fun Double.asPercent() = this * 100

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ReportPathsProvider(

companion object {
private val LOG: Logger = Loggers.get(ReportPathsProvider::class.java)
private val DEFAULT_PATHS = arrayOf("target/process-test-coverage/**/report.json")
private val DEFAULT_PATHS = arrayOf("**/process-test-coverage/**/report.json")
const val REPORT_PATHS_PROPERTY_KEY = "sonar.process-test-coverage.jsonReportPaths"
}

Expand All @@ -24,6 +24,7 @@ class ReportPathsProvider(
val reportPaths: MutableSet<Path> = HashSet()
if (patternPathList.isNotEmpty()) {
for (patternPath in patternPathList) {
LOG.info("Scanning {} with pattern {}", baseDir, patternPath)
val paths: List<Path> = WildcardPatternFileScanner.scan(baseDir, patternPath)
if (paths.isEmpty() && patternPathList.size > 1) {
LOG.info("Coverage report doesn't exist for pattern: '{}'", patternPath)
Expand Down
Loading

0 comments on commit 996417a

Please sign in to comment.