Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for software metrics #333

Merged
merged 14 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The Jenkins Coverage Plug-in collects reports of code coverage or mutation cover
- [JUnit](https://ant.apache.org/manual/Tasks/junitreport.html): Test Results
- [NUnit](https://nunit.org/): Test Results
- [XUnit](https://xunit.net/): Test Results
- Metrics XML report

If your coverage tool is not yet supported by the coverage plugin, feel free to provide a pull request for the [Coverage Model](https://github.com/jenkinsci/coverage-model/pulls).

Expand Down Expand Up @@ -113,6 +114,7 @@ The Coverage Plug-in supports the following report formats:
- [JUnit](https://ant.apache.org/manual/Tasks/junitreport.html): Test Results
- [NUnit](https://nunit.org/): Test Results
- [XUnit](https://xunit.net/): Test Results
- Metrics XML report

Some of these report files are generated by other tools and may contain invalid or inconsistent information. By default, the plugin tries to fail fast if such a broken file is detected. You can disable this behavior by setting the property `ignoreParsingErrors` to `true`. In this case, the plugin will try to parse as much information as possible from the report file.

Expand Down
16 changes: 10 additions & 6 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
<parent>
<groupId>org.jvnet.hudson.plugins</groupId>
<artifactId>analysis-pom</artifactId>
<version>8.6.0</version>
<version>10.4.0</version>
<relativePath />
</parent>

<groupId>io.jenkins.plugins</groupId>
<artifactId>coverage</artifactId>
<version>${revision}${changelist}</version>
<version>2.0.0-SNAPSHOT</version>
<packaging>hpi</packaging>

<name>Coverage Plugin</name>
Expand All @@ -20,16 +20,14 @@
<url>https://github.com/jenkinsci/coverage-plugin</url>

<properties>
<revision>1.17.0</revision>
<changelist>-SNAPSHOT</changelist>
<gitHubRepo>jenkinsci/coverage-plugin</gitHubRepo>

<!-- Library Dependencies Versions -->
<coverage-model.version>0.46.0</coverage-model.version>
<coverage-model.version>0.53.0</coverage-model.version>
<jsoup.version>1.18.3</jsoup.version>

<!-- Jenkins Plug-in Dependencies Versions -->
<git-forensics.version>2.2.1</git-forensics.version>
<git-forensics.version>3.1.0</git-forensics.version>

<!-- Test Library Dependencies Versions -->
<xmlunit.version>2.10.0</xmlunit.version>
Expand Down Expand Up @@ -146,10 +144,14 @@
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>forensics-api</artifactId>
<!-- TODO: remove the version when a matching BOM is available -->
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>plugin-util-api</artifactId>
<!-- TODO: remove the version when a matching BOM is available -->
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
Expand All @@ -164,6 +166,8 @@
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>plugin-util-api</artifactId>
<!-- TODO: remove the version when a matching BOM is available -->
<version>6.0.0</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
package io.jenkins.plugins.coverage.metrics.charts;

import java.util.HashMap;
import java.util.Map;

import edu.hm.hafner.coverage.Coverage;
import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.echarts.line.SeriesBuilder;

import java.util.HashMap;
import java.util.Map;

import io.jenkins.plugins.coverage.metrics.model.Baseline;
import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;

/**
* Builds one x-axis point for the series of a line chart showing the line and branch coverage of a project.
* Builds one x-axis point for the series of a line chart showing the coverage metrics of a project.
*
* @author Ullrich Hafner
*/
public class CoverageSeriesBuilder extends SeriesBuilder<CoverageStatistics> {
static final String LINE_COVERAGE = "line";
static final String BRANCH_COVERAGE = "branch";
static final String MUTATION_COVERAGE = "mutation";
static final String TEST_STRENGTH = "test-strength";
static final String MCDC_PAIR_COVERAGE = "mcdc-pair";
static final String FUNCTION_CALL_COVERAGE = "function-call";
static final String METHOD_COVERAGE = "method";
static final String LINE_COVERAGE = Metric.LINE.toTagName();
static final String BRANCH_COVERAGE = Metric.BRANCH.toTagName();
static final String MUTATION_COVERAGE = Metric.MUTATION.toTagName();
static final String TEST_STRENGTH = Metric.TEST_STRENGTH.toTagName();
static final String MCDC_PAIR_COVERAGE = Metric.MCDC_PAIR.toTagName();
static final String FUNCTION_CALL_COVERAGE = Metric.FUNCTION_CALL.toTagName();
static final String METHOD_COVERAGE = Metric.METHOD.toTagName();

@Override
protected Map<String, Double> computeSeries(final CoverageStatistics statistics) {
Map<String, Double> series = new HashMap<>();

series.put(LINE_COVERAGE, getRoundedPercentage(statistics, Metric.LINE));
add(statistics, Metric.LINE, LINE_COVERAGE, series);
add(statistics, Metric.BRANCH, BRANCH_COVERAGE, series);
add(statistics, Metric.MUTATION, MUTATION_COVERAGE, series);
add(statistics, Metric.TEST_STRENGTH, TEST_STRENGTH, series);
add(statistics, Metric.MCDC_PAIR, MCDC_PAIR_COVERAGE, series);
add(statistics, Metric.FUNCTION_CALL, FUNCTION_CALL_COVERAGE, series);

if (statistics.containsValue(Baseline.PROJECT, Metric.MCDC_PAIR)
|| statistics.containsValue(Baseline.PROJECT, Metric.FUNCTION_CALL)) {
if (statistics.containsValue(Metric.MCDC_PAIR, Baseline.PROJECT)
|| statistics.containsValue(Metric.FUNCTION_CALL, Baseline.PROJECT)) {
// Method coverage is only relevant if MC/DC pair or function call coverage is available
add(statistics, Metric.METHOD, METHOD_COVERAGE, series);
}
Expand All @@ -46,14 +45,8 @@ protected Map<String, Double> computeSeries(final CoverageStatistics statistics)

private void add(final CoverageStatistics statistics, final Metric metric, final String chartId,
final Map<String, Double> series) {
if (statistics.containsValue(Baseline.PROJECT, metric)) {
series.put(chartId, getRoundedPercentage(statistics, metric));
if (statistics.containsValue(metric, Baseline.PROJECT)) {
series.put(chartId, statistics.roundValue(Baseline.PROJECT, metric));
}
}

private double getRoundedPercentage(final CoverageStatistics statistics, final Metric metric) {
Coverage coverage = (Coverage) statistics.getValue(Baseline.PROJECT, metric)
.orElse(Coverage.nullObject(metric));
return coverage.getCoveredPercentage().toDouble() / 100.0 * 100.0;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package io.jenkins.plugins.coverage.metrics.charts;

import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.echarts.BuildResult;
import edu.hm.hafner.echarts.ChartModelConfiguration;
import edu.hm.hafner.echarts.JacksonFacade;
import edu.hm.hafner.echarts.line.LineSeries;
import edu.hm.hafner.echarts.line.LineSeries.FilledMode;
import edu.hm.hafner.echarts.line.LineSeries.StackedMode;
import edu.hm.hafner.echarts.line.LinesChartModel;
import edu.hm.hafner.echarts.line.LinesDataSet;

import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
import io.jenkins.plugins.coverage.metrics.model.Messages;
import io.jenkins.plugins.echarts.JenkinsPalette;

/**
Expand All @@ -22,48 +20,27 @@
* @author Ullrich Hafner
* @see JacksonFacade
*/
public class CoverageTrendChart {
/**
* Creates the chart for the specified results.
*
* @param results
* the forensics results to render - these results must be provided in descending order, i.e. the current *
* build is the head of the list, then the previous builds, and so on
* @param configuration
* the chart configuration to be used
*
* @return the chart model, ready to be serialized to JSON
*/
public class CoverageTrendChart extends TrendChart {
@Override
public LinesChartModel create(final Iterable<BuildResult<CoverageStatistics>> results,
final ChartModelConfiguration configuration) {
CoverageSeriesBuilder builder = new CoverageSeriesBuilder();
LinesDataSet dataSet = builder.createDataSet(configuration, results);

var filledMode = computeFilledMode(dataSet);
var dataSet = new CoverageSeriesBuilder().createDataSet(configuration, results);

LinesChartModel model = new LinesChartModel(dataSet);
if (dataSet.isNotEmpty()) {
LineSeries lineSeries = new LineSeries(Messages.Metric_LINE(),
JenkinsPalette.GREEN.normal(), StackedMode.SEPARATE_LINES, filledMode,
dataSet.getSeries(CoverageSeriesBuilder.LINE_COVERAGE));
model.addSeries(lineSeries);
model.useContinuousRangeAxis();
model.setRangeMax(100);
model.setRangeMin(dataSet.getMinimumValue());

addSeries(dataSet, model, Messages.Metric_BRANCH(), CoverageSeriesBuilder.BRANCH_COVERAGE,
JenkinsPalette.GREEN.dark(), filledMode);
addSeries(dataSet, model, Messages.Metric_MUTATION(), CoverageSeriesBuilder.MUTATION_COVERAGE,
JenkinsPalette.GREEN.dark(), filledMode);
addSeries(dataSet, model, Messages.Metric_TEST_STRENGTH(), CoverageSeriesBuilder.TEST_STRENGTH,
JenkinsPalette.GREEN.light(), filledMode);
var filledMode = computeFilledMode(dataSet);
addSeriesIfAvailable(dataSet, model, Metric.LINE, JenkinsPalette.GREEN.normal(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.BRANCH, JenkinsPalette.GREEN.dark(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.MUTATION, JenkinsPalette.GREEN.dark(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.TEST_STRENGTH, JenkinsPalette.GREEN.light(), filledMode);

addSeries(dataSet, model, Messages.Metric_MCDC_PAIR(), CoverageSeriesBuilder.MCDC_PAIR_COVERAGE,
JenkinsPalette.RED.light(), filledMode);
addSeries(dataSet, model, Messages.Metric_METHOD(), CoverageSeriesBuilder.METHOD_COVERAGE,
JenkinsPalette.RED.normal(), filledMode);
addSeries(dataSet, model, Messages.Metric_FUNCTION_CALL(), CoverageSeriesBuilder.FUNCTION_CALL_COVERAGE,
JenkinsPalette.RED.dark(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.MCDC_PAIR, JenkinsPalette.RED.light(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.METHOD, JenkinsPalette.RED.normal(), filledMode);
addSeriesIfAvailable(dataSet, model, Metric.FUNCTION_CALL, JenkinsPalette.RED.dark(), filledMode);
}
return model;
}
Expand All @@ -84,14 +61,4 @@ private FilledMode computeFilledMode(final LinesDataSet dataSet) {
}
return FilledMode.FILLED;
}

private void addSeries(final LinesDataSet dataSet, final LinesChartModel model,
final String name, final String seriesId, final String color, final FilledMode filledMode) {
if (dataSet.containsSeries(seriesId)) {
LineSeries branchSeries = new LineSeries(name,
color, StackedMode.SEPARATE_LINES, filledMode, dataSet.getSeries(seriesId));

model.addSeries(branchSeries);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.jenkins.plugins.coverage.metrics.charts;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.echarts.line.SeriesBuilder;

import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;

/**
* Builds one x-axis point for the series of a line chart showing the line and branch coverage of a project.
*
* @author Ullrich Hafner
*/
public class MetricSeriesBuilder extends SeriesBuilder<CoverageStatistics> {
@Override
protected Map<String, Double> computeSeries(final CoverageStatistics statistics) {
return Arrays.stream(Metric.values())
.filter(Predicate.not(Metric::isCoverage))
.filter(statistics::containsValue)
.collect(Collectors.toMap(Metric::toTagName, statistics::roundValue));

Check warning on line 24 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/MetricSeriesBuilder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 18-24 are not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.jenkins.plugins.coverage.metrics.charts;

import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.echarts.BuildResult;
import edu.hm.hafner.echarts.ChartModelConfiguration;
import edu.hm.hafner.echarts.JacksonFacade;
import edu.hm.hafner.echarts.line.LineSeries.FilledMode;
import edu.hm.hafner.echarts.line.LinesChartModel;
import edu.hm.hafner.echarts.line.LinesDataSet;

import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
import io.jenkins.plugins.echarts.JenkinsPalette;

/**
* Builds the Java side model for a trend chart showing the metrics of a project. The number of builds
* to consider is controlled by a {@link ChartModelConfiguration} instance. The created model object can be serialized
* to JSON (e.g., using the {@link JacksonFacade}) and can be used 1:1 as ECharts configuration object in the
* corresponding JS file.
*
* @author Ullrich Hafner
* @see JacksonFacade
*/
public class MetricsTrendChart extends TrendChart {
@Override
public LinesChartModel create(final Iterable<BuildResult<CoverageStatistics>> results,
final ChartModelConfiguration configuration) {
LinesDataSet dataSet = new MetricSeriesBuilder().createDataSet(configuration, results);

LinesChartModel model = new LinesChartModel(dataSet);
if (dataSet.isNotEmpty()) {
model.useContinuousRangeAxis();
model.setRangeMax(dataSet.getMaximumValue());
model.setRangeMin(dataSet.getMinimumValue());

int colorIndex = 0;
for (var tag : dataSet.getDataSetIds()) {
Metric metric = Metric.fromTag(tag);
addSeriesIfAvailable(dataSet, model, metric.getDisplayName(),
tag, JenkinsPalette.chartColor(colorIndex++).normal(),
FilledMode.LINES);
}
}
return model;

Check warning on line 43 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/MetricsTrendChart.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 23-43 are not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@

import edu.hm.hafner.coverage.Coverage;
import edu.hm.hafner.coverage.FileNode;
import edu.hm.hafner.coverage.IntegerValue;
import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.coverage.ModuleNode;
import edu.hm.hafner.coverage.Node;
import edu.hm.hafner.coverage.Value;
import edu.hm.hafner.echarts.ItemStyle;
import edu.hm.hafner.echarts.Label;
import edu.hm.hafner.echarts.LabeledTreeMapNode;
import edu.hm.hafner.echarts.TreeMapNode;

import hudson.Functions;

import io.jenkins.plugins.coverage.metrics.color.ColorProvider;
import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors;
import io.jenkins.plugins.coverage.metrics.color.ColorProviderFactory;
Expand Down Expand Up @@ -79,20 +81,16 @@
if (rootValue instanceof Coverage) {
return Optional.of(createCoverageTree((Coverage) rootValue, colorProvider, node, metric));
}
if (rootValue instanceof IntegerValue) {
return Optional.of(createCoverageTree((IntegerValue) rootValue, node, metric));
}
throw new IllegalArgumentException("Unsupported value type: " + rootValue);
return Optional.of(createMetricsTree(rootValue, node, metric));

Check warning on line 84 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 84 is not covered by tests
}

return Optional.empty();
}

private LabeledTreeMapNode createCoverageTree(final Coverage coverage, final ColorProvider colorProvider,
final Node node, final Metric metric) {
double coveragePercentage = coverage.getCoveredPercentage().toDouble();
DisplayColors colors = CoverageLevel.getDisplayColorsOfCoverageLevel(coverage.asDouble(), colorProvider);

DisplayColors colors = CoverageLevel.getDisplayColorsOfCoverageLevel(coveragePercentage, colorProvider);
String lineColor = colors.getLineColorAsRGBHex();
String fillColor = colors.getFillColorAsRGBHex();

Expand All @@ -119,29 +117,29 @@
String.valueOf(coverage.getTotal()), FORMATTER.getTooltip(coverage));
}

private LabeledTreeMapNode createCoverageTree(final IntegerValue coverage, final Node node,
private LabeledTreeMapNode createMetricsTree(final Value value, final Node node,
final Metric metric) {
Label label = new Label(true, JenkinsPalette.BLACK.normal());

String fillColor = metric == Metric.TESTS ? JenkinsPalette.GREEN.light() : JenkinsPalette.ORANGE.normal();
if (node instanceof FileNode) {
return createValueNode(coverage, node, new ItemStyle(fillColor), label);
return createValueNode(value, node, new ItemStyle(fillColor), label);
}

LabeledTreeMapNode treeNode = createValueNode(coverage, node,
LabeledTreeMapNode treeNode = createValueNode(value, node,
new ItemStyle(fillColor, fillColor, 4), label);

node.getChildren().stream()
.map(n -> toTreeMapNode(n, metric, ColorProviderFactory.createDefaultColorProvider()))
.flatMap(Optional::stream)
.forEach(treeNode::insertNode);

return treeNode;
}

private LabeledTreeMapNode createValueNode(final IntegerValue value, final Node node,
private LabeledTreeMapNode createValueNode(final Value value, final Node node,
final ItemStyle itemStyle, final Label label) {
return new LabeledTreeMapNode(getId(node), node.getName(), itemStyle, label, label,
String.valueOf(value.getValue()), FORMATTER.getTooltip(value));
value.asText(Functions.getCurrentLocale()), FORMATTER.getTooltip(value));

Check warning on line 143 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 126-143 are not covered by tests
}
}
Loading
Loading