Skip to content

Commit

Permalink
Merge pull request #195 from simonsymhoven/pull-request-monitoring-po…
Browse files Browse the repository at this point in the history
…rtlet

Add coverage portlet for pull-request-monitoring plugin
  • Loading branch information
uhafner committed Jun 4, 2021
2 parents 4399614 + 5a89f78 commit b05873c
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 0 deletions.
35 changes: 35 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
<assertj-core.version>3.16.1</assertj-core.version>
<echarts-api.version>5.1.0-2</echarts-api.version>
<forensics-api.version>1.0.0</forensics-api.version>
<pull-request-monitoring.version>1.7.1</pull-request-monitoring.version>
<plugin-util-api.version>2.2.0</plugin-util-api.version>
<fontawesome-api.version>5.15.3-2</fontawesome-api.version>
<gson.version>2.8.6</gson.version>
<workflow-cps.version>2.92</workflow-cps.version>
<workflow-multibranch.version>2.24</workflow-multibranch.version>
<!-- Other properties you may want to use:
~ 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.
~ hpi-plugin.version: The HPI Maven Plugin version used by the plugin..
Expand Down Expand Up @@ -66,6 +72,13 @@
<version>${saxon-he.version}</version>
</dependency>

<!-- Workflow Dependencies -->
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-multibranch</artifactId>
<version>${workflow-multibranch.version}</version>
</dependency>

