Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

Commit b05873c

Browse files
authored
Merge pull request #195 from simonsymhoven/pull-request-monitoring-portlet
Add coverage portlet for pull-request-monitoring plugin
2 parents 4399614 + 5a89f78 commit b05873c

File tree

4 files changed

+310
-0
lines changed

4 files changed

+310
-0
lines changed

pom.xml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
<assertj-core.version>3.16.1</assertj-core.version>
2323
<echarts-api.version>5.1.0-2</echarts-api.version>
2424
<forensics-api.version>1.0.0</forensics-api.version>
25+
<pull-request-monitoring.version>1.7.1</pull-request-monitoring.version>
26+
<plugin-util-api.version>2.2.0</plugin-util-api.version>
27+
<fontawesome-api.version>5.15.3-2</fontawesome-api.version>
28+
<gson.version>2.8.6</gson.version>
29+
<workflow-cps.version>2.92</workflow-cps.version>
30+
<workflow-multibranch.version>2.24</workflow-multibranch.version>
2531
<!-- Other properties you may want to use:
2632
~ jenkins-test-harness.version: Jenkins Test Harness version you use to test the plugin. For Jenkins version >= 1.580.1 use JTH 2.0 or higher.
2733
~ hpi-plugin.version: The HPI Maven Plugin version used by the plugin..
@@ -66,6 +72,13 @@
6672
<version>${saxon-he.version}</version>
6773
</dependency>
6874

75+
<!-- Workflow Dependencies -->
76+
<dependency>
77+
<groupId>org.jenkins-ci.plugins.workflow</groupId>
78+
<artifactId>workflow-multibranch</artifactId>
79+
<version>${workflow-multibranch.version}</version>
80+
</dependency>
81+
6982
<!-- Plugin Dependencies -->
7083
<dependency>
7184
<groupId>org.jenkins-ci.plugins</groupId>
@@ -89,6 +102,27 @@
89102
<artifactId>forensics-api</artifactId>
90103
<version>${forensics-api.version}</version>
91104
</dependency>
105+
<dependency>
106+
<groupId>io.jenkins.plugins</groupId>
107+
<artifactId>plugin-util-api</artifactId>
108+
<version>${plugin-util-api.version}</version>
109+
</dependency>
110+
<dependency>
111+
<groupId>io.jenkins.plugins</groupId>
112+
<artifactId>font-awesome-api</artifactId>
113+
<version>${fontawesome-api.version}</version>
114+
</dependency>
115+
<dependency>
116+
<groupId>io.jenkins.plugins</groupId>
117+
<artifactId>pull-request-monitoring</artifactId>
118+
<version>${pull-request-monitoring.version}</version>
119+
<optional>true</optional>
120+
</dependency>
121+
<dependency>
122+
<groupId>com.google.code.gson</groupId>
123+
<artifactId>gson</artifactId>
124+
<version>${gson.version}</version>
125+
</dependency>
92126

