Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package io.jenkins.plugins.forensics.reference;

import org.apache.commons.lang3.StringUtils;

import edu.umd.cs.findbugs.annotations.CheckForNull;

import java.util.Arrays;
import java.util.Optional;

import hudson.model.Action;
import hudson.model.Run;

/**
* Filters builds by the {@link Action actions} they provide. An action matches if its class name equals the required
* type (the fully qualified or the simple name of the action, one of its supertypes or one of its interfaces) and if
* its {@link Action#getUrlName() URL name} equals the required ID. Both criteria are optional: an empty value matches
* every action.
*
* @author Akash Manna
*/
class ActionFilter {
private final String type;
private final String id;

ActionFilter(final String type, final String id) {
this.type = StringUtils.stripToEmpty(type);
this.id = StringUtils.stripToEmpty(id);
}

boolean isEnabled() {
return StringUtils.isNotBlank(type) || StringUtils.isNotBlank(id);
}

/**
* Returns whether the specified build provides a matching action.
*
* @param build
* the build to check
*
* @return {@code true} if the build provides a matching action or if this filter is disabled
*/
boolean accepts(final Run<?, ?> build) {
return !isEnabled() || build.getAllActions().stream().anyMatch(this::matches);
}

/**
* Returns the first build that provides a matching action, starting with the specified build and continuing with
* its predecessors.
*
* @param start
* the first build to check
*
* @return the matching build (or empty if no such build exists)
*/
Optional<Run<?, ?>> findFirstAcceptedBuild(final Run<?, ?> start) {
for (Run<?, ?> build = start; build != null; build = build.getPreviousCompletedBuild()) {
if (accepts(build)) {
return Optional.of(build);
}
}
return Optional.empty();
}

/**
* Returns a suffix that can be appended to a log message to describe the requirement of this filter.
*
* @return the suffix, or an empty string if this filter is disabled
*/
String getRequirementSuffix() {
return isEnabled() ? " and provide an action %s".formatted(this) : StringUtils.EMPTY;
}

private boolean matches(final Action action) {
return (StringUtils.isBlank(type) || isOfRequiredType(action.getClass()))
&& (StringUtils.isBlank(id) || id.equals(action.getUrlName()));
}

private boolean isOfRequiredType(@CheckForNull final Class<?> candidate) {
if (candidate == null) {
return false;
}
return type.equals(candidate.getName())
|| type.equals(candidate.getSimpleName())
|| isOfRequiredType(candidate.getSuperclass())
|| Arrays.stream(candidate.getInterfaces()).anyMatch(this::isOfRequiredType);
}

@Override
public String toString() {
if (StringUtils.isBlank(id)) {
return "of type '%s'".formatted(type);
}
if (StringUtils.isBlank(type)) {
return "with ID '%s'".formatted(id);
}
return "of type '%s' with ID '%s'".formatted(type, id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,20 @@ private Optional<ReferenceBuild> searchForReferenceBuildWithRequiredStatus(final
}
logger.logInfo("No reference build with required status found that contains matching commits");
if (isLatestBuildIfNotFound()) {
var filter = createActionFilter();
var fallback = filter.findFirstAcceptedBuild(lastCompletedBuild);
if (fallback.isEmpty()) {
logger.logInfo("-> no build that provides an action %s found in the history of '%s'",
filter, lastCompletedBuild.getDisplayName());

return Optional.empty();
}

var latestBuild = fallback.get();
logger.logInfo("Falling back to latest completed build of reference job: '%s'",
lastCompletedBuild.getDisplayName());
latestBuild.getDisplayName());

return Optional.of(new ReferenceBuild(run, logger.getInfoMessages(), getRequiredResult(), lastCompletedBuild));
return Optional.of(new ReferenceBuild(run, logger.getInfoMessages(), getRequiredResult(), latestBuild));
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildableItem;
import hudson.model.Item;
import hudson.model.Job;
Expand Down Expand Up @@ -69,6 +70,8 @@
private String referenceJob = StringUtils.EMPTY;
private Result requiredResult = Result.UNSTABLE; // @since 2.4.0
private boolean considerRunningBuild;
private String requiredAction = StringUtils.EMPTY; // @since 2.6.0
private String requiredActionId = StringUtils.EMPTY; // @since 2.6.0

/**
* Creates a new instance of {@link SimpleReferenceRecorder}.
Expand Down Expand Up @@ -100,6 +103,8 @@
if (requiredResult == null) {
requiredResult = Result.UNSTABLE;
}
requiredAction = StringUtils.stripToEmpty(requiredAction);
requiredActionId = StringUtils.stripToEmpty(requiredActionId);

Check warning on line 107 in src/main/java/io/jenkins/plugins/forensics/reference/SimpleReferenceRecorder.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 106-107 are not covered by tests
return this;
}

Expand Down Expand Up @@ -173,6 +178,48 @@
return result != null && result.isBetterOrEqualTo(requiredResult);
}

/**
* Sets the type of an {@link Action} that the reference build must provide: builds without such an action will be
* skipped. The type is matched against the fully qualified or the simple class name of the action, its supertypes
* and its interfaces.
*
* @param requiredAction
* the class name of the required action, or an empty string if the type of the action is not relevant
*/
@DataBoundSetter
public void setRequiredAction(final String requiredAction) {
this.requiredAction = StringUtils.stripToEmpty(requiredAction);
}

public String getRequiredAction() {
return requiredAction;
}

/**
* Sets the ID of an {@link Action} that the reference build must provide: builds without such an action will be
* skipped. The ID of an action is given by its {@link Action#getUrlName() URL name}.
*
* @param requiredActionId
* the ID of the required action, or an empty string if the ID of the action is not relevant
*/
@DataBoundSetter
public void setRequiredActionId(final String requiredActionId) {
this.requiredActionId = StringUtils.stripToEmpty(requiredActionId);
}

public String getRequiredActionId() {
return requiredActionId;
}

/**
* Creates the filter that selects the reference build by the actions it provides.
*
* @return the action filter
*/
ActionFilter createActionFilter() {
return new ActionFilter(requiredAction, requiredActionId);
}

@Override
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
Expand Down Expand Up @@ -301,17 +348,26 @@
* @return the reference build that satisfies the required status (or empty if no such build is found)
*/
protected Optional<ReferenceBuild> getReferenceBuildWithRequiredStatus(final Run<?, ?> run, final Run<?, ?> start, final FilteredLog log) {
var filter = createActionFilter();
if (filter.isEnabled()) {
log.logInfo("Considering only builds that provide an action %s", filter);
}
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));
if (filter.accepts(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.logInfo("-> skipping %s '%s' since it does not provide an action %s",
StringUtils.uncapitalize(getBuildName(start, reference)),
reference.getDisplayName(), filter);
}
}
log.logInfo("-> ignoring reference build '%s' or one of its predecessors since none have a result of %s or better",
start.getDisplayName(), requiredResult);
log.logInfo("-> ignoring reference build '%s' or one of its predecessors since none have a result of %s or better%s",
start.getDisplayName(), requiredResult, filter.getRequirementSuffix());
return Optional.empty();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,13 @@
<f:entry title="${%title.considerRunningBuild}" field="considerRunningBuild">
<f:checkbox />
</f:entry>
<f:advanced>
<f:entry title="${%title.requiredAction}" field="requiredAction">
<f:textbox />
</f:entry>
<f:entry title="${%title.requiredActionId}" field="requiredActionId">
<f:textbox />
</f:entry>
</f:advanced>

</j:jelly>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
title.referenceJob=Reference Job
title.requiredResult=Required Build Result
title.considerRunningBuild=Consider running builds as reference
title.requiredAction=Required Action Type
title.requiredActionId=Required Action ID
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Some plugins do not record their results in every build: code coverage, for example, can be disabled on a
per-build basis. If the automatically selected reference build does not contain such a report, then no delta
report can be computed at all - the delta results are then silently empty.

<p>
With this parameter you can restrict the search for a reference build to those builds that provide a specific
action (i.e., a specific report). All builds that do not provide such an action will be skipped, and the search
continues with the predecessors of these builds.
</p>

<p>
The action can be specified either by its fully qualified class name or by its simple class name. Supertypes and
interfaces of the actual action are considered as well, so it is possible to filter for a whole family of
actions. Some examples:
</p>
<dl>
<dt>io.jenkins.plugins.coverage.metrics.steps.CoverageBuildAction</dt>
<dd>The reference build must contain a code coverage report of the coverage plugin</dd>
<dt>CoverageBuildAction</dt>
<dd>The same filter, using the simple class name</dd>
<dt>&lt;empty&gt;</dt>
<dd>The type of the action is not relevant (this is the default behavior if unset)</dd>
</dl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Plugins that can record several results in the same build (like the coverage plugin, which can report a code
coverage and a mutation coverage result) use an ID to distinguish these results. This ID is exposed as the URL
name of the corresponding action.

<p>
With this parameter you can restrict the search for a reference build to those builds that provide an action
with the given ID. All builds that do not provide such an action will be skipped, and the search continues with
the predecessors of these builds.
</p>

<p>
This parameter can be combined with the required action type: then the reference build must provide an action
that matches both the type and the ID. If the type is left empty, then the ID alone is used to select the
matching action. If this parameter is left empty, then the ID of the action is not relevant.
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.jenkins.plugins.forensics.reference;

import edu.umd.cs.findbugs.annotations.CheckForNull;

import hudson.model.Action;

/**
* Simulates a report that is not recorded in every build (like the code coverage report of the coverage plugin). The
* ID of the report is exposed as the URL name of this action, so that the tests can select a build by the type or by
* the ID of the report it provides.
*
* @author Akash Manna
*/
class CoverageReportAction implements Action {
private final String id;

CoverageReportAction(final String id) {
this.id = id;
}

@Override @CheckForNull
public String getIconFileName() {
return null;
}

@Override @CheckForNull
public String getDisplayName() {
return null;
}

@Override
public String getUrlName() {
return id;
}

/**
* A subtype that verifies that the supertypes of an action are considered by the filter as well.
*
* @author Akash Manna
*/
static final class MutationCoverageReportAction extends CoverageReportAction {
MutationCoverageReportAction(final String id) {
super(id);
}
}
}
Loading
Loading