<!-- Plugin Dependencies -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
Expand All @@ -89,6 +102,27 @@
<artifactId>forensics-api</artifactId>
<version>${forensics-api.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>plugin-util-api</artifactId>
<version>${plugin-util-api.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>font-awesome-api</artifactId>
<version>${fontawesome-api.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>pull-request-monitoring</artifactId>
<version>${pull-request-monitoring.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>

<!-- Test Dependencies -->
<dependency>
Expand All @@ -99,6 +133,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>${workflow-cps.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package io.jenkins.plugins.coverage;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import hudson.Extension;
import hudson.model.Run;
import io.jenkins.plugins.coverage.targets.CoverageElement;
import io.jenkins.plugins.coverage.targets.Ratio;
import io.jenkins.plugins.monitoring.MonitorPortlet;
import io.jenkins.plugins.monitoring.MonitorPortletFactory;

import java.util.*;

/**
* A portlet that can be used for the
* <a href="https://github.com/jenkinsci/pull-request-monitoring-plugin">pull-request-monitoring</a> dashboard.
*
* It renders the aggregated line and conditional coverage in a stacked bar chart and displays the delta,
* if a reference build is found.
*
* @author Simon Symhoven
*/
public class CoveragePullRequestMonitoringPortlet extends MonitorPortlet {
private final CoverageAction action;

/**
* Creates a new {@link CoveragePullRequestMonitoringPortlet}.
*
* @param action
* the {@link CoverageAction} of corresponding run.
*/
public CoveragePullRequestMonitoringPortlet(final CoverageAction action) {
super();
this.action = action;
}

@Override
public String getTitle() {
return action.getDisplayName();
}

@Override
public String getId() {
return "code-coverage";
}

@Override
public boolean isDefault() {
return true;
}

@Override
public int getPreferredWidth() {
return 600;
}

@Override
public int getPreferredHeight() {
return 350;
}

@Override
public Optional<String> getIconUrl() {
return Optional.of("/images/48x48/graph.png");
}

@Override
public Optional<String> getDetailViewUrl() {
return Optional.ofNullable(action.getUrlName());
}

/**
* Get the json data for the stacked bar chart. (used by jelly view)
*
* @return
* the data as json string.
*/
public String getCoverageResultsAsJsonModel() {
Ratio line = action.getResult().getResults().get(CoverageElement.LINE);
Ratio conditional = action.getResult().getResults().get(CoverageElement.CONDITIONAL);

JsonObject data = new JsonObject();

JsonArray metrics = new JsonArray();
metrics.add(CoverageElement.LINE.getName());
metrics.add(CoverageElement.CONDITIONAL.getName());
data.add("metrics", metrics);

JsonArray covered = new JsonArray();
covered.add(line.numerator);
covered.add(conditional.numerator);
data.add("covered", covered);

JsonArray missed = new JsonArray();
missed.add(line.denominator - line.numerator);
missed.add(conditional.denominator - conditional.numerator);
data.add("missed", missed);

JsonArray coveredPercentage = new JsonArray();
coveredPercentage.add(line.denominator == 0 ? 0 : (double) (100 * (covered.get(0).getAsInt() / line.denominator)));
coveredPercentage.add(conditional.denominator == 0 ? 0 : (double) (100 * (covered.get(1).getAsInt() / conditional.denominator)));
data.add("coveredPercentage", coveredPercentage);

JsonArray missedPercentage = new JsonArray();
missedPercentage.add(100 - coveredPercentage.get(0).getAsDouble());
missedPercentage.add(100 - coveredPercentage.get(1).getAsDouble());
data.add("missedPercentage", missedPercentage);

String deltaLineLabel = getReferenceBuildUrl().isPresent()
? String.format("%.2f%% (%s %.2f%%)", coveredPercentage.get(0).getAsDouble(), (char) 0x0394,
action.getResult().getCoverageDelta(CoverageElement.LINE))
: String.format("%.2f%% (%s unknown)", coveredPercentage.get(0).getAsDouble(), (char) 0x0394);

String deltaConditionalLabel = getReferenceBuildUrl().isPresent()
? String.format("%.2f%% (%s %.2f%%)", coveredPercentage.get(1).getAsDouble(), (char) 0x0394,
action.getResult().getCoverageDelta(CoverageElement.CONDITIONAL))
: String.format("%.2f%% (%s unknown)", coveredPercentage.get(1).getAsDouble(), (char) 0x0394);

JsonArray coveredPercentageLabels = new JsonArray();
coveredPercentageLabels.add(deltaLineLabel);
coveredPercentageLabels.add(deltaConditionalLabel);
data.add("coveredPercentageLabels", coveredPercentageLabels);

return data.toString();
}

/**
* Get the link to the build, that was used to compare the result with.
*
* @return
* optional of the link to the build or empty optional.
*/
public Optional<String> getReferenceBuildUrl() {
return Optional.ofNullable(action.getResult().getReferenceBuildUrl());
}

/**
* The factory for the {@link CoveragePullRequestMonitoringPortlet}.
*/
@Extension(optional = true)
public static class PortletFactory extends MonitorPortletFactory {

@Override
public Collection<MonitorPortlet> getPortlets(Run<?, ?> build) {
CoverageAction action = build.getAction(CoverageAction.class);

if (action == null) {
return Collections.emptyList();
}

return Collections.singleton(new CoveragePullRequestMonitoringPortlet(action));
}

@Override
public String getDisplayName() {
return "Code Coverage API";
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?jelly escape-by-default='true'?>

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">

<st:adjunct includes="io.jenkins.plugins.echarts"/>
<st:adjunct includes="io.jenkins.plugins.bootstrap5"/>

<div id="coverage-pr-portlet" data="${it.getCoverageResultsAsJsonModel()}"
style="width: ${it.preferredWidth}px; height: ${it.preferredHeight - 100}px;"/>

<script type="text/javascript" src="${rootURL}/plugin/code-coverage-api/scripts/coverage-portlet.js"/>

<script type="text/javascript">
new CoveragePortletChart('coverage-pr-portlet');
</script>

</j:jelly>
98 changes: 98 additions & 0 deletions src/main/webapp/scripts/coverage-portlet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

var CoveragePortletChart = function (id) {

const chartDom = document.getElementById(id);
const portletChart = echarts.init(chartDom);
const data = JSON.parse(chartDom.getAttribute('data'));

const covered = data.covered;
const missed = data.missed;

const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (obj) {
if (Array.isArray(obj)) {
if (obj.length === 2) {
return '<div style="text-align: left"><b>' + obj[0].name + '</b><br/>'
+ obj[0].marker + ' ' + obj[0].seriesName + '&nbsp;&nbsp;' + covered[obj[0].dataIndex] + '<br/>'
+ obj[1].marker + ' ' + obj[1].seriesName + '&nbsp;&nbsp;&nbsp;&nbsp;' + missed[obj[1].dataIndex] + '</div>';

} else if (obj.length === 1) {
return '<div style="text-align: left"><b>' + obj[0].name + '</b><br/>'
+ obj[0].marker + ' ' + obj[0].seriesName + '&nbsp;&nbsp;'
+ (obj[0].seriesName === 'Covered' ? covered[obj[0].dataIndex] : missed[obj[0].dataIndex]) + '</div>';
}
}
}
},
legend: {
data: ['Covered', 'Missed']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
name: 'in %',
},
yAxis: [{
type: 'category',
data: data.metrics,
axisLine: {
show: false
},
axisTick: {
show: false
}
}, {
type: 'category',
data: data.coveredPercentageLabels,
position: 'right',
axisLine: {
show: false
},
axisTick: {
show: false
}
}],
series: [
{
name: 'Covered',
type: 'bar',
stack: 'sum',
itemStyle: {
normal: {
color: '#A5D6A7'
}
},
emphasis: {
focus: 'series'
},
data: data.coveredPercentage
},
{
name: 'Missed',
type: 'bar',
stack: 'sum',
itemStyle: {
normal: {
color: '#EF9A9A'
}
},
emphasis: {
focus: 'series'
},
data: data.missedPercentage
}
]
};

option && portletChart.setOption(option)
}

0 comments on commit b05873c

Please sign in to comment.