numbers) {
10 | if (numbers.isEmpty()) {
11 | return 0;
12 | }
13 |
14 | double average =
15 | numbers.stream().mapToDouble(Number::longValue).average().getAsDouble();
16 |
17 | double variance = numbers.stream()
18 | .mapToDouble(Number::doubleValue)
19 | .map(d -> d - average)
20 | .map(d -> d * d)
21 | .average()
22 | .getAsDouble();
23 |
24 | return Math.sqrt(variance);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/Metric.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * Annotation used to mark methods that calculate and return metric values.
10 | * This annotation is used by the additional metrics plugin to identify methods
11 | * that provide metric data for Jenkins list view columns.
12 | *
13 | * Methods annotated with @Metric should:
14 | * - Be public
15 | * - Return a metric value (Duration, Rate, RunWithDuration, etc.)
16 | * - Accept a Job parameter to calculate metrics for
17 | */
18 | @Target(ElementType.METHOD)
19 | @Retention(RetentionPolicy.RUNTIME)
20 | public @interface Metric {}
21 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/additionalmetrics/MaxCheckoutDurationColumn/column.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ${longestCheckoutRun.duration().asString}
8 | -
9 |
10 | ${longestCheckoutRun.run().displayName}
11 |
12 |
13 |
14 | ${%N/A}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/additionalmetrics/MinCheckoutDurationColumn/column.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ${shortestCheckoutRun.duration().asString}
8 | -
9 |
10 | ${shortestCheckoutRun.run().displayName}
11 |
12 |
13 |
14 | ${%N/A}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/RateStringParameterizedTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 | import java.util.Arrays;
6 | import org.junit.jupiter.params.ParameterizedTest;
7 | import org.junit.jupiter.params.provider.MethodSource;
8 |
9 | class RateStringParameterizedTest {
10 |
11 | static Iterable data() {
12 | return Arrays.asList(new Object[][] {
13 | {0, "0.00%"},
14 | {0.333333, "33.33%"},
15 | {0.5, "50.00%"},
16 | {0.666667, "66.67%"},
17 | {1, "100.00%"},
18 | });
19 | }
20 |
21 | @ParameterizedTest(name = "{index}: rate[{0}]={1}")
22 | @MethodSource("data")
23 | void test(double input, String expected) {
24 | Rate rate = new Rate(input);
25 | assertEquals(expected, rate.getAsString());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/Duration.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import hudson.Util;
4 |
5 | /**
6 | * Represents a time duration in milliseconds.
7 | * Provides methods to access the duration as a long value or as a human-readable string.
8 | */
9 | public record Duration(long milliseconds) {
10 |
11 | /**
12 | * Returns the duration in milliseconds.
13 | *
14 | * @return the duration value in milliseconds
15 | */
16 | public long getAsLong() {
17 | return milliseconds;
18 | }
19 |
20 | /**
21 | * Returns the duration as a human-readable time span string.
22 | * Uses Jenkins' utility method to format the duration (e.g., "2 hr 30 min").
23 | *
24 | * @return the duration formatted as a human-readable string
25 | */
26 | public String getAsString() {
27 | return Util.getTimeSpanString(milliseconds);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
18 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/additionalmetrics/Messages.properties:
--------------------------------------------------------------------------------
1 | AvgDurationColumn.DisplayName=Average Duration
2 | AvgCheckoutDurationColumn.DisplayName=Average Checkout Duration
3 | AvgSuccessDurationColumn.DisplayName=Average Success Duration
4 | StdevSuccessDurationColumn.DisplayName=Standard Deviation Success Duration
5 | StdevDurationColumn.DisplayName=Standard Deviation Duration
6 | FailureRateColumn.DisplayName=Failure Rate
7 | UnstableRateColumn.DisplayName=Unstable Rate
8 | FailureTimeRateColumn.DisplayName=Failure Time Rate
9 | MaxDurationColumn.DisplayName=Max Duration
10 | MaxCheckoutDurationColumn.DisplayName=Max Checkout Duration
11 | MaxSuccessDurationColumn.DisplayName=Max Success Duration
12 | MinDurationColumn.DisplayName=Min Duration
13 | MinCheckoutDurationColumn.DisplayName=Min Checkout Duration
14 | MinSuccessDurationColumn.DisplayName=Min Success Duration
15 | SuccessRateColumn.DisplayName=Success Rate
16 | SuccessTimeRateColumn.DisplayName=Success Time Rate
17 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/Rate.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import java.text.DecimalFormat;
4 | import java.text.NumberFormat;
5 |
6 | /**
7 | * Represents a rate as a double value between 0.0 and 1.0.
8 | * Provides methods to access the rate as a double or as a formatted percentage string.
9 | */
10 | public record Rate(double rate) {
11 |
12 | /**
13 | * Returns the rate as a double value.
14 | *
15 | * @return the rate value as a double between 0.0 and 1.0
16 | */
17 | public double getAsDouble() {
18 | return rate;
19 | }
20 |
21 | /**
22 | * Returns the rate as a formatted percentage string.
23 | * The percentage is formatted to two decimal places with a "%" suffix.
24 | *
25 | * @return the rate formatted as a percentage string (e.g., "75.25%")
26 | */
27 | public String getAsString() {
28 | NumberFormat formatter = new DecimalFormat("0.00");
29 | return (formatter.format(rate * 100) + "%");
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/MinCheckoutDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.*;
4 |
5 | import hudson.Extension;
6 | import hudson.model.Job;
7 | import hudson.model.Run;
8 | import hudson.views.ListViewColumn;
9 | import org.jenkinsci.Symbol;
10 | import org.kohsuke.stapler.DataBoundConstructor;
11 |
12 | public class MinCheckoutDurationColumn extends ListViewColumn {
13 |
14 | @DataBoundConstructor
15 | public MinCheckoutDurationColumn() {
16 | super();
17 | }
18 |
19 | @Metric
20 | public RunWithDuration getShortestCheckoutRun(Job extends Job, ? extends Run> job) {
21 | return Utils.findRun(job.getBuilds(), COMPLETED, RUN_CHECKOUT_DURATION, MIN)
22 | .orElse(null);
23 | }
24 |
25 | @Extension
26 | @Symbol("minCheckoutDuration")
27 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
28 |
29 | public DescriptorImpl() {
30 | super(Messages.MinCheckoutDurationColumn_DisplayName());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Chadi El Masri and additional-metrics-plugin contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/MaxSuccessDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.Utils.findRun;
5 |
6 | import hudson.Extension;
7 | import hudson.model.Job;
8 | import hudson.model.Run;
9 | import hudson.views.ListViewColumn;
10 | import org.jenkinsci.Symbol;
11 | import org.kohsuke.stapler.DataBoundConstructor;
12 |
13 | public class MaxSuccessDurationColumn extends ListViewColumn {
14 |
15 | @DataBoundConstructor
16 | public MaxSuccessDurationColumn() {
17 | super();
18 | }
19 |
20 | @Metric
21 | public RunWithDuration getLongestSuccessfulRun(Job extends Job, ? extends Run> job) {
22 | return findRun(job.getBuilds(), SUCCESS, RUN_DURATION, MAX).orElse(null);
23 | }
24 |
25 | @Extension
26 | @Symbol("maxSuccessDuration")
27 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
28 |
29 | public DescriptorImpl() {
30 | super(Messages.MaxSuccessDurationColumn_DisplayName());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/MinSuccessDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.Utils.findRun;
5 |
6 | import hudson.Extension;
7 | import hudson.model.Job;
8 | import hudson.model.Run;
9 | import hudson.views.ListViewColumn;
10 | import org.jenkinsci.Symbol;
11 | import org.kohsuke.stapler.DataBoundConstructor;
12 |
13 | public class MinSuccessDurationColumn extends ListViewColumn {
14 |
15 | @DataBoundConstructor
16 | public MinSuccessDurationColumn() {
17 | super();
18 | }
19 |
20 | @Metric
21 | public RunWithDuration getShortestSuccessfulRun(Job extends Job, ? extends Run> job) {
22 | return findRun(job.getBuilds(), SUCCESS, RUN_DURATION, MIN).orElse(null);
23 | }
24 |
25 | @Extension
26 | @Symbol("minSuccessDuration")
27 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
28 |
29 | public DescriptorImpl() {
30 | super(Messages.MinSuccessDurationColumn_DisplayName());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/MaxCheckoutDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.Utils.findRun;
5 |
6 | import hudson.Extension;
7 | import hudson.model.Job;
8 | import hudson.model.Run;
9 | import hudson.views.ListViewColumn;
10 | import org.jenkinsci.Symbol;
11 | import org.kohsuke.stapler.DataBoundConstructor;
12 |
13 | public class MaxCheckoutDurationColumn extends ListViewColumn {
14 |
15 | @DataBoundConstructor
16 | public MaxCheckoutDurationColumn() {
17 | super();
18 | }
19 |
20 | @Metric
21 | public RunWithDuration getLongestCheckoutRun(Job extends Job, ? extends Run> job) {
22 | return findRun(job.getBuilds(), COMPLETED, RUN_CHECKOUT_DURATION, MAX).orElse(null);
23 | }
24 |
25 | @Extension
26 | @Symbol("maxCheckoutDuration")
27 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
28 |
29 | public DescriptorImpl() {
30 | super(Messages.MaxCheckoutDurationColumn_DisplayName());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/SuccessRateColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.SUCCESS;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.rateOf;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class SuccessRateColumn extends ListViewColumn {
15 |
16 | @DataBoundConstructor
17 | public SuccessRateColumn() {
18 | super();
19 | }
20 |
21 | @Metric
22 | public Rate getSuccessRate(Job extends Job, ? extends Run> job) {
23 | return rateOf(job.getBuilds(), COMPLETED, SUCCESS).orElse(null);
24 | }
25 |
26 | @Extension
27 | @Symbol("successRate")
28 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
29 |
30 | public DescriptorImpl() {
31 | super(Messages.SuccessRateColumn_DisplayName());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/UnstableRateColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.UNSTABLE;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.rateOf;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class UnstableRateColumn extends ListViewColumn {
15 |
16 | @DataBoundConstructor
17 | public UnstableRateColumn() {
18 | super();
19 | }
20 |
21 | @Metric
22 | public Rate getUnstableRate(Job extends Job, ? extends Run> job) {
23 | return rateOf(job.getBuilds(), COMPLETED, UNSTABLE).orElse(null);
24 | }
25 |
26 | @Extension
27 | @Symbol("unstableRate")
28 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
29 |
30 | public DescriptorImpl() {
31 | super(Messages.UnstableRateColumn_DisplayName());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/StdevDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.RUN_DURATION;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.stdDevDuration;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class StdevDurationColumn extends ListViewColumn {
15 | @DataBoundConstructor
16 | public StdevDurationColumn() {
17 | super();
18 | }
19 |
20 | @Metric
21 | public Duration getStdevDuration(Job extends Job, ? extends Run> job) {
22 | return stdDevDuration(job.getBuilds(), COMPLETED, RUN_DURATION).orElse(null);
23 | }
24 |
25 | @Extension
26 | @Symbol("stdevDuration")
27 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
28 |
29 | public DescriptorImpl() {
30 | super(Messages.StdevDurationColumn_DisplayName());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/SuccessTimeRateColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.SUCCESS;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.timeRateOf;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class SuccessTimeRateColumn extends ListViewColumn {
15 |
16 | @DataBoundConstructor
17 | public SuccessTimeRateColumn() {
18 | super();
19 | }
20 |
21 | @Metric
22 | public Rate getSuccessTimeRate(Job extends Job, ? extends Run> job) {
23 | return timeRateOf(job.getBuilds(), COMPLETED, SUCCESS).orElse(null);
24 | }
25 |
26 | @Extension
27 | @Symbol("successTimeRate")
28 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
29 |
30 | public DescriptorImpl() {
31 | super(Messages.SuccessTimeRateColumn_DisplayName());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/FailureTimeRateColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.NOT_SUCCESS;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.timeRateOf;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class FailureTimeRateColumn extends ListViewColumn {
15 |
16 | @DataBoundConstructor
17 | public FailureTimeRateColumn() {
18 | super();
19 | }
20 |
21 | @Metric
22 | public Rate getFailureTimeRate(Job extends Job, ? extends Run> job) {
23 | return timeRateOf(job.getBuilds(), COMPLETED, NOT_SUCCESS).orElse(null);
24 | }
25 |
26 | @Extension
27 | @Symbol("failureTimeRate")
28 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
29 |
30 | public DescriptorImpl() {
31 | super(Messages.FailureTimeRateColumn_DisplayName());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/AvgSuccessDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.RUN_DURATION;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.SUCCESS;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.averageDuration;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class AvgSuccessDurationColumn extends ListViewColumn {
15 |
16 | @DataBoundConstructor
17 | public AvgSuccessDurationColumn() {
18 | super();
19 | }
20 |
21 | @Metric
22 | public Duration getAverageSuccessDuration(Job extends Job, ? extends Run> job) {
23 | return averageDuration(job.getBuilds(), SUCCESS, RUN_DURATION).orElse(null);
24 | }
25 |
26 | @Extension
27 | @Symbol("avgSuccessDuration")
28 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
29 |
30 | public DescriptorImpl() {
31 | super(Messages.AvgSuccessDurationColumn_DisplayName());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/StdevSuccessDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.RUN_DURATION;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.SUCCESS;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.stdDevDuration;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class StdevSuccessDurationColumn extends ListViewColumn {
15 | @DataBoundConstructor
16 | public StdevSuccessDurationColumn() {
17 | super();
18 | }
19 |
20 | @Metric
21 | public Duration getStdevSuccessDuration(Job extends Job, ? extends Run> job) {
22 | return stdDevDuration(job.getBuilds(), SUCCESS, RUN_DURATION).orElse(null);
23 | }
24 |
25 | @Extension
26 | @Symbol("stdevSuccessDuration")
27 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
28 |
29 | public DescriptorImpl() {
30 | super(Messages.StdevSuccessDurationColumn_DisplayName());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/AvgCheckoutDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.RUN_CHECKOUT_DURATION;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.averageDuration;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | public class AvgCheckoutDurationColumn extends ListViewColumn {
15 |
16 | @DataBoundConstructor
17 | public AvgCheckoutDurationColumn() {
18 | super();
19 | }
20 |
21 | @Metric
22 | public Duration getAverageCheckoutDuration(Job extends Job, ? extends Run> job) {
23 | return averageDuration(job.getBuilds(), COMPLETED, RUN_CHECKOUT_DURATION)
24 | .orElse(null);
25 | }
26 |
27 | @Extension
28 | @Symbol("avgCheckoutDuration")
29 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
30 |
31 | public DescriptorImpl() {
32 | super(Messages.AvgCheckoutDurationColumn_DisplayName());
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/Helpers.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import hudson.model.Result;
4 | import hudson.model.Run;
5 | import java.util.Comparator;
6 | import java.util.function.BinaryOperator;
7 | import java.util.function.Predicate;
8 | import java.util.function.ToLongFunction;
9 |
10 | class Helpers {
11 |
12 | static final ToLongFunction RUN_DURATION = Run::getDuration;
13 | static final ToLongFunction RUN_CHECKOUT_DURATION = CheckoutDuration::checkoutDurationOf;
14 |
15 | static final Predicate SUCCESS = run -> run.getResult() == Result.SUCCESS;
16 | static final Predicate UNSTABLE = run -> run.getResult() == Result.UNSTABLE;
17 | static final Predicate NOT_SUCCESS = SUCCESS.negate();
18 | static final Predicate COMPLETED = run -> !run.isBuilding();
19 |
20 | private static final Comparator DURATION_ORDERING =
21 | Comparator.comparing(runWithDuration -> runWithDuration.duration().getAsLong());
22 |
23 | static final BinaryOperator MIN = BinaryOperator.minBy(DURATION_ORDERING);
24 | static final BinaryOperator MAX = BinaryOperator.maxBy(DURATION_ORDERING);
25 |
26 | private Helpers() {
27 | // utility class
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/MetricsActionFactory.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import edu.umd.cs.findbugs.annotations.NonNull;
4 | import hudson.Extension;
5 | import hudson.model.Action;
6 | import hudson.model.Job;
7 | import java.util.Collection;
8 | import java.util.Collections;
9 | import jenkins.model.TransientActionFactory;
10 | import org.kohsuke.stapler.export.Exported;
11 | import org.kohsuke.stapler.export.ExportedBean;
12 |
13 | @Extension
14 | public class MetricsActionFactory extends TransientActionFactory {
15 | @Override
16 | public Class type() {
17 | return Job.class;
18 | }
19 |
20 | @NonNull
21 | @Override
22 | public Collection extends Action> createFor(@NonNull Job target) {
23 | return Collections.singleton(new MetricsAction(target));
24 | }
25 |
26 | @ExportedBean
27 | public static class MetricsAction implements Action {
28 | private final Job target;
29 |
30 | MetricsAction(Job target) {
31 | this.target = target;
32 | }
33 |
34 | @Override
35 | public String getIconFileName() {
36 | return null;
37 | }
38 |
39 | @Override
40 | public String getDisplayName() {
41 | return null;
42 | }
43 |
44 | @Override
45 | public String getUrlName() {
46 | return null;
47 | }
48 |
49 | @Exported
50 | public JobMetrics getJobMetrics() {
51 | return new JobMetrics(target);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/Utilities.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import com.google.common.collect.Iterables;
4 | import com.google.common.reflect.ClassPath;
5 | import hudson.views.ListViewColumn;
6 | import java.io.IOException;
7 | import java.lang.reflect.Method;
8 | import java.util.Arrays;
9 | import java.util.List;
10 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
11 |
12 | class Utilities {
13 | static final CharSequence[] TIME_UNITS = {" sec", " ms"};
14 |
15 | static void terminateWorkflowRun(WorkflowRun workflowRun) {
16 | workflowRun.doTerm();
17 | workflowRun.doKill();
18 | }
19 |
20 | static List extends Class>> getColumns() throws IOException {
21 | Package p = Utilities.class.getPackage();
22 |
23 | return ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().stream()
24 | .filter(cl -> cl.getPackageName().equals(p.getName()))
25 | .map(ClassPath.ClassInfo::load)
26 | .filter(ListViewColumn.class::isAssignableFrom)
27 | .toList();
28 | }
29 |
30 | static Method getMetricMethod(Class extends ListViewColumn> clazz) {
31 | List methods = Arrays.stream(clazz.getDeclaredMethods())
32 | .filter(m -> m.isAnnotationPresent(Metric.class))
33 | .toList();
34 |
35 | if (methods.isEmpty()) {
36 | throw new RuntimeException("Expected at least one method annotated with @Metric");
37 | }
38 |
39 | return Iterables.getOnlyElement(methods);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/UIHelpers.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import hudson.model.ListView;
4 | import hudson.model.TopLevelItem;
5 | import hudson.views.ListViewColumn;
6 | import java.io.IOException;
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import jenkins.model.Jenkins;
10 | import org.htmlunit.html.DomElement;
11 | import org.htmlunit.html.DomNode;
12 | import org.htmlunit.html.HtmlPage;
13 |
14 | class UIHelpers {
15 |
16 | private UIHelpers() {
17 | // utility class
18 | }
19 |
20 | static DomNode getListViewCell(HtmlPage page, ListView view, String jobName, String fieldName) {
21 | int i = 0;
22 | Map textToIndex = new HashMap<>();
23 | for (ListViewColumn column : view.getColumns()) {
24 | textToIndex.put(column.getColumnCaption(), i++);
25 | }
26 |
27 | DomElement tr = page.getElementById("job_" + jobName);
28 | DomNode td = tr.getChildNodes().get(textToIndex.get(fieldName));
29 |
30 | return td;
31 | }
32 |
33 | static ListView createAndAddListView(Jenkins instance, String listName, ListViewColumn column, TopLevelItem job)
34 | throws IOException {
35 | ListView listView = new ListView(listName, instance);
36 | listView.getColumns().add(column);
37 | listView.add(job);
38 |
39 | instance.addView(listView);
40 |
41 | return listView;
42 | }
43 |
44 | static String dataOf(DomNode columnNode) {
45 | return columnNode.getAttributes().getNamedItem("data").getNodeValue();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/AdditionalMetricColumnDescriptor.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import edu.umd.cs.findbugs.annotations.NonNull;
4 | import hudson.views.ListViewColumnDescriptor;
5 |
6 | /**
7 | * Abstract base class for additional metric column descriptors.
8 | * Provides common functionality for metric columns including display name handling
9 | * and default visibility configuration.
10 | */
11 | abstract class AdditionalMetricColumnDescriptor extends ListViewColumnDescriptor {
12 |
13 | private final String displayName;
14 |
15 | /**
16 | * Creates a new additional metric column descriptor with the specified display name.
17 | *
18 | * @param displayName the human-readable name for this column type
19 | */
20 | AdditionalMetricColumnDescriptor(String displayName) {
21 | this.displayName = displayName;
22 | }
23 |
24 | /**
25 | * Indicates whether this column should be shown by default in list views.
26 | * Additional metric columns are not shown by default and must be explicitly added by users.
27 | *
28 | * @return false, indicating this column is not shown by default
29 | */
30 | @Override
31 | public boolean shownByDefault() {
32 | return false;
33 | }
34 |
35 | /**
36 | * Returns the display name for this column type.
37 | * This name appears in the Jenkins UI when users are configuring list view columns.
38 | *
39 | * @return the human-readable display name for this column type
40 | */
41 | @NonNull
42 | @Override
43 | public String getDisplayName() {
44 | return displayName;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/NoRunsTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.getColumns;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.getMetricMethod;
6 | import static org.junit.jupiter.api.Assertions.assertNull;
7 |
8 | import hudson.views.ListViewColumn;
9 | import java.io.IOException;
10 | import java.lang.reflect.Method;
11 | import java.util.List;
12 | import org.junit.jupiter.api.BeforeAll;
13 | import org.junit.jupiter.params.ParameterizedTest;
14 | import org.junit.jupiter.params.provider.MethodSource;
15 | import org.jvnet.hudson.test.JenkinsRule;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class NoRunsTest {
20 |
21 | static List extends Class>> data() throws IOException {
22 | List extends Class>> columns = getColumns();
23 | assertThat(columns).isNotEmpty();
24 | return columns;
25 | }
26 |
27 | private static JobRunner.WorkflowBuilder runner;
28 |
29 | @BeforeAll
30 | static void setUp(JenkinsRule rule) throws Exception {
31 | runner = JobRunner.createWorkflowJob(rule);
32 | }
33 |
34 | @ParameterizedTest(name = "{0}")
35 | @MethodSource("data")
36 | void no_runs_should_return_no_data(Class extends ListViewColumn> clazz) throws Exception {
37 | Object instance = clazz.getDeclaredConstructor().newInstance();
38 | Method method = getMetricMethod(clazz);
39 |
40 | Object res = method.invoke(instance, runner.getJob());
41 |
42 | assertNull(res);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/AvgDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.RUN_DURATION;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.averageDuration;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | /**
15 | * A Jenkins list view column that displays the average duration of completed builds for a job.
16 | */
17 | public class AvgDurationColumn extends ListViewColumn {
18 |
19 | /**
20 | * Creates a new average duration column.
21 | * This constructor is used by Jenkins for data binding.
22 | */
23 | @DataBoundConstructor
24 | public AvgDurationColumn() {
25 | super();
26 | }
27 |
28 | /**
29 | * Calculates and returns the average duration of completed builds for the specified job.
30 | * Only considers builds that have completed (not currently building).
31 | *
32 | * @param job the Jenkins job to calculate the average duration for
33 | * @return the average duration of completed builds, or null if no completed builds exist
34 | */
35 | @Metric
36 | public Duration getAverageDuration(Job extends Job, ? extends Run> job) {
37 | return averageDuration(job.getBuilds(), COMPLETED, RUN_DURATION).orElse(null);
38 | }
39 |
40 | @Extension
41 | @Symbol("avgDuration")
42 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
43 |
44 | public DescriptorImpl() {
45 | super(Messages.AvgDurationColumn_DisplayName());
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/MaxDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.Utils.findRun;
5 |
6 | import hudson.Extension;
7 | import hudson.model.Job;
8 | import hudson.model.Run;
9 | import hudson.views.ListViewColumn;
10 | import org.jenkinsci.Symbol;
11 | import org.kohsuke.stapler.DataBoundConstructor;
12 |
13 | /**
14 | * A Jenkins list view column that displays the longest running completed build for a job.
15 | * Shows both the build information and its duration.
16 | */
17 | public class MaxDurationColumn extends ListViewColumn {
18 |
19 | /**
20 | * Creates a new maximum duration column.
21 | * This constructor is used by Jenkins for data binding.
22 | */
23 | @DataBoundConstructor
24 | public MaxDurationColumn() {
25 | super();
26 | }
27 |
28 | /**
29 | * Finds and returns the completed build with the longest duration for the specified job.
30 | * Only considers builds that have completed (not currently building) and have a positive duration.
31 | *
32 | * @param job the Jenkins job to find the longest running build for
33 | * @return a RunWithDuration containing the longest running build and its duration,
34 | * or null if no completed builds with positive duration exist
35 | */
36 | @Metric
37 | public RunWithDuration getLongestRun(Job extends Job, ? extends Run> job) {
38 | return findRun(job.getBuilds(), COMPLETED, RUN_DURATION, MAX).orElse(null);
39 | }
40 |
41 | @Extension
42 | @Symbol("maxDuration")
43 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
44 |
45 | public DescriptorImpl() {
46 | super(Messages.MaxDurationColumn_DisplayName());
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/MinDurationColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.Utils.findRun;
5 |
6 | import hudson.Extension;
7 | import hudson.model.Job;
8 | import hudson.model.Run;
9 | import hudson.views.ListViewColumn;
10 | import org.jenkinsci.Symbol;
11 | import org.kohsuke.stapler.DataBoundConstructor;
12 |
13 | /**
14 | * A Jenkins list view column that displays the shortest running completed build for a job.
15 | * Shows both the build information and its duration.
16 | */
17 | public class MinDurationColumn extends ListViewColumn {
18 |
19 | /**
20 | * Creates a new minimum duration column.
21 | * This constructor is used by Jenkins for data binding.
22 | */
23 | @DataBoundConstructor
24 | public MinDurationColumn() {
25 | super();
26 | }
27 |
28 | /**
29 | * Finds and returns the completed build with the shortest duration for the specified job.
30 | * Only considers builds that have completed (not currently building) and have a positive duration.
31 | *
32 | * @param job the Jenkins job to find the shortest running build for
33 | * @return a RunWithDuration containing the shortest running build and its duration,
34 | * or null if no completed builds with positive duration exist
35 | */
36 | @Metric
37 | public RunWithDuration getShortestRun(Job extends Job, ? extends Run> job) {
38 | return findRun(job.getBuilds(), COMPLETED, RUN_DURATION, MIN).orElse(null);
39 | }
40 |
41 | @Extension
42 | @Symbol("minDuration")
43 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
44 |
45 | public DescriptorImpl() {
46 | super(Messages.MinDurationColumn_DisplayName());
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/FailureRateColumn.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.COMPLETED;
4 | import static org.jenkinsci.plugins.additionalmetrics.Helpers.NOT_SUCCESS;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utils.rateOf;
6 |
7 | import hudson.Extension;
8 | import hudson.model.Job;
9 | import hudson.model.Run;
10 | import hudson.views.ListViewColumn;
11 | import org.jenkinsci.Symbol;
12 | import org.kohsuke.stapler.DataBoundConstructor;
13 |
14 | /**
15 | * A Jenkins list view column that displays the failure rate of completed builds for a job.
16 | * The failure rate is calculated as the percentage of completed builds that did not succeed.
17 | */
18 | public class FailureRateColumn extends ListViewColumn {
19 |
20 | /**
21 | * Creates a new failure rate column.
22 | * This constructor is used by Jenkins for data binding.
23 | */
24 | @DataBoundConstructor
25 | public FailureRateColumn() {
26 | super();
27 | }
28 |
29 | /**
30 | * Calculates and returns the failure rate of completed builds for the specified job.
31 | * The failure rate includes all non-successful builds (failed, unstable, aborted, etc.).
32 | * Only considers builds that have completed (not currently building).
33 | *
34 | * @param job the Jenkins job to calculate the failure rate for
35 | * @return the failure rate as a Rate object, or null if no completed builds exist
36 | */
37 | @Metric
38 | public Rate getFailureRate(Job extends Job, ? extends Run> job) {
39 | return rateOf(job.getBuilds(), COMPLETED, NOT_SUCCESS).orElse(null);
40 | }
41 |
42 | @Extension
43 | @Symbol("failureRate")
44 | public static class DescriptorImpl extends AdditionalMetricColumnDescriptor {
45 |
46 | public DescriptorImpl() {
47 | super(Messages.FailureRateColumn_DisplayName());
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/BuildingRunsTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.VERY_SLOW_60S;
5 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.getColumns;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.getMetricMethod;
7 | import static org.junit.jupiter.api.Assertions.assertNull;
8 |
9 | import hudson.views.ListViewColumn;
10 | import java.io.IOException;
11 | import java.lang.reflect.Method;
12 | import java.util.List;
13 | import org.junit.jupiter.api.AfterAll;
14 | import org.junit.jupiter.api.BeforeAll;
15 | import org.junit.jupiter.params.ParameterizedTest;
16 | import org.junit.jupiter.params.provider.MethodSource;
17 | import org.jvnet.hudson.test.JenkinsRule;
18 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
19 |
20 | @WithJenkins
21 | class BuildingRunsTest {
22 |
23 | static List extends Class>> data() throws IOException {
24 | List extends Class>> columns = getColumns();
25 | assertThat(columns).isNotEmpty();
26 | return columns;
27 | }
28 |
29 | private static JobRunner.WorkflowBuilder runner;
30 |
31 | @BeforeAll
32 | static void setUp(JenkinsRule rule) throws Exception {
33 | runner = JobRunner.createWorkflowJob(rule)
34 | .configurePipelineDefinition(VERY_SLOW_60S)
35 | .scheduleNoWait();
36 | }
37 |
38 | @AfterAll
39 | static void stopRun() {
40 | Utilities.terminateWorkflowRun(runner.getRuns()[0]);
41 | }
42 |
43 | @ParameterizedTest(name = "{0}")
44 | @MethodSource("data")
45 | void building_runs_should_be_excluded(Class extends ListViewColumn> clazz) throws Exception {
46 | Object instance = clazz.getDeclaredConstructor().newInstance();
47 | Method method = getMetricMethod(clazz);
48 |
49 | Object res = method.invoke(instance, runner.getJob());
50 |
51 | assertNull(res);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/CheckoutDuration.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import hudson.model.Run;
4 | import jenkins.model.Jenkins;
5 | import org.jenkinsci.plugins.workflow.actions.TimingAction;
6 | import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
7 | import org.jenkinsci.plugins.workflow.flow.FlowExecution;
8 | import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker;
9 | import org.jenkinsci.plugins.workflow.graph.FlowNode;
10 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
11 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
12 | import org.jenkinsci.plugins.workflow.steps.scm.GenericSCMStep;
13 |
14 | class CheckoutDuration {
15 |
16 | private CheckoutDuration() {
17 | // not instantiatable
18 | }
19 |
20 | static long checkoutDurationOf(Run run) {
21 | Jenkins instance = Jenkins.getInstanceOrNull();
22 | if (instance == null || instance.getPlugin("workflow-job") == null) {
23 | return 0;
24 | }
25 |
26 | if (!(run instanceof WorkflowRun currentBuild)) {
27 | return 0;
28 | }
29 |
30 | FlowExecution execution = currentBuild.getExecution();
31 | if (execution == null) {
32 | return 0;
33 | }
34 |
35 | return countCheckoutDuration(execution);
36 | }
37 |
38 | private static long countCheckoutDuration(FlowExecution execution) {
39 | long totalCheckoutTime = 0;
40 |
41 | FlowGraphWalker graphWalker = new FlowGraphWalker(execution);
42 | FlowNode nextNode = null;
43 | for (FlowNode node : graphWalker) {
44 | if (node instanceof StepAtomNode stepNode) {
45 | StepDescriptor descriptor = stepNode.getDescriptor();
46 | if (descriptor != null && descriptor.clazz.equals(GenericSCMStep.class)) {
47 | totalCheckoutTime += (TimingAction.getStartTime(nextNode) - TimingAction.getStartTime(node));
48 | }
49 | }
50 | nextNode = node;
51 | }
52 |
53 | return totalCheckoutTime;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/FailureRateColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 |
7 | import hudson.model.ListView;
8 | import org.htmlunit.html.DomNode;
9 | import org.junit.jupiter.api.BeforeAll;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.Test;
12 | import org.jvnet.hudson.test.JenkinsRule;
13 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
14 |
15 | @WithJenkins
16 | class FailureRateColumnTest {
17 |
18 | private FailureRateColumn failureRateColumn;
19 |
20 | private static JenkinsRule jenkinsRule;
21 |
22 | @BeforeAll
23 | static void setUp(JenkinsRule rule) {
24 | jenkinsRule = rule;
25 | }
26 |
27 | @BeforeEach
28 | void before() {
29 | failureRateColumn = new FailureRateColumn();
30 | }
31 |
32 | @Test
33 | void one_failed_job_over_two_failure_rate_should_be_50_percent() throws Exception {
34 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
35 | .configurePipelineDefinition(FAILURE)
36 | .schedule()
37 | .configurePipelineDefinition(SUCCESS)
38 | .schedule();
39 |
40 | Rate failureRate = failureRateColumn.getFailureRate(runner.getJob());
41 |
42 | assertEquals(0.5, failureRate.getAsDouble(), 0);
43 | }
44 |
45 | @Test
46 | void unstable_run_are_considered_failures() throws Exception {
47 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
48 | .configurePipelineDefinition(UNSTABLE)
49 | .schedule();
50 |
51 | Rate failureRate = failureRateColumn.getFailureRate(runner.getJob());
52 |
53 | assertEquals(1, failureRate.getAsDouble(), 0);
54 | }
55 |
56 | @Test
57 | void no_runs_should_display_as_NA_in_UI() throws Exception {
58 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
59 |
60 | ListView listView =
61 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", failureRateColumn, runner.getJob());
62 |
63 | DomNode columnNode;
64 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
65 | columnNode = getListViewCell(
66 | webClient.getPage(listView),
67 | listView,
68 | runner.getJob().getName(),
69 | failureRateColumn.getColumnCaption());
70 | }
71 |
72 | assertEquals("N/A", columnNode.asNormalizedText());
73 | assertEquals("0.0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
74 | }
75 |
76 | @Test
77 | void one_run_should_display_percentage_in_UI() throws Exception {
78 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
79 | .configurePipelineDefinition(FAILURE)
80 | .schedule();
81 |
82 | ListView listView =
83 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", failureRateColumn, runner.getJob());
84 |
85 | DomNode columnNode;
86 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
87 | columnNode = getListViewCell(
88 | webClient.getPage(listView),
89 | listView,
90 | runner.getJob().getName(),
91 | failureRateColumn.getColumnCaption());
92 | }
93 |
94 | assertEquals("100.00%", columnNode.asNormalizedText());
95 | assertEquals("1.0", dataOf(columnNode));
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/SuccessRateColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 |
7 | import hudson.model.ListView;
8 | import org.htmlunit.html.DomNode;
9 | import org.junit.jupiter.api.BeforeAll;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.Test;
12 | import org.jvnet.hudson.test.JenkinsRule;
13 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
14 |
15 | @WithJenkins
16 | class SuccessRateColumnTest {
17 |
18 | private SuccessRateColumn successRateColumn;
19 |
20 | private static JenkinsRule jenkinsRule;
21 |
22 | @BeforeAll
23 | static void setUp(JenkinsRule rule) {
24 | jenkinsRule = rule;
25 | }
26 |
27 | @BeforeEach
28 | void before() {
29 | successRateColumn = new SuccessRateColumn();
30 | }
31 |
32 | @Test
33 | void one_failed_job_over_two_success_rate_should_be_50_percent() throws Exception {
34 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
35 | .configurePipelineDefinition(FAILURE)
36 | .schedule()
37 | .configurePipelineDefinition(SUCCESS)
38 | .schedule();
39 |
40 | Rate successRate = successRateColumn.getSuccessRate(runner.getJob());
41 |
42 | assertEquals(0.5, successRate.getAsDouble(), 0);
43 | }
44 |
45 | @Test
46 | void unstable_run_are_considered_failures() throws Exception {
47 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
48 | .configurePipelineDefinition(UNSTABLE)
49 | .schedule();
50 |
51 | Rate successRate = successRateColumn.getSuccessRate(runner.getJob());
52 |
53 | assertEquals(0, successRate.getAsDouble(), 0);
54 | }
55 |
56 | @Test
57 | void no_runs_should_display_as_NA_in_UI() throws Exception {
58 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
59 |
60 | ListView listView =
61 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", successRateColumn, runner.getJob());
62 |
63 | DomNode columnNode;
64 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
65 | columnNode = getListViewCell(
66 | webClient.getPage(listView),
67 | listView,
68 | runner.getJob().getName(),
69 | successRateColumn.getColumnCaption());
70 | }
71 |
72 | assertEquals("N/A", columnNode.asNormalizedText());
73 | assertEquals("0.0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
74 | }
75 |
76 | @Test
77 | void one_run_should_display_percentage_in_UI() throws Exception {
78 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
79 | .configurePipelineDefinition(SUCCESS)
80 | .schedule();
81 |
82 | ListView listView =
83 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", successRateColumn, runner.getJob());
84 |
85 | DomNode columnNode;
86 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
87 | columnNode = getListViewCell(
88 | webClient.getPage(listView),
89 | listView,
90 | runner.getJob().getName(),
91 | successRateColumn.getColumnCaption());
92 | }
93 |
94 | assertEquals("100.00%", columnNode.asNormalizedText());
95 | assertEquals("1.0", dataOf(columnNode));
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/UnstableRateColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
4 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
5 | import static org.junit.jupiter.api.Assertions.assertEquals;
6 |
7 | import hudson.model.ListView;
8 | import org.htmlunit.html.DomNode;
9 | import org.junit.jupiter.api.BeforeAll;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.Test;
12 | import org.jvnet.hudson.test.JenkinsRule;
13 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
14 |
15 | @WithJenkins
16 | class UnstableRateColumnTest {
17 |
18 | private UnstableRateColumn unstableRateColumn;
19 |
20 | private static JenkinsRule jenkinsRule;
21 |
22 | @BeforeAll
23 | static void setUp(JenkinsRule rule) {
24 | jenkinsRule = rule;
25 | }
26 |
27 | @BeforeEach
28 | void before() {
29 | unstableRateColumn = new UnstableRateColumn();
30 | }
31 |
32 | @Test
33 | void no_unstable_job_should_be_0_percent() throws Exception {
34 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
35 | .configurePipelineDefinition(FAILURE)
36 | .schedule()
37 | .configurePipelineDefinition(SUCCESS)
38 | .schedule();
39 |
40 | Rate unstableRate = unstableRateColumn.getUnstableRate(runner.getJob());
41 |
42 | assertEquals(0.0, unstableRate.getAsDouble(), 0);
43 | }
44 |
45 | @Test
46 | void one_unstable_job_over_two_failed_should_be_50_percent() throws Exception {
47 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
48 | .configurePipelineDefinition(UNSTABLE)
49 | .schedule()
50 | .configurePipelineDefinition(FAILURE)
51 | .schedule();
52 |
53 | Rate unstableRate = unstableRateColumn.getUnstableRate(runner.getJob());
54 |
55 | assertEquals(0.5, unstableRate.getAsDouble(), 0);
56 | }
57 |
58 | @Test
59 | void no_runs_should_display_as_NA_in_UI() throws Exception {
60 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
61 |
62 | ListView listView =
63 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", unstableRateColumn, runner.getJob());
64 |
65 | DomNode columnNode;
66 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
67 | columnNode = getListViewCell(
68 | webClient.getPage(listView),
69 | listView,
70 | runner.getJob().getName(),
71 | unstableRateColumn.getColumnCaption());
72 | }
73 |
74 | assertEquals("N/A", columnNode.asNormalizedText());
75 | assertEquals("0.0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
76 | }
77 |
78 | @Test
79 | void one_unstable_over_one_failed_should_display_percentage_in_UI() throws Exception {
80 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
81 | .configurePipelineDefinition(UNSTABLE)
82 | .schedule();
83 |
84 | ListView listView =
85 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", unstableRateColumn, runner.getJob());
86 |
87 | DomNode columnNode;
88 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
89 | columnNode = getListViewCell(
90 | webClient.getPage(listView),
91 | listView,
92 | runner.getJob().getName(),
93 | unstableRateColumn.getColumnCaption());
94 | }
95 |
96 | assertEquals("100.00%", columnNode.asNormalizedText());
97 | assertEquals("1.0", dataOf(columnNode));
98 | }
99 |
100 | @Test
101 | void one_unstable_job_over_four_jobs_should_be_25_percent() throws Exception {
102 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
103 | .configurePipelineDefinition(UNSTABLE)
104 | .schedule()
105 | .configurePipelineDefinition(FAILURE)
106 | .schedule()
107 | .schedule()
108 | .configurePipelineDefinition(SUCCESS)
109 | .schedule();
110 |
111 | Rate unstableRate = unstableRateColumn.getUnstableRate(runner.getJob());
112 |
113 | assertEquals(0.25, unstableRate.getAsDouble(), 0);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/FailureTimeRateColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.FAILURE;
5 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.SUCCESS;
6 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
16 |
17 | @WithJenkins
18 | class FailureTimeRateColumnTest {
19 |
20 | private FailureTimeRateColumn failureTimeRateColumn;
21 |
22 | private static JenkinsRule jenkinsRule;
23 |
24 | @BeforeAll
25 | static void setUp(JenkinsRule rule) {
26 | jenkinsRule = rule;
27 | }
28 |
29 | @BeforeEach
30 | void before() {
31 | failureTimeRateColumn = new FailureTimeRateColumn();
32 | }
33 |
34 | @Test
35 | void one_success_run_failure_time_rate_should_be_0_percent() throws Exception {
36 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
37 | .configurePipelineDefinition(SUCCESS)
38 | .schedule();
39 |
40 | Rate failureTimeRate = failureTimeRateColumn.getFailureTimeRate(runner.getJob());
41 |
42 | assertEquals(0, failureTimeRate.getAsDouble(), 0);
43 | }
44 |
45 | @Test
46 | void two_success_runs_failure_time_rate_should_be_0_percent() throws Exception {
47 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
48 | .configurePipelineDefinition(SUCCESS)
49 | .schedule()
50 | .schedule();
51 |
52 | Rate failureTimeRate = failureTimeRateColumn.getFailureTimeRate(runner.getJob());
53 |
54 | assertEquals(0, failureTimeRate.getAsDouble(), 0);
55 | }
56 |
57 | @Test
58 | void one_success_run_followed_by_one_failure_run() throws Exception {
59 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
60 | .configurePipelineDefinition(SUCCESS)
61 | .schedule()
62 | .configurePipelineDefinition(FAILURE)
63 | .schedule();
64 |
65 | Rate failureTimeRate = failureTimeRateColumn.getFailureTimeRate(runner.getJob());
66 |
67 | assertThat(failureTimeRate.getAsDouble()).isGreaterThan(0.0);
68 | assertThat(failureTimeRate.getAsDouble()).isLessThan(1.0);
69 | }
70 |
71 | @Test
72 | void no_runs_should_display_as_NA_in_UI() throws Exception {
73 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
74 |
75 | ListView listView =
76 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", failureTimeRateColumn, runner.getJob());
77 |
78 | DomNode columnNode;
79 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
80 | columnNode = getListViewCell(
81 | webClient.getPage(listView),
82 | listView,
83 | runner.getJob().getName(),
84 | failureTimeRateColumn.getColumnCaption());
85 | }
86 |
87 | assertEquals("N/A", columnNode.asNormalizedText());
88 | assertEquals("0.0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
89 | }
90 |
91 | @Test
92 | void one_run_should_display_percentage_in_UI() throws Exception {
93 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
94 | .configurePipelineDefinition(FAILURE)
95 | .schedule();
96 |
97 | ListView listView =
98 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", failureTimeRateColumn, runner.getJob());
99 |
100 | DomNode columnNode;
101 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
102 | columnNode = getListViewCell(
103 | webClient.getPage(listView),
104 | listView,
105 | runner.getJob().getName(),
106 | failureTimeRateColumn.getColumnCaption());
107 | }
108 |
109 | assertEquals("100.00%", columnNode.asNormalizedText());
110 | assertEquals("1.0", dataOf(columnNode));
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/SuccessTimeRateColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.FAILURE;
5 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.SUCCESS;
6 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
16 |
17 | @WithJenkins
18 | class SuccessTimeRateColumnTest {
19 |
20 | private SuccessTimeRateColumn successTimeRateColumn;
21 |
22 | private static JenkinsRule jenkinsRule;
23 |
24 | @BeforeAll
25 | static void setUp(JenkinsRule rule) {
26 | jenkinsRule = rule;
27 | }
28 |
29 | @BeforeEach
30 | void before() {
31 | successTimeRateColumn = new SuccessTimeRateColumn();
32 | }
33 |
34 | @Test
35 | void one_failed_run_success_time_rate_should_be_0_percent() throws Exception {
36 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
37 | .configurePipelineDefinition(FAILURE)
38 | .schedule();
39 |
40 | Rate successTimeRate = successTimeRateColumn.getSuccessTimeRate(runner.getJob());
41 |
42 | assertEquals(0, successTimeRate.getAsDouble(), 0);
43 | }
44 |
45 | @Test
46 | void two_failed_runs_success_time_rate_should_be_0_percent() throws Exception {
47 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
48 | .configurePipelineDefinition(FAILURE)
49 | .schedule()
50 | .schedule();
51 |
52 | Rate successTimeRate = successTimeRateColumn.getSuccessTimeRate(runner.getJob());
53 |
54 | assertEquals(0, successTimeRate.getAsDouble(), 0);
55 | }
56 |
57 | @Test
58 | void one_failed_run_followed_by_one_success_run() throws Exception {
59 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
60 | .configurePipelineDefinition(FAILURE)
61 | .schedule()
62 | .configurePipelineDefinition(SUCCESS)
63 | .schedule();
64 |
65 | Rate successTimeRate = successTimeRateColumn.getSuccessTimeRate(runner.getJob());
66 |
67 | assertThat(successTimeRate.getAsDouble()).isGreaterThan(0.0);
68 | assertThat(successTimeRate.getAsDouble()).isLessThan(1.0);
69 | }
70 |
71 | @Test
72 | void no_runs_should_display_as_NA_in_UI() throws Exception {
73 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
74 |
75 | ListView listView =
76 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", successTimeRateColumn, runner.getJob());
77 |
78 | DomNode columnNode;
79 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
80 | columnNode = getListViewCell(
81 | webClient.getPage(listView),
82 | listView,
83 | runner.getJob().getName(),
84 | successTimeRateColumn.getColumnCaption());
85 | }
86 |
87 | assertEquals("N/A", columnNode.asNormalizedText());
88 | assertEquals("0.0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
89 | }
90 |
91 | @Test
92 | void one_run_should_display_percentage_in_UI() throws Exception {
93 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
94 | .configurePipelineDefinition(SUCCESS)
95 | .schedule();
96 |
97 | ListView listView =
98 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", successTimeRateColumn, runner.getJob());
99 |
100 | DomNode columnNode;
101 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
102 | columnNode = getListViewCell(
103 | webClient.getPage(listView),
104 | listView,
105 | runner.getJob().getName(),
106 | successTimeRateColumn.getColumnCaption());
107 | }
108 |
109 | assertEquals("100.00%", columnNode.asNormalizedText());
110 | assertEquals("1.0", dataOf(columnNode));
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/AvgSuccessDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
16 |
17 | @WithJenkins
18 | class AvgSuccessDurationColumnTest {
19 |
20 | private AvgSuccessDurationColumn avgSuccessDurationColumn;
21 |
22 | private static JenkinsRule jenkinsRule;
23 |
24 | @BeforeAll
25 | static void setUp(JenkinsRule rule) {
26 | jenkinsRule = rule;
27 | }
28 |
29 | @BeforeEach
30 | void before() {
31 | avgSuccessDurationColumn = new AvgSuccessDurationColumn();
32 | }
33 |
34 | @Test
35 | void two_successful_runs_should_return_their_average_duration() throws Exception {
36 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
37 | .configurePipelineDefinition(SUCCESS)
38 | .schedule()
39 | .configurePipelineDefinition(SLOW_3S)
40 | .schedule();
41 |
42 | Duration avgDuration = avgSuccessDurationColumn.getAverageSuccessDuration(runner.getJob());
43 |
44 | assertEquals(
45 | (runner.getRuns()[0].getDuration() + runner.getRuns()[1].getDuration()) / 2, avgDuration.getAsLong());
46 | }
47 |
48 | @Test
49 | void failed_runs_should_be_excluded() throws Exception {
50 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
51 | .configurePipelineDefinition(FAILURE)
52 | .schedule();
53 |
54 | Duration avgDuration = avgSuccessDurationColumn.getAverageSuccessDuration(runner.getJob());
55 |
56 | assertNull(avgDuration);
57 | }
58 |
59 | @Test
60 | void unstable_runs_should_be_excluded() throws Exception {
61 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
62 | .configurePipelineDefinition(UNSTABLE)
63 | .schedule();
64 |
65 | Duration avgDuration = avgSuccessDurationColumn.getAverageSuccessDuration(runner.getJob());
66 |
67 | assertNull(avgDuration);
68 | }
69 |
70 | @Test
71 | void no_runs_should_display_as_NA_in_UI() throws Exception {
72 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
73 |
74 | ListView listView = createAndAddListView(
75 | jenkinsRule.getInstance(), "MyListNoRuns", avgSuccessDurationColumn, runner.getJob());
76 |
77 | DomNode columnNode;
78 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
79 | columnNode = getListViewCell(
80 | webClient.getPage(listView),
81 | listView,
82 | runner.getJob().getName(),
83 | avgSuccessDurationColumn.getColumnCaption());
84 | }
85 |
86 | assertEquals("N/A", columnNode.asNormalizedText());
87 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
88 | }
89 |
90 | @Test
91 | void one_run_should_display_avg_duration_in_UI() throws Exception {
92 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
93 | .configurePipelineDefinition(SUCCESS)
94 | .schedule();
95 |
96 | ListView listView = createAndAddListView(
97 | jenkinsRule.getInstance(), "MyListOneRun", avgSuccessDurationColumn, runner.getJob());
98 |
99 | DomNode columnNode;
100 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
101 | columnNode = getListViewCell(
102 | webClient.getPage(listView),
103 | listView,
104 | runner.getJob().getName(),
105 | avgSuccessDurationColumn.getColumnCaption());
106 | }
107 |
108 | // sample output: 1.1 sec
109 | String text = columnNode.asNormalizedText();
110 |
111 | assertThat(text).containsAnyOf(TIME_UNITS);
112 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/MaxSuccessDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.JenkinsRule.WebClient;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class MaxSuccessDurationColumnTest {
20 |
21 | private MaxSuccessDurationColumn maxSuccessDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | maxSuccessDurationColumn = new MaxSuccessDurationColumn();
33 | }
34 |
35 | @Test
36 | void two_successful_runs_should_return_the_longest() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(SUCCESS)
39 | .schedule()
40 | .configurePipelineDefinition(SLOW_3S)
41 | .schedule();
42 |
43 | RunWithDuration longestRun = maxSuccessDurationColumn.getLongestSuccessfulRun(runner.getJob());
44 |
45 | assertSame(runner.getRuns()[1], longestRun.run());
46 | }
47 |
48 | @Test
49 | void failed_runs_should_be_excluded() throws Exception {
50 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
51 | .configurePipelineDefinition(FAILURE)
52 | .schedule();
53 |
54 | RunWithDuration longestRun = maxSuccessDurationColumn.getLongestSuccessfulRun(runner.getJob());
55 |
56 | assertNull(longestRun);
57 | }
58 |
59 | @Test
60 | void unstable_runs_should_be_excluded() throws Exception {
61 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
62 | .configurePipelineDefinition(UNSTABLE)
63 | .schedule();
64 |
65 | RunWithDuration longestRun = maxSuccessDurationColumn.getLongestSuccessfulRun(runner.getJob());
66 |
67 | assertNull(longestRun);
68 | }
69 |
70 | @Test
71 | void no_runs_should_display_as_NA_in_UI() throws Exception {
72 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
73 |
74 | ListView listView = createAndAddListView(
75 | jenkinsRule.getInstance(), "MyListNoRuns", maxSuccessDurationColumn, runner.getJob());
76 |
77 | DomNode columnNode;
78 | try (WebClient webClient = jenkinsRule.createWebClient()) {
79 | columnNode = getListViewCell(
80 | webClient.getPage(listView),
81 | listView,
82 | runner.getJob().getName(),
83 | maxSuccessDurationColumn.getColumnCaption());
84 | }
85 |
86 | assertEquals("N/A", columnNode.asNormalizedText());
87 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
88 | }
89 |
90 | @Test
91 | void one_run_should_display_time_and_build_in_UI() throws Exception {
92 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
93 | .configurePipelineDefinition(SUCCESS)
94 | .schedule();
95 |
96 | ListView listView = createAndAddListView(
97 | jenkinsRule.getInstance(), "MyListOneRun", maxSuccessDurationColumn, runner.getJob());
98 |
99 | DomNode columnNode;
100 | try (WebClient webClient = jenkinsRule.createWebClient()) {
101 | columnNode = getListViewCell(
102 | webClient.getPage(listView),
103 | listView,
104 | runner.getJob().getName(),
105 | maxSuccessDurationColumn.getColumnCaption());
106 | }
107 |
108 | // sample output: 1.1 sec - #1
109 | String text = columnNode.asNormalizedText();
110 |
111 | assertThat(text).containsAnyOf(TIME_UNITS);
112 | assertThat(text).contains("#" + runner.getRuns()[0].getId());
113 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/MinSuccessDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.JenkinsRule.WebClient;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class MinSuccessDurationColumnTest {
20 |
21 | private MinSuccessDurationColumn minSuccessDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | minSuccessDurationColumn = new MinSuccessDurationColumn();
33 | }
34 |
35 | @Test
36 | void two_successful_runs_should_return_the_shortest() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(SUCCESS)
39 | .schedule()
40 | .configurePipelineDefinition(SLOW_3S)
41 | .schedule();
42 |
43 | RunWithDuration shortestRun = minSuccessDurationColumn.getShortestSuccessfulRun(runner.getJob());
44 |
45 | assertSame(runner.getRuns()[0], shortestRun.run());
46 | }
47 |
48 | @Test
49 | void failed_runs_should_be_excluded() throws Exception {
50 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
51 | .configurePipelineDefinition(FAILURE)
52 | .schedule();
53 |
54 | RunWithDuration shortestRun = minSuccessDurationColumn.getShortestSuccessfulRun(runner.getJob());
55 |
56 | assertNull(shortestRun);
57 | }
58 |
59 | @Test
60 | void unstable_runs_should_be_excluded() throws Exception {
61 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
62 | .configurePipelineDefinition(UNSTABLE)
63 | .schedule();
64 |
65 | RunWithDuration shortestRun = minSuccessDurationColumn.getShortestSuccessfulRun(runner.getJob());
66 |
67 | assertNull(shortestRun);
68 | }
69 |
70 | @Test
71 | void no_runs_should_display_as_NA_in_UI() throws Exception {
72 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
73 |
74 | ListView listView = createAndAddListView(
75 | jenkinsRule.getInstance(), "MyListNoRuns", minSuccessDurationColumn, runner.getJob());
76 |
77 | DomNode columnNode;
78 | try (WebClient webClient = jenkinsRule.createWebClient()) {
79 | columnNode = getListViewCell(
80 | webClient.getPage(listView),
81 | listView,
82 | runner.getJob().getName(),
83 | minSuccessDurationColumn.getColumnCaption());
84 | }
85 |
86 | assertEquals("N/A", columnNode.asNormalizedText());
87 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
88 | }
89 |
90 | @Test
91 | void one_run_should_display_time_and_build_in_UI() throws Exception {
92 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
93 | .configurePipelineDefinition(SUCCESS)
94 | .schedule();
95 |
96 | ListView listView = createAndAddListView(
97 | jenkinsRule.getInstance(), "MyListOneRun", minSuccessDurationColumn, runner.getJob());
98 |
99 | DomNode columnNode;
100 | try (WebClient webClient = jenkinsRule.createWebClient()) {
101 | columnNode = getListViewCell(
102 | webClient.getPage(listView),
103 | listView,
104 | runner.getJob().getName(),
105 | minSuccessDurationColumn.getColumnCaption());
106 | }
107 |
108 | // sample output: 1.1 sec - #1
109 | String text = columnNode.asNormalizedText();
110 |
111 | assertThat(text).containsAnyOf(TIME_UNITS);
112 | assertThat(text).contains("#" + runner.getRuns()[0].getId());
113 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/Utils.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import com.google.common.collect.Iterables;
4 | import hudson.model.Run;
5 | import java.util.List;
6 | import java.util.Optional;
7 | import java.util.OptionalDouble;
8 | import java.util.function.BinaryOperator;
9 | import java.util.function.Function;
10 | import java.util.function.Predicate;
11 | import java.util.function.ToLongFunction;
12 | import java.util.stream.LongStream;
13 | import java.util.stream.Stream;
14 |
15 | class Utils {
16 |
17 | private Utils() {
18 | // utility class
19 | }
20 |
21 | static Optional rateOf(List extends Run> runs, Predicate preFilter, Predicate predicateRate) {
22 | List extends Run> filteredRuns = preFilter(runs, preFilter).toList();
23 |
24 | if (filteredRuns.isEmpty()) {
25 | return Optional.empty();
26 | }
27 |
28 | int totalRuns = 0;
29 | int predicateApplicableRuns = 0;
30 |
31 | for (Run run : filteredRuns) {
32 | totalRuns++;
33 | if (predicateRate.test(run)) {
34 | predicateApplicableRuns++;
35 | }
36 | }
37 |
38 | return Optional.of(new Rate((double) predicateApplicableRuns / totalRuns));
39 | }
40 |
41 | static Optional timeRateOf(List extends Run> runs, Predicate preFilter, Predicate predicateRate) {
42 | List extends Run> filteredRuns = preFilter(runs, preFilter).toList();
43 |
44 | if (filteredRuns.isEmpty()) {
45 | return Optional.empty();
46 | }
47 |
48 | Run firstRun = Iterables.getLast(filteredRuns, null);
49 | long startTime = firstRun.getStartTimeInMillis();
50 | long endTime = System.currentTimeMillis();
51 |
52 | long previousTime = endTime;
53 | long accumulatedPredicateTime = 0L;
54 |
55 | for (Run run : filteredRuns) {
56 | long runStartTime = run.getStartTimeInMillis();
57 |
58 | if (predicateRate.test(run)) {
59 | accumulatedPredicateTime += previousTime - runStartTime;
60 | }
61 |
62 | previousTime = runStartTime;
63 | }
64 |
65 | return Optional.of(new Rate((double) accumulatedPredicateTime / (endTime - startTime)));
66 | }
67 |
68 | static Optional findRun(
69 | List extends Run> runs,
70 | Predicate preFilter,
71 | ToLongFunction durationFunction,
72 | BinaryOperator operator) {
73 | return preFilter(runs, preFilter)
74 | .filter(r -> durationFunction.applyAsLong(r) > 0)
75 | .map(r -> new RunWithDuration(r, new Duration(durationFunction.applyAsLong(r))))
76 | .reduce(operator);
77 | }
78 |
79 | static Optional averageDuration(
80 | List extends Run> runs, Predicate preFilter, ToLongFunction durationFunction) {
81 | return durationFunction(runs, preFilter, durationFunction, LongStream::average);
82 | }
83 |
84 | static Optional stdDevDuration(
85 | List extends Run> runs, Predicate preFilter, ToLongFunction durationFunction) {
86 | List durations = preFilter(runs, preFilter)
87 | .filter(r -> durationFunction.applyAsLong(r) > 0)
88 | .mapToLong(durationFunction)
89 | .boxed()
90 | .toList();
91 |
92 | if (!durations.isEmpty()) {
93 | return Optional.of(new Duration((long) MathCommons.standardDeviation(durations)));
94 | } else {
95 | return Optional.empty();
96 | }
97 | }
98 |
99 | private static Optional durationFunction(
100 | List extends Run> runs,
101 | Predicate preFilter,
102 | ToLongFunction durationFunction,
103 | Function durationCollector) {
104 | LongStream longStream = preFilter(runs, preFilter)
105 | .filter(r -> durationFunction.applyAsLong(r) > 0)
106 | .mapToLong(durationFunction);
107 |
108 | OptionalDouble val = durationCollector.apply(longStream);
109 |
110 | if (val.isPresent()) {
111 | return Optional.of(new Duration((long) val.getAsDouble()));
112 | } else {
113 | return Optional.empty();
114 | }
115 | }
116 |
117 | private static Stream extends Run> preFilter(List extends Run> runs, Predicate preFilter) {
118 | Stream extends Run> stream = runs.size() > 100 ? runs.parallelStream() : runs.stream();
119 | return stream.filter(preFilter);
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/AvgCheckoutDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
16 |
17 | @WithJenkins
18 | class AvgCheckoutDurationColumnTest {
19 |
20 | private AvgCheckoutDurationColumn avgCheckoutDurationColumn;
21 |
22 | private static JenkinsRule jenkinsRule;
23 |
24 | @BeforeAll
25 | static void setUp(JenkinsRule rule) {
26 | jenkinsRule = rule;
27 | }
28 |
29 | @BeforeEach
30 | void before() {
31 | avgCheckoutDurationColumn = new AvgCheckoutDurationColumn();
32 | }
33 |
34 | @Test
35 | void two_successful_runs_should_return_a_positive_average_checkout_duration() throws Exception {
36 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
37 | .configurePipelineDefinition(CHECKOUT)
38 | .schedule()
39 | .schedule();
40 |
41 | Duration avgDuration = avgCheckoutDurationColumn.getAverageCheckoutDuration(runner.getJob());
42 |
43 | assertThat(avgDuration.getAsLong()).isGreaterThan(0L);
44 | }
45 |
46 | @Test
47 | void failed_runs_are_not_excluded() throws Exception {
48 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
49 | .configurePipelineDefinition(CHECKOUT, FAILURE)
50 | .schedule();
51 |
52 | Duration avgDuration = avgCheckoutDurationColumn.getAverageCheckoutDuration(runner.getJob());
53 |
54 | assertThat(avgDuration.getAsLong()).isGreaterThan(0L);
55 | }
56 |
57 | @Test
58 | void unstable_runs_are_not_excluded() throws Exception {
59 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
60 | .configurePipelineDefinition(CHECKOUT, UNSTABLE)
61 | .schedule();
62 |
63 | Duration avgDuration = avgCheckoutDurationColumn.getAverageCheckoutDuration(runner.getJob());
64 |
65 | assertThat(avgDuration.getAsLong()).isGreaterThan(0L);
66 | }
67 |
68 | @Test
69 | void freestyle_jobs_are_not_counted() throws Exception {
70 | var runner = JobRunner.createFreestyleJob(jenkinsRule)
71 | .configureCheckout()
72 | .addSuccessExecution()
73 | .schedule();
74 |
75 | Duration avgDuration = avgCheckoutDurationColumn.getAverageCheckoutDuration(runner.getJob());
76 |
77 | assertNull(avgDuration);
78 | }
79 |
80 | @Test
81 | void no_runs_should_display_as_NA_in_UI() throws Exception {
82 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
83 |
84 | ListView listView = createAndAddListView(
85 | jenkinsRule.getInstance(), "MyListNoRuns", avgCheckoutDurationColumn, runner.getJob());
86 |
87 | DomNode columnNode;
88 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
89 | columnNode = getListViewCell(
90 | webClient.getPage(listView),
91 | listView,
92 | runner.getJob().getName(),
93 | avgCheckoutDurationColumn.getColumnCaption());
94 | }
95 |
96 | assertEquals("N/A", columnNode.asNormalizedText());
97 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
98 | }
99 |
100 | @Test
101 | void one_run_should_display_avg_duration_in_UI() throws Exception {
102 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
103 | .configurePipelineDefinition(CHECKOUT)
104 | .schedule();
105 |
106 | ListView listView = createAndAddListView(
107 | jenkinsRule.getInstance(), "MyListOneRun", avgCheckoutDurationColumn, runner.getJob());
108 |
109 | DomNode columnNode;
110 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
111 | columnNode = getListViewCell(
112 | webClient.getPage(listView),
113 | listView,
114 | runner.getJob().getName(),
115 | avgCheckoutDurationColumn.getColumnCaption());
116 | }
117 |
118 | // sample output: 1.1 sec
119 | String text = columnNode.asNormalizedText();
120 |
121 | assertThat(text).containsAnyOf(TIME_UNITS);
122 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/MaxDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.JenkinsRule.WebClient;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class MaxDurationColumnTest {
20 |
21 | private MaxDurationColumn maxDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | maxDurationColumn = new MaxDurationColumn();
33 | }
34 |
35 | @Test
36 | void two_successful_runs_should_return_the_longest() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(SUCCESS)
39 | .schedule()
40 | .configurePipelineDefinition(SLOW_3S)
41 | .schedule();
42 |
43 | RunWithDuration longestRun = maxDurationColumn.getLongestRun(runner.getJob());
44 |
45 | assertSame(runner.getRuns()[1], longestRun.run());
46 | }
47 |
48 | @Test
49 | void two_runs_including_one_failure_should_return_the_longest() throws Exception {
50 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
51 | .configurePipelineDefinition(SUCCESS)
52 | .schedule()
53 | .configurePipelineDefinition(SLOW_3S, FAILURE)
54 | .schedule();
55 |
56 | RunWithDuration longestRun = maxDurationColumn.getLongestRun(runner.getJob());
57 |
58 | assertSame(runner.getRuns()[1], longestRun.run());
59 | }
60 |
61 | @Test
62 | void failed_runs_are_not_excluded() throws Exception {
63 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
64 | .configurePipelineDefinition(FAILURE)
65 | .schedule();
66 |
67 | RunWithDuration longestRun = maxDurationColumn.getLongestRun(runner.getJob());
68 |
69 | assertSame(runner.getRuns()[0], longestRun.run());
70 | }
71 |
72 | @Test
73 | void unstable_runs_are_not_excluded() throws Exception {
74 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
75 | .configurePipelineDefinition(UNSTABLE)
76 | .schedule();
77 |
78 | RunWithDuration longestRun = maxDurationColumn.getLongestRun(runner.getJob());
79 |
80 | assertSame(runner.getRuns()[0], longestRun.run());
81 | }
82 |
83 | @Test
84 | void no_runs_should_display_as_NA_in_UI() throws Exception {
85 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
86 |
87 | ListView listView =
88 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", maxDurationColumn, runner.getJob());
89 |
90 | DomNode columnNode;
91 | try (WebClient webClient = jenkinsRule.createWebClient()) {
92 | columnNode = getListViewCell(
93 | webClient.getPage(listView),
94 | listView,
95 | runner.getJob().getName(),
96 | maxDurationColumn.getColumnCaption());
97 | }
98 |
99 | assertEquals("N/A", columnNode.asNormalizedText());
100 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
101 | }
102 |
103 | @Test
104 | void one_run_should_display_time_and_build_in_UI() throws Exception {
105 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
106 | .configurePipelineDefinition(SUCCESS)
107 | .schedule();
108 |
109 | ListView listView =
110 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", maxDurationColumn, runner.getJob());
111 |
112 | DomNode columnNode;
113 | try (WebClient webClient = jenkinsRule.createWebClient()) {
114 | columnNode = getListViewCell(
115 | webClient.getPage(listView),
116 | listView,
117 | runner.getJob().getName(),
118 | maxDurationColumn.getColumnCaption());
119 | }
120 |
121 | // sample output: 1.1 sec - #1
122 | String text = columnNode.asNormalizedText();
123 |
124 | assertThat(text).containsAnyOf(TIME_UNITS);
125 | assertThat(text).contains("#" + runner.getRuns()[0].getId());
126 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/MinDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.JenkinsRule.WebClient;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class MinDurationColumnTest {
20 |
21 | private MinDurationColumn minDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | minDurationColumn = new MinDurationColumn();
33 | }
34 |
35 | @Test
36 | void two_successful_runs_should_return_the_shortest() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(SUCCESS)
39 | .schedule()
40 | .configurePipelineDefinition(SLOW_3S)
41 | .schedule();
42 |
43 | RunWithDuration shortestRun = minDurationColumn.getShortestRun(runner.getJob());
44 |
45 | assertSame(runner.getRuns()[0], shortestRun.run());
46 | }
47 |
48 | @Test
49 | void two_runs_including_one_failure_should_return_the_shortest() throws Exception {
50 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
51 | .configurePipelineDefinition(FAILURE)
52 | .schedule()
53 | .configurePipelineDefinition(SLOW_3S)
54 | .schedule();
55 |
56 | RunWithDuration shortestRun = minDurationColumn.getShortestRun(runner.getJob());
57 |
58 | assertSame(runner.getRuns()[0], shortestRun.run());
59 | }
60 |
61 | @Test
62 | void failed_runs_are_not_excluded() throws Exception {
63 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
64 | .configurePipelineDefinition(FAILURE)
65 | .schedule();
66 |
67 | RunWithDuration shortestRun = minDurationColumn.getShortestRun(runner.getJob());
68 |
69 | assertSame(runner.getRuns()[0], shortestRun.run());
70 | }
71 |
72 | @Test
73 | void unstable_runs_are_not_excluded() throws Exception {
74 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
75 | .configurePipelineDefinition(UNSTABLE)
76 | .schedule();
77 |
78 | RunWithDuration shortestRun = minDurationColumn.getShortestRun(runner.getJob());
79 |
80 | assertSame(runner.getRuns()[0], shortestRun.run());
81 | }
82 |
83 | @Test
84 | void no_runs_should_display_as_NA_in_UI() throws Exception {
85 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
86 |
87 | ListView listView =
88 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", minDurationColumn, runner.getJob());
89 |
90 | DomNode columnNode;
91 | try (WebClient webClient = jenkinsRule.createWebClient()) {
92 | columnNode = getListViewCell(
93 | webClient.getPage(listView),
94 | listView,
95 | runner.getJob().getName(),
96 | minDurationColumn.getColumnCaption());
97 | }
98 |
99 | assertEquals("N/A", columnNode.asNormalizedText());
100 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
101 | }
102 |
103 | @Test
104 | void one_run_should_display_time_and_build_in_UI() throws Exception {
105 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
106 | .configurePipelineDefinition(SUCCESS)
107 | .schedule();
108 |
109 | ListView listView =
110 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", minDurationColumn, runner.getJob());
111 |
112 | DomNode columnNode;
113 | try (WebClient webClient = jenkinsRule.createWebClient()) {
114 | columnNode = getListViewCell(
115 | webClient.getPage(listView),
116 | listView,
117 | runner.getJob().getName(),
118 | minDurationColumn.getColumnCaption());
119 | }
120 |
121 | // sample output: 1.1 sec - #1
122 | String text = columnNode.asNormalizedText();
123 |
124 | assertThat(text).containsAnyOf(TIME_UNITS);
125 | assertThat(text).contains("#" + runner.getRuns()[0].getId());
126 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/MaxCheckoutDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.JenkinsRule.WebClient;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class MaxCheckoutDurationColumnTest {
20 |
21 | private MaxCheckoutDurationColumn maxCheckoutDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | maxCheckoutDurationColumn = new MaxCheckoutDurationColumn();
33 | }
34 |
35 | @Test
36 | void one_run_with_checkout_should_return_checkout() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(CHECKOUT)
39 | .schedule();
40 |
41 | RunWithDuration longestCheckoutRun = maxCheckoutDurationColumn.getLongestCheckoutRun(runner.getJob());
42 |
43 | assertThat(longestCheckoutRun.duration().getAsLong()).isGreaterThan(0L);
44 | }
45 |
46 | @Test
47 | void failed_runs_are_included_in_the_checkout_time_calculation() throws Exception {
48 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
49 | .configurePipelineDefinition(CHECKOUT, FAILURE)
50 | .schedule();
51 |
52 | RunWithDuration longestCheckoutRun = maxCheckoutDurationColumn.getLongestCheckoutRun(runner.getJob());
53 |
54 | assertSame(runner.getRuns()[0], longestCheckoutRun.run());
55 | }
56 |
57 | @Test
58 | void unstable_runs_are_included_in_the_checkout_time_calculation() throws Exception {
59 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
60 | .configurePipelineDefinition(CHECKOUT, UNSTABLE)
61 | .schedule();
62 |
63 | RunWithDuration longestCheckoutRun = maxCheckoutDurationColumn.getLongestCheckoutRun(runner.getJob());
64 |
65 | assertSame(runner.getRuns()[0], longestCheckoutRun.run());
66 | }
67 |
68 | @Test
69 | void freestyle_jobs_are_not_counted() throws Exception {
70 | var runner = JobRunner.createFreestyleJob(jenkinsRule)
71 | .configureCheckout()
72 | .addSuccessExecution()
73 | .schedule();
74 |
75 | RunWithDuration longestCheckoutRun = maxCheckoutDurationColumn.getLongestCheckoutRun(runner.getJob());
76 |
77 | assertNull(longestCheckoutRun);
78 | }
79 |
80 | @Test
81 | void no_runs_should_display_as_NA_in_UI() throws Exception {
82 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
83 |
84 | ListView listView = createAndAddListView(
85 | jenkinsRule.getInstance(), "MyListNoRuns", maxCheckoutDurationColumn, runner.getJob());
86 |
87 | DomNode columnNode;
88 | try (WebClient webClient = jenkinsRule.createWebClient()) {
89 | columnNode = getListViewCell(
90 | webClient.getPage(listView),
91 | listView,
92 | runner.getJob().getName(),
93 | maxCheckoutDurationColumn.getColumnCaption());
94 | }
95 |
96 | assertEquals("N/A", columnNode.asNormalizedText());
97 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
98 | }
99 |
100 | @Test
101 | void one_run_should_display_time_and_build_in_UI() throws Exception {
102 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
103 | .configurePipelineDefinition(CHECKOUT)
104 | .schedule();
105 |
106 | ListView listView = createAndAddListView(
107 | jenkinsRule.getInstance(), "MyListOneRun", maxCheckoutDurationColumn, runner.getJob());
108 |
109 | DomNode columnNode;
110 | try (WebClient webClient = jenkinsRule.createWebClient()) {
111 | columnNode = getListViewCell(
112 | webClient.getPage(listView),
113 | listView,
114 | runner.getJob().getName(),
115 | maxCheckoutDurationColumn.getColumnCaption());
116 | }
117 |
118 | // sample output: 1.1 sec - #1
119 | String text = columnNode.asNormalizedText();
120 |
121 | assertThat(text).containsAnyOf(TIME_UNITS);
122 | assertThat(text).contains("#" + runner.getRuns()[0].getId());
123 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/MinCheckoutDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.JenkinsRule.WebClient;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class MinCheckoutDurationColumnTest {
20 |
21 | private MinCheckoutDurationColumn minCheckoutDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | minCheckoutDurationColumn = new MinCheckoutDurationColumn();
33 | }
34 |
35 | @Test
36 | void one_run_with_checkout_should_return_checkout() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(CHECKOUT)
39 | .schedule();
40 |
41 | RunWithDuration shortestCheckoutRun = minCheckoutDurationColumn.getShortestCheckoutRun(runner.getJob());
42 |
43 | assertThat(shortestCheckoutRun.duration().getAsLong()).isGreaterThan(0L);
44 | }
45 |
46 | @Test
47 | void failed_runs_are_included_in_the_checkout_time_calculation() throws Exception {
48 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
49 | .configurePipelineDefinition(CHECKOUT, FAILURE)
50 | .schedule();
51 |
52 | RunWithDuration shortestCheckoutRun = minCheckoutDurationColumn.getShortestCheckoutRun(runner.getJob());
53 |
54 | assertSame(runner.getRuns()[0], shortestCheckoutRun.run());
55 | }
56 |
57 | @Test
58 | void unstable_runs_are_included_in_the_checkout_time_calculation() throws Exception {
59 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
60 | .configurePipelineDefinition(CHECKOUT, UNSTABLE)
61 | .schedule();
62 |
63 | RunWithDuration shortestCheckoutRun = minCheckoutDurationColumn.getShortestCheckoutRun(runner.getJob());
64 |
65 | assertSame(runner.getRuns()[0], shortestCheckoutRun.run());
66 | }
67 |
68 | @Test
69 | void freestyle_jobs_are_not_counted() throws Exception {
70 | var runner = JobRunner.createFreestyleJob(jenkinsRule)
71 | .configureCheckout()
72 | .addSuccessExecution()
73 | .schedule();
74 |
75 | RunWithDuration shortestCheckoutRun = minCheckoutDurationColumn.getShortestCheckoutRun(runner.getJob());
76 |
77 | assertNull(shortestCheckoutRun);
78 | }
79 |
80 | @Test
81 | void no_runs_should_display_as_NA_in_UI() throws Exception {
82 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
83 |
84 | ListView listView = createAndAddListView(
85 | jenkinsRule.getInstance(), "MyListNoRuns", minCheckoutDurationColumn, runner.getJob());
86 |
87 | DomNode columnNode;
88 | try (WebClient webClient = jenkinsRule.createWebClient()) {
89 | columnNode = getListViewCell(
90 | webClient.getPage(listView),
91 | listView,
92 | runner.getJob().getName(),
93 | minCheckoutDurationColumn.getColumnCaption());
94 | }
95 |
96 | assertEquals("N/A", columnNode.asNormalizedText());
97 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
98 | }
99 |
100 | @Test
101 | void one_run_should_display_time_and_build_in_UI() throws Exception {
102 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
103 | .configurePipelineDefinition(CHECKOUT)
104 | .schedule();
105 |
106 | ListView listView = createAndAddListView(
107 | jenkinsRule.getInstance(), "MyListOneRun", minCheckoutDurationColumn, runner.getJob());
108 |
109 | DomNode columnNode;
110 | try (WebClient webClient = jenkinsRule.createWebClient()) {
111 | columnNode = getListViewCell(
112 | webClient.getPage(listView),
113 | listView,
114 | runner.getJob().getName(),
115 | minCheckoutDurationColumn.getColumnCaption());
116 | }
117 |
118 | // sample output: 1.1 sec - #1
119 | String text = columnNode.asNormalizedText();
120 |
121 | assertThat(text).containsAnyOf(TIME_UNITS);
122 | assertThat(text).contains("#" + runner.getRuns()[0].getId());
123 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/AvgDurationColumnTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 |
9 | import hudson.model.ListView;
10 | import org.htmlunit.html.DomNode;
11 | import org.junit.jupiter.api.BeforeAll;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.jvnet.hudson.test.JenkinsRule;
15 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
16 |
17 | @WithJenkins
18 | class AvgDurationColumnTest {
19 |
20 | private AvgDurationColumn avgDurationColumn;
21 |
22 | private static JenkinsRule jenkinsRule;
23 |
24 | @BeforeAll
25 | static void setUp(JenkinsRule rule) {
26 | jenkinsRule = rule;
27 | }
28 |
29 | @BeforeEach
30 | void before() {
31 | avgDurationColumn = new AvgDurationColumn();
32 | }
33 |
34 | @Test
35 | void two_successful_runs_should_return_their_average_duration() throws Exception {
36 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
37 | .configurePipelineDefinition(SUCCESS)
38 | .schedule()
39 | .configurePipelineDefinition(SLOW_3S)
40 | .schedule();
41 |
42 | Duration avgDuration = avgDurationColumn.getAverageDuration(runner.getJob());
43 |
44 | assertEquals(
45 | (runner.getRuns()[0].getDuration() + runner.getRuns()[1].getDuration()) / 2, avgDuration.getAsLong());
46 | }
47 |
48 | @Test
49 | void two_runs_including_one_failure_should_return_their_average_duration() throws Exception {
50 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
51 | .configurePipelineDefinition(SUCCESS)
52 | .schedule()
53 | .configurePipelineDefinition(SLOW_3S, FAILURE)
54 | .schedule();
55 |
56 | Duration avgDuration = avgDurationColumn.getAverageDuration(runner.getJob());
57 |
58 | assertEquals(
59 | (runner.getRuns()[0].getDuration() + runner.getRuns()[1].getDuration()) / 2, avgDuration.getAsLong());
60 | }
61 |
62 | @Test
63 | void failed_runs_are_not_excluded() throws Exception {
64 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
65 | .configurePipelineDefinition(FAILURE)
66 | .schedule();
67 |
68 | Duration avgDuration = avgDurationColumn.getAverageDuration(runner.getJob());
69 |
70 | assertEquals(runner.getRuns()[0].getDuration(), avgDuration.getAsLong());
71 | }
72 |
73 | @Test
74 | void unstable_runs_are_not_excluded() throws Exception {
75 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
76 | .configurePipelineDefinition(UNSTABLE)
77 | .schedule();
78 |
79 | Duration avgDuration = avgDurationColumn.getAverageDuration(runner.getJob());
80 |
81 | assertEquals(runner.getRuns()[0].getDuration(), avgDuration.getAsLong());
82 | }
83 |
84 | @Test
85 | void no_runs_should_display_as_NA_in_UI() throws Exception {
86 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
87 |
88 | ListView listView =
89 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", avgDurationColumn, runner.getJob());
90 |
91 | DomNode columnNode;
92 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
93 | columnNode = getListViewCell(
94 | webClient.getPage(listView),
95 | listView,
96 | runner.getJob().getName(),
97 | avgDurationColumn.getColumnCaption());
98 | }
99 |
100 | assertEquals("N/A", columnNode.asNormalizedText());
101 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
102 | }
103 |
104 | @Test
105 | void one_run_should_display_avg_duration_in_UI() throws Exception {
106 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
107 | .configurePipelineDefinition(SUCCESS)
108 | .schedule();
109 |
110 | ListView listView =
111 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", avgDurationColumn, runner.getJob());
112 |
113 | DomNode columnNode;
114 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
115 | columnNode = getListViewCell(
116 | webClient.getPage(listView),
117 | listView,
118 | runner.getJob().getName(),
119 | avgDurationColumn.getColumnCaption());
120 | }
121 |
122 | // sample output: 1.1 sec
123 | String text = columnNode.asNormalizedText();
124 |
125 | assertThat(text).containsAnyOf(TIME_UNITS);
126 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/JobRunner.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import hudson.model.*;
4 | import hudson.plugins.git.GitSCM;
5 | import hudson.scm.SCM;
6 | import hudson.tasks.Builder;
7 | import java.io.IOException;
8 | import java.util.ArrayList;
9 | import java.util.Arrays;
10 | import java.util.List;
11 | import java.util.Random;
12 | import java.util.concurrent.ExecutionException;
13 | import java.util.stream.Collectors;
14 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
15 | import org.jenkinsci.plugins.workflow.job.WorkflowJob;
16 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
17 | import org.jvnet.hudson.test.JenkinsRule;
18 | import org.jvnet.hudson.test.SleepBuilder;
19 |
20 | class JobRunner {
21 |
22 | private static final String SCM_URL = "https://github.com/jenkinsci/additional-metrics-plugin.git";
23 |
24 | static FreestyleBuilder createFreestyleJob(JenkinsRule jenkinsRule) throws IOException {
25 | return new FreestyleBuilder(jenkinsRule);
26 | }
27 |
28 | static WorkflowBuilder createWorkflowJob(JenkinsRule jenkinsRule) throws IOException {
29 | return new WorkflowBuilder(jenkinsRule);
30 | }
31 |
32 | private static String randomProjectName() {
33 | Random random = new Random();
34 |
35 | String generatedString = random.ints(97, 123)
36 | .limit(10)
37 | .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
38 | .toString();
39 |
40 | return "Project-" + generatedString;
41 | }
42 |
43 | static class WorkflowBuilder {
44 | private final WorkflowJob project;
45 | private final List runs;
46 |
47 | WorkflowBuilder(JenkinsRule jenkinsRule) throws IOException {
48 | this.project = jenkinsRule.createProject(WorkflowJob.class, randomProjectName());
49 | this.runs = new ArrayList<>();
50 | }
51 |
52 | WorkflowBuilder configurePipelineDefinition(StepDefinitions... steps) throws Descriptor.FormException {
53 | project.setDefinition(new CpsFlowDefinition(
54 | "node { "
55 | + Arrays.stream(steps)
56 | .map(StepDefinitions::pipelineCode)
57 | .collect(Collectors.joining("; "))
58 | + " }",
59 | true));
60 | return this;
61 | }
62 |
63 | WorkflowBuilder schedule() throws ExecutionException, InterruptedException {
64 | runs.add(project.scheduleBuild2(0).get());
65 | return this;
66 | }
67 |
68 | WorkflowBuilder scheduleNoWait() throws ExecutionException, InterruptedException {
69 | runs.add(project.scheduleBuild2(0).waitForStart());
70 | return this;
71 | }
72 |
73 | WorkflowJob getJob() {
74 | return project;
75 | }
76 |
77 | WorkflowRun[] getRuns() {
78 | return runs.toArray(WorkflowRun[]::new);
79 | }
80 |
81 | enum StepDefinitions {
82 | SUCCESS("echo 'Hello, World!'"),
83 | FAILURE("ech"),
84 | UNSTABLE("currentBuild.result = 'UNSTABLE'"),
85 | SLOW_3S("sleep 3"),
86 | CHECKOUT(
87 | "checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[url: '"
88 | + SCM_URL + "']]])"),
89 | VERY_SLOW_60S("sleep 60");
90 |
91 | private final String pipelineCode;
92 |
93 | StepDefinitions(String pipelineCode) {
94 | this.pipelineCode = pipelineCode;
95 | }
96 |
97 | private String pipelineCode() {
98 | return pipelineCode;
99 | }
100 | }
101 | }
102 |
103 | static class FreestyleBuilder {
104 | private final FreeStyleProject project;
105 | private final List runs;
106 |
107 | FreestyleBuilder(JenkinsRule jenkinsRule) throws IOException {
108 | this.project = jenkinsRule.createFreeStyleProject(randomProjectName());
109 | this.runs = new ArrayList<>();
110 | }
111 |
112 | FreestyleBuilder configureCheckout() throws IOException {
113 | project.setScm(Definitions.checkout());
114 | return this;
115 | }
116 |
117 | FreestyleBuilder addSuccessExecution() {
118 | project.getBuildersList().add(Definitions.successExecution());
119 | return this;
120 | }
121 |
122 | FreestyleBuilder schedule() throws ExecutionException, InterruptedException {
123 | runs.add(project.scheduleBuild2(0).get());
124 | return this;
125 | }
126 |
127 | FreeStyleProject getJob() {
128 | return project;
129 | }
130 |
131 | static class Definitions {
132 | static SCM checkout() {
133 | return new GitSCM(JobRunner.SCM_URL);
134 | }
135 |
136 | static Builder successExecution() {
137 | return new SleepBuilder(200);
138 | }
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/StdevDurationTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.assertEquals;
8 |
9 | import hudson.model.ListView;
10 | import java.util.List;
11 | import org.htmlunit.html.DomNode;
12 | import org.junit.jupiter.api.BeforeAll;
13 | import org.junit.jupiter.api.BeforeEach;
14 | import org.junit.jupiter.api.Test;
15 | import org.jvnet.hudson.test.JenkinsRule;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class StdevDurationTest {
20 |
21 | private StdevDurationColumn stdevDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | stdevDurationColumn = new StdevDurationColumn();
33 | }
34 |
35 | @Test
36 | void two_successful_runs_should_return_their_stdev_duration() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(SUCCESS)
39 | .schedule()
40 | .configurePipelineDefinition(SLOW_3S)
41 | .schedule();
42 |
43 | Duration stdevDuration = stdevDurationColumn.getStdevDuration(runner.getJob());
44 |
45 | assertEquals(
46 | (long) MathCommons.standardDeviation(
47 | List.of(runner.getRuns()[0].getDuration(), runner.getRuns()[1].getDuration())),
48 | stdevDuration.getAsLong());
49 | }
50 |
51 | @Test
52 | void failed_runs_should_be_included() throws Exception {
53 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
54 | .configurePipelineDefinition(FAILURE)
55 | .schedule()
56 | .configurePipelineDefinition(SLOW_3S)
57 | .schedule();
58 |
59 | Duration stdevDuration = stdevDurationColumn.getStdevDuration(runner.getJob());
60 |
61 | assertEquals(
62 | (long) MathCommons.standardDeviation(
63 | List.of(runner.getRuns()[0].getDuration(), runner.getRuns()[1].getDuration())),
64 | stdevDuration.getAsLong());
65 | }
66 |
67 | @Test
68 | void no_runs_should_display_as_NA_in_UI() throws Exception {
69 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
70 |
71 | ListView listView =
72 | createAndAddListView(jenkinsRule.getInstance(), "MyListNoRuns", stdevDurationColumn, runner.getJob());
73 |
74 | DomNode columnNode;
75 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
76 | columnNode = getListViewCell(
77 | webClient.getPage(listView),
78 | listView,
79 | runner.getJob().getName(),
80 | stdevDurationColumn.getColumnCaption());
81 | }
82 |
83 | assertEquals("N/A", columnNode.asNormalizedText());
84 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
85 | }
86 |
87 | @Test
88 | void one_run_should_display_0_in_UI() throws Exception {
89 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
90 | .configurePipelineDefinition(SUCCESS)
91 | .schedule();
92 |
93 | ListView listView =
94 | createAndAddListView(jenkinsRule.getInstance(), "MyListOneRun", stdevDurationColumn, runner.getJob());
95 |
96 | DomNode columnNode;
97 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
98 | columnNode = getListViewCell(
99 | webClient.getPage(listView),
100 | listView,
101 | runner.getJob().getName(),
102 | stdevDurationColumn.getColumnCaption());
103 | }
104 |
105 | assertEquals("0 ms", columnNode.asNormalizedText());
106 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
107 | }
108 |
109 | @Test
110 | void two_runs_should_display_stdev_duration_in_UI() throws Exception {
111 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
112 | .configurePipelineDefinition(SUCCESS)
113 | .schedule()
114 | .configurePipelineDefinition(SLOW_3S)
115 | .schedule();
116 |
117 | ListView listView =
118 | createAndAddListView(jenkinsRule.getInstance(), "MyListTwoRuns", stdevDurationColumn, runner.getJob());
119 |
120 | DomNode columnNode;
121 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
122 | columnNode = getListViewCell(
123 | webClient.getPage(listView),
124 | listView,
125 | runner.getJob().getName(),
126 | stdevDurationColumn.getColumnCaption());
127 | }
128 |
129 | // sample output: 1.1 sec
130 | String text = columnNode.asNormalizedText();
131 |
132 | assertThat(text).containsAnyOf(TIME_UNITS);
133 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | org.jenkins-ci.plugins
7 | plugin
8 | 5.2098.v4d48a_c4c68e7
9 |
10 |
11 | additional-metrics
12 | ${changelist}
13 | hpi
14 | Additional Metrics Plugin
15 | https://github.com/jenkinsci/additional-metrics-plugin
16 | 2018
17 |
18 |
19 | Chadi El Masri
20 |
21 |
22 |
23 |
24 | MIT License
25 | https://opensource.org/licenses/MIT
26 |
27 |
28 |
29 |
30 |
31 | chadiem
32 | Chadi El Masri
33 | chadimsr@gmail.com
34 |
35 |
36 | dainerx
37 | Oussama Ben Ghorbel
38 | d.oussamabenghorbel@gmail.com
39 |
40 |
41 |
42 |
43 | scm:git:https://github.com/${gitHubRepo}.git
44 | scm:git:https@github.com:${gitHubRepo}.git
45 | ${scmTag}
46 | https://github.com/${gitHubRepo}
47 |
48 |
49 |
50 | 999999-SNAPSHOT
51 | jenkinsci/additional-metrics-plugin
52 |
53 | 2.479
54 | ${jenkins.baseline}.3
55 | Max
56 | false
57 | false
58 |
59 |
60 |
61 |
62 |
63 | io.jenkins.tools.bom
64 | bom-${jenkins.baseline}.x
65 | 5054.v620b_5d2b_d5e6
66 | pom
67 | import
68 |
69 |
70 |
71 |
72 |
73 |
74 | com.github.spotbugs
75 | spotbugs-annotations
76 |
77 |
78 | org.jenkins-ci.plugins.workflow
79 | workflow-api
80 | true
81 |
82 |
83 | org.jenkins-ci.plugins.workflow
84 | workflow-cps
85 | true
86 |
87 |
88 | org.jenkins-ci.plugins.workflow
89 | workflow-job
90 | true
91 |
92 |
93 | org.jenkins-ci.plugins.workflow
94 | workflow-scm-step
95 | true
96 |
97 |
98 | org.jenkins-ci.plugins.workflow
99 | workflow-step-api
100 | true
101 |
102 |
103 | org.assertj
104 | assertj-core
105 | 3.27.6
106 | test
107 |
108 |
109 | org.jenkins-ci.plugins
110 | git
111 | test
112 |
113 |
114 | org.jenkins-ci.plugins.workflow
115 | workflow-basic-steps
116 | test
117 |
118 |
119 | org.jenkins-ci.plugins.workflow
120 | workflow-durable-task-step
121 | test
122 |
123 |
124 |
125 |
126 |
127 | repo.jenkins-ci.org
128 | https://repo.jenkins-ci.org/public/
129 |
130 |
131 |
132 |
133 |
134 | repo.jenkins-ci.org
135 | https://repo.jenkins-ci.org/public/
136 |
137 |
138 |
139 |
140 |
141 |
142 | org.openrewrite.maven
143 | rewrite-maven-plugin
144 | 6.26.0
145 |
146 | true
147 |
148 | org.openrewrite.jenkins.ModernizePlugin
149 |
150 |
151 |
152 |
153 | org.openrewrite.recipe
154 | rewrite-jenkins
155 | 0.33.2
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/StdevSuccessDurationTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.*;
5 | import static org.jenkinsci.plugins.additionalmetrics.UIHelpers.*;
6 | import static org.jenkinsci.plugins.additionalmetrics.Utilities.TIME_UNITS;
7 | import static org.junit.jupiter.api.Assertions.*;
8 |
9 | import hudson.model.ListView;
10 | import java.util.List;
11 | import org.htmlunit.html.DomNode;
12 | import org.junit.jupiter.api.BeforeAll;
13 | import org.junit.jupiter.api.BeforeEach;
14 | import org.junit.jupiter.api.Test;
15 | import org.jvnet.hudson.test.JenkinsRule;
16 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
17 |
18 | @WithJenkins
19 | class StdevSuccessDurationTest {
20 |
21 | private StdevSuccessDurationColumn stdevSuccessDurationColumn;
22 |
23 | private static JenkinsRule jenkinsRule;
24 |
25 | @BeforeAll
26 | static void setUp(JenkinsRule rule) {
27 | jenkinsRule = rule;
28 | }
29 |
30 | @BeforeEach
31 | void before() {
32 | stdevSuccessDurationColumn = new StdevSuccessDurationColumn();
33 | }
34 |
35 | @Test
36 | void two_successful_runs_should_return_their_sd_duration() throws Exception {
37 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
38 | .configurePipelineDefinition(SUCCESS)
39 | .schedule()
40 | .configurePipelineDefinition(SLOW_3S)
41 | .schedule();
42 |
43 | Duration stdevDuration = stdevSuccessDurationColumn.getStdevSuccessDuration(runner.getJob());
44 |
45 | assertEquals(
46 | (long) MathCommons.standardDeviation(
47 | List.of(runner.getRuns()[0].getDuration(), runner.getRuns()[1].getDuration())),
48 | stdevDuration.getAsLong());
49 | }
50 |
51 | @Test
52 | void failed_runs_should_be_excluded() throws Exception {
53 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
54 | .configurePipelineDefinition(FAILURE)
55 | .schedule();
56 |
57 | Duration stdevDuration = stdevSuccessDurationColumn.getStdevSuccessDuration(runner.getJob());
58 |
59 | assertNull(stdevDuration);
60 | }
61 |
62 | @Test
63 | void unstable_runs_should_be_excluded() throws Exception {
64 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
65 | .configurePipelineDefinition(UNSTABLE)
66 | .schedule();
67 |
68 | Duration stdevDuration = stdevSuccessDurationColumn.getStdevSuccessDuration(runner.getJob());
69 |
70 | assertNull(stdevDuration);
71 | }
72 |
73 | @Test
74 | void no_runs_should_display_as_NA_in_UI() throws Exception {
75 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
76 |
77 | ListView listView = createAndAddListView(
78 | jenkinsRule.getInstance(), "MyListNoRuns", stdevSuccessDurationColumn, runner.getJob());
79 |
80 | DomNode columnNode;
81 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
82 | columnNode = getListViewCell(
83 | webClient.getPage(listView),
84 | listView,
85 | runner.getJob().getName(),
86 | stdevSuccessDurationColumn.getColumnCaption());
87 | }
88 |
89 | assertEquals("N/A", columnNode.asNormalizedText());
90 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
91 | }
92 |
93 | @Test
94 | void one_run_should_display_as_0_in_UI() throws Exception {
95 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
96 | .configurePipelineDefinition(SUCCESS)
97 | .schedule();
98 |
99 | ListView listView = createAndAddListView(
100 | jenkinsRule.getInstance(), "MyListOneRun", stdevSuccessDurationColumn, runner.getJob());
101 |
102 | DomNode columnNode;
103 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
104 | columnNode = getListViewCell(
105 | webClient.getPage(listView),
106 | listView,
107 | runner.getJob().getName(),
108 | stdevSuccessDurationColumn.getColumnCaption());
109 | }
110 |
111 | assertEquals("0 ms", columnNode.asNormalizedText());
112 | assertEquals("0", columnNode.getAttributes().getNamedItem("data").getNodeValue());
113 | }
114 |
115 | @Test
116 | void two_runs_should_display_sd_duration_in_UI() throws Exception {
117 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
118 | .configurePipelineDefinition(SUCCESS)
119 | .schedule()
120 | .configurePipelineDefinition(SLOW_3S)
121 | .schedule();
122 |
123 | ListView listView = createAndAddListView(
124 | jenkinsRule.getInstance(), "MyListTwoRuns", stdevSuccessDurationColumn, runner.getJob());
125 |
126 | DomNode columnNode;
127 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
128 | columnNode = getListViewCell(
129 | webClient.getPage(listView),
130 | listView,
131 | runner.getJob().getName(),
132 | stdevSuccessDurationColumn.getColumnCaption());
133 | }
134 |
135 | // sample output: 1.1 sec
136 | String text = columnNode.asNormalizedText();
137 |
138 | assertThat(text).containsAnyOf(TIME_UNITS);
139 | assertThat(Long.parseLong(dataOf(columnNode))).isGreaterThan(0L);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Additional Metrics Plugin
2 | =========================
3 |
4 | [](https://github.com/jenkinsci/additional-metrics-plugin/releases/latest)
5 | [](https://plugins.jenkins.io/additional-metrics)
6 | [](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master/)
7 | [](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master)
8 | [](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master)
9 | 
10 | [](https://github.com/jenkinsci/additional-metrics-plugin/blob/master/LICENSE)
11 |
12 | Provides additional metrics via columns in Jenkins' List View.
13 |
14 | ### Provided Metrics
15 | - Minimum, Maximum, Average, and Standard Deviation build times for all, or only successful builds.
16 | - Minimum, Maximum, and Average checkout times for Pipeline builds.
17 | - Success, Failure, and Unstable rates.
18 | - Success and Failure time rates (ie Uptime and Downtime).
19 |
20 | 
21 |
22 | ### REST API
23 | All provided metrics are also exposed in the Job's REST API as a job Action.
24 |
25 | #### Examples
26 |
27 | ##### Single Job
28 |
29 | ###### JSON
30 | There is no filtering in JSON tree that I am aware of. Nevertheless, you can still do:
31 | ```
32 | /api/json?depth=3&tree=jobs[fullName,actions[jobMetrics[*]]]
33 | ```
34 | And then do the filtering post execution. You may need to adjust the depth, depending on how deeply nested your project is.
35 |
36 | ###### XML
37 | Using XPath, it is possible to get the metrics for one project. You may need to adjust the depth, depending on how deeply nested your project is.
38 | ```
39 | /api/xml?depth=4&xpath=(//job[fullName='github/repo1/master']/action/jobMetrics[node()])[1]
40 | ```
41 |
42 | ```
43 |
44 |
45 | 0
46 | 446
47 | 240
48 | 0.125
49 | 0.00000711702773729547-5
50 | 0
51 | 3024
52 | 1961
53 | 0
54 | 45
55 | 45
56 | 819
57 | 493
58 | 0.875
59 | 0.9999928829722644
60 | 0.875
61 |
62 | ```
63 |
64 | ##### List View / [Build Monitor View](https://plugins.jenkins.io/build-monitor-plugin)
65 | Assuming the view name is `MyView`,
66 | ```
67 | /view/MyView/api/json?tree=jobs[fullName,actions[jobMetrics[*]]]
68 | ```
69 |
70 | You will get an output similar to the below (modified for clarity):
71 |
72 | ```
73 | {
74 | "_class": "com.smartcodeltd.jenkinsci.plugins.buildmonitor.BuildMonitorView",
75 | "jobs": [
76 | {
77 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob",
78 | "actions": [
79 | {
80 | "_class": "org.jenkinsci.plugins.additionalmetrics.MetricsActionFactory$MetricsAction",
81 | "jobMetrics": {
82 | "avgCheckoutDuration": 19581,
83 | "avgDuration": 2641447,
84 | "avgSuccessDuration": 394148,
85 | "failureRate": 0.75,
86 | "failureTimeRate": 0.09493349090072016,
87 | "maxCheckoutDuration": 24734,
88 | "maxDuration": 3439386,
89 | "maxSuccessDuration": 394148,
90 | "minCheckoutDuration": 8226,
91 | "minDuration": 394148,
92 | "minSuccessDuration": 394148,
93 | "standardDeviationDuration": 42447,
94 | "standardDeviationSuccessDuration": 71238,
95 | "successRate": 0.25,
96 | "successTimeRate": 0.9050665090992799,
97 | "unstableRate": 0
98 | }
99 | }
100 | ],
101 | "fullName": "github/repo1/master"
102 | },
103 | {
104 | "_class": "org.jenkinsci.plugins.workflow.job.WorkflowJob",
105 | "actions": [
106 | {
107 | "_class": "org.jenkinsci.plugins.additionalmetrics.MetricsActionFactory$MetricsAction",
108 | "jobMetrics": {
109 | "avgCheckoutDuration": 34089,
110 | "avgDuration": 316825,
111 | "avgSuccessDuration": 316825,
112 | "failureRate": 0,
113 | "failureTimeRate": 0,
114 | "maxCheckoutDuration": 43413,
115 | "maxDuration": 443877,
116 | "maxSuccessDuration": 443877,
117 | "minCheckoutDuration": 24766,
118 | "minDuration": 189773,
119 | "minSuccessDuration": 189773,
120 | "standardDeviationDuration": 8089,
121 | "standardDeviationSuccessDuration": 3014,
122 | "successRate": 1,
123 | "successTimeRate": 1,
124 | "unstableRate": 0
125 | }
126 | }
127 | ],
128 | "fullName": "github/repo2/master"
129 | }
130 | ]
131 | }
132 | ```
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/additionalmetrics/JobMetrics.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import hudson.model.Job;
4 | import org.kohsuke.stapler.export.Exported;
5 | import org.kohsuke.stapler.export.ExportedBean;
6 |
7 | @ExportedBean
8 | public record JobMetrics(Job job) {
9 |
10 | @Exported
11 | public long getAvgCheckoutDuration() {
12 | AvgCheckoutDurationColumn avgCheckoutDurationColumn = new AvgCheckoutDurationColumn();
13 | Duration avgCheckoutDuration = avgCheckoutDurationColumn.getAverageCheckoutDuration(job);
14 | return durationOrDefaultToZero(avgCheckoutDuration);
15 | }
16 |
17 | @Exported
18 | public long getAvgDuration() {
19 | AvgDurationColumn avgDurationColumn = new AvgDurationColumn();
20 | Duration avgDuration = avgDurationColumn.getAverageDuration(job);
21 | return durationOrDefaultToZero(avgDuration);
22 | }
23 |
24 | @Exported
25 | public long getAvgSuccessDuration() {
26 | AvgSuccessDurationColumn avgSuccessDurationColumn = new AvgSuccessDurationColumn();
27 | Duration avgSuccessDuration = avgSuccessDurationColumn.getAverageSuccessDuration(job);
28 | return durationOrDefaultToZero(avgSuccessDuration);
29 | }
30 |
31 | @Exported
32 | public long getMaxCheckoutDuration() {
33 | MaxCheckoutDurationColumn maxCheckoutDurationColumn = new MaxCheckoutDurationColumn();
34 | RunWithDuration longestCheckoutRun = maxCheckoutDurationColumn.getLongestCheckoutRun(job);
35 | return durationOrDefaultToZero(longestCheckoutRun);
36 | }
37 |
38 | @Exported
39 | public long getMaxDuration() {
40 | MaxDurationColumn maxDurationColumn = new MaxDurationColumn();
41 | RunWithDuration longestRun = maxDurationColumn.getLongestRun(job);
42 | return durationOrDefaultToZero(longestRun);
43 | }
44 |
45 | @Exported
46 | public long getMaxSuccessDuration() {
47 | MaxSuccessDurationColumn maxSuccessDurationColumn = new MaxSuccessDurationColumn();
48 | RunWithDuration longestSuccessfulRun = maxSuccessDurationColumn.getLongestSuccessfulRun(job);
49 | return durationOrDefaultToZero(longestSuccessfulRun);
50 | }
51 |
52 | @Exported
53 | public long getMinCheckoutDuration() {
54 | MinCheckoutDurationColumn minCheckoutDurationColumn = new MinCheckoutDurationColumn();
55 | RunWithDuration shortestCheckoutRun = minCheckoutDurationColumn.getShortestCheckoutRun(job);
56 | return durationOrDefaultToZero(shortestCheckoutRun);
57 | }
58 |
59 | @Exported
60 | public long getMinDuration() {
61 | MinDurationColumn minDurationColumn = new MinDurationColumn();
62 | RunWithDuration shortestRun = minDurationColumn.getShortestRun(job);
63 | return durationOrDefaultToZero(shortestRun);
64 | }
65 |
66 | @Exported
67 | public long getMinSuccessDuration() {
68 | MinSuccessDurationColumn minSuccessDurationColumn = new MinSuccessDurationColumn();
69 | RunWithDuration shortestSuccessfulRun = minSuccessDurationColumn.getShortestSuccessfulRun(job);
70 | return durationOrDefaultToZero(shortestSuccessfulRun);
71 | }
72 |
73 | @Exported
74 | public double getSuccessRate() {
75 | SuccessRateColumn successRateColumn = new SuccessRateColumn();
76 | Rate successRate = successRateColumn.getSuccessRate(job);
77 | return rateOrDefaultToZero(successRate);
78 | }
79 |
80 | @Exported
81 | public double getFailureRate() {
82 | FailureRateColumn failureRateColumn = new FailureRateColumn();
83 | Rate failureRate = failureRateColumn.getFailureRate(job);
84 | return rateOrDefaultToZero(failureRate);
85 | }
86 |
87 | @Exported
88 | public double getSuccessTimeRate() {
89 | SuccessTimeRateColumn successTimeRateColumn = new SuccessTimeRateColumn();
90 | Rate successTimeRate = successTimeRateColumn.getSuccessTimeRate(job);
91 | return rateOrDefaultToZero(successTimeRate);
92 | }
93 |
94 | @Exported
95 | public double getFailureTimeRate() {
96 | FailureTimeRateColumn failureTimeRateColumn = new FailureTimeRateColumn();
97 | Rate failureTimeRate = failureTimeRateColumn.getFailureTimeRate(job);
98 | return rateOrDefaultToZero(failureTimeRate);
99 | }
100 |
101 | @Exported
102 | public long getStandardDeviationDuration() {
103 | StdevDurationColumn stdevDurationColumn = new StdevDurationColumn();
104 | Duration standardDeviationDuration = stdevDurationColumn.getStdevDuration(job);
105 | return durationOrDefaultToZero(standardDeviationDuration);
106 | }
107 |
108 | @Exported
109 | public long getStandardDeviationSuccessDuration() {
110 | StdevSuccessDurationColumn stdevSuccessDurationColumn = new StdevSuccessDurationColumn();
111 | Duration standardDeviationSuccessDuration = stdevSuccessDurationColumn.getStdevSuccessDuration(job);
112 | return durationOrDefaultToZero(standardDeviationSuccessDuration);
113 | }
114 |
115 | @Exported
116 | public double getUnstableRate() {
117 | UnstableRateColumn unstableRateColumn = new UnstableRateColumn();
118 | Rate unstableRate = unstableRateColumn.getUnstableRate(job);
119 | return rateOrDefaultToZero(unstableRate);
120 | }
121 |
122 | private static double rateOrDefaultToZero(Rate rate) {
123 | if (rate != null) {
124 | return rate.getAsDouble();
125 | }
126 | return 0.0;
127 | }
128 |
129 | private static long durationOrDefaultToZero(Duration duration) {
130 | if (duration != null) {
131 | return duration.getAsLong();
132 | }
133 | return 0;
134 | }
135 |
136 | private static long durationOrDefaultToZero(RunWithDuration runWithDuration) {
137 | if (runWithDuration != null) {
138 | return runWithDuration.duration().getAsLong();
139 | }
140 | return 0;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | <# : batch portion
2 | @REM ----------------------------------------------------------------------------
3 | @REM Licensed to the Apache Software Foundation (ASF) under one
4 | @REM or more contributor license agreements. See the NOTICE file
5 | @REM distributed with this work for additional information
6 | @REM regarding copyright ownership. The ASF licenses this file
7 | @REM to you under the Apache License, Version 2.0 (the
8 | @REM "License"); you may not use this file except in compliance
9 | @REM with the License. You may obtain a copy of the License at
10 | @REM
11 | @REM http://www.apache.org/licenses/LICENSE-2.0
12 | @REM
13 | @REM Unless required by applicable law or agreed to in writing,
14 | @REM software distributed under the License is distributed on an
15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | @REM KIND, either express or implied. See the License for the
17 | @REM specific language governing permissions and limitations
18 | @REM under the License.
19 | @REM ----------------------------------------------------------------------------
20 |
21 | @REM ----------------------------------------------------------------------------
22 | @REM Apache Maven Wrapper startup batch script, version 3.3.4
23 | @REM
24 | @REM Optional ENV vars
25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution
26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
28 | @REM ----------------------------------------------------------------------------
29 |
30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
31 | @SET __MVNW_CMD__=
32 | @SET __MVNW_ERROR__=
33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
34 | @SET PSModulePath=
35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
37 | )
38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
39 | @SET __MVNW_PSMODULEP_SAVE=
40 | @SET __MVNW_ARG0_NAME__=
41 | @SET MVNW_USERNAME=
42 | @SET MVNW_PASSWORD=
43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
44 | @echo Cannot start maven from wrapper >&2 && exit /b 1
45 | @GOTO :EOF
46 | : end batch / begin powershell #>
47 |
48 | $ErrorActionPreference = "Stop"
49 | if ($env:MVNW_VERBOSE -eq "true") {
50 | $VerbosePreference = "Continue"
51 | }
52 |
53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
55 | if (!$distributionUrl) {
56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
57 | }
58 |
59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
60 | "maven-mvnd-*" {
61 | $USE_MVND = $true
62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
63 | $MVN_CMD = "mvnd.cmd"
64 | break
65 | }
66 | default {
67 | $USE_MVND = $false
68 | $MVN_CMD = $script -replace '^mvnw','mvn'
69 | break
70 | }
71 | }
72 |
73 | # apply MVNW_REPOURL and calculate MAVEN_HOME
74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
75 | if ($env:MVNW_REPOURL) {
76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
78 | }
79 | $distributionUrlName = $distributionUrl -replace '^.*/',''
80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
84 |
85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
88 | exit $?
89 | }
90 |
91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
93 | }
94 |
95 | # prepare tmp dir
96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
99 | trap {
100 | if ($TMP_DOWNLOAD_DIR.Exists) {
101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
103 | }
104 | }
105 |
106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
107 |
108 | # Download and Install Apache Maven
109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
110 | Write-Verbose "Downloading from: $distributionUrl"
111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
112 |
113 | $webclient = New-Object System.Net.WebClient
114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
116 | }
117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
119 |
120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file
121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
122 | if ($distributionSha256Sum) {
123 | if ($USE_MVND) {
124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
125 | }
126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
128 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
129 | }
130 | }
131 |
132 | # unzip and move
133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
135 | try {
136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
137 | } catch {
138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
139 | Write-Error "fail to move MAVEN_HOME"
140 | }
141 | } finally {
142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
144 | }
145 |
146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
147 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/additionalmetrics/MetricsActionFactoryTest.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.additionalmetrics;
2 |
3 | import static org.hamcrest.MatcherAssert.assertThat;
4 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.CHECKOUT;
5 | import static org.jenkinsci.plugins.additionalmetrics.JobRunner.WorkflowBuilder.StepDefinitions.SUCCESS;
6 |
7 | import java.io.IOException;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 | import org.hamcrest.Description;
11 | import org.hamcrest.Matcher;
12 | import org.hamcrest.TypeSafeMatcher;
13 | import org.htmlunit.html.DomElement;
14 | import org.htmlunit.html.DomNode;
15 | import org.htmlunit.xml.XmlPage;
16 | import org.junit.jupiter.api.BeforeAll;
17 | import org.junit.jupiter.api.Test;
18 | import org.jvnet.hudson.test.JenkinsRule;
19 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
20 | import org.xml.sax.SAXException;
21 |
22 | @WithJenkins
23 | class MetricsActionFactoryTest {
24 |
25 | private static JenkinsRule jenkinsRule;
26 |
27 | @BeforeAll
28 | static void setUp(JenkinsRule rule) {
29 | jenkinsRule = rule;
30 | }
31 |
32 | @Test
33 | void no_runs_metrics_should_be_zeros() throws IOException, SAXException {
34 | var runner = JobRunner.createWorkflowJob(jenkinsRule);
35 |
36 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
37 | XmlPage xmlPage = webClient.goToXml(
38 | "api/xml?depth=3&xpath=/hudson/job[name='" + runner.getJob().getName() + "']/action/jobMetrics");
39 |
40 | Map metrics = childrenAsMap(xmlPage.getDocumentElement());
41 |
42 | assertThat(
43 | metrics,
44 | match(
45 | "avgCheckoutDuration",
46 | isEqualTo(0),
47 | "avgDuration",
48 | isEqualTo(0),
49 | "avgSuccessDuration",
50 | isEqualTo(0),
51 | "failureRate",
52 | isEqualTo(0.0),
53 | "failureTimeRate",
54 | isEqualTo(0.0),
55 | "maxCheckoutDuration",
56 | isEqualTo(0),
57 | "maxDuration",
58 | isEqualTo(0),
59 | "maxSuccessDuration",
60 | isEqualTo(0),
61 | "minCheckoutDuration",
62 | isEqualTo(0),
63 | "minDuration",
64 | isEqualTo(0),
65 | "minSuccessDuration",
66 | isEqualTo(0),
67 | "successRate",
68 | isEqualTo(0.0),
69 | "successTimeRate",
70 | isEqualTo(0.0),
71 | "standardDeviationDuration",
72 | isEqualTo(0),
73 | "standardDeviationSuccessDuration",
74 | isEqualTo(0),
75 | "unstableRate",
76 | isEqualTo(0.0)));
77 | }
78 | }
79 |
80 | @Test
81 | void one_run_should_have_appropriate_metrics() throws Exception {
82 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
83 | .configurePipelineDefinition(SUCCESS)
84 | .schedule();
85 |
86 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
87 | XmlPage xmlPage = webClient.goToXml(
88 | "api/xml?depth=3&xpath=/hudson/job[name='" + runner.getJob().getName() + "']/action/jobMetrics");
89 |
90 | Map metrics = childrenAsMap(xmlPage.getDocumentElement());
91 |
92 | assertThat(
93 | metrics,
94 | match(
95 | "avgDuration",
96 | isGreaterThan(0),
97 | "avgSuccessDuration",
98 | isGreaterThan(0),
99 | "failureRate",
100 | isEqualTo(0.0),
101 | "failureTimeRate",
102 | isEqualTo(0.0),
103 | "maxDuration",
104 | isGreaterThan(0),
105 | "maxSuccessDuration",
106 | isGreaterThan(0),
107 | "minDuration",
108 | isGreaterThan(0),
109 | "minSuccessDuration",
110 | isGreaterThan(0),
111 | "successRate",
112 | isEqualTo(1.0),
113 | "successTimeRate",
114 | isEqualTo(1.0),
115 | "standardDeviationDuration",
116 | isEqualTo(0),
117 | "standardDeviationSuccessDuration",
118 | isEqualTo(0),
119 | "unstableRate",
120 | isEqualTo(0.0)));
121 | }
122 | }
123 |
124 | @Test
125 | void one_checkout_run_should_have_checkout_metrics() throws Exception {
126 | var runner = JobRunner.createWorkflowJob(jenkinsRule)
127 | .configurePipelineDefinition(CHECKOUT)
128 | .schedule();
129 |
130 | try (JenkinsRule.WebClient webClient = jenkinsRule.createWebClient()) {
131 | XmlPage xmlPage = webClient.goToXml(
132 | "api/xml?depth=3&xpath=/hudson/job[name='" + runner.getJob().getName() + "']/action/jobMetrics");
133 |
134 | Map metrics = childrenAsMap(xmlPage.getDocumentElement());
135 |
136 | assertThat(
137 | metrics,
138 | match(
139 | "avgCheckoutDuration",
140 | isGreaterThan(0),
141 | "minCheckoutDuration",
142 | isGreaterThan(0),
143 | "maxCheckoutDuration",
144 | isGreaterThan(0)));
145 | }
146 | }
147 |
148 | private Matcher isGreaterThan(final Number value) {
149 | return new TypeSafeMatcher<>() {
150 | @Override
151 | protected boolean matchesSafely(String item) {
152 | return Double.parseDouble(item) > value.doubleValue();
153 | }
154 |
155 | @Override
156 | public void describeTo(Description description) {
157 | description.appendValue(value);
158 | }
159 | };
160 | }
161 |
162 | private Matcher isEqualTo(final Number value) {
163 | return new TypeSafeMatcher<>() {
164 | @Override
165 | protected boolean matchesSafely(String item) {
166 | return item.equals(value.toString());
167 | }
168 |
169 | @Override
170 | public void describeTo(Description description) {
171 | description.appendValue(value);
172 | }
173 | };
174 | }
175 |
176 | private Matcher> match(final Object... data) {
177 | return new TypeSafeMatcher<>() {
178 | @Override
179 | protected boolean matchesSafely(Map item) {
180 | for (int i = 0; i < data.length; i += 2) {
181 | String key = data[i].toString();
182 | Matcher matcher = (Matcher) data[i + 1];
183 |
184 | if (!item.containsKey(key) || !matcher.matches(item.get(key))) {
185 | return false;
186 | }
187 | }
188 | return true;
189 | }
190 |
191 | @Override
192 | public void describeTo(Description description) {}
193 | };
194 | }
195 |
196 | private Map childrenAsMap(DomElement parent) {
197 | Map elements = new HashMap<>();
198 |
199 | for (DomNode domNode : parent.getChildNodes()) {
200 | elements.put(domNode.getNodeName(), domNode.getTextContent());
201 | }
202 |
203 | return elements;
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Apache Maven Wrapper startup batch script, version 3.3.4
23 | #
24 | # Optional ENV vars
25 | # -----------------
26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source
27 | # MVNW_REPOURL - repo url base for downloading maven distribution
28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
30 | # ----------------------------------------------------------------------------
31 |
32 | set -euf
33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x
34 |
35 | # OS specific support.
36 | native_path() { printf %s\\n "$1"; }
37 | case "$(uname)" in
38 | CYGWIN* | MINGW*)
39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
40 | native_path() { cygpath --path --windows "$1"; }
41 | ;;
42 | esac
43 |
44 | # set JAVACMD and JAVACCMD
45 | set_java_home() {
46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
47 | if [ -n "${JAVA_HOME-}" ]; then
48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then
49 | # IBM's JDK on AIX uses strange locations for the executables
50 | JAVACMD="$JAVA_HOME/jre/sh/java"
51 | JAVACCMD="$JAVA_HOME/jre/sh/javac"
52 | else
53 | JAVACMD="$JAVA_HOME/bin/java"
54 | JAVACCMD="$JAVA_HOME/bin/javac"
55 |
56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
59 | return 1
60 | fi
61 | fi
62 | else
63 | JAVACMD="$(
64 | 'set' +e
65 | 'unset' -f command 2>/dev/null
66 | 'command' -v java
67 | )" || :
68 | JAVACCMD="$(
69 | 'set' +e
70 | 'unset' -f command 2>/dev/null
71 | 'command' -v javac
72 | )" || :
73 |
74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
76 | return 1
77 | fi
78 | fi
79 | }
80 |
81 | # hash string like Java String::hashCode
82 | hash_string() {
83 | str="${1:-}" h=0
84 | while [ -n "$str" ]; do
85 | char="${str%"${str#?}"}"
86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
87 | str="${str#?}"
88 | done
89 | printf %x\\n $h
90 | }
91 |
92 | verbose() { :; }
93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
94 |
95 | die() {
96 | printf %s\\n "$1" >&2
97 | exit 1
98 | }
99 |
100 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
101 | while IFS="=" read -r key value; do
102 | case "${key-}" in
103 | distributionUrl) distributionUrl="${value-}" ;;
104 | distributionSha256Sum) distributionSha256Sum="${value-}" ;;
105 | esac
106 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
107 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
108 |
109 | case "${distributionUrl##*/}" in
110 | maven-mvnd-*bin.*)
111 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
112 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
113 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
114 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
115 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
116 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
117 | *)
118 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
119 | distributionPlatform=linux-amd64
120 | ;;
121 | esac
122 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
123 | ;;
124 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
125 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
126 | esac
127 |
128 | # apply MVNW_REPOURL and calculate MAVEN_HOME
129 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
130 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
131 | distributionUrlName="${distributionUrl##*/}"
132 | distributionUrlNameMain="${distributionUrlName%.*}"
133 | distributionUrlNameMain="${distributionUrlNameMain%-bin}"
134 | MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
135 |
136 | exec_maven() {
137 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
138 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
139 | }
140 |
141 | if [ -d "$MAVEN_HOME" ]; then
142 | verbose "found existing MAVEN_HOME at $MAVEN_HOME"
143 | exec_maven "$@"
144 | fi
145 |
146 | case "${distributionUrl-}" in
147 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
148 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
149 | esac
150 |
151 | # prepare tmp dir
152 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
153 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
154 | trap clean HUP INT TERM EXIT
155 | else
156 | die "cannot create temp dir"
157 | fi
158 |
159 | mkdir -p -- "${MAVEN_HOME%/*}"
160 |
161 | # Download and Install Apache Maven
162 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
163 | verbose "Downloading from: $distributionUrl"
164 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
165 |
166 | # select .zip or .tar.gz
167 | if ! command -v unzip >/dev/null; then
168 | distributionUrl="${distributionUrl%.zip}.tar.gz"
169 | distributionUrlName="${distributionUrl##*/}"
170 | fi
171 |
172 | # verbose opt
173 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
174 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
175 |
176 | # normalize http auth
177 | case "${MVNW_PASSWORD:+has-password}" in
178 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
179 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
180 | esac
181 |
182 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
183 | verbose "Found wget ... using wget"
184 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
185 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
186 | verbose "Found curl ... using curl"
187 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
188 | elif set_java_home; then
189 | verbose "Falling back to use Java to download"
190 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
191 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
192 | cat >"$javaSource" <<-END
193 | public class Downloader extends java.net.Authenticator
194 | {
195 | protected java.net.PasswordAuthentication getPasswordAuthentication()
196 | {
197 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
198 | }
199 | public static void main( String[] args ) throws Exception
200 | {
201 | setDefault( new Downloader() );
202 | java.nio.file.Files.copy( new java.net.URL( args[0] ).openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
203 | }
204 | }
205 | END
206 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java
207 | verbose " - Compiling Downloader.java ..."
208 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
209 | verbose " - Running Downloader.java ..."
210 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
211 | fi
212 |
213 | # If specified, validate the SHA-256 sum of the Maven distribution zip file
214 | if [ -n "${distributionSha256Sum-}" ]; then
215 | distributionSha256Result=false
216 | if [ "$MVN_CMD" = mvnd.sh ]; then
217 | echo "Checksum validation is not supported for maven-mvnd." >&2
218 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
219 | exit 1
220 | elif command -v sha256sum >/dev/null; then
221 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
222 | distributionSha256Result=true
223 | fi
224 | elif command -v shasum >/dev/null; then
225 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
226 | distributionSha256Result=true
227 | fi
228 | else
229 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
230 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
231 | exit 1
232 | fi
233 | if [ $distributionSha256Result = false ]; then
234 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
235 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
236 | exit 1
237 | fi
238 | fi
239 |
240 | # unzip and move
241 | if command -v unzip >/dev/null; then
242 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
243 | else
244 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
245 | fi
246 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
247 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
248 |
249 | clean || :
250 | exec_maven "$@"
251 |
--------------------------------------------------------------------------------