├── .github ├── FUNDING.yml ├── CODEOWNERS └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .gitignore ├── .mvn ├── maven.config ├── extensions.xml └── wrapper │ └── maven-wrapper.properties ├── images └── screenshot.png ├── renovate.json ├── src ├── main │ ├── resources │ │ ├── index.jelly │ │ └── org │ │ │ └── jenkinsci │ │ │ └── plugins │ │ │ └── additionalmetrics │ │ │ ├── FailureRateColumn │ │ │ └── column.jelly │ │ │ ├── SuccessRateColumn │ │ │ └── column.jelly │ │ │ ├── UnstableRateColumn │ │ │ └── column.jelly │ │ │ ├── AvgDurationColumn │ │ │ └── column.jelly │ │ │ ├── FailureTimeRateColumn │ │ │ └── column.jelly │ │ │ ├── SuccessTimeRateColumn │ │ │ └── column.jelly │ │ │ ├── AvgSuccessDurationColumn │ │ │ └── column.jelly │ │ │ ├── StdevDurationColumn │ │ │ └── column.jelly │ │ │ ├── AvgCheckoutDurationColumn │ │ │ └── column.jelly │ │ │ ├── StdevSuccessDurationColumn │ │ │ └── column.jelly │ │ │ ├── MaxDurationColumn │ │ │ └── column.jelly │ │ │ ├── MinDurationColumn │ │ │ └── column.jelly │ │ │ ├── MaxSuccessDurationColumn │ │ │ └── column.jelly │ │ │ ├── MinSuccessDurationColumn │ │ │ └── column.jelly │ │ │ ├── MaxCheckoutDurationColumn │ │ │ └── column.jelly │ │ │ ├── MinCheckoutDurationColumn │ │ │ └── column.jelly │ │ │ └── Messages.properties │ └── java │ │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── additionalmetrics │ │ ├── RunWithDuration.java │ │ ├── MathCommons.java │ │ ├── Metric.java │ │ ├── Duration.java │ │ ├── Rate.java │ │ ├── MinCheckoutDurationColumn.java │ │ ├── MaxSuccessDurationColumn.java │ │ ├── MinSuccessDurationColumn.java │ │ ├── MaxCheckoutDurationColumn.java │ │ ├── SuccessRateColumn.java │ │ ├── UnstableRateColumn.java │ │ ├── StdevDurationColumn.java │ │ ├── SuccessTimeRateColumn.java │ │ ├── FailureTimeRateColumn.java │ │ ├── AvgSuccessDurationColumn.java │ │ ├── StdevSuccessDurationColumn.java │ │ ├── AvgCheckoutDurationColumn.java │ │ ├── Helpers.java │ │ ├── MetricsActionFactory.java │ │ ├── AdditionalMetricColumnDescriptor.java │ │ ├── AvgDurationColumn.java │ │ ├── MaxDurationColumn.java │ │ ├── MinDurationColumn.java │ │ ├── FailureRateColumn.java │ │ ├── CheckoutDuration.java │ │ ├── Utils.java │ │ └── JobMetrics.java └── test │ └── java │ └── org │ └── jenkinsci │ └── plugins │ └── additionalmetrics │ ├── MathCommonsTest.java │ ├── RateStringParameterizedTest.java │ ├── Utilities.java │ ├── UIHelpers.java │ ├── NoRunsTest.java │ ├── BuildingRunsTest.java │ ├── FailureRateColumnTest.java │ ├── SuccessRateColumnTest.java │ ├── UnstableRateColumnTest.java │ ├── FailureTimeRateColumnTest.java │ ├── SuccessTimeRateColumnTest.java │ ├── AvgSuccessDurationColumnTest.java │ ├── MaxSuccessDurationColumnTest.java │ ├── MinSuccessDurationColumnTest.java │ ├── AvgCheckoutDurationColumnTest.java │ ├── MaxDurationColumnTest.java │ ├── MinDurationColumnTest.java │ ├── MaxCheckoutDurationColumnTest.java │ ├── MinCheckoutDurationColumnTest.java │ ├── AvgDurationColumnTest.java │ ├── JobRunner.java │ ├── StdevDurationTest.java │ ├── StdevSuccessDurationTest.java │ └── MetricsActionFactoryTest.java ├── RELEASE.md ├── Jenkinsfile ├── LICENSE ├── pom.xml ├── README.md ├── mvnw.cmd └── mvnw /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: chadiem 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/additional-metrics-plugin-developers 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | target/ 4 | work/ 5 | .mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/additional-metrics-plugin/master/images/screenshot.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "labels": [ 7 | "dependencies" 8 | ], 9 | "automerge": true 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Provides additional metrics for List Views: Success/Failure/Unstable Rates, Min/Max/Avg run and checkout 4 | durations, Success/Failure 5 | Time Rates. 6 |
7 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | io.jenkins.tools.incrementals 5 | git-changelist-maven-extension 6 | 1.13 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Release notes 2 | 3 | ### 1.4 4 | - Raised minimal Jenkins requirement to 2.289.3 5 | - Raised minimal Java requirement to Java 11 6 | - Used BOMs to simplify plugin development 7 | 8 | ### 1.3 9 | - Added checkout time metric. 10 | 11 | ### 1.2 12 | - Metrics are now exposed via REST API on the job's Action 13 | 14 | ### 1.1 15 | - Added new columns: Average and Average Success durations 16 | - Fixed bug with missing data, required for sorting 17 | 18 | ### 1.0 19 | - Initial release -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/additionalmetrics/RunWithDuration.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.additionalmetrics; 2 | 3 | import hudson.model.Run; 4 | 5 | /** 6 | * A record that pairs a Jenkins build run with its associated duration. 7 | * This is used to store and pass around run information along with calculated duration metrics. 8 | * 9 | * @param run the Jenkins build run 10 | * @param duration the calculated duration for the run 11 | */ 12 | public record RunWithDuration(Run run, Duration duration) {} 13 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | https://github.com/jenkins-infra/pipeline-library/ 4 | */ 5 | buildPlugin( 6 | forkCount: '1C', // run this number of tests in parallel for faster feedback. If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores 7 | useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests 8 | configurations: [ 9 | [platform: 'linux', jdk: 21], 10 | [platform: 'windows', jdk: 17], 11 | ]) 12 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/FailureRateColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${failureRate.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/SuccessRateColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${successRate.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/UnstableRateColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${unstableRate.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/AvgDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${averageDuration.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/FailureTimeRateColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${failureTimeRate.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/SuccessTimeRateColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${successTimeRate.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/AvgSuccessDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${averageSuccessDuration.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/StdevDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${standardDeviationDuration.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/AvgCheckoutDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${averageCheckoutDuration.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/additionalmetrics/MathCommonsTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.additionalmetrics; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.util.List; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class MathCommonsTest { 9 | 10 | @Test 11 | void stdev_of_empty_durations_should_return_0() { 12 | assertEquals(0, MathCommons.standardDeviation(List.of())); 13 | } 14 | 15 | @Test 16 | void stdev_of_multiple_durations() { 17 | assertEquals(81.64965809277261, MathCommons.standardDeviation(List.of(100L, 200L, 300L))); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/StdevSuccessDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${standardDeviationSuccessDuration.asString} 8 | 9 | 10 | ${%N/A} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/MaxDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${longestRun.run().durationString} 8 | - 9 | 10 | ${longestRun.run().displayName} 11 | 12 | 13 | 14 | ${%N/A} 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/MinDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${shortestRun.run().durationString} 8 | - 9 | 10 | ${shortestRun.run().displayName} 11 | 12 | 13 | 14 | ${%N/A} 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/MaxSuccessDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${longestRun.run().durationString} 8 | - 9 | 10 | ${longestRun.run().displayName} 11 | 12 | 13 | 14 | ${%N/A} 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/additionalmetrics/MinSuccessDurationColumn/column.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${shortestRun.run().durationString} 8 | - 9 | 10 | ${shortestRun.run().displayName} 11 | 12 | 13 | 14 | ${%N/A} 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/additionalmetrics/MathCommons.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.additionalmetrics; 2 | 3 | import java.util.List; 4 | 5 | final class MathCommons { 6 | 7 | private MathCommons() {} 8 | 9 | static double standardDeviation(List 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 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 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 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 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 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 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 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 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 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 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 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 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 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> 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 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> data() throws IOException { 22 | List> 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 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 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 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 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 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> data() throws IOException { 24 | List> 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 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 runs, Predicate preFilter, Predicate predicateRate) { 22 | List 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 runs, Predicate preFilter, Predicate predicateRate) { 42 | List 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 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 runs, Predicate preFilter, ToLongFunction durationFunction) { 81 | return durationFunction(runs, preFilter, durationFunction, LongStream::average); 82 | } 83 | 84 | static Optional stdDevDuration( 85 | List 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 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 preFilter(List runs, Predicate preFilter) { 118 | Stream 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 | [![GitHub release](https://img.shields.io/github/release/jenkinsci/additional-metrics-plugin.svg?label=changelog)](https://github.com/jenkinsci/additional-metrics-plugin/releases/latest) 5 | [![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/additional-metrics.svg?color=blue)](https://plugins.jenkins.io/additional-metrics) 6 | [![Build](https://ci.jenkins.io/buildStatus/icon?job=Plugins/additional-metrics-plugin/master)](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master/) 7 | [![Coverage](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master/badge/icon?status=${instructionCoverage}&subject=coverage&color=${colorInstructionCoverage})](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master) 8 | [![LOC](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master/badge/icon?job=test&status=${lineOfCode}&subject=lines%20of%20code&color=blue)](https://ci.jenkins.io/job/Plugins/job/additional-metrics-plugin/job/master) 9 | ![Contributors](https://img.shields.io/github/contributors/jenkinsci/additional-metrics-plugin.svg?color=blue) 10 | [![License](https://img.shields.io/github/license/jenkinsci/additional-metrics-plugin)](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 | ![](images/screenshot.png) 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 | --------------------------------------------------------------------------------