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 extends AbstractProject> 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);
+ }
+}