Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,17 @@
private void add(final ListBoxModel options, final Baseline baseline) {
options.add(getDisplayName(baseline), baseline.name());
}

/**
* Returns all available aggregation modes as a {@link ListBoxModel}.
*
* @return the aggregation modes in a {@link ListBoxModel}
*/
public ListBoxModel getAggregationItems() {
var options = new ListBoxModel();
for (MetricAggregation aggregation : MetricAggregation.values()) {
options.add(aggregation.name(), aggregation.name());
}
return options;

Check warning on line 440 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/ElementFormatter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 436-440 are not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.jenkins.plugins.coverage.metrics.model;

import edu.hm.hafner.coverage.Metric;

/**
* Defines the aggregation mode for software metrics that can be aggregated in different ways (e.g., cyclomatic
* complexity can be reported as total, maximum, or average). For coverage metrics, this aggregation is not applicable
* and will be ignored.
*
* @author Akash Manna
*/
public enum MetricAggregation {
/** The total value of the metric (sum of all values). */
TOTAL,
/** The maximum value of the metric. */
MAXIMUM,
/** The average value of the metric. */
AVERAGE;

/**
* Returns whether the specified metric supports aggregation modes.
*
* @param metric
* the metric to check
*
* @return {@code true} if the metric supports aggregation modes, {@code false} otherwise
*/
public static boolean isSupported(final Metric metric) {
return !metric.isCoverage();
}

/**
* Returns the default aggregation mode for the specified metric.
*
* @param metric
* the metric to get the default aggregation for
*
* @return the default aggregation mode
*/
public static MetricAggregation getDefault(final Metric metric) {
return TOTAL;

Check warning on line 41 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/model/MetricAggregation.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 41 is not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import io.jenkins.plugins.coverage.metrics.model.Baseline;
import io.jenkins.plugins.coverage.metrics.model.ElementFormatter;
import io.jenkins.plugins.coverage.metrics.model.MetricAggregation;
import io.jenkins.plugins.util.JenkinsFacade;
import io.jenkins.plugins.util.QualityGate;

Expand All @@ -24,7 +25,7 @@
*
* @author Johannes Walter
*/
public class CoverageQualityGate extends QualityGate {

Check warning on line 28 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate.java

View check run for this annotation

ci.jenkins.io / PMD

DataClass

NORMAL: The class 'CoverageQualityGate' is suspected to be a Data Class (WOC=16.667%, NOPA=0, NOAM=5, WMC=13).
Raw output
Data Classes are simple data holders, which reveal most of their state, and without complex functionality. The lack of functionality may indicate that their behaviour is defined elsewhere, which is a sign of poor data-behaviour proximity. By directly exposing their internals, Data Classes break encapsulation, and therefore reduce the system's maintainability and understandability. Moreover, classes tend to strongly rely on their data representation, which makes for a brittle design. Refactoring a Data Class should focus on restoring a good data-behaviour proximity. In most cases, that means moving the operations defined on the data back into the class. In some other cases it may make sense to remove entirely the class and move the data into the former client classes. The rule uses metrics to implement its detection strategy. The violation message gives information about the values of these metrics: * WMC: a class complexity measure for a class, see {% jdoc java::lang.java.metrics.JavaMetrics#WEIGHED_METHOD_COUNT %} * WOC: a 'non-triviality' measure for a class, see {% jdoc java::lang.java.metrics.JavaMetrics#WEIGHT_OF_CLASS %} * NOPA: number of public attributes, see {% jdoc java::lang.java.metrics.JavaMetrics#NUMBER_OF_PUBLIC_FIELDS %} * NOAM: number of public accessor methods, see {% jdoc java::lang.java.metrics.JavaMetrics#NUMBER_OF_ACCESSORS %} The rule identifies a god class by looking for classes which have all of the following properties: * High NOPA + NOAM * Low WOC * Low WMC <pre> <code> public class DataClass { // class exposes public attributes public String name = &quot;&quot;; public int bar = 0; public int na = 0; private int bee = 0; // and private ones through getters public void setBee(int n) { bee = n; } } </code> </pre> <a href="https://docs.pmd-code.org/pmd-doc-7.19.0/pmd_rules_java_design.html#dataclass"> See PMD documentation. </a>

Check warning on line 28 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate.java

View workflow job for this annotation

GitHub Actions / Quality Monitor - Quality gates failed

PMD: DataClass

The class 'CoverageQualityGate' is suspected to be a Data Class (WOC=16.667%, NOPA=0, NOAM=5, WMC=13).
@Serial
private static final long serialVersionUID = -397278599489426668L;

Expand All @@ -32,6 +33,7 @@

private final Metric metric;
private Baseline baseline = Baseline.PROJECT;
private MetricAggregation aggregation = MetricAggregation.TOTAL;

/**
* Creates a new instance of {@link CoverageQualityGate}.
Expand Down Expand Up @@ -62,6 +64,16 @@
setCriticality(criticality);
}

CoverageQualityGate(final double threshold, final Metric metric,
final Baseline baseline, final QualityGateCriticality criticality,
final MetricAggregation aggregation) {
this(metric, threshold);

setBaseline(baseline);
setCriticality(criticality);
setAggregation(aggregation);
}

Check warning on line 75 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 70-75 are not covered by tests

/**
* Sets the baseline that will be used for the quality gate evaluation.
*
Expand All @@ -73,13 +85,31 @@
this.baseline = baseline;
}

/**
* Sets the aggregation mode for software metrics (total, maximum, or average). This is only applicable for
* software metrics like cyclomatic complexity. For coverage metrics, this setting is ignored.
*
* @param aggregation
* the aggregation mode to use
*/
@DataBoundSetter
public final void setAggregation(final MetricAggregation aggregation) {
if (MetricAggregation.isSupported(metric)) {
this.aggregation = aggregation;
}
}

/**
* Returns a human-readable name of the quality gate.
*
* @return a human-readable name
*/
@Override
public String getName() {
if (MetricAggregation.isSupported(metric) && aggregation != MetricAggregation.TOTAL) {
return "%s - %s (%s)".formatted(FORMATTER.getDisplayName(getBaseline()),
FORMATTER.getDisplayName(getMetric()), aggregation);
}
return "%s - %s".formatted(FORMATTER.getDisplayName(getBaseline()),
FORMATTER.getDisplayName(getMetric()));
}
Expand All @@ -92,6 +122,10 @@
return baseline;
}

public MetricAggregation getAggregation() {
return aggregation;
}

/**
* Descriptor of the {@link CoverageQualityGate}.
*/
Expand Down Expand Up @@ -141,5 +175,19 @@
}
return new ListBoxModel();
}

/**
* Returns a model with all {@link MetricAggregation aggregation modes}.
*
* @return a model with all {@link MetricAggregation aggregation modes}.
*/
@POST
@SuppressWarnings("unused") // used by Stapler view data binding
public ListBoxModel doFillAggregationItems() {
if (jenkins.hasPermission(Jenkins.READ)) {
return FORMATTER.getAggregationItems();
}
return new ListBoxModel();

Check warning on line 190 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 187-190 are not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package io.jenkins.plugins.coverage.metrics.steps;

import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Stream;

import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.coverage.Node;
import edu.hm.hafner.coverage.Value;

import io.jenkins.plugins.coverage.metrics.model.Baseline;
import io.jenkins.plugins.coverage.metrics.model.CoverageStatistics;
import io.jenkins.plugins.coverage.metrics.model.ElementFormatter;
import io.jenkins.plugins.coverage.metrics.model.MetricAggregation;
import io.jenkins.plugins.util.QualityGateEvaluator;
import io.jenkins.plugins.util.QualityGateResult;
import io.jenkins.plugins.util.QualityGateStatus;
Expand All @@ -17,18 +26,35 @@
class CoverageQualityGateEvaluator extends QualityGateEvaluator<CoverageQualityGate> {
private static final ElementFormatter FORMATTER = new ElementFormatter();
private final CoverageStatistics statistics;
private final Node rootNode;

CoverageQualityGateEvaluator(final Collection<? extends CoverageQualityGate> qualityGates,
final CoverageStatistics statistics) {
this(qualityGates, statistics, null);

Check warning on line 33 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View check run for this annotation

ci.jenkins.io / SpotBugs

NP_NONNULL_PARAM_VIOLATION

HIGH: Null passed for non-null parameter of new CoverageQualityGateEvaluator(Collection, CoverageStatistics, Node) in new io.jenkins.plugins.coverage.metrics.steps.CoverageQualityGateEvaluator(Collection, CoverageStatistics)
Raw output
<p> This method passes a null value as the parameter of a method which must be non-null. Either this parameter has been explicitly marked as @Nonnull, or analysis has determined that this parameter is always dereferenced. </p>

Check warning on line 33 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View workflow job for this annotation

GitHub Actions / Quality Monitor - Quality gates failed

SpotBugs: NP_NONNULL_PARAM_VIOLATION

Null passed for non-null parameter of new CoverageQualityGateEvaluator(Collection, CoverageStatistics, Node) in new io.jenkins.plugins.coverage.metrics.steps.CoverageQualityGateEvaluator(Collection, CoverageStatistics)
}

CoverageQualityGateEvaluator(final Collection<? extends CoverageQualityGate> qualityGates,
final CoverageStatistics statistics, final Node rootNode) {
super(qualityGates);

this.statistics = statistics;
this.rootNode = rootNode;
}

@Override
protected void evaluate(final CoverageQualityGate qualityGate, final QualityGateResult result) {
var baseline = qualityGate.getBaseline();
var possibleValue = statistics.getValue(baseline, qualityGate.getMetric());
var metric = qualityGate.getMetric();
var aggregation = qualityGate.getAggregation();

Optional<Value> possibleValue;
if (MetricAggregation.isSupported(metric) && aggregation != MetricAggregation.TOTAL && rootNode != null) {
possibleValue = computeAggregatedValue(rootNode, metric, aggregation, baseline);
}
else {
possibleValue = statistics.getValue(baseline, metric);
}

if (possibleValue.isPresent()) {
var actualValue = possibleValue.get();
var status = actualValue.isOutOfValidRange(
Expand All @@ -39,4 +65,91 @@
result.add(qualityGate, QualityGateStatus.INACTIVE, "n/a");
}
}

/**
* Computes an aggregated value (maximum or average) for a metric from the node tree.
*
* @param node
* the root node to compute from
* @param metric
* the metric to compute
* @param aggregation
* the aggregation mode (MAXIMUM or AVERAGE)
* @param baseline
* the baseline (currently only PROJECT is supported for custom aggregation)
*
* @return the computed value, or empty if not computable
*/
private Optional<Value> computeAggregatedValue(final Node node, final Metric metric,
final MetricAggregation aggregation, final Baseline baseline) {
if (baseline != Baseline.PROJECT) {
return statistics.getValue(baseline, metric);
}

var allValues = collectLeafValues(node, metric).toList();

if (allValues.isEmpty()) {
return Optional.empty();
}

if (aggregation == MetricAggregation.MAXIMUM) {
return allValues.stream().reduce(Value::max);
}
else if (aggregation == MetricAggregation.AVERAGE) {

Check warning on line 98 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 98 is only partially covered, one branch is missing
return computeAverage(allValues);
}

return Optional.empty();

Check warning on line 102 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 102 is not covered by tests
}

/**
* Collects all leaf values for a metric from a node tree. For metrics computed at the method level (like
* complexity), this collects values from all methods. For class-level metrics, it collects from all classes.
*
* @param node
* the node to start from
* @param metric
* the metric to collect
*
* @return a stream of all leaf values
*/
private Stream<Value> collectLeafValues(final Node node, final Metric metric) {
Stream<Value> nodeValue = node.getValue(metric).stream();

Stream<Value> childValues = node.getChildren().stream()
.flatMap(child -> collectLeafValues(child, metric));

if (node.getMetric() == Metric.METHOD
|| node.getMetric() == Metric.CLASS) {
return Stream.concat(nodeValue, childValues);
}

var childValuesList = childValues.toList();
return childValuesList.isEmpty() ? nodeValue : childValuesList.stream();
}

/**
* Computes the average of a list of values. For integer metrics like complexity, this computes the arithmetic
* mean. For coverage metrics, this computes the average percentage.
*
* @param values
* the values to average
*
* @return the average value, or empty if no values
*/
private Optional<Value> computeAverage(final List<Value> values) {
if (values.isEmpty()) {

Check warning on line 141 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 141 is only partially covered, one branch is missing
return Optional.empty();

Check warning on line 142 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 142 is not covered by tests
}

var sum = values.stream().reduce(Value::add);
if (sum.isEmpty()) {

Check warning on line 146 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 146 is only partially covered, one branch is missing
return Optional.empty();

Check warning on line 147 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageQualityGateEvaluator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 147 is not covered by tests
}

var metric = values.get(0).getMetric();
var totalValue = sum.get();

return Optional.of(new Value(metric, totalValue.asDouble() / values.size()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private CoverageBuildAction computeActionWithoutHistory(
final FilteredLog log) throws InterruptedException {
var statistics = new CoverageStatistics(rootNode.aggregateValues(),
List.of(), List.<Difference>of(), List.of(), EMPTY_VALUES, List.of());
var evaluator = new CoverageQualityGateEvaluator(qualityGates, statistics);
var evaluator = new CoverageQualityGateEvaluator(qualityGates, statistics, rootNode);
var qualityGateStatus = evaluator.evaluate(notifier, log);

paintSourceFiles(build, workspace, sourceCodeEncoding, sourceCodeRetention, id, rootNode,
Expand Down Expand Up @@ -122,7 +122,7 @@ private CoverageBuildAction computeCoverageBasedOnReferenceBuild(

var statistics = new CoverageStatistics(overallValues, overallDelta,
modifiedLinesValues, modifiedLinesDelta, modifiedFilesValues, modifiedFilesDelta);
var evaluator = new CoverageQualityGateEvaluator(qualityGates, statistics);
var evaluator = new CoverageQualityGateEvaluator(qualityGates, statistics, rootNode);
var qualityGateResult = evaluator.evaluate(notifier, log);

var filesToStore = computePaintedFiles(rootNode, sourceCodeRetention, log, modifiedLinesCoverageRoot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,30 @@

xStream.registerConverter(new FractionConverter());
xStream.registerConverter(new SimpleConverter<>(Value.class, Value::serialize, Value::valueOf));
xStream.registerConverter(new SimpleConverter<>(Metric.class, Metric::name, Metric::valueOf));
xStream.registerConverter(new SimpleConverter<>(Metric.class, Metric::name, CoverageXmlStream::metricValueOf));
}

@Override
protected Node createDefaultValue() {
return new ModuleNode("Empty");
}

/**
* Converts a string to a {@link Metric} value. Handles legacy metric names like COMPLEXITY_MAXIMUM by mapping
* them to the new metric names.
*
* @param metricName
* the name of the metric
*
* @return the metric value
*/
private static Metric metricValueOf(final String metricName) {
if ("COMPLEXITY_MAXIMUM".equals(metricName)) {

Check warning on line 130 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStream.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 130 is only partially covered, one branch is missing

Check warning on line 130 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStream.java

View check run for this annotation

ci.jenkins.io / PMD

AvoidLiteralsInIfCondition

NORMAL: Avoid using literals such as "COMPLEXITY_MAXIMUM" in if statements.
Raw output
Avoid using hard-coded literals in conditional statements. By declaring them as static variables or private members with descriptive names maintainability is enhanced. By default, the literals "-1" and "0" are ignored. More exceptions can be defined with the property "ignoreMagicNumbers". The rule doesn't consider deeper expressions by default, but this can be enabled via the property `ignoreExpressions`. With this property set to false, if-conditions like `i == 1 + 5` are reported as well. Note that in that case, the property ignoreMagicNumbers is not taken into account, if there are multiple literals involved in such an expression. <pre> <code> private static final int MAX_NUMBER_OF_REQUESTS = 10; public void checkRequests() { if (i == 10) { // magic number, buried in a method doSomething(); } if (i == MAX_NUMBER_OF_REQUESTS) { // preferred approach doSomething(); } if (aString.indexOf(&#x27;.&#x27;) != -1) {} // magic number -1, by default ignored if (aString.indexOf(&#x27;.&#x27;) &gt;= 0) { } // alternative approach if (aDouble &gt; 0.0) {} // magic number 0.0 if (aDouble &gt;= Double.MIN_VALUE) {} // preferred approach // with rule property &quot;ignoreExpressions&quot; set to &quot;false&quot; if (i == pos + 5) {} // violation: magic number 5 within an (additive) expression if (i == pos + SUFFIX_LENGTH) {} // preferred approach if (i == 5 &amp;&amp; &quot;none&quot;.equals(aString)) {} // 2 violations: magic number 5 and literal &quot;none&quot; } </code> </pre> <a href="https://docs.pmd-code.org/pmd-doc-7.19.0/pmd_rules_java_errorprone.html#avoidliteralsinifcondition"> See PMD documentation. </a>

Check warning on line 130 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStream.java

View workflow job for this annotation

GitHub Actions / Quality Monitor - Quality gates failed

PMD: AvoidLiteralsInIfCondition

Avoid using literals such as "COMPLEXITY_MAXIMUM" in if statements.
return Metric.CYCLOMATIC_COMPLEXITY;

Check warning on line 131 in plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/CoverageXmlStream.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 131 is not covered by tests
}
return Metric.valueOf(metricName);
}

/**
* {@link Converter} for {@link Fraction} instances so that only the values will be serialized. After reading the
* values back from the stream, the string representation will be converted to an actual instance again.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<f:select/>
</f:entry>

<f:entry title="${%title.aggregation}" field="aggregation">
<f:select/>
</f:entry>

<f:entry title="${%title.warning}" field="criticality">
<f:select/>
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
title.threshold=Threshold
title.baseline=Baseline
title.metric=Metric
title.aggregation=Aggregation
title.warning=Step or Build Result
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div>
Defines how to aggregate the metric values for software metrics (e.g., cyclomatic complexity).
<ul>
<li><strong>TOTAL</strong>: Sum of all values (default)</li>
<li><strong>MAXIMUM</strong>: Maximum value found in any method or class</li>
<li><strong>AVERAGE</strong>: Average value across all methods or classes</li>
</ul>
This setting is only applicable for software metrics like cyclomatic complexity, cognitive complexity, and NPath
complexity. For coverage metrics (e.g., line coverage), this setting is ignored and the aggregated coverage
percentage is always used.
</div>
Loading
Loading