Skip to content

Commit

Permalink
Fix Issue #93: Support columns detail for Clover reports
Browse files Browse the repository at this point in the history
Introduce a new closure backed configuration element in reports
following the columns nested element for clover-report. This is
implemented as a dynamic method construct so that the syntax is
easier to read.

We do not support the expression column since it is rather obsure
and would require additional syntax support to work.

Migrated several support classes to Java to prepare for the overall
Gradle 4 work which will eventually convert most of the production
code into Java from Groovy.
  • Loading branch information
Alex-Vol-SV committed Jul 10, 2017
1 parent 78592c1 commit 310dd2e
Show file tree
Hide file tree
Showing 16 changed files with 425 additions and 80 deletions.
15 changes: 15 additions & 0 deletions README.md
Expand Up @@ -129,6 +129,13 @@ is applied.
* `testResultsDir`: Specifies the location of the JUnit4 test results XML report. This is necessary when the test use the new JUnit4 @Rule mechanism to declare expected exceptions. Clover fails to detect coverage for methods when this mechanism is used. This solution uses a feature of Clover that takes the coverage information directly from the JUnit XML report.
* `testResultsInclude`: If testResultsDir is specified this must be an Ant file name pattern to select the correct XML files within the directory (defaults to TEST-*.xml).

Within `report` closure you can define a closure named `columns` to enable selection of columns for the report output. This feature implements support for the columns defined in Clover documentation [Clover ReportComumns Nested Element](https://confluence.atlassian.com/clover/clover-report-71600095.html#clover-report-Columns). Each line in the closure must begin with the name of the column to add followed by a Groovy map with the 4 optional attributes for the column. We support `format`, `min`, `max` and `scope`. The format and scope values are checked against the documented supported contents and will throw errors if unsupported values are used. We do not implement support for the `expression` column at this time, if you attempt to use it the plugin will throw an error:

* `format`: Determines how the value is rendered. Depending on the column, this may be one of raw, bar, % or longbar.
* `min`: Sets a minimum threshold on the value of the column. If the value is less than this it will be highlighted.
* `max`: Sets a maximum threshold on the value of the column. If the value is greater than this it will be highlighted.
* `scope`: Controls at which level in the report the column will appear. The scope attribute can be one of: "package", "class" or "method". If omitted, the column will be used at every level in the report. Note that only the following columns support the scope attribute: expression, complexity, complexityDensity, coveredXXX, uncoveredXXX and totalXXX.


Within `report` closure you can define a closure named `historical` to enable Clover historical reports generation:

Expand Down Expand Up @@ -217,6 +224,14 @@ The Clover plugin defines the following convention properties in the `clover` cl
testResultsDir = project.tasks.getByName('test').reports.junitXml.destination
testResultsInclude = 'TEST-*.xml'

// Clover report nested columns support
columns {
coveredMethods format: 'longbar', min: '75'
coveredStatements format: '%'
coveredBranches format: 'raw'
totalPercentageCovered format: '%', scope: 'package'
}

// Clover history generation support
// See Clover documentation for details of the values supported
historical {
Expand Down
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
@@ -1,5 +1,9 @@
### Version 2.1.2 (July 10, 2017)

New features
* Support columns detail for Clover reports - [Issue 93](https://github.com/bmuschko/gradle-clover-plugin/issues/93)
See the documentation in [README.md](README.md) for the new usage.

* Fix Add **/*Spec.groovy to default clover.testIncludes - [Issue 90](https://github.com/bmuschko/gradle-clover-plugin/issues/90)
* Fix Tests fail due to differences in polish characters - [Issue 92](https://github.com/bmuschko/gradle-clover-plugin/issues/92)

Expand Down
7 changes: 7 additions & 0 deletions src/integTest/projects/java-project/build.gradle
Expand Up @@ -47,6 +47,13 @@ clover {
testResultsDir = project.tasks.getByName('test').reports.junitXml.destination
testResultsInclude = 'TEST-*.xml'

columns {
coveredMethods format: 'longbar', min: '75'
coveredStatements format: '%'
coveredBranches format: 'raw'
totalPercentageCovered format: '%', scope: 'package'
}

historical {
enabled = true
added {
Expand Down
Expand Up @@ -238,6 +238,8 @@ class CloverPlugin implements Plugin<Project> {
map('json') { cloverPluginConvention.report.json }
map('html') { cloverPluginConvention.report.html }
map('pdf') { cloverPluginConvention.report.pdf }

map('additionalColumns') { cloverPluginConvention.report.columns.getColumns() }

cloverPluginConvention.report.historical.with {
map('historical') { enabled }
Expand Down
Expand Up @@ -36,4 +36,10 @@ class CloverReportConvention {
def historical(Closure closure) {
ConfigureUtil.configure(closure, historical)
}

CloverReportColumnsConvention columns = new CloverReportColumnsConvention()

def columns(Closure closure) {
ConfigureUtil.configure(closure, columns)
}
}

This file was deleted.

10 changes: 10 additions & 0 deletions src/main/groovy/com/bmuschko/gradle/clover/CloverReportTask.groovy
Expand Up @@ -58,6 +58,8 @@ abstract class CloverReportTask extends DefaultTask {
Boolean pdf
Boolean historical

Collection<CloverReportColumn> additionalColumns

/**
* Optional Clover history directory.
*/
Expand Down Expand Up @@ -179,6 +181,14 @@ abstract class CloverReportTask extends DefaultTask {
if (testResultsDir) {
testresults(dir: testResultsDir, includes: testResultsInclude)
}
if (getAdditionalColumns()) {
columns {
for (CloverReportColumn col in getAdditionalColumns()) {
String name = col.getColumn()
"$name"(col.getAttributes())
}
}
}
}

// Historical report is supported only for HTML and PDF reports
Expand Down

This file was deleted.

This file was deleted.

134 changes: 134 additions & 0 deletions src/main/java/com/bmuschko/gradle/clover/CloverReportColumn.java
@@ -0,0 +1,134 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bmuschko.gradle.clover;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import org.gradle.api.InvalidUserDataException;

public class CloverReportColumn {
public CloverReportColumn(String column, Map<String, String> attributes) {
this.column = column;
this.attributes = new HashMap<>(attributes.size());
for (Map.Entry<String, String> entry : attributes.entrySet()) {
String key = entry.getKey();
if (key.equals("format")) {
assertValidFormat(column, entry.getValue());
} else if (key.equals("min") || key.equals("max")) {
assertValidNumber(entry.getValue());
} else if (key.equals("scope")) {
assertValidScope(entry.getValue());
} else {
throw new InvalidUserDataException("Invalid column attribute name: " + key);
}
this.attributes.put(key, entry.getValue());
}
}

private final String column;
public String getColumn() {
return column;
}

private final Map<String, String> attributes;
public Map<String, String> getAttributes() {
return Collections.unmodifiableMap(attributes);
}

private void assertValidFormat(String name, String format) {
FormatValidator validator = validColumns.get(name);
if (!validator.valid(format)) {
throw new InvalidUserDataException("Invalid column format specification: " + format);
}
}

private void assertValidNumber(String value) {
Integer.parseInt(value, 10);
}

private static final Pattern scopeValidator = Pattern.compile("(package|class|method)");
private void assertValidScope(String scope) {
if (!scopeValidator.matcher(scope).matches()) {
throw new InvalidUserDataException("Invalid column scope specification: " + scope);
}
}

public static boolean validColumn(String column) {
return validColumns.containsKey(column);
}

private static final Map<String, FormatValidator> validColumns;
static {
RawFormatValidator raw = new RawFormatValidator();
MultiFormatValidator multi = new MultiFormatValidator();

validColumns = new HashMap<String, FormatValidator>();
validColumns.put("avgClassesPerFile", raw);
validColumns.put("avgMethodComplexity", raw);
validColumns.put("avgMethodsPerClass", raw);
validColumns.put("avgStatementsPerMethod", raw);
validColumns.put("complexity", raw);
validColumns.put("complexityDensity", raw);
validColumns.put("coveredBranches", multi);
validColumns.put("coveredElements", multi);
validColumns.put("coveredMethods", multi);
validColumns.put("coveredStatements", multi);
//validColumns.put("expression", raw);
validColumns.put("filteredElements", multi);
validColumns.put("ncLineCount", raw);
validColumns.put("lineCount", raw);
validColumns.put("SUM", raw);
validColumns.put("percentageCoveredContribution", multi);
validColumns.put("percentageUncoveredContribution", multi);
validColumns.put("totalBranches", raw);
validColumns.put("totalChildren", raw);
validColumns.put("totalClasses", raw);
validColumns.put("totalElements", raw);
validColumns.put("totalFiles", raw);
validColumns.put("totalMethods", raw);
validColumns.put("totalPercentageCovered", multi);
validColumns.put("totalStatements", raw);
validColumns.put("uncoveredBranches", multi);
validColumns.put("uncoveredElements", multi);
validColumns.put("uncoveredMethods", multi);
validColumns.put("uncoveredStatements", multi);

validColumns.put("files", raw);
validColumns.put("methods", raw);
}

private interface FormatValidator {
boolean valid(String format);
}

private static class RawFormatValidator implements FormatValidator {
@Override
public boolean valid(String format) {
return "raw".equals(format);
}
}

private static class MultiFormatValidator implements FormatValidator {
private static final Pattern formatValidator = Pattern.compile("(raw|bar|longbar|%)");
@Override
public boolean valid(String format) {
return formatValidator.matcher(format).matches();
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bmuschko.gradle.clover;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import org.gradle.internal.metaobject.InvokeMethodResult;
import org.gradle.internal.metaobject.MethodAccess;
import org.gradle.internal.metaobject.MethodMixIn;

import groovy.lang.GroovyObjectSupport;

public class CloverReportColumnsConvention extends GroovyObjectSupport implements MethodMixIn {
private final Collection<CloverReportColumn> columns;
private final DynamicMethods dynamicMethods;

public CloverReportColumnsConvention() {
columns = new ArrayList<CloverReportColumn>();
dynamicMethods = new DynamicMethods();
}

public Collection<CloverReportColumn> getColumns() {
return Collections.unmodifiableCollection(columns);
}

private void add(String column, Map<String, String> attributes) {
columns.add(new CloverReportColumn(column, attributes));
}

@Override
public MethodAccess getAdditionalMethods() {
return dynamicMethods;
}

private class DynamicMethods implements MethodAccess {
@Override
public boolean hasMethod(String name, Object... arguments) {
return arguments.length == 1 && arguments[0] instanceof Map && validColumnName(name);
}

private boolean validColumnName(String name) {
return CloverReportColumn.validColumn(name);
}

@SuppressWarnings("unchecked")
@Override
public void invokeMethod(String name, InvokeMethodResult result, Object... arguments) {
CloverReportColumnsConvention.this.add(name, (Map<String, String>) arguments[0]);
result.result(null);
}
}
}

0 comments on commit 310dd2e

Please sign in to comment.