diff --git a/Jenkinsfile b/Jenkinsfile index e98bbeed..5d0ce2ae 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,7 +5,6 @@ def configurations = [ def params = [ failFast: false, - pit: [skip: false, sourceCodeRetention: 'MODIFIED'], configurations: configurations, checkstyle: [qualityGates: [[threshold: 1, type: 'NEW', unstable: true]]], pmd: [qualityGates: [[threshold: 1, type: 'NEW', unstable: true]]], diff --git a/pom.xml b/pom.xml index 1604bb09..dce0430a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,5 @@ - + + 4.0.0 @@ -8,12 +9,12 @@ - forensics-api io.jenkins.plugins - hpi - Forensics API Plugin + forensics-api ${revision}${changelist} + hpi + Forensics API Plugin Defines an API for Jenkins to mine and analyze data from a source code repository. https://github.com/jenkinsci/forensics-api-plugin @@ -43,6 +44,24 @@ + + scm:git:https://github.com/jenkinsci/forensics-api-plugin.git + scm:git:git@github.com:jenkinsci/forensics-api-plugin.git + https://github.com/jenkinsci/forensics-api-plugin + ${scmTag} + + + + 2.4.0 + -SNAPSHOT + + ${project.groupId}.forensics.api + + + 1.19.5 + + + @@ -97,8 +116,8 @@ io.jenkins.plugins plugin-util-api - test tests + test org.jenkins-ci.plugins @@ -110,6 +129,21 @@ ssh-slaves test + + org.jenkins-ci.plugins.workflow + workflow-basic-steps + test + + + org.jenkins-ci.plugins.workflow + workflow-durable-task-step + test + + + org.jenkinsci.plugins + pipeline-model-definition + test + org.testcontainers testcontainers @@ -125,6 +159,23 @@ + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + incrementals.jenkins-ci.org + https://repo.jenkins-ci.org/incrementals/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + @@ -170,31 +221,4 @@ - - - scm:git:https://github.com/jenkinsci/forensics-api-plugin.git - scm:git:git@github.com:jenkinsci/forensics-api-plugin.git - https://github.com/jenkinsci/forensics-api-plugin - ${scmTag} - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - incrementals.jenkins-ci.org - https://repo.jenkins-ci.org/incrementals/ - - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - - diff --git a/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceBuild.java b/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceBuild.java index 50ce59e2..545aca4a 100644 --- a/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceBuild.java +++ b/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceBuild.java @@ -11,6 +11,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.kohsuke.stapler.StaplerProxy; +import hudson.model.Result; import hudson.model.Run; import jenkins.model.RunAction2; @@ -20,8 +21,8 @@ import static j2html.TagCreator.*; /** - * Stores the reference build for a given build. The reference build is a build in a different Jenkins job that can be - * used to compute delta reports. + * Stores the selected reference build for a given build. The reference build is a build in a different (or same) + * Jenkins job that can be used to compute delta reports. * * @author Ullrich Hafner * @see ReferenceRecorder @@ -31,7 +32,7 @@ public class ReferenceBuild implements RunAction2, Serializable, StaplerProxy { /** * Indicates that no reference build has been found. Note that this value is not used when the build has been found - * initially but has been deleted afterwards. + * initially but has been deleted afterward. */ public static final String NO_REFERENCE_BUILD = "-"; @@ -58,6 +59,7 @@ public static String getReferenceBuildLink(final String referenceBuildId) { } private final String referenceBuildId; + private Result requiredResult; // @since 2.4.0 private final JenkinsFacade jenkinsFacade; private final List messages; @@ -65,44 +67,92 @@ public static String getReferenceBuildLink(final String referenceBuildId) { private transient Run owner; /** - * Creates a new instance of {@link ReferenceBuild} that indicates that no reference build has been found. + * Creates a new instance of {@link ReferenceBuild} that indicates that no reference build has been found. * * @param owner - * the current run as owner of this action + * the current run as the owner of this action * @param messages * messages that show the steps the resolution process + * @param requiredResult + * the required build result of the chosen reference build */ + public ReferenceBuild(final Run owner, final List messages, final Result requiredResult) { + this(owner, messages, NO_REFERENCE_BUILD, requiredResult); + } + + /** + * Creates a new instance of {@link ReferenceBuild} that points to the specified reference build. + * + * @param owner + * the current build as the owner of this action + * @param messages + * messages that show the steps of the resolution process + * @param requiredResult + * the required build result of the chosen reference build + * @param referenceBuild + * the found reference build + */ + public ReferenceBuild(final Run owner, final List messages, final Result requiredResult, + final Run referenceBuild) { + this(owner, messages, referenceBuild.getExternalizableId(), requiredResult); + } + + /** + * Creates a new instance of {@link ReferenceBuild} that indicates that no reference build has been found. + * + * @param owner + * the current run as the owner of this action + * @param messages + * messages that show the steps the resolution process + * @deprecated use {@link #ReferenceBuild(Run, List, Result)} instead + */ + @Deprecated public ReferenceBuild(final Run owner, final List messages) { - this(owner, messages, NO_REFERENCE_BUILD); + this(owner, messages, NO_REFERENCE_BUILD, Result.UNSTABLE); } /** * Creates a new instance of {@link ReferenceBuild} that points to the specified reference build. * * @param owner - * the current build as owner of this action + * the current build as the owner of this action * @param messages * messages that show the steps of the resolution process * @param referenceBuild * the found reference build + * @deprecated use {@link #ReferenceBuild(Run, List, Result, Run)} instead */ + @Deprecated public ReferenceBuild(final Run owner, final List messages, final Run referenceBuild) { - this(owner, messages, referenceBuild.getExternalizableId()); + this(owner, messages, referenceBuild.getExternalizableId(), Result.UNSTABLE); } - private ReferenceBuild(final Run owner, final List messages, final String referenceBuildId) { - this(owner, messages, referenceBuildId, new JenkinsFacade()); + private ReferenceBuild(final Run owner, final List messages, final String referenceBuildId, final Result requiredResult) { + this(owner, messages, referenceBuildId, requiredResult, new JenkinsFacade()); } @VisibleForTesting ReferenceBuild(final Run owner, final List messages, final String referenceBuildId, - final JenkinsFacade jenkinsFacade) { + final Result requiredResult, final JenkinsFacade jenkinsFacade) { this.owner = owner; this.messages = new ArrayList<>(messages); this.referenceBuildId = referenceBuildId; + this.requiredResult = requiredResult; this.jenkinsFacade = jenkinsFacade; } + /** + * Called after deserialization to retain backward compatibility. + * + * @return this + */ + protected Object readResolve() { + if (requiredResult == null) { + requiredResult = Result.UNSTABLE; + } + return this; + } + @Override public void onAttached(final Run run) { this.owner = run; @@ -174,6 +224,15 @@ public String getReferenceBuildId() { return Optional.empty(); } + /** + * Returns the required build result of the chosen reference build. + * + * @return the required build result + */ + public Result getRequiredResult() { + return requiredResult; + } + @Override public String getDisplayName() { return null; diff --git a/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidation.java b/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidation.java index 0e13cc22..60617cd0 100644 --- a/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidation.java +++ b/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidation.java @@ -1,5 +1,7 @@ package io.jenkins.plugins.forensics.reference; +import org.apache.commons.lang3.StringUtils; + import edu.hm.hafner.util.VisibleForTesting; import hudson.util.ComboBoxModel; @@ -45,7 +47,8 @@ public ComboBoxModel getAllJobs() { * @return the validation result */ public FormValidation validateJob(final String referenceJobName) { - if (jenkins.getJob(referenceJobName).isPresent()) { + if (StringUtils.isEmpty(referenceJobName) + || jenkins.getJob(referenceJobName).isPresent()) { return FormValidation.ok(); } return FormValidation.error(Messages.FieldValidator_Error_ReferenceJobDoesNotExist()); diff --git a/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceRecorder.java b/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceRecorder.java index 5ead0edd..e19a2491 100644 --- a/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceRecorder.java +++ b/src/main/java/io/jenkins/plugins/forensics/reference/ReferenceRecorder.java @@ -54,12 +54,20 @@ public abstract class ReferenceRecorder extends SimpleReferenceRecorder { private boolean latestBuildIfNotFound = false; private String scm = StringUtils.EMPTY; + /** + * Creates a new instance of {@link ReferenceRecorder}. + */ + protected ReferenceRecorder() { + super(); + } + /** * Creates a new instance of {@link ReferenceRecorder}. * * @param jenkins * facade to Jenkins */ + @VisibleForTesting protected ReferenceRecorder(final JenkinsFacade jenkins) { super(jenkins); } @@ -80,34 +88,6 @@ public boolean isLatestBuildIfNotFound() { return latestBuildIfNotFound; } - /** - * Sets the target branch for {@link MultiBranchProject multi-branch projects}: the target branch is considered the - * base branch in your repository. The builds of all other branches and pull requests will use this target branch as - * baseline to search for a matching reference build. - * - * @param defaultBranch - * the name of the default branch - * @deprecated use {@link #setTargetBranch(String)} instead - */ - @Deprecated - @DataBoundSetter - public void setDefaultBranch(final String defaultBranch) { - this.defaultBranch = StringUtils.stripToEmpty(defaultBranch); - } - - /** - * Returns the target branch for {@link MultiBranchProject multi-branch projects}: the target branch is considered - * the base branch in your repository. The builds of all other branches and pull requests will use this target branch - * as baseline to search for a matching reference build. - * - * @return the name of the target branch - * @deprecated use {@link #getTargetBranch()} instead - */ - @Deprecated - public String getDefaultBranch() { - return defaultBranch; - } - /** * Sets the target branch for {@link MultiBranchProject multi-branch projects}: the target branch is considered the * base branch in your repository. The builds of all other branches and pull requests will use this target branch as @@ -153,26 +133,38 @@ protected ReferenceBuild findReferenceBuild(final Run run, final FilteredL Job reference = actualReferenceJob.get(); Run lastCompletedBuild = reference.getLastCompletedBuild(); if (lastCompletedBuild == null) { - logger.logInfo("No completed build found"); + logger.logInfo("No completed build found for reference job '%s'", reference.getDisplayName()); } else { - Optional> referenceBuild = find(run, lastCompletedBuild, logger); + var referenceBuild = searchForReferenceBuildWithRequiredStatus(run, lastCompletedBuild, logger); if (referenceBuild.isPresent()) { - Run result = referenceBuild.get(); - logger.logInfo("Found reference build '%s' for target branch", result.getDisplayName()); - - return new ReferenceBuild(run, logger.getInfoMessages(), result); + return referenceBuild.get(); } - logger.logInfo("No reference build found that contains matching commits"); - if (isLatestBuildIfNotFound()) { - logger.logInfo("Falling back to latest build of reference job: '%s'", - lastCompletedBuild.getDisplayName()); + } + } + return createEmptyReferenceBuild(run, logger); + } - return new ReferenceBuild(run, logger.getInfoMessages(), lastCompletedBuild); - } + private Optional searchForReferenceBuildWithRequiredStatus(final Run run, + final Run lastCompletedBuild, final FilteredLog logger) { + Optional> referenceBuild = find(run, lastCompletedBuild, logger); + if (referenceBuild.isPresent()) { + Run result = referenceBuild.get(); + logger.logInfo("Found reference build '%s' for target branch", result.getDisplayName()); + + var referenceBuildWithRequiredStatus = getReferenceBuildWithRequiredStatus(run, result, logger); + if (referenceBuildWithRequiredStatus.isPresent()) { + return referenceBuildWithRequiredStatus; } } - return new ReferenceBuild(run, logger.getInfoMessages()); + logger.logInfo("No reference build with required status found that contains matching commits"); + if (isLatestBuildIfNotFound()) { + logger.logInfo("Falling back to latest completed build of reference job: '%s'", + lastCompletedBuild.getDisplayName()); + + return Optional.of(new ReferenceBuild(run, logger.getInfoMessages(), getRequiredResult(), lastCompletedBuild)); + } + return Optional.empty(); } private Optional> findReferenceJob(final Run run, final FilteredLog log) { @@ -222,7 +214,12 @@ protected ReferenceBuild findReferenceBuild(final Run run, final FilteredL return findJobForTargetBranch(multiBranchProject, job, DEFAULT_TARGET_BRANCH, logger); } else { - logger.logInfo("Consider configuring a reference job using the 'referenceJob' property"); + if (StringUtils.isEmpty(getReferenceJob())) { + logger.logInfo("Falling back to current job '%s'", job.getDisplayName()); + + return Optional.of(job); + } + logger.logInfo("Do not use a reference job and skip delta reporting"); } return Optional.empty(); } diff --git a/src/main/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder.java b/src/main/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder.java index 7fd4fabd..9a84d0d6 100644 --- a/src/main/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder.java +++ b/src/main/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder.java @@ -1,11 +1,11 @@ package io.jenkins.plugins.forensics.reference; -import java.util.List; import java.util.Optional; import org.apache.commons.lang3.StringUtils; import edu.hm.hafner.util.FilteredLog; +import edu.hm.hafner.util.VisibleForTesting; import edu.umd.cs.findbugs.annotations.NonNull; import org.kohsuke.stapler.AncestorInPath; @@ -22,6 +22,7 @@ import hudson.model.BuildableItem; import hudson.model.Item; import hudson.model.Job; +import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; @@ -30,6 +31,7 @@ import hudson.tasks.Recorder; import hudson.util.ComboBoxModel; import hudson.util.FormValidation; +import hudson.util.ListBoxModel; import jenkins.branch.MultiBranchProject; import jenkins.tasks.SimpleBuildStep; @@ -54,8 +56,8 @@ *

* *

- * This recorder unifies the computation of the reference build so consuming plugins can simply use the resulting {@link - * ReferenceBuild} in order to get a reference for their delta reports. + * This recorder unifies the computation of the reference build so consuming plugins can simply use the resulting + * {@link ReferenceBuild} in order to get a reference for their delta reports. *

* * @author Arne Schöntag @@ -63,11 +65,9 @@ */ @SuppressWarnings("PMD.ExcessiveImports") public class SimpleReferenceRecorder extends Recorder implements SimpleBuildStep { - /** Indicates that no reference job has been defined yet. */ - public static final String NO_REFERENCE_JOB = "-"; - private final JenkinsFacade jenkins; private String referenceJob = StringUtils.EMPTY; + private Result requiredResult = Result.UNSTABLE; // @since 2.4.0 /** * Creates a new instance of {@link SimpleReferenceRecorder}. @@ -83,6 +83,7 @@ public SimpleReferenceRecorder() { * @param jenkins * facade to Jenkins */ + @VisibleForTesting protected SimpleReferenceRecorder(final JenkinsFacade jenkins) { super(); @@ -90,33 +91,71 @@ protected SimpleReferenceRecorder(final JenkinsFacade jenkins) { } /** - * Sets the reference job: this job will be used as baseline to search for the best matching reference build. If the - * reference job should be computed automatically (supported by {@link MultiBranchProject multi-branch projects} - * only), then let this field empty. + * Called after deserialization to retain backward compatibility. + * + * @return this + */ + protected Object readResolve() { + if (requiredResult == null) { + requiredResult = Result.UNSTABLE; + } + return this; + } + + /** + * Sets the reference job: this job will be used as a baseline to search for the best matching reference build. If + * the reference job should be computed automatically (supported by {@link MultiBranchProject multi-branch projects} + * only), then let this field empty. If the reference job is not defined and cannot be computed automatically, then + * the current job will be used. * * @param referenceJob * the name of reference job */ @DataBoundSetter public void setReferenceJob(final String referenceJob) { - if (NO_REFERENCE_JOB.equals(referenceJob)) { - this.referenceJob = StringUtils.EMPTY; - } this.referenceJob = StringUtils.strip(referenceJob); } /** - * Returns the name of the reference job. If the job is not defined, then {@link #NO_REFERENCE_JOB} is returned. + * Returns the name of the reference job. * - * @return the name of reference job, or {@link #NO_REFERENCE_JOB} if undefined + * @return the name of reference job */ public String getReferenceJob() { - if (StringUtils.isBlank(referenceJob)) { - return NO_REFERENCE_JOB; - } return referenceJob; } + /** + * Sets that required build result of the reference build. If the reference build has a worse status, then the + * selected reference build will not be used. + * + * @param requiredResult + * the minimum required build result + */ + @DataBoundSetter + public void setRequiredResult(final Result requiredResult) { + this.requiredResult = requiredResult; + } + + public Result getRequiredResult() { + return requiredResult; + } + + /** + * Returns whether the specified build has a result that is better or equal than the required result. + * + * @param referenceBuild + * the reference build to check + * + * @return {@code true} if the build has a result that is better or equal than the required result, {@code false} + * otherwise + */ + protected boolean hasRequiredResult(final Run referenceBuild) { + var result = referenceBuild.getResult(); + + return result != null && result.isBetterOrEqualTo(requiredResult); + } + @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; @@ -139,30 +178,68 @@ public void perform(@NonNull final Run run, @NonNull final FilePath worksp } protected ReferenceBuild findReferenceBuild(final Run run, final FilteredLog log) { - Optional> actualReferenceJob = resolveReferenceJob(log); - if (actualReferenceJob.isPresent()) { - Job reference = actualReferenceJob.get(); - Run lastCompletedBuild = reference.getLastCompletedBuild(); - if (lastCompletedBuild == null) { - log.logInfo("No completed build found"); - } - else { - log.logInfo("Found reference build '%s' for target branch", lastCompletedBuild.getDisplayName()); + Job reference = resolveReferenceJob(log).orElseGet(() -> fallBackToCurrentJob(run, log)); + Run lastCompletedBuild = reference.getLastCompletedBuild(); + if (lastCompletedBuild == null) { + log.logInfo("No completed build found for reference job '%s'", reference.getDisplayName()); + + return createEmptyReferenceBuild(run, log); + } + else { + log.logInfo("Found last completed build '%s' of reference job '%s'", + lastCompletedBuild.getDisplayName(), reference.getDisplayName()); - return new ReferenceBuild(run, log.getInfoMessages(), lastCompletedBuild); + return getReferenceBuildWithRequiredStatus(run, lastCompletedBuild, log) + .orElse(createEmptyReferenceBuild(run, log)); + } + } + + /** + * Returns a reference build that satisfied the required status. Starting with the specified {@code start} build, + * the history of builds is searched for a build that satisfies the required status. If no such build is found, then + * a null object is returned. + * + * @param run + * the run that is currently built + * @param start + * the first build to start the search + * @param log + * the logger + * + * @return the reference build that satisfies the required status (or empty if no such build is found) + */ + protected Optional getReferenceBuildWithRequiredStatus(final Run run, final Run start, final FilteredLog log) { + for (Run reference = start; reference != null; reference = reference.getPreviousCompletedBuild()) { + if (hasRequiredResult(reference)) { + log.logInfo("-> %s '%s' has a result %s", + getBuildName(start, reference), + reference.getDisplayName(), reference.getResult()); + + return Optional.of(new ReferenceBuild(run, log.getInfoMessages(), requiredResult, reference)); } } - log.logError("You need to define a valid reference job using the 'referenceJob' property"); + log.logInfo("-> ignoring reference build '%s' or one of its predecessors since none have a result of %s or better", + start.getDisplayName(), requiredResult); + return Optional.empty(); + } - return createEmptyReferenceBuild(run, log.getInfoMessages()); + @SuppressWarnings("PMD.CompareObjectsWithEquals") + private String getBuildName(final Run start, final Run reference) { + return reference == start ? "Build" : "Previous build"; } - protected ReferenceBuild createEmptyReferenceBuild(final Run run, final List messages) { - return new ReferenceBuild(run, messages); + private Job fallBackToCurrentJob(final Run run, final FilteredLog log) { + Job parent = run.getParent(); + log.logInfo("Falling back to current job '%s'", parent.getDisplayName()); + return parent; + } + + protected ReferenceBuild createEmptyReferenceBuild(final Run run, final FilteredLog messages) { + return new ReferenceBuild(run, messages.getInfoMessages(), requiredResult); } /** - * Finds a reference job that should be used as starting point to find the reference build. + * Finds a reference job that should be used as a starting point to find the reference build. * * @param log * the logger @@ -177,6 +254,7 @@ protected ReferenceBuild createEmptyReferenceBuild(final Run run, final Li return findJob(jobName, log); } + log.logInfo("No reference job configured"); return Optional.empty(); } @@ -192,14 +270,14 @@ protected ReferenceBuild createEmptyReferenceBuild(final Run run, final Li */ protected Optional> findJob(final String jobName, final FilteredLog log) { Optional> job = jenkins.getJob(jobName); - if (!job.isPresent()) { + if (job.isEmpty()) { log.logInfo("There is no such job - maybe the job has been renamed or deleted?"); } return job; } private boolean isValidJobName(final String name) { - return StringUtils.isNotBlank(name) && !NO_REFERENCE_JOB.equals(name); + return StringUtils.isNotBlank(name); } /** @@ -208,7 +286,28 @@ private boolean isValidJobName(final String name) { @Extension @Symbol("discoverReferenceBuild") public static class SimpleReferenceRecorderDescriptor extends BuildStepDescriptor { - private static final JenkinsFacade JENKINS = new JenkinsFacade(); + private final JenkinsFacade jenkins; + private final ReferenceJobModelValidation model; + + /** + * Creates a new descriptor with the concrete services. + */ + public SimpleReferenceRecorderDescriptor() { + this(new JenkinsFacade(), new ReferenceJobModelValidation()); + } + + @VisibleForTesting + protected SimpleReferenceRecorderDescriptor(final JenkinsFacade jenkins) { + this(jenkins, new ReferenceJobModelValidation(jenkins)); + } + + @VisibleForTesting + SimpleReferenceRecorderDescriptor(final JenkinsFacade jenkins, final ReferenceJobModelValidation model) { + super(); + + this.jenkins = jenkins; + this.model = model; + } @NonNull @Override @@ -221,18 +320,17 @@ public boolean isApplicable(final Class jobType) { return true; } - private final ReferenceJobModelValidation model = new ReferenceJobModelValidation(); - /** * Returns the model with the possible reference jobs. * * @param project * the project that is configured + * * @return the model with the possible reference jobs */ @POST public ComboBoxModel doFillReferenceJobItems(@AncestorInPath final BuildableItem project) { - if (JENKINS.hasPermission(Item.CONFIGURE, project)) { + if (jenkins.hasPermission(Item.CONFIGURE, project)) { return model.getAllJobs(); } return new ComboBoxModel(); @@ -252,10 +350,29 @@ public ComboBoxModel doFillReferenceJobItems(@AncestorInPath final BuildableItem @SuppressWarnings("unused") // Used in jelly validation public FormValidation doCheckReferenceJob(@AncestorInPath final BuildableItem project, @QueryParameter final String referenceJob) { - if (!JENKINS.hasPermission(Item.CONFIGURE, project)) { + if (!jenkins.hasPermission(Item.CONFIGURE, project)) { return FormValidation.ok(); } return model.validateJob(referenceJob); } + + /** + * Returns the model with the possible build results. + * + * @param project + * the project that is configured + * + * @return the model with the possible build results + */ + @POST + public ListBoxModel doFillRequiredResultItems(@AncestorInPath final BuildableItem project) { + var resultModel = new ListBoxModel(); + if (jenkins.hasPermission(Item.CONFIGURE, project)) { + resultModel.add(Messages.RequiredResult_Failure(), Result.FAILURE.toString()); + resultModel.add(Messages.RequiredResult_Unstable(), Result.UNSTABLE.toString()); + resultModel.add(Messages.RequiredResult_Success(), Result.SUCCESS.toString()); + } + return resultModel; + } } } diff --git a/src/main/resources/io/jenkins/plugins/forensics/reference/Messages.properties b/src/main/resources/io/jenkins/plugins/forensics/reference/Messages.properties index a74ad66c..197c1790 100644 --- a/src/main/resources/io/jenkins/plugins/forensics/reference/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/forensics/reference/Messages.properties @@ -1,3 +1,7 @@ Recorder.DisplayName=Discover reference build Messages.DisplayName=Reference build FieldValidator.Error.ReferenceJobDoesNotExist=There is no such job - maybe the job has been renamed? + +RequiredResult.Failure=All build results +RequiredResult.Unstable=Only unstable or successful +RequiredResult.Success=Only successful diff --git a/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.jelly b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.jelly index d0a4e38f..ff163c63 100644 --- a/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.jelly +++ b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.jelly @@ -4,5 +4,8 @@ + + + diff --git a/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.properties b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.properties index 1de9f543..e2b9467f 100644 --- a/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.properties +++ b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/config.properties @@ -1,2 +1,4 @@ title.referenceJob=Reference Job -description.referenceJob=A reference job defines the baseline for tools that want to create relative build reports. +description.referenceJob=Select the baseline job to create relative build reports. +title.requiredResult=Required Build Result +description.requiredResult=Consider only builds with this result as reference builds. diff --git a/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/help-referenceJob.html b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/help-referenceJob.html index 38545b0e..85fe1581 100644 --- a/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/help-referenceJob.html +++ b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/help-referenceJob.html @@ -1,3 +1,14 @@ -The reference job is the baseline that is used to determine which of the issues in the current -build are new, outstanding, or fixed. This baseline is also used to determine the number of new issues for the -quality gate evaluation. \ No newline at end of file +

+ The reference job is the baseline used to determine the relative differences to the current build. + Several plugins that report build statistics (test results, code coverage, metrics, static + analysis warnings) typically show their reports in two different ways: either as absolute report + (e.g., total number of tests or warnings, overall code coverage) or as relative delta report (e.g., additional + tests, increased or decreased coverage, new or fixed warnings). To compute a relative delta report, a plugin needs + to select another build to compare the current results to (a so-called reference build). This reference build is + obtained from the specified reference job. +

+

+ In Multibranch Pipelines this parameter can be left empty: in these jobs, the plugin selects the build automatically + from the associated job that builds the main target branch. For all other jobs, the last completed build of the + current job will be used as reference build if the reference job is not specified. +

diff --git a/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/help-requiredResult.html b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/help-requiredResult.html new file mode 100644 index 00000000..6f12e271 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder/help-requiredResult.html @@ -0,0 +1,15 @@ +When a reference build is searched for in the baseline (see the parameter reference job) then it +sometimes makes sense to only consider builds that have a certain result. +For example, you may want to skip the computation of a delta coverage when the reference build has +failed tests. Or you may want to skip the computation of new warnings when the reference build has +a compilation error. + +The following enumeration shows the possible values for this parameter and the corresponding behavior: +
+
SUCCESS
+
The selected build must be successful
+
UNSTABLE
+
The selected build must be successful or unstable
+
FAILURE
+
The result of the build is not relevant (this is the default behavior if unset)
+
diff --git a/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceBuildTest.java b/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceBuildTest.java index 7005a875..63587c49 100644 --- a/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceBuildTest.java +++ b/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceBuildTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; +import hudson.model.Result; import hudson.model.Run; import io.jenkins.plugins.bootstrap5.MessagesViewModel; @@ -27,7 +28,7 @@ class ReferenceBuildTest { void shouldAttachBuild() { Run currentBuild = mock(Run.class); - ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES); + ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES, Result.UNSTABLE); assertThat(referenceBuild).hasOwner(currentBuild); Run newBuild = mock(Run.class); @@ -41,16 +42,16 @@ void shouldAttachBuild() { void shouldHandleMissingReferenceBuild() { Run currentBuild = mock(Run.class); - ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES); + ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES, Result.FAILURE); - assertThat(referenceBuild).doesNotHaveReferenceBuild(); - assertThat(referenceBuild).hasOnlyMessages(MESSAGES); - assertThat(referenceBuild).hasOwner(currentBuild); - assertThat(referenceBuild).hasReferenceBuildId(ReferenceBuild.NO_REFERENCE_BUILD); - assertThat(referenceBuild).hasReferenceLink( - "Reference build '-' not found anymore - maybe the build has been renamed or deleted?"); - assertThat(referenceBuild.getReferenceBuild()).isEmpty(); + assertThat(referenceBuild).doesNotHaveReferenceBuild() + .hasRequiredResult(Result.FAILURE) + .hasOnlyMessages(MESSAGES) + .hasOwner(currentBuild) + .hasReferenceBuildId(ReferenceBuild.NO_REFERENCE_BUILD) + .hasReferenceLink("Reference build '-' not found anymore - maybe the build has been renamed or deleted?"); + assertThat(referenceBuild.getReferenceBuild()).isEmpty(); assertThat(referenceBuild.getIconFileName()).isNull(); assertThat(referenceBuild.getDisplayName()).isNull(); assertThat(referenceBuild.getUrlName()).isEqualTo(ReferenceBuild.REFERENCE_DETAILS_URL); @@ -69,12 +70,14 @@ void shouldHandleValidReferenceBuild() { Run targetBuild = mock(Run.class); when(targetBuild.getExternalizableId()).thenReturn(ID); - ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES, targetBuild); + ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES, Result.FAILURE, targetBuild); - assertThat(referenceBuild).hasReferenceBuild(); - assertThat(referenceBuild).hasOnlyMessages(MESSAGES); - assertThat(referenceBuild).hasOwner(currentBuild); - assertThat(referenceBuild).hasReferenceBuildId(ID); + assertThat(referenceBuild) + .hasReferenceBuild() + .hasOnlyMessages(MESSAGES) + .hasOwner(currentBuild) + .hasReferenceBuildId(ID) + .hasRequiredResult(Result.FAILURE); assertThat(referenceBuild.getIconFileName()).isNull(); assertThat(referenceBuild.getDisplayName()).isNull(); @@ -91,7 +94,7 @@ void shouldHandleValidReferenceBuildId() { when(jenkinsFacade.getAbsoluteUrl(any())).thenReturn("URL"); when(targetBuild.getFullDisplayName()).thenReturn("Name"); - ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES, ID, jenkinsFacade); + ReferenceBuild referenceBuild = new ReferenceBuild(currentBuild, MESSAGES, ID, Result.UNSTABLE, jenkinsFacade); assertThat(referenceBuild).hasReferenceBuild(); assertThat(referenceBuild).hasOnlyMessages(MESSAGES); diff --git a/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidationTest.java b/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidationTest.java index 0955eb36..8f969d76 100644 --- a/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidationTest.java +++ b/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceJobModelValidationTest.java @@ -12,9 +12,7 @@ import io.jenkins.plugins.util.JenkinsFacade; -import static io.jenkins.plugins.forensics.reference.ReferenceRecorder.*; import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.SoftAssertions.*; import static org.mockito.Mockito.*; /** @@ -40,11 +38,8 @@ void shouldValidateToOkIfEmpty() { JenkinsFacade jenkins = mock(JenkinsFacade.class); ReferenceJobModelValidation model = new ReferenceJobModelValidation(jenkins); - assertSoftly(softly -> { - softly.assertThat(model.validateJob(NO_REFERENCE_JOB).kind).isEqualTo(Kind.ERROR); - softly.assertThat(model.validateJob("").kind).isEqualTo(Kind.ERROR); - softly.assertThat(model.validateJob(null).kind).isEqualTo(Kind.ERROR); - }); + assertThat(model.validateJob("").kind).isEqualTo(Kind.OK); + assertThat(model.validateJob(null).kind).isEqualTo(Kind.OK); } @Test diff --git a/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceRecorderTest.java b/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceRecorderTest.java index 0139dfa6..bb290314 100644 --- a/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceRecorderTest.java +++ b/src/test/java/io/jenkins/plugins/forensics/reference/ReferenceRecorderTest.java @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Optional; +import java.util.function.Consumer; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; @@ -13,6 +14,7 @@ import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; import hudson.model.Item; import hudson.model.Job; +import hudson.model.Result; import hudson.model.Run; import jenkins.scm.api.SCMHead; import jenkins.scm.api.metadata.PrimaryInstanceMetadataAction; @@ -38,19 +40,87 @@ void shouldInitCorrectly() { ReferenceRecorder recorder = new NullReferenceRecorder(); assertThat(recorder) - .hasDefaultBranch(StringUtils.EMPTY) .hasTargetBranch(StringUtils.EMPTY) - .hasScm(StringUtils.EMPTY); + .hasScm(StringUtils.EMPTY) + .hasRequiredResult(Result.UNSTABLE); } /** * Verifies the first alternative: the current build is for a pull request part in a multi-branch project. In this - * case the target branch stored in the PR will be used as reference job. + * case, the target branch stored in the PR will be used as the reference job. */ @Test void shouldObtainReferenceFromPullRequestTarget() { FilteredLog log = createLog(); + var referenceBuild = findReferenceBuild(log, build -> { }); + + assertThat(log.getInfoMessages()).contains( + "No reference job configured", + "Found a `MultiBranchProject`, trying to resolve the target branch from the configuration", + "-> no target branch configured in step", + "-> detected a pull or merge request for target branch 'pr-target'", + "-> inferred job for target branch: 'pr-target'", + "Found reference build 'pr-id' for target branch", + "-> Build 'pr-id' has a result SUCCESS"); + + assertThat(referenceBuild).hasReferenceBuildId("pr-id"); + } + + /** + * Verifies a variation of the first alternative: the found reference build has an insufficient result, + * so the predecessor will be chosen. + */ + @Test + void shouldObtainReferenceFromPullRequestTargetWithMatchingResult() { + FilteredLog log = createLog(); + + var referenceBuild = findReferenceBuild(log, build -> { + var successful = createBuild("successful", Result.SUCCESS); + when(build.getPreviousCompletedBuild()).thenAnswer((a) -> successful); + when(build.getResult()).thenReturn(Result.FAILURE); // reference failed + }); + + assertThat(log.getInfoMessages()).contains( + "No reference job configured", + "Found a `MultiBranchProject`, trying to resolve the target branch from the configuration", + "-> no target branch configured in step", + "-> detected a pull or merge request for target branch 'pr-target'", + "-> inferred job for target branch: 'pr-target'", + "Found reference build 'pr-id' for target branch", + "-> Previous build 'successful' has a result SUCCESS"); + + assertThat(referenceBuild).hasReferenceBuildId("successful"); + } + + /** + * Verifies another variation of the first alternative: the found reference build has an insufficient result, + * but the predecessor as well. + */ + @Test + void shouldFailToGetReferenceBecauseOfFailure() { + FilteredLog log = createLog(); + + var referenceBuild = findReferenceBuild(log, build -> { + var failure = createBuild("failure", Result.FAILURE); + when(build.getPreviousCompletedBuild()).thenAnswer((a) -> failure); + when(build.getResult()).thenReturn(Result.FAILURE); // reference failed as well + }); + + assertThat(log.getInfoMessages()).contains( + "No reference job configured", + "Found a `MultiBranchProject`, trying to resolve the target branch from the configuration", + "-> no target branch configured in step", + "-> detected a pull or merge request for target branch 'pr-target'", + "-> inferred job for target branch: 'pr-target'", + "Found reference build 'pr-id' for target branch", + "-> ignoring reference build 'pr-id' or one of its predecessors since none have a result of UNSTABLE or better", + "No reference build with required status found that contains matching commits"); + + assertThat(referenceBuild).doesNotHaveReferenceBuild(); + } + + private ReferenceBuild findReferenceBuild(final FilteredLog log, final Consumer> configuration) { Run build = mock(Run.class); Job job = createJob(build); WorkflowMultiBranchProject topLevel = createMultiBranch(job); @@ -60,19 +130,24 @@ void shouldObtainReferenceFromPullRequestTarget() { Run prBuild = configurePrJobAndBuild(recorder, topLevel, job); when(recorder.find(build, prBuild, log)).thenReturn(Optional.of(prBuild)); - ReferenceBuild referenceBuild = recorder.findReferenceBuild(build, log); + configuration.accept(prBuild); - assertThat(log.getInfoMessages()) - .anySatisfy(m -> assertThat(m).contains("no target branch configured in step")) - .anySatisfy(m -> assertThat(m).contains("detected a pull or merge request for target branch 'pr-target'")); + return recorder.findReferenceBuild(build, log); + } - assertThat(referenceBuild.getReferenceBuildId()).isEqualTo("pr-id"); + private Run createBuild(final String displayName, final Result result) { + Run build = mock(Run.class); + when(build.getFullDisplayName()).thenReturn(displayName); + when(build.getDisplayName()).thenReturn(displayName); + when(build.getExternalizableId()).thenReturn(displayName); + when(build.getResult()).thenReturn(result); + return build; } /** * Verifies the second alternative: the current build is part of a multi-branch project (but not for a PR). * Additionally, a primary branch has been configured for the multi-branch project using the action {@link - * PrimaryInstanceMetadataAction}. In this case this configured primary target will be used as reference job. + * PrimaryInstanceMetadataAction}. In this case, this configured primary target will be used as the reference job. */ @Test void shouldFindReferenceJobUsingPrimaryBranch() { @@ -88,18 +163,20 @@ void shouldFindReferenceJobUsingPrimaryBranch() { ReferenceBuild referenceBuild = recorder.findReferenceBuild(build, log); - assertThat(log.getInfoMessages()) - .anySatisfy( - m -> assertThat(m).contains("Found a `MultiBranchProject`")) - .anySatisfy( - m -> assertThat(m).contains("using configured primary branch 'main' of SCM as target branch")); + assertThat(log.getInfoMessages()).contains( + "No reference job configured", + "Found a `MultiBranchProject`, trying to resolve the target branch from the configuration", + "-> no target branch configured in step", + "-> using configured primary branch 'main' of SCM as target branch", + "Found reference build 'main-id' for target branch", + "-> Build 'main-id' has a result SUCCESS"); - assertThat(referenceBuild.getReferenceBuildId()).isEqualTo("main-id"); + assertThat(referenceBuild).hasReferenceBuildId("main-id"); } /** * Verifies the third alternative: the current build is for a pull request part in a multi-branch project. However, - * the step has been configured with the parameter {@code targetBranch}. In this case the configured target branch + * the step has been configured with the parameter {@code targetBranch}. In this case, the configured target branch * will override the target branch of the pull PR. */ @Test @@ -110,25 +187,30 @@ void targetShouldHavePrecedenceBeforePullRequestTarget() { Job job = createJob(build); WorkflowMultiBranchProject topLevel = createMultiBranch(job); - ReferenceRecorder recorder = createSut(); + var scmFacade = mock(ScmFacade.class); + ReferenceRecorder recorder = createSut(scmFacade); - configurePrJobAndBuild(recorder, topLevel, job); // will not be used since target branch has been set + configurePrJobAndBuild(recorder, topLevel, job); // will not be used since the target branch has been set configureTargetJobAndBuild(recorder, topLevel, build, log); ReferenceBuild referenceBuild = recorder.findReferenceBuild(build, log); - assertThat(log.getInfoMessages()) - .anySatisfy(m -> assertThat(m).contains("using target branch 'target' as configured in step")) - .noneSatisfy(m -> assertThat(m).contains("detected a pull or merge request")); + assertThat(log.getInfoMessages()).contains( + "No reference job configured", + "Found a `MultiBranchProject`, trying to resolve the target branch from the configuration", + "-> using target branch 'target' as configured in step", + "-> inferred job for target branch: 'target'", + "Found reference build 'target-id' for target branch", + "-> Build 'target-id' has a result SUCCESS"); - assertThat(referenceBuild.getReferenceBuildId()).isEqualTo("target-id"); + assertThat(referenceBuild).hasReferenceBuildId("target-id"); } /** * Verifies the fourth alternative: the current build is part of a multi-branch project (but not for a PR). * Additionally, a primary branch has been configured for the multi-branch project using the action {@link * PrimaryInstanceMetadataAction}. However, the step has been configured with the parameter {@code targetBranch}. In - * this case the configured target branch will override the primary branch. + * this case, the configured target branch will override the primary branch. */ @Test void targetShouldHavePrecedenceBeforePrimaryBranchTarget() { @@ -146,10 +228,14 @@ void targetShouldHavePrecedenceBeforePrimaryBranchTarget() { ReferenceBuild referenceBuild = recorder.findReferenceBuild(build, log); assertThat(log.getInfoMessages()) - .anySatisfy(m -> assertThat(m).contains("using target branch 'target' as configured in step")) - .noneSatisfy(m -> assertThat(m).contains("detected a pull or merge request")); - - assertThat(referenceBuild.getReferenceBuildId()).isEqualTo("target-id"); + .contains("No reference job configured", + "Found a `MultiBranchProject`, trying to resolve the target branch from the configuration", + "-> using target branch 'target' as configured in step", + "-> inferred job for target branch: 'target'", + "Found reference build 'target-id' for target branch", + "-> Build 'target-id' has a result SUCCESS"); + + assertThat(referenceBuild).hasReferenceBuildId("target-id"); } /** @@ -175,7 +261,13 @@ void shouldNotFindReferenceJobForMultiBranchProject() { } private ReferenceRecorder createSut() { - return mock(ReferenceRecorder.class, CALLS_REAL_METHODS); + return createSut(mock(ScmFacade.class)); + } + + private ReferenceRecorder createSut(final ScmFacade scmFacade) { + var recorder = spy(ReferenceRecorder.class); + when(recorder.getScmFacade()).thenReturn(scmFacade); + return recorder; } private FilteredLog createLog() { @@ -198,11 +290,10 @@ private void configurePrimaryBranch(final ReferenceRecorder recorder, final WorkflowMultiBranchProject topLevel, final Job job, final Run build, final FilteredLog log) { Job main = mock(Job.class); - Run mainBuild = mock(Run.class); - when(mainBuild.getExternalizableId()).thenReturn("main-id"); + Run mainBuild = createBuild("main-id", Result.SUCCESS); + when(main.getLastCompletedBuild()).thenAnswer(i -> mainBuild); when(main.getDisplayName()).thenReturn("main"); - when(main.getAction(PrimaryInstanceMetadataAction.class)).thenReturn(mock(PrimaryInstanceMetadataAction.class)); Item item = mock(Item.class); @@ -216,14 +307,14 @@ private void configurePrimaryBranch(final ReferenceRecorder recorder, private Run configurePrJobAndBuild(final ReferenceRecorder recorder, final WorkflowMultiBranchProject parent, final Job job) { Job prJob = mock(Job.class); + when(prJob.getDisplayName()).thenReturn("pr-target"); when(parent.getItemByBranchName("pr-target")).thenAnswer(i -> prJob); - Run prBuild = mock(Run.class); - when(prBuild.getExternalizableId()).thenReturn("pr-id"); + Run prBuild = createBuild("pr-id", Result.SUCCESS); when(prJob.getLastCompletedBuild()).thenAnswer(i -> prBuild); - ScmFacade scmFacade = mock(ScmFacade.class); ChangeRequestSCMHead pr = mock(ChangeRequestSCMHead.class); when(pr.toString()).thenReturn("pr"); + ScmFacade scmFacade = mock(ScmFacade.class); when(scmFacade.findHead(job)).thenReturn(Optional.of(pr)); SCMHead target = mock(SCMHead.class); when(pr.getTarget()).thenReturn(target); @@ -239,9 +330,9 @@ private void configurePrimaryBranch(final ReferenceRecorder recorder, recorder.setTargetBranch("target"); Job targetJob = mock(Job.class); + when(targetJob.getDisplayName()).thenReturn("target"); when(parent.getItemByBranchName("target")).thenAnswer(i -> targetJob); - Run targetBuild = mock(Run.class); - when(targetBuild.getExternalizableId()).thenReturn("target-id"); + Run targetBuild = createBuild("target-id", Result.SUCCESS); when(targetJob.getLastCompletedBuild()).thenAnswer(i -> targetBuild); when(recorder.find(build, targetBuild, log)).thenReturn(Optional.of(targetBuild)); diff --git a/src/test/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorderITest.java b/src/test/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorderITest.java index c2403986..4d666f75 100644 --- a/src/test/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorderITest.java +++ b/src/test/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorderITest.java @@ -1,13 +1,21 @@ package io.jenkins.plugins.forensics.reference; +import java.util.Arrays; import java.util.Optional; +import java.util.StringJoiner; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.Issue; import edu.hm.hafner.util.FilteredLog; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; import hudson.model.FreeStyleProject; +import hudson.model.Result; import hudson.model.Run; import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerSuite; @@ -20,21 +28,35 @@ * @author Ullrich Hafner */ class SimpleReferenceRecorderITest extends IntegrationTestWithJenkinsPerSuite { - /** Ensures that an error is shown if the publisher has been registered with an invalid configuration. */ @Test - void shouldReportErrorBecauseReferenceJobIsUndefined() { + void shouldReportErrorBecauseReferenceJobIsUndefinedAndNoBuildsInSameSob() { FreeStyleProject job = createJob(StringUtils.EMPTY); Run build = buildSuccessfully(job); assertThat(findReferenceBuild(build)).isEmpty(); assertThat(getConsoleLog(build)) - .contains("You need to define a valid reference job using the 'referenceJob' property"); + .contains("No completed build found for reference job '" + job.getDisplayName()); } - /** Finds the reference build in the selected job. */ @Test - void shouldFindReferenceBuild() { + void shouldUseSameJobWhenNoReferenceJobIsDefined() { + FreeStyleProject job = createJob(StringUtils.EMPTY); + + Run referenceBuild = buildSuccessfully(job); + + Run build = buildSuccessfully(job); + + assertThat(findReferenceBuild(build)).contains(referenceBuild); + assertThat(getConsoleLog(build)) + .contains("No reference job configured", + "Falling back to current job", + "Found last completed build '#1' of reference job", + "-> Build '#1' has a result SUCCESS"); + } + + @Test + void shouldFindReferenceBuildOfSelectedReferenceJob() { FreeStyleProject reference = createFreeStyleProject(); Run baseline = buildSuccessfully(reference); @@ -48,7 +70,7 @@ void shouldFindReferenceBuild() { @Test void shouldUseLatestReferenceBuild() { FreeStyleProject reference = createFreeStyleProject(); - buildSuccessfully(reference); // first build is ignored + buildSuccessfully(reference); // the first build is ignored Run baseline = buildSuccessfully(reference); FreeStyleProject job = createJob(reference.getName()); @@ -57,9 +79,8 @@ void shouldUseLatestReferenceBuild() { assertThat(findReferenceBuild(current)).contains(baseline); } - /** Finds the reference build in the selected job. */ @Test - void shouldFindNoReferenceBuild() { + void shouldFindNoReferenceBuildBecauseNoBuildHasBeenCompletedYet() { FreeStyleProject reference = createFreeStyleProject(); FreeStyleProject job = createJob(reference.getName()); @@ -69,6 +90,45 @@ void shouldFindNoReferenceBuild() { assertThat(getConsoleLog(current)).contains("No completed build found"); } + @ParameterizedTest(name = "[{index}] Required result: \"{0}\"") + @ValueSource(strings = {"SUCCESS", "UNSTABLE", ""}) + @Issue("JENKINS-72015") + void shouldSkipFailedBuildsIfResultIsWorseThanRequired(final String requiredResult) { + WorkflowJob reference = createPipeline(); + reference.setDefinition(new CpsFlowDefinition( + "node {\n" + + "brokenCommand()\n" + + " }\n", true)); + buildWithResult(reference, Result.FAILURE); + + WorkflowJob job = createPipeline(); + String script; + if (StringUtils.isBlank(requiredResult)) { + script = "node {\n" + + discoverReferenceJob(reference.getName()) + + " }\n"; + } + else { + script = "node {\n" + + discoverReferenceJob(reference.getName(), String.format("requiredResult: '%s'", requiredResult)) + + " }\n"; + } + job.setDefinition(new CpsFlowDefinition(script, true)); + + Run current = buildSuccessfully(job); + + assertThat(findReferenceBuild(current)).isEmpty(); + assertThat(getConsoleLog(current)).contains( + String.format("-> ignoring reference build '#1' or one of its predecessors since none have a result of %s or better", + StringUtils.defaultIfBlank(requiredResult, "UNSTABLE"))); + } + + private String discoverReferenceJob(final String referenceJobName, final String... arguments) { + var joiner = new StringJoiner(", ", ", ", "").setEmptyValue(""); + Arrays.stream(arguments).forEach(joiner::add); + return String.format("discoverReferenceBuild(referenceJob: '%s'%s)%n", referenceJobName, joiner); + } + private Optional> findReferenceBuild(final Run current) { ReferenceFinder referenceFinder = new ReferenceFinder(); return referenceFinder.findReference(current, new FilteredLog("LOG")); diff --git a/src/test/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorderTest.java b/src/test/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorderTest.java new file mode 100644 index 00000000..ab35310e --- /dev/null +++ b/src/test/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorderTest.java @@ -0,0 +1,55 @@ +package io.jenkins.plugins.forensics.reference; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import hudson.model.BuildableItem; +import hudson.model.Item; +import hudson.util.FormValidation; +import hudson.util.FormValidation.Kind; + +import io.jenkins.plugins.forensics.reference.SimpleReferenceRecorder.SimpleReferenceRecorderDescriptor; +import io.jenkins.plugins.util.JenkinsFacade; + +import static io.jenkins.plugins.forensics.assertions.Assertions.*; +import static org.mockito.Mockito.*; + +class SimpleReferenceRecorderTest { + @Test + void shouldFillModel() { + var jenkins = mock(JenkinsFacade.class); + var job = mock(BuildableItem.class); + + var descriptor = new SimpleReferenceRecorderDescriptor(jenkins); + + assertThat(descriptor.doFillReferenceJobItems(job)).isEmpty(); + assertThat(descriptor.doFillRequiredResultItems(job)).isEmpty(); + + var jobs = Set.of("one", "two"); + when(jenkins.getAllJobNames()).thenReturn(jobs); + when(jenkins.hasPermission(Item.CONFIGURE, job)).thenReturn(true); + + assertThat(descriptor.doFillReferenceJobItems(job)) + .containsExactlyInAnyOrderElementsOf(jobs); + assertThat(descriptor.doFillRequiredResultItems(job)) + .extracting("value") + .containsExactlyInAnyOrder("FAILURE", "SUCCESS", "UNSTABLE"); + } + + @Test + void shouldValidateJob() { + var jenkins = mock(JenkinsFacade.class); + var job = mock(BuildableItem.class); + var model = mock(ReferenceJobModelValidation.class); + + when(jenkins.hasPermission(Item.CONFIGURE, job)).thenReturn(true); + when(model.validateJob("one")).thenReturn(FormValidation.ok()); + when(model.validateJob("two")).thenReturn(FormValidation.error("error")); + + var descriptor = new SimpleReferenceRecorderDescriptor(jenkins, model); + + assertThat(descriptor.doCheckReferenceJob(job, "one").kind).isEqualTo(FormValidation.Kind.OK); + assertThat(descriptor.doCheckReferenceJob(job, "two").kind).isEqualTo(Kind.ERROR); + } +}