-
-
Notifications
You must be signed in to change notification settings - Fork 29
Add support for maximum or average values in metrics #669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
242bcd4
e553fb7
86aeedd
2361e1f
e5953fa
49ea836
01b5cf6
97f2e38
cfeefc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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
|
||
| @Serial | ||
| private static final long serialVersionUID = -397278599489426668L; | ||
|
|
||
|
|
@@ -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}. | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
| * Sets the baseline that will be used for the quality gate evaluation. | ||
| * | ||
|
|
@@ -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; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if this statement is save: when the metric is set after the aggregation, it will fail. Can't we set the aggregation in any case and ignore it later on? |
||
| } | ||
|
|
||
| /** | ||
| * 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())); | ||
| } | ||
|
|
@@ -92,6 +122,10 @@ | |
| return baseline; | ||
| } | ||
|
|
||
| public MetricAggregation getAggregation() { | ||
| return aggregation; | ||
| } | ||
|
|
||
| /** | ||
| * Descriptor of the {@link CoverageQualityGate}. | ||
| */ | ||
|
|
@@ -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(); | ||
| } | ||
| } | ||
| } | ||
| 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; | ||||||
|
|
@@ -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
|
||||||
| } | ||||||
|
|
||||||
| 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) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How can the node get null?
Suggested change
|
||||||
| possibleValue = computeAggregatedValue(rootNode, metric, aggregation, baseline); | ||||||
| } | ||||||
| else { | ||||||
| possibleValue = statistics.getValue(baseline, metric); | ||||||
| } | ||||||
|
|
||||||
| if (possibleValue.isPresent()) { | ||||||
| var actualValue = possibleValue.get(); | ||||||
| var status = actualValue.isOutOfValidRange( | ||||||
|
|
@@ -39,4 +65,91 @@ | |||||
| result.add(qualityGate, QualityGateStatus.INACTIVE, "n/a"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code after this line actually should be part of the coverage model. |
||||||
| /** | ||||||
| * 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) { | ||||||
| return computeAverage(allValues); | ||||||
| } | ||||||
|
|
||||||
| return Optional.empty(); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * 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()) { | ||||||
| return Optional.empty(); | ||||||
| } | ||||||
|
|
||||||
| var sum = values.stream().reduce(Value::add); | ||||||
| if (sum.isEmpty()) { | ||||||
| return Optional.empty(); | ||||||
| } | ||||||
|
|
||||||
| 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 |
|---|---|---|
| @@ -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> | ||
|
Comment on lines
+5
to
+6
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to check an example, but shouldn't these options work on the method level only? |
||
| </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> | ||
Uh oh!
There was an error while loading. Please reload this page.