Skip to content

Commit 29e3bd4

Browse files
committed
Added binary compatibility report
1 parent 560eafc commit 29e3bd4

File tree

4 files changed

+318
-1
lines changed

4 files changed

+318
-1
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ buildscript {
1717
dependencies {
1818
classpath 'org.asciidoctor:asciidoctor-gradle-plugin:0.7.3'
1919
classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:2.2.3'
20+
classpath 'me.champeau.gradle:japicmp-gradle-plugin:0.1.0'
2021
}
2122
}
2223

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2003-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* A template which generates an HTML report from the bincompat XML report
19+
*/
20+
modelTypes = {
21+
String title
22+
String baseline
23+
String archive
24+
Map<String,Map<String,List<String>>> violations
25+
}
26+
27+
def severityMapping = [
28+
error : 'danger',
29+
warning: 'warning',
30+
info : 'info',
31+
ignore : 'success'
32+
]
33+
34+
yieldUnescaped '<!DOCTYPE html>'
35+
36+
37+
html {
38+
head {
39+
meta 'charset': "utf-8"
40+
meta 'http-equiv': "content-type", content: "text/html; charset=utf-8"
41+
meta 'http-equiv': "X-UA-Compatible", content: "IE=edge"
42+
meta name: "viewport", content: "width=device-width, initial-scale=1"
43+
44+
title(title)
45+
link href: "http://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css", rel: "stylesheet"
46+
link href: "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css", rel: "stylesheet"
47+
link href: "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css", rel: "stylesheet"
48+
}
49+
50+
body {
51+
div(class:'navbar navbar-inverse navbar-fixed-top', role:'navigation') {
52+
div(class:'container') {
53+
div(class:'navbar-header') {
54+
button(type:'button', class:'navbar-toggle', 'data-toggle':'collapse', 'data-target':'navbar-collaspe') {
55+
span(class:'sr-only', 'Toggle navigation')
56+
span(class:'icon-bar'){}
57+
span(class:'icon-bar'){}
58+
span(class:'icon-bar'){}
59+
}
60+
a(class:'navbar-brand',href:'#', 'Binary compatibility report')
61+
}
62+
div(class:'navbar-collapse collapse') {
63+
ul(class:"nav navbar-nav") {
64+
li(class: 'dropdown') {
65+
a(id: 'severityDropdown', href: '#', class: 'dropdown-toggle', 'data-toggle': 'dropdown', 'Severity <span class="caret"></span>')
66+
ul(class: "dropdown-menu dropdown-severity", role: "menu") {
67+
li(role: 'presentation', class: 'active') {
68+
a(role: 'menuitem', tabindex: '-1', href: '#', 'All levels')
69+
}
70+
li(role: 'presentation') { a(role: 'menuitem', tabindex: '-1', href: '#', 'Error') }
71+
li(role: 'presentation') { a(role: 'menuitem', tabindex: '-1', href: '#', 'Warning') }
72+
li(role: 'presentation') { a(role: 'menuitem', tabindex: '-1', href: '#', 'Info') }
73+
}
74+
}
75+
76+
}
77+
}
78+
}
79+
}
80+
81+
82+
div(class: 'container') {
83+
div(class:'jumbotron') {
84+
div(class:'container') {
85+
div(class: 'page-header') {
86+
h1 'Binary compatibility'
87+
p "Comparing ${archive} to reference ${baseline}"
88+
p {
89+
yield "Be warned that this report is not perfect and depends on what "
90+
a(href: 'https://github.com/siom79/japicmp', 'JApicmp')
91+
yield " is capable to detect."
92+
}
93+
}
94+
}
95+
}
96+
violations.each { fqcn, classViolations ->
97+
def errors = classViolations.keySet()
98+
def severities = errors.collect { "severity-${it}" }
99+
div(class: "panel panel-default ${severities.join(' ')}") {
100+
div(class: "panel-heading") {
101+
h3(class: 'panel-title', "Class $fqcn")
102+
}
103+
div(class: 'panel-body') {
104+
table(class: "table table-striped table-bordered") {
105+
tbody {
106+
classViolations.each { err, list ->
107+
list.each { item ->
108+
tr(class: "bincompat-error severity-${err}") {
109+
td {
110+
h4 {
111+
span(class: "label label-${severityMapping[err]}", err.capitalize())
112+
}
113+
}
114+
td { span(item) }
115+
}
116+
}
117+
}
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
script(src: "http://code.jquery.com/jquery-1.11.0.min.js") {}
125+
script(src: "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js") {}
126+
script {
127+
yieldUnescaped '''
128+
$(document).ready(function () {
129+
var severity = null;
130+
doFilter();
131+
function doFilter() {
132+
var severityClass = "severity-" + severity;
133+
$('.panel').hide();
134+
$('.bincompat-error').hide();
135+
$('.bincompat-error').filter(function () {
136+
return (severity==null || $(this).hasClass(severityClass));
137+
}).show();
138+
$('.panel').filter(function () {
139+
return (severity==null || $(this).hasClass(severityClass));
140+
}).show();
141+
}
142+
$(".dropdown-severity li a").click(function() {
143+
severity = $(this).text().toLowerCase();
144+
if (severity==="all levels") {
145+
severity = null;
146+
}
147+
doFilter();
148+
});
149+
});'''
150+
}
151+
}
152+
}
153+
}

gradle/binarycompatibility.gradle

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2003-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import groovy.text.markup.MarkupTemplateEngine
18+
import groovy.text.markup.TemplateConfiguration
19+
20+
buildscript {
21+
// this block should not be necessary, but for some reason it fails without!
22+
repositories {
23+
jcenter()
24+
}
25+
26+
dependencies {
27+
classpath 'me.champeau.gradle:japicmp-gradle-plugin:0.1.0'
28+
}
29+
}
30+
31+
apply plugin: 'me.champeau.gradle.japicmp'
32+
33+
def referenceMinorVersion = '2.3.6'
34+
35+
def reportGenerator = { model ->
36+
outputProcessor {
37+
38+
def skipClass = { c ->
39+
c.fullyQualifiedName.startsWith('org.codehaus.groovy.runtime.dgm$') ||
40+
c.fullyQualifiedName.contains('_closure')
41+
}
42+
def skipMethod = { c, m -> skipClass(c) || m.name =~ /access\$[0-9]+/ }
43+
def violations = [:].withDefault {
44+
// key = class name
45+
// value = map of violations
46+
[:].withDefault { [] }
47+
}
48+
removedMethod { c, m ->
49+
if (!skipMethod(c, m)) {
50+
def level = m.name.startsWith('super$')?'info':'error'
51+
violations[c.fullyQualifiedName][level] << "Method ${m.name} has been removed"
52+
}
53+
}
54+
removedClass { c ->
55+
if (!skipClass(c)) {
56+
violations[c.fullyQualifiedName].error << "Class has been removed"
57+
}
58+
}
59+
60+
modifiedMethod { c, m ->
61+
if (!skipMethod(c,m)) {
62+
violations[c.fullyQualifiedName].warning << """<p>Method ${m.name} has been modified</p>
63+
<p>From <pre>${m.oldMethod.get()?.longName}</pre> to <pre>${m.newMethod.get()?.longName}</pre></p>"""
64+
}
65+
}
66+
67+
newClass { c ->
68+
if (!skipClass(c)) {
69+
violations[c.fullyQualifiedName].info << "Class has been added"
70+
}
71+
}
72+
newMethod { c, m ->
73+
if (!skipMethod(c,m)) {
74+
violations[c.fullyQualifiedName].info << """<p>Method ${m.name} has been added</p>
75+
<p>Signature: <pre>${m.newMethod.get()?.longName}</pre></p>"""
76+
}
77+
}
78+
after {
79+
model.violations = violations
80+
}
81+
}
82+
}
83+
84+
// using a global engine for all tasks in order to increase performance
85+
def configDir = file("$rootProject.projectDir/config/binarycompatibility")
86+
def templateFile = 'binarycompat-report.groovy'
87+
def templateConfiguration = new TemplateConfiguration()
88+
templateConfiguration.with {
89+
autoIndent = true
90+
autoNewLine = true
91+
}
92+
def engine = new MarkupTemplateEngine(this.class.classLoader, configDir, templateConfiguration)
93+
94+
95+
allprojects {
96+
97+
task japicmp(type: me.champeau.gradle.ArtifactJapicmpTask) {
98+
dependsOn jarjar
99+
baseline = "org.codehaus.groovy:${project.name}:${referenceMinorVersion}@jar"
100+
to = jarjar.archivePath
101+
accessModifier = 'protected'
102+
onlyModified = true
103+
failOnModification = false
104+
txtOutputFile = file("$buildDir/reports/japi.txt")
105+
106+
def htmlReportFile = file("${buildDir}/reports/binary-compat-${project.name}.html")
107+
inputs.file file("$configDir/$templateFile")
108+
inputs.file templateFile
109+
outputs.file htmlReportFile
110+
111+
def model = [title : "Binary compatibility report for ${project.name}",
112+
project : project,
113+
baseline: baseline,
114+
archive : to.name]
115+
outputProcessor(reportGenerator.curry(model))
116+
117+
doLast {
118+
htmlReportFile.withWriter('utf-8') { wrt ->
119+
engine.createTemplateByPath(templateFile).make(model).writeTo(wrt)
120+
}
121+
}
122+
123+
}
124+
}
125+
126+
task japicmpAll(type: me.champeau.gradle.ArtifactJapicmpTask) {
127+
dependsOn jarAll
128+
baseline = "org.codehaus.groovy:groovy-all:${referenceMinorVersion}@jar"
129+
to = jarAll.archivePath
130+
accessModifier = 'protected'
131+
onlyModified = true
132+
failOnModification = false
133+
txtOutputFile = file("$buildDir/reports/japi.txt")
134+
135+
def htmlReportFile = file("${buildDir}/reports/binary-compat-${project.name}-all.html")
136+
inputs.file file("$configDir/$templateFile")
137+
inputs.file templateFile
138+
outputs.file htmlReportFile
139+
140+
def model = [title: "Binary compatibility report for ${project.name}",
141+
project: project,
142+
baseline: baseline,
143+
archive: to.name]
144+
outputProcessor(reportGenerator.curry(model))
145+
146+
doLast {
147+
htmlReportFile.withWriter('utf-8') { wrt ->
148+
engine.createTemplateByPath(templateFile).make(model).writeTo(wrt)
149+
}
150+
}
151+
}
152+
153+
task checkBinaryCompatibility {
154+
description = 'Generates binary compatibility reports'
155+
}
156+
check.dependsOn(checkBinaryCompatibility)
157+
158+
allprojects {
159+
tasks.withType(me.champeau.gradle.ArtifactJapicmpTask) { task ->
160+
checkBinaryCompatibility.dependsOn(task)
161+
}
162+
}

gradle/quality.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,5 @@ allprojects {
8686

8787
}
8888

89-
apply from: 'gradle/jacoco/jacoco.gradle'
89+
apply from: 'gradle/jacoco/jacoco.gradle'
90+
apply from: 'gradle/binarycompatibility.gradle'

0 commit comments

Comments
 (0)