Skip to content

Commit 4251b13

Browse files
authored
Merge pull request #333 from jenkinsci/feat/metrics-report
Add support for software metrics
2 parents 4ed1765 + b434f1d commit 4251b13

File tree

61 files changed

+1558
-1028
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1558
-1028
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The Jenkins Coverage Plug-in collects reports of code coverage or mutation cover
1717
- [JUnit](https://ant.apache.org/manual/Tasks/junitreport.html): Test Results
1818
- [NUnit](https://nunit.org/): Test Results
1919
- [XUnit](https://xunit.net/): Test Results
20+
- Metrics XML report
2021

2122
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).
2223

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

117119
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.
118120

plugin/pom.xml

+10-6
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
<parent>
66
<groupId>org.jvnet.hudson.plugins</groupId>
77
<artifactId>analysis-pom</artifactId>
8-
<version>8.6.0</version>
8+
<version>10.4.0</version>
99
<relativePath />
1010
</parent>
1111

1212
<groupId>io.jenkins.plugins</groupId>
1313
<artifactId>coverage</artifactId>
14-
<version>${revision}${changelist}</version>
14+
<version>2.0.0-SNAPSHOT</version>
1515
<packaging>hpi</packaging>
1616

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

2222
<properties>
23-
<revision>1.17.0</revision>
24-
<changelist>-SNAPSHOT</changelist>
2523
<gitHubRepo>jenkinsci/coverage-plugin</gitHubRepo>
2624

2725
<!-- Library Dependencies Versions -->
28-
<coverage-model.version>0.46.0</coverage-model.version>
26+
<coverage-model.version>0.53.0</coverage-model.version>
2927
<jsoup.version>1.18.3</jsoup.version>
3028

3129
<!-- Jenkins Plug-in Dependencies Versions -->
32-
<git-forensics.version>2.2.1</git-forensics.version>
30+
<git-forensics.version>3.1.0</git-forensics.version>
3331

3432
<!-- Test Library Dependencies Versions -->
3533
<xmlunit.version>2.10.0</xmlunit.version>
@@ -146,10 +144,14 @@
146144
<dependency>
147145
<groupId>io.jenkins.plugins</groupId>
148146
<artifactId>forensics-api</artifactId>
147+
<!-- TODO: remove the version when a matching BOM is available -->
148+
<version>3.0.0</version>
149149
</dependency>
150150
<dependency>
151151
<groupId>io.jenkins.plugins</groupId>
152152
<artifactId>plugin-util-api</artifactId>
153+
<!-- TODO: remove the version when a matching BOM is available -->
154+
<version>6.0.0</version>
153155
</dependency>
154156
<dependency>
155157
<groupId>io.jenkins.plugins</groupId>
@@ -164,6 +166,8 @@
164166
<dependency>
165167
<groupId>io.jenkins.plugins</groupId>
166168
<artifactId>plugin-util-api</artifactId>
169+
<!-- TODO: remove the version when a matching BOM is available -->
170+
<version>6.0.0</version>
167171
<classifier>tests</classifier>
168172
<scope>test</scope>
169173
</dependency>
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
11
package io.jenkins.plugins.coverage.metrics.charts;
22

3-
import java.util.HashMap;
4-
import java.util.Map;
5-
6-
import edu.hm.hafner.coverage.Coverage;
73
import edu.hm.hafner.coverage.Metric;
84
import edu.hm.hafner.echarts.line.SeriesBuilder;
95

6+
import java.util.HashMap;
7+
import java.util.Map;
8+
109
import io.jenkins.plugins.coverage.metrics.model.Baseline;
1110
import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
1211

1312
/**
14-
* Builds one x-axis point for the series of a line chart showing the line and branch coverage of a project.
13+
* Builds one x-axis point for the series of a line chart showing the coverage metrics of a project.
1514
*
1615
* @author Ullrich Hafner
1716
*/
1817
public class CoverageSeriesBuilder extends SeriesBuilder<CoverageStatistics> {
19-
static final String LINE_COVERAGE = "line";
20-
static final String BRANCH_COVERAGE = "branch";
21-
static final String MUTATION_COVERAGE = "mutation";
22-
static final String TEST_STRENGTH = "test-strength";
23-
static final String MCDC_PAIR_COVERAGE = "mcdc-pair";
24-
static final String FUNCTION_CALL_COVERAGE = "function-call";
25-
static final String METHOD_COVERAGE = "method";
18+
static final String LINE_COVERAGE = Metric.LINE.toTagName();
19+
static final String BRANCH_COVERAGE = Metric.BRANCH.toTagName();
20+
static final String MUTATION_COVERAGE = Metric.MUTATION.toTagName();
21+
static final String TEST_STRENGTH = Metric.TEST_STRENGTH.toTagName();
22+
static final String MCDC_PAIR_COVERAGE = Metric.MCDC_PAIR.toTagName();
23+
static final String FUNCTION_CALL_COVERAGE = Metric.FUNCTION_CALL.toTagName();
24+
static final String METHOD_COVERAGE = Metric.METHOD.toTagName();
2625

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

31-
series.put(LINE_COVERAGE, getRoundedPercentage(statistics, Metric.LINE));
30+
add(statistics, Metric.LINE, LINE_COVERAGE, series);
3231
add(statistics, Metric.BRANCH, BRANCH_COVERAGE, series);
3332
add(statistics, Metric.MUTATION, MUTATION_COVERAGE, series);
3433
add(statistics, Metric.TEST_STRENGTH, TEST_STRENGTH, series);
3534
add(statistics, Metric.MCDC_PAIR, MCDC_PAIR_COVERAGE, series);
3635
add(statistics, Metric.FUNCTION_CALL, FUNCTION_CALL_COVERAGE, series);
3736

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

4746
private void add(final CoverageStatistics statistics, final Metric metric, final String chartId,
4847
final Map<String, Double> series) {
49-
if (statistics.containsValue(Baseline.PROJECT, metric)) {
50-
series.put(chartId, getRoundedPercentage(statistics, metric));
48+
if (statistics.containsValue(metric, Baseline.PROJECT)) {
49+
series.put(chartId, statistics.roundValue(Baseline.PROJECT, metric));
5150
}
5251
}
53-
54-
private double getRoundedPercentage(final CoverageStatistics statistics, final Metric metric) {
55-
Coverage coverage = (Coverage) statistics.getValue(Baseline.PROJECT, metric)
56-
.orElse(Coverage.nullObject(metric));
57-
return coverage.getCoveredPercentage().toDouble() / 100.0 * 100.0;
58-
}
5952
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
package io.jenkins.plugins.coverage.metrics.charts;
22

3+
import edu.hm.hafner.coverage.Metric;
34
import edu.hm.hafner.echarts.BuildResult;
45
import edu.hm.hafner.echarts.ChartModelConfiguration;
56
import edu.hm.hafner.echarts.JacksonFacade;
6-
import edu.hm.hafner.echarts.line.LineSeries;
77
import edu.hm.hafner.echarts.line.LineSeries.FilledMode;
8-
import edu.hm.hafner.echarts.line.LineSeries.StackedMode;
98
import edu.hm.hafner.echarts.line.LinesChartModel;
109
import edu.hm.hafner.echarts.line.LinesDataSet;
1110

1211
import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
13-
import io.jenkins.plugins.coverage.metrics.model.Messages;
1412
import io.jenkins.plugins.echarts.JenkinsPalette;
1513

1614
/**
@@ -22,48 +20,27 @@
2220
* @author Ullrich Hafner
2321
* @see JacksonFacade
2422
*/
25-
public class CoverageTrendChart {
26-
/**
27-
* Creates the chart for the specified results.
28-
*
29-
* @param results
30-
* the forensics results to render - these results must be provided in descending order, i.e. the current *
31-
* build is the head of the list, then the previous builds, and so on
32-
* @param configuration
33-
* the chart configuration to be used
34-
*
35-
* @return the chart model, ready to be serialized to JSON
36-
*/
23+
public class CoverageTrendChart extends TrendChart {
24+
@Override
3725
public LinesChartModel create(final Iterable<BuildResult<CoverageStatistics>> results,
3826
final ChartModelConfiguration configuration) {
39-
CoverageSeriesBuilder builder = new CoverageSeriesBuilder();
40-
LinesDataSet dataSet = builder.createDataSet(configuration, results);
41-
42-
var filledMode = computeFilledMode(dataSet);
27+
var dataSet = new CoverageSeriesBuilder().createDataSet(configuration, results);
4328

4429
LinesChartModel model = new LinesChartModel(dataSet);
4530
if (dataSet.isNotEmpty()) {
46-
LineSeries lineSeries = new LineSeries(Messages.Metric_LINE(),
47-
JenkinsPalette.GREEN.normal(), StackedMode.SEPARATE_LINES, filledMode,
48-
dataSet.getSeries(CoverageSeriesBuilder.LINE_COVERAGE));
49-
model.addSeries(lineSeries);
5031
model.useContinuousRangeAxis();
5132
model.setRangeMax(100);
5233
model.setRangeMin(dataSet.getMinimumValue());
5334

54-
addSeries(dataSet, model, Messages.Metric_BRANCH(), CoverageSeriesBuilder.BRANCH_COVERAGE,
55-
JenkinsPalette.GREEN.dark(), filledMode);
56-
addSeries(dataSet, model, Messages.Metric_MUTATION(), CoverageSeriesBuilder.MUTATION_COVERAGE,
57-
JenkinsPalette.GREEN.dark(), filledMode);
58-
addSeries(dataSet, model, Messages.Metric_TEST_STRENGTH(), CoverageSeriesBuilder.TEST_STRENGTH,
59-
JenkinsPalette.GREEN.light(), filledMode);
35+
var filledMode = computeFilledMode(dataSet);
36+
addSeriesIfAvailable(dataSet, model, Metric.LINE, JenkinsPalette.GREEN.normal(), filledMode);
37+
addSeriesIfAvailable(dataSet, model, Metric.BRANCH, JenkinsPalette.GREEN.dark(), filledMode);
38+
addSeriesIfAvailable(dataSet, model, Metric.MUTATION, JenkinsPalette.GREEN.dark(), filledMode);
39+
addSeriesIfAvailable(dataSet, model, Metric.TEST_STRENGTH, JenkinsPalette.GREEN.light(), filledMode);
6040

61-
addSeries(dataSet, model, Messages.Metric_MCDC_PAIR(), CoverageSeriesBuilder.MCDC_PAIR_COVERAGE,
62-
JenkinsPalette.RED.light(), filledMode);
63-
addSeries(dataSet, model, Messages.Metric_METHOD(), CoverageSeriesBuilder.METHOD_COVERAGE,
64-
JenkinsPalette.RED.normal(), filledMode);
65-
addSeries(dataSet, model, Messages.Metric_FUNCTION_CALL(), CoverageSeriesBuilder.FUNCTION_CALL_COVERAGE,
66-
JenkinsPalette.RED.dark(), filledMode);
41+
addSeriesIfAvailable(dataSet, model, Metric.MCDC_PAIR, JenkinsPalette.RED.light(), filledMode);
42+
addSeriesIfAvailable(dataSet, model, Metric.METHOD, JenkinsPalette.RED.normal(), filledMode);
43+
addSeriesIfAvailable(dataSet, model, Metric.FUNCTION_CALL, JenkinsPalette.RED.dark(), filledMode);
6744
}
6845
return model;
6946
}
@@ -84,14 +61,4 @@ private FilledMode computeFilledMode(final LinesDataSet dataSet) {
8461
}
8562
return FilledMode.FILLED;
8663
}
87-
88-
private void addSeries(final LinesDataSet dataSet, final LinesChartModel model,
89-
final String name, final String seriesId, final String color, final FilledMode filledMode) {
90-
if (dataSet.containsSeries(seriesId)) {
91-
LineSeries branchSeries = new LineSeries(name,
92-
color, StackedMode.SEPARATE_LINES, filledMode, dataSet.getSeries(seriesId));
93-
94-
model.addSeries(branchSeries);
95-
}
96-
}
9764
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.jenkins.plugins.coverage.metrics.charts;
2+
3+
import java.util.Arrays;
4+
import java.util.Map;
5+
import java.util.function.Predicate;
6+
import java.util.stream.Collectors;
7+
8+
import edu.hm.hafner.coverage.Metric;
9+
import edu.hm.hafner.echarts.line.SeriesBuilder;
10+
11+
import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
12+
13+
/**
14+
* Builds one x-axis point for the series of a line chart showing the line and branch coverage of a project.
15+
*
16+
* @author Ullrich Hafner
17+
*/
18+
public class MetricSeriesBuilder extends SeriesBuilder<CoverageStatistics> {
19+
@Override
20+
protected Map<String, Double> computeSeries(final CoverageStatistics statistics) {
21+
return Arrays.stream(Metric.values())
22+
.filter(Predicate.not(Metric::isCoverage))
23+
.filter(statistics::containsValue)
24+
.collect(Collectors.toMap(Metric::toTagName, statistics::roundValue));
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.jenkins.plugins.coverage.metrics.charts;
2+
3+
import edu.hm.hafner.coverage.Metric;
4+
import edu.hm.hafner.echarts.BuildResult;
5+
import edu.hm.hafner.echarts.ChartModelConfiguration;
6+
import edu.hm.hafner.echarts.JacksonFacade;
7+
import edu.hm.hafner.echarts.line.LineSeries.FilledMode;
8+
import edu.hm.hafner.echarts.line.LinesChartModel;
9+
import edu.hm.hafner.echarts.line.LinesDataSet;
10+
11+
import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
12+
import io.jenkins.plugins.echarts.JenkinsPalette;
13+
14+
/**
15+
* Builds the Java side model for a trend chart showing the metrics of a project. The number of builds
16+
* to consider is controlled by a {@link ChartModelConfiguration} instance. The created model object can be serialized
17+
* to JSON (e.g., using the {@link JacksonFacade}) and can be used 1:1 as ECharts configuration object in the
18+
* corresponding JS file.
19+
*
20+
* @author Ullrich Hafner
21+
* @see JacksonFacade
22+
*/
23+
public class MetricsTrendChart extends TrendChart {
24+
@Override
25+
public LinesChartModel create(final Iterable<BuildResult<CoverageStatistics>> results,
26+
final ChartModelConfiguration configuration) {
27+
LinesDataSet dataSet = new MetricSeriesBuilder().createDataSet(configuration, results);
28+
29+
LinesChartModel model = new LinesChartModel(dataSet);
30+
if (dataSet.isNotEmpty()) {
31+
model.useContinuousRangeAxis();
32+
model.setRangeMax(dataSet.getMaximumValue());
33+
model.setRangeMin(dataSet.getMinimumValue());
34+
35+
int colorIndex = 0;
36+
for (var tag : dataSet.getDataSetIds()) {
37+
Metric metric = Metric.fromTag(tag);
38+
addSeriesIfAvailable(dataSet, model, metric.getDisplayName(),
39+
tag, JenkinsPalette.chartColor(colorIndex++).normal(),
40+
FilledMode.LINES);
41+
}
42+
}
43+
return model;
44+
}
45+
}

plugin/src/main/java/io/jenkins/plugins/coverage/metrics/charts/TreeMapNodeConverter.java

+10-12
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44

55
import edu.hm.hafner.coverage.Coverage;
66
import edu.hm.hafner.coverage.FileNode;
7-
import edu.hm.hafner.coverage.IntegerValue;
87
import edu.hm.hafner.coverage.Metric;
98
import edu.hm.hafner.coverage.ModuleNode;
109
import edu.hm.hafner.coverage.Node;
10+
import edu.hm.hafner.coverage.Value;
1111
import edu.hm.hafner.echarts.ItemStyle;
1212
import edu.hm.hafner.echarts.Label;
1313
import edu.hm.hafner.echarts.LabeledTreeMapNode;
1414
import edu.hm.hafner.echarts.TreeMapNode;
1515

16+
import hudson.Functions;
17+
1618
import io.jenkins.plugins.coverage.metrics.color.ColorProvider;
1719
import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors;
1820
import io.jenkins.plugins.coverage.metrics.color.ColorProviderFactory;
@@ -79,20 +81,16 @@ private Optional<LabeledTreeMapNode> toTreeMapNode(final Node node, final Metric
7981
if (rootValue instanceof Coverage) {
8082
return Optional.of(createCoverageTree((Coverage) rootValue, colorProvider, node, metric));
8183
}
82-
if (rootValue instanceof IntegerValue) {
83-
return Optional.of(createCoverageTree((IntegerValue) rootValue, node, metric));
84-
}
85-
throw new IllegalArgumentException("Unsupported value type: " + rootValue);
84+
return Optional.of(createMetricsTree(rootValue, node, metric));
8685
}
8786

8887
return Optional.empty();
8988
}
9089

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

95-
DisplayColors colors = CoverageLevel.getDisplayColorsOfCoverageLevel(coveragePercentage, colorProvider);
9694
String lineColor = colors.getLineColorAsRGBHex();
9795
String fillColor = colors.getFillColorAsRGBHex();
9896

@@ -119,16 +117,16 @@ private LabeledTreeMapNode createTreeNode(final Coverage coverage, final Node no
119117
String.valueOf(coverage.getTotal()), FORMATTER.getTooltip(coverage));
120118
}
121119

122-
private LabeledTreeMapNode createCoverageTree(final IntegerValue coverage, final Node node,
120+
private LabeledTreeMapNode createMetricsTree(final Value value, final Node node,
123121
final Metric metric) {
124122
Label label = new Label(true, JenkinsPalette.BLACK.normal());
125123

126124
String fillColor = metric == Metric.TESTS ? JenkinsPalette.GREEN.light() : JenkinsPalette.ORANGE.normal();
127125
if (node instanceof FileNode) {
128-
return createValueNode(coverage, node, new ItemStyle(fillColor), label);
126+
return createValueNode(value, node, new ItemStyle(fillColor), label);
129127
}
130128

131-
LabeledTreeMapNode treeNode = createValueNode(coverage, node,
129+
LabeledTreeMapNode treeNode = createValueNode(value, node,
132130
new ItemStyle(fillColor, fillColor, 4), label);
133131

134132
node.getChildren().stream()
@@ -139,9 +137,9 @@ private LabeledTreeMapNode createCoverageTree(final IntegerValue coverage, final
139137
return treeNode;
140138
}
141139

142-
private LabeledTreeMapNode createValueNode(final IntegerValue value, final Node node,
140+
private LabeledTreeMapNode createValueNode(final Value value, final Node node,
143141
final ItemStyle itemStyle, final Label label) {
144142
return new LabeledTreeMapNode(getId(node), node.getName(), itemStyle, label, label,
145-
String.valueOf(value.getValue()), FORMATTER.getTooltip(value));
143+
value.asText(Functions.getCurrentLocale()), FORMATTER.getTooltip(value));
146144
}
147145
}

0 commit comments

Comments
 (0)