93127
<!-- Test Dependencies -->
94128
<dependency>
@@ -99,6 +133,7 @@
99133
<dependency>
100134
<groupId>org.jenkins-ci.plugins.workflow</groupId>
101135
<artifactId>workflow-cps</artifactId>
136+
<version>${workflow-cps.version}</version>
102137
<scope>test</scope>
103138
</dependency>
104139
<dependency>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package io.jenkins.plugins.coverage;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonObject;
5+
import hudson.Extension;
6+
import hudson.model.Run;
7+
import io.jenkins.plugins.coverage.targets.CoverageElement;
8+
import io.jenkins.plugins.coverage.targets.Ratio;
9+
import io.jenkins.plugins.monitoring.MonitorPortlet;
10+
import io.jenkins.plugins.monitoring.MonitorPortletFactory;
11+
12+
import java.util.*;
13+
14+
/**
15+
* A portlet that can be used for the
16+
* <a href="https://github.com/jenkinsci/pull-request-monitoring-plugin">pull-request-monitoring</a> dashboard.
17+
*
18+
* It renders the aggregated line and conditional coverage in a stacked bar chart and displays the delta,
19+
* if a reference build is found.
20+
*
21+
* @author Simon Symhoven
22+
*/
23+
public class CoveragePullRequestMonitoringPortlet extends MonitorPortlet {
24+
private final CoverageAction action;
25+
26+
/**
27+
* Creates a new {@link CoveragePullRequestMonitoringPortlet}.
28+
*
29+
* @param action
30+
* the {@link CoverageAction} of corresponding run.
31+
*/
32+
public CoveragePullRequestMonitoringPortlet(final CoverageAction action) {
33+
super();
34+
this.action = action;
35+
}
36+
37+
@Override
38+
public String getTitle() {
39+
return action.getDisplayName();
40+
}
41+
42+
@Override
43+
public String getId() {
44+
return "code-coverage";
45+
}
46+
47+
@Override
48+
public boolean isDefault() {
49+
return true;
50+
}
51+
52+
@Override
53+
public int getPreferredWidth() {
54+
return 600;
55+
}
56+
57+
@Override
58+
public int getPreferredHeight() {
59+
return 350;
60+
}
61+
62+
@Override
63+
public Optional<String> getIconUrl() {
64+
return Optional.of("/images/48x48/graph.png");
65+
}
66+
67+
@Override
68+
public Optional<String> getDetailViewUrl() {
69+
return Optional.ofNullable(action.getUrlName());
70+
}
71+
72+
/**
73+
* Get the json data for the stacked bar chart. (used by jelly view)
74+
*
75+
* @return
76+
* the data as json string.
77+
*/
78+
public String getCoverageResultsAsJsonModel() {
79+
Ratio line = action.getResult().getResults().get(CoverageElement.LINE);
80+
Ratio conditional = action.getResult().getResults().get(CoverageElement.CONDITIONAL);
81+
82+
JsonObject data = new JsonObject();
83+
84+
JsonArray metrics = new JsonArray();
85+
metrics.add(CoverageElement.LINE.getName());
86+
metrics.add(CoverageElement.CONDITIONAL.getName());
87+
data.add("metrics", metrics);
88+
89+
JsonArray covered = new JsonArray();
90+
covered.add(line.numerator);
91+
covered.add(conditional.numerator);
92+
data.add("covered", covered);
93+
94+
JsonArray missed = new JsonArray();
95+
missed.add(line.denominator - line.numerator);
96+
missed.add(conditional.denominator - conditional.numerator);
97+
data.add("missed", missed);
98+
99+
JsonArray coveredPercentage = new JsonArray();
100+
coveredPercentage.add(line.denominator == 0 ? 0 : (double) (100 * (covered.get(0).getAsInt() / line.denominator)));
101+
coveredPercentage.add(conditional.denominator == 0 ? 0 : (double) (100 * (covered.get(1).getAsInt() / conditional.denominator)));
102+
data.add("coveredPercentage", coveredPercentage);
103+
104+
JsonArray missedPercentage = new JsonArray();
105+
missedPercentage.add(100 - coveredPercentage.get(0).getAsDouble());
106+
missedPercentage.add(100 - coveredPercentage.get(1).getAsDouble());
107+
data.add("missedPercentage", missedPercentage);
108+
109+
String deltaLineLabel = getReferenceBuildUrl().isPresent()
110+
? String.format("%.2f%% (%s %.2f%%)", coveredPercentage.get(0).getAsDouble(), (char) 0x0394,
111+
action.getResult().getCoverageDelta(CoverageElement.LINE))
112+
: String.format("%.2f%% (%s unknown)", coveredPercentage.get(0).getAsDouble(), (char) 0x0394);
113+
114+
String deltaConditionalLabel = getReferenceBuildUrl().isPresent()
115+
? String.format("%.2f%% (%s %.2f%%)", coveredPercentage.get(1).getAsDouble(), (char) 0x0394,
116+
action.getResult().getCoverageDelta(CoverageElement.CONDITIONAL))
117+
: String.format("%.2f%% (%s unknown)", coveredPercentage.get(1).getAsDouble(), (char) 0x0394);
118+
119+
JsonArray coveredPercentageLabels = new JsonArray();
120+
coveredPercentageLabels.add(deltaLineLabel);
121+
coveredPercentageLabels.add(deltaConditionalLabel);
122+
data.add("coveredPercentageLabels", coveredPercentageLabels);
123+
124+
return data.toString();
125+
}
126+
127+
/**
128+
* Get the link to the build, that was used to compare the result with.
129+
*
130+
* @return
131+
* optional of the link to the build or empty optional.
132+
*/
133+
public Optional<String> getReferenceBuildUrl() {
134+
return Optional.ofNullable(action.getResult().getReferenceBuildUrl());
135+
}
136+
137+
/**
138+
* The factory for the {@link CoveragePullRequestMonitoringPortlet}.
139+
*/
140+
@Extension(optional = true)
141+
public static class PortletFactory extends MonitorPortletFactory {
142+
143+
@Override
144+
public Collection<MonitorPortlet> getPortlets(Run<?, ?> build) {
145+
CoverageAction action = build.getAction(CoverageAction.class);
146+
147+
if (action == null) {
148+
return Collections.emptyList();
149+
}
150+
151+
return Collections.singleton(new CoveragePullRequestMonitoringPortlet(action));
152+
}
153+
154+
@Override
155+
public String getDisplayName() {
156+
return "Code Coverage API";
157+
}
158+
}
159+
160+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?jelly escape-by-default='true'?>
2+
3+
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
4+
5+
<st:adjunct includes="io.jenkins.plugins.echarts"/>
6+
<st:adjunct includes="io.jenkins.plugins.bootstrap5"/>
7+
8+
<div id="coverage-pr-portlet" data="${it.getCoverageResultsAsJsonModel()}"
9+
style="width: ${it.preferredWidth}px; height: ${it.preferredHeight - 100}px;"/>
10+
11+
<script type="text/javascript" src="${rootURL}/plugin/code-coverage-api/scripts/coverage-portlet.js"/>
12+
13+
<script type="text/javascript">
14+
new CoveragePortletChart('coverage-pr-portlet');
15+
</script>
16+
17+
</j:jelly>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
2+
var CoveragePortletChart = function (id) {
3+
4+
const chartDom = document.getElementById(id);
5+
const portletChart = echarts.init(chartDom);
6+
const data = JSON.parse(chartDom.getAttribute('data'));
7+
8+
const covered = data.covered;
9+
const missed = data.missed;
10+
11+
const option = {
12+
tooltip: {
13+
trigger: 'axis',
14+
axisPointer: {
15+
type: 'shadow'
16+
},
17+
formatter: function (obj) {
18+
if (Array.isArray(obj)) {
19+
if (obj.length === 2) {
20+
return '<div style="text-align: left"><b>' + obj[0].name + '</b><br/>'
21+
+ obj[0].marker + ' ' + obj[0].seriesName + '&nbsp;&nbsp;' + covered[obj[0].dataIndex] + '<br/>'
22+
+ obj[1].marker + ' ' + obj[1].seriesName + '&nbsp;&nbsp;&nbsp;&nbsp;' + missed[obj[1].dataIndex] + '</div>';
23+
24+
} else if (obj.length === 1) {
25+
return '<div style="text-align: left"><b>' + obj[0].name + '</b><br/>'
26+
+ obj[0].marker + ' ' + obj[0].seriesName + '&nbsp;&nbsp;'
27+
+ (obj[0].seriesName === 'Covered' ? covered[obj[0].dataIndex] : missed[obj[0].dataIndex]) + '</div>';
28+
}
29+
}
30+
}
31+
},
32+
legend: {
33+
data: ['Covered', 'Missed']
34+
},
35+
grid: {
36+
left: '3%',
37+
right: '4%',
38+
bottom: '3%',
39+
containLabel: true
40+
},
41+
xAxis: {
42+
type: 'value',
43+
name: 'in %',
44+
},
45+
yAxis: [{
46+
type: 'category',
47+
data: data.metrics,
48+
axisLine: {
49+
show: false
50+
},
51+
axisTick: {
52+
show: false
53+
}
54+
}, {
55+
type: 'category',
56+
data: data.coveredPercentageLabels,
57+
position: 'right',
58+
axisLine: {
59+
show: false
60+
},
61+
axisTick: {
62+
show: false
63+
}
64+
}],
65+
series: [
66+
{
67+
name: 'Covered',
68+
type: 'bar',
69+
stack: 'sum',
70+
itemStyle: {
71+
normal: {
72+
color: '#A5D6A7'
73+
}
74+
},
75+
emphasis: {
76+
focus: 'series'
77+
},
78+
data: data.coveredPercentage
79+
},
80+
{
81+
name: 'Missed',
82+
type: 'bar',
83+
stack: 'sum',
84+
itemStyle: {
85+
normal: {
86+
color: '#EF9A9A'
87+
}
88+
},
89+
emphasis: {
90+
focus: 'series'
91+
},
92+
data: data.missedPercentage
93+
}
94+
]
95+
};
96+
97+
option && portletChart.setOption(option)
98+
}

0 commit comments

Comments
 (